第一章:Go signal处理底层机制总览
Go 语言的 signal 处理并非直接映射操作系统信号,而是通过运行时(runtime)在用户态构建的一套协作式信号拦截与分发机制。其核心依赖于 sigtramp 汇编桩、信号掩码(signal mask)隔离、以及一个专用的 sigrecv goroutine,三者协同实现安全、非抢占式的信号接收。
信号注册与内核级拦截
当调用 signal.Notify(c, os.Interrupt, syscall.SIGTERM) 时,Go 运行时执行以下关键动作:
- 调用
runtime.sigignore()清除目标信号的默认行为(如终止或 core dump); - 使用
rt_sigaction()系统调用将信号处理器设为runtime.sigtramp(位于src/runtime/sys_linux_amd64.s); - 同时将该信号加入
runtime.sigmask,确保仅由 Go 的信号线程响应,避免干扰其他线程。
专用信号接收协程
Go 运行时在启动阶段自动创建一个永不退出的 sigrecv goroutine,它阻塞在 runtime.sigrecv() 上,该函数本质是轮询 runtime.signals 环形缓冲区(大小为 128)。所有被 sigtramp 捕获的信号均经此缓冲区中转,再由 sigrecv 统一分发至用户注册的 channel。
用户通道接收逻辑示例
package main
import (
"os"
"os/signal"
"syscall"
"time"
)
func main() {
sigChan := make(chan os.Signal, 1)
// 注册信号:SIGINT 和 SIGTERM 将被转发到 sigChan
signal.Notify(sigChan, os.Interrupt, syscall.SIGTERM)
// 阻塞等待首个信号(实际由 sigrecv goroutine 填充)
sig := <-sigChan
println("Received signal:", sig.String()) // 输出如 "interrupt"
}
该代码中,signal.Notify 触发底层信号注册,而 <-sigChan 的阻塞由 runtime 内部的 sigrecv 协程唤醒——不涉及系统调用阻塞,全程在 Go 调度器控制下完成。
| 关键组件 | 作用说明 |
|---|---|
sigtramp |
汇编级入口,将内核信号转为 runtime 事件 |
signals 缓冲区 |
无锁环形队列,承载信号传递的中间载体 |
sigrecv goroutine |
唯一消费者,将信号投递至用户 channel |
sigmask |
线程级信号屏蔽字,保障信号只由 runtime 处理 |
第二章:操作系统信号传递与sigtramp汇编桩剖析
2.1 信号中断触发与内核态到用户态的上下文切换
当硬件中断(如定时器)到来,CPU自动保存当前寄存器状态并跳转至中断向量表对应入口,进入内核态执行 do_IRQ()。若该中断最终引发待处理信号(如 SIGALRM),内核在 exit_to_user_mode_prepare() 中标记 TIF_SIGPENDING。
信号检查与返回路径
用户态进程从系统调用或中断返回前,内核必经 syscall_exit_to_user_mode() → handle_signal() 流程:
// arch/x86/kernel/entry.c
void do_syscall_64(struct pt_regs *regs) {
// ... 系统调用执行 ...
syscall_exit_to_user_mode(regs); // 关键钩子:检查信号
}
regs 指向用户栈上保存的完整上下文;syscall_exit_to_user_mode() 调用 signal_pending() 判定是否需递送信号,决定是否跳转至 handle_signal() 构建用户态信号帧。
上下文切换关键动作
- 保存用户态
RIP/RSP/CS/RFLAGS至内核栈 - 压入信号处理函数地址、
siginfo_t和ucontext_t - 修改
regs->rip指向__kernel_rt_sigreturn
| 阶段 | 寄存器操作 | 触发条件 |
|---|---|---|
| 中断进入 | 自动压栈 RIP/CS/RFLAGS/... |
硬件中断发生 |
| 信号判定 | 检查 current->pending 位图 |
TIF_SIGPENDING 置位 |
| 用户态跳转 | 覆写 regs->rip, regs->rsp |
handle_signal() 成功 |
graph TD
A[硬件中断] --> B[CPU自动切内核态<br>保存用户寄存器]
B --> C[do_IRQ → signal_wake_up]
C --> D[exit_to_user_mode_prepare]
D --> E{signal_pending?}
E -->|Yes| F[setup_rt_frame → 修改regs]
E -->|No| G[ret_from_fork/syscall]
F --> H[用户态执行handler]
2.2 sigtramp汇编桩的生成逻辑与平台差异(amd64/arm64)
sigtramp(signal trampoline)是内核在用户态信号处理前插入的临时执行桩,用于安全保存/恢复寄存器上下文并跳转至信号处理函数。其生成由arch_sigreturn路径动态构造,而非静态链接。
平台核心差异点
- amd64:依赖
syscall指令触发rt_sigreturn,需压栈r11/rcx等被破坏寄存器 - arm64:使用
svc #0系统调用,且必须显式保存x8(syscall号)、x30(lr)及sp对齐
典型amd64 sigtramp片段
# sigtramp-amd64.s
movq %rsp, %rdi # 传入当前栈指针作为rt_sigreturn参数
movq $15, %rax # sys_rt_sigreturn syscall number
syscall # 触发内核信号返回路径
rdi承载ucontext_t*地址;rax=15为__NR_rt_sigreturn;syscall自动保存rcx/r11,故无需手动压栈。
arm64 sigtramp关键约束
| 寄存器 | 用途 | 是否需显式保存 |
|---|---|---|
x0 |
ucontext_t*地址 |
是 |
x8 |
syscall号(139) | 是 |
sp |
必须16字节对齐 | 是(调整SP) |
graph TD
A[用户态中断] --> B{架构分支}
B -->|amd64| C[push r11/rcx → syscall]
B -->|arm64| D[stp x0,x1,[sp,-16]! → svc #0]
C --> E[内核校验sigframe完整性]
D --> E
2.3 runtime·sigtramp函数的汇编实现与寄存器保存/恢复实践
sigtramp 是 Go 运行时中处理信号中断的关键汇编桩函数,位于 runtime/sys_x86_64.s,负责在用户态信号 handler 执行前后原子性保存与恢复全部浮点及通用寄存器。
寄存器保存策略
- 使用
pushq %rax至pushq %r15顺序压栈(共16个通用寄存器) xsave指令保存 FPU/SSE/AVX 状态到对齐的栈空间(%rsp-256起)- 保存
rip和rflags后跳转至runtime.sigtrampgo
核心汇编片段(x86-64)
TEXT runtime·sigtramp(SB), NOSPLIT, $512-0
pushq %rax
pushq %rbx
pushq %rcx
pushq %rdx
// ... 其余寄存器压栈(省略)
subq $256, %rsp // 为 xsave 预留空间
xsave (%rsp) // 保存扩展状态(含 MXCSR、XMM0–15 等)
movq %rsp, %rdi // 第一参数:保存现场指针
call runtime·sigtrampgo(SB)
xrstor (%rsp) // 恢复扩展状态
addq $256, %rsp
popq %rdx // 逆序弹出寄存器
popq %rcx
popq %rbx
popq %rax
ret
逻辑说明:该函数以
$512-0帧大小声明(512 字节栈空间),确保xsave对齐要求;%rdi传入gsignal关联的sigctxt地址;xrstor必须与xsave使用相同内存基址,否则触发 #GP 异常。
| 寄存器类别 | 保存指令 | 恢复指令 | 是否由 sigtramp 管理 |
|---|---|---|---|
| 通用寄存器 | pushq |
popq |
✅ |
| XMM/YMM | xsave |
xrstor |
✅ |
| RSP/RIP | 隐式/调用约定 | 隐式/ret |
✅ |
graph TD
A[信号中断触发] --> B[sigtramp 入口]
B --> C[压栈通用寄存器]
C --> D[xsave 扩展状态]
D --> E[调用 sigtrampgo]
E --> F[xrstor 恢复状态]
F --> G[弹出寄存器并 ret]
2.4 通过GDB动态追踪sigtramp执行路径与栈帧变化
sigtramp 是内核在用户态信号处理前自动插入的特殊代码片段,位于 VDSO 或 vdso 映射区域,负责保存寄存器上下文、调用信号处理函数,再恢复现场。
启动带信号的调试目标
# 编译时保留调试信息,并触发 SIGUSR1
gcc -g -o sigtest sigtest.c && ./sigtest &
GDB中捕获并追踪 sigtramp
(gdb) handle SIGUSR1 stop print nopass
(gdb) b *$rip # 在信号中断点下断
(gdb) c
(gdb) info proc mappings | grep vdso # 定位 sigtramp 所在页
(gdb) x/10i $rip # 查看当前指令流(常含 mov %rsp,%rdi; call __kernel_rt_sigreturn)
该命令序列强制 GDB 在信号投递瞬间停住,x/10i $rip 展示 sigtramp 入口处的汇编,其中关键指令完成栈帧切换与控制权移交。
栈帧演化关键观察点
| 阶段 | $rbp 值 | $rsp 偏移 | 特征 |
|---|---|---|---|
| 信号前 | 用户栈帧 | 正常 | 指向局部变量 |
| 进入 sigtramp | 新栈帧(内核构造) | +128~256 | 包含 ucontext_t 结构体 |
| 调用 handler | handler 栈帧 | 再次偏移 | $rbp 指向 handler 参数区 |
graph TD
A[用户态主函数] -->|SIGUSR1 触发| B[内核构建 sigframe]
B --> C[sigtramp 执行:保存寄存器/设置新栈]
C --> D[跳转至用户 signal handler]
D --> E[__restore_rt 返回用户栈]
2.5 修改sigtramp行为验证信号拦截机制(含unsafe.Pointer绕过检查实验)
sigtramp重定向原理
Linux内核在用户态信号交付时,通过sigtramp代码段跳转至信号处理函数。Go运行时将其替换为自定义桩,实现对SIGUSR1等信号的透明拦截。
unsafe.Pointer绕过类型检查实验
// 将函数指针强制转为*byte以写入sigtramp页
sigtrampPage := mmapSigtrampPage() // RWX内存页
handlerAddr := (*[3]uintptr)(unsafe.Pointer(&mySignalHandler))[0]
*(*uintptr)(unsafe.Pointer(uintptr(sigtrampPage) + 0x10)) = handlerAddr
该操作绕过Go的类型安全检查,直接覆写机器码跳转目标;需配合mprotect(RWX)与runtime.LockOSThread()确保线程绑定。
关键参数说明
sigtrampPage: 映射的可执行内存页,起始地址对齐到页边界0x10: x86-64下跳转指令(jmp *%rax)的操作数偏移mySignalHandler: 符合func(int, *siginfo_t, uintptr)签名的C ABI兼容函数
| 风险项 | 触发条件 | 后果 |
|---|---|---|
| GC扫描误判 | 未标记为runtime.systemstack |
指针被回收导致崩溃 |
| SELinux拒绝 | 未调用mprotect(PROT_EXEC) |
SIGSEGV终止进程 |
graph TD
A[触发SIGUSR1] --> B{内核调用sigtramp}
B --> C[跳转至篡改后地址]
C --> D[执行mySignalHandler]
D --> E[调用runtime.sigtrampgo]
E --> F[恢复goroutine调度]
第三章:Go运行时信号注册与状态管理
3.1 os/signal.Notify背后的runtime.signal_enable调用链分析
os/signal.Notify 的核心在于将用户注册的信号转发至 Go 运行时的信号处理系统,其关键路径最终抵达 runtime.signal_enable。
信号注册的底层入口
调用链为:
Notify(c, sigs...) → signal.enable(sig) → runtime.signal_enable(uint32(sig))
runtime.signal_enable 的作用
该函数在运行时层启用指定信号的捕获能力,并确保信号不被忽略或默认终止:
// runtime/signal_unix.go(简化示意)
func signal_enable(sig uint32) {
var itim timer
sigprocmask(_SIG_UNBLOCK, &sig, nil) // 解除内核对该信号的阻塞
sigaction(sig, &sa, nil) // 安装 runtime 自定义 handler
}
参数
sig是 POSIX 信号编号(如syscall.SIGINT = 2),sigprocmask解除阻塞是前提,否则sigaction无法生效。
关键状态表:信号在 Go 运行时中的三态控制
| 状态 | 含义 | 对应 runtime 函数 |
|---|---|---|
Disabled |
信号被阻塞且 handler 为空 | signal_disable |
Enabled |
可投递至 signal delivery | signal_enable |
Ignored |
被显式忽略(非 Go 管理) | signal_ignore |
graph TD
A[Notify] --> B[signal.enable]
B --> C[runtime.signal_enable]
C --> D[sigprocmask: unblock]
C --> E[sigaction: install handler]
3.2 sigtab信号表结构、mask位图与goroutine信号屏蔽策略
Go 运行时通过 sigtab 全局数组管理 POSIX 信号到运行时处理逻辑的映射:
// runtime/signal_unix.go
var sigtab = [numSig]sigTabT{
_SIGQUIT: {fn: sigquit, flags: _SigNotify | _SigKill},
_SIGUSR1: {fn: sigenable, flags: _SigNotify},
}
sigtab[i] 定义信号 i 的处理函数与标志位,其中 _SigNotify 表示转发至 sigsend 通道,_SigKill 表示不可屏蔽。
每个 g(goroutine)携带独立的 sigmask 字段(uint64 位图),用于记录该 goroutine 屏蔽的信号集合。调度器在 gopark 前调用 sigprocmask 应用此掩码,实现细粒度信号隔离。
核心机制对比
| 维度 | 进程级 mask | Goroutine 级 mask |
|---|---|---|
| 作用范围 | 整个 OS 线程 | 单个 goroutine 执行上下文 |
| 更新时机 | sigprocmask() 系统调用 |
g.sched.sigmask 调度切换时加载 |
信号屏蔽流程
graph TD
A[goroutine 准备 park] --> B[读取 g.sigmask]
B --> C[调用 sigprocmask 临时屏蔽]
C --> D[执行 park]
D --> E[恢复原 mask]
3.3 通过/proc/PID/status与runtime.ReadMemStats观测信号状态迁移
Linux 进程的信号处理状态(如 SigPnd、SigBlk)隐式影响 goroutine 调度器对系统调用中断的响应,而 Go 运行时内存统计可间接反映信号触发的 GC 协作行为。
/proc/PID/status 中的关键信号字段
查看进程信号掩码与待决信号:
# 示例:读取当前 Go 进程的信号状态(PID=12345)
cat /proc/12345/status | grep -E "^(Sig|State)"
SigPnd: 待决信号位图(尚未被任何线程处理)SigBlk: 当前被阻塞的信号集State:T(stopped)或S(sleeping)可能暗示SIGSTOP或SIGURG引发的调度暂停
runtime.ReadMemStats 的协变线索
var m runtime.MemStats
runtime.ReadMemStats(&m)
fmt.Printf("NumGC: %d, PauseTotalNs: %d\n", m.NumGC, m.PauseTotalNs)
PauseTotalNs突增可能关联SIGURG触发的栈扫描协作NumGC跳变常伴随SIGUSR1(debug trap)导致的强制 GC
| 字段 | 含义 | 信号相关性 |
|---|---|---|
SigPnd |
待决信号位图 | SIGCHLD 积压提示子进程退出未 wait |
PauseTotalNs |
GC 暂停总纳秒数 | SIGURG 可能触发 STW 前同步 |
graph TD
A[内核投递 SIGURG] --> B[Go runtime 拦截]
B --> C[标记 M 需协作扫描栈]
C --> D[runtime.ReadMemStats 显示 PauseTotalNs 上升]
第四章:从用户态信号接收至runtime.sigsend队列的全链路
4.1 sigrecv循环与signal_recv goroutine的启动时机与调度特征
signal_recv goroutine 是 Go 运行时信号处理的核心协程,由 runtime.sighandler 初始化后立即启动,早于用户 main.main 执行。
启动时机关键点
- 在
runtime.mstart()中调用siginit()完成信号掩码设置后触发 - 通过
go signal_recv()启动,属于 runtime.init 阶段的硬编码 goroutine - 仅启动一次,绑定至主 M(非 P 绑定),永不退出
调度特征
- 使用
runtime.notetsleepg阻塞等待signote.note,避免轮询开销 - 响应内核
tgkill发送的SIGURG(或SIGWINCH等)时被唤醒 - 唤醒后立即消费
sigsend写入的sigTab队列,转发至注册的signal.Notifychannel
// runtime/signal_unix.go
func signal_recv() {
for {
n := sigrecv() // 阻塞读取 runtime.sigTab
if n == 0 {
continue
}
dispatchSig(n) // 分发至 Go signal channel 或 runtime handler
}
}
sigrecv() 底层调用 sigNoteSleep,依赖 futex 系统调用实现轻量级等待;参数 n 为信号编号,范围 [1, _NSIG),超出则静默丢弃。
| 特征 | 表现 |
|---|---|
| 启动阶段 | runtime.init → sighandler → signal_recv |
| 调度优先级 | 普通 goroutine,但因阻塞在 notetsleepg 实际获得高响应性 |
| P 绑定 | 不绑定 P,运行在主 M 的 g0 栈上 |
4.2 runtime.sigsend队列的MPSC无锁设计与内存屏障应用
核心设计目标
runtime.sigsend 队列为信号发送侧(单生产者)与运行时调度器(多消费者)间提供低延迟、无锁的信号传递通道,需满足:
- 严格顺序性(信号不乱序)
- 零分配(避免堆分配)
- 内存可见性保障(跨M/P边界)
MPSC队列结构
type sigsendQueue struct {
head uint64 // atomic, 消费端推进(多个P并发读/改)
tail uint64 // atomic, 生产端独占写
buf [64]uintptr // 环形缓冲区,固定大小
}
head和tail均为uint64原子变量;buf采用栈内静态分配,规避 GC 压力。环形索引通过idx & (len(buf)-1)实现(需 2 的幂次长度)。
内存屏障关键点
| 操作位置 | 屏障类型 | 作用 |
|---|---|---|
tail 更新后 |
atomic.StoreAcq |
确保 buf[tail%N] 写入对消费者可见 |
head 读取前 |
atomic.LoadRel |
防止后续 buf[head%N] 读取被重排至 head 读取前 |
信号入队流程(简化)
graph TD
A[生产者获取 tail] --> B[计算 slot = tail & mask]
B --> C[写入信号值到 buf[slot]]
C --> D[StoreAcq tail+1]
D --> E[消费者 LoadRel head 触发可见性同步]
同步机制
- 生产端使用
StoreAcq:保证信号值写入完成后再更新tail; - 消费端使用
LoadRel:确保head读取后,对应buf[head]数据已就绪; - 无锁但非无等待——消费者可能因
head == tail自旋等待。
4.3 信号分发路径:sigsend → gopark → signal delivery to channel
Go 运行时的信号处理并非直接投递至用户 goroutine,而是经由内核信号→runtime.sigsend→goroutine park→channel 通知的协同链路。
信号捕获与转发
// runtime/signal_unix.go 中关键调用
func sigsend(sig uint32) {
// 将信号转为 runtime 内部事件,写入全局 sigrecv 通道
sigrecv <- sig
}
sigsend 不触发立即调度,仅将信号编号写入无缓冲 channel sigrecv,确保异步安全;参数 sig 是 POSIX 信号编号(如 _SIGUSR1)。
Goroutine 阻塞等待信号
func signal_recv() {
for {
sig := <-sigrecv // 阻塞读取
if !canDeliver(sig) { continue }
gopark(nil, nil, waitReasonSignalDelivery, traceEvGoBlock, 1)
}
}
gopark 将当前 M 绑定的 G 挂起,等待信号被 signal_delivery 逻辑唤醒——该唤醒由 sighandler 触发,并最终向用户注册的 signal.Notify(c, os.Signal) 通道发送值。
信号投递状态映射
| 阶段 | 执行者 | 目标对象 | 同步性 |
|---|---|---|---|
sigsend |
用户/内核 | sigrecv channel |
异步 |
gopark |
runtime | 当前 G | 协程级阻塞 |
| channel send | sighandler |
用户 chan os.Signal |
同步(select 可感知) |
graph TD
A[内核信号] --> B[sigsend]
B --> C[sigrecv channel]
C --> D[signal_recv loop]
D --> E[gopark 当前 G]
E --> F[sighandler 唤醒]
F --> G[向 notify channel 发送 os.Signal]
4.4 构造高并发信号洪流场景,压测sigsend队列吞吐与goroutine阻塞行为
为复现 sigsend 队列饱和与 goroutine 阻塞现象,我们启动 1024 个 goroutine 并发调用 syscall.Kill(os.Getpid(), syscall.SIGUSR1):
// 模拟高并发信号洪流:每 goroutine 发送 100 次 SIGUSR1
for i := 0; i < 1024; i++ {
go func() {
for j := 0; j < 100; j++ {
syscall.Kill(syscall.Getpid(), syscall.SIGUSR1)
}
}()
}
该调用经内核 do_tkill → signal_wake_up 路径,最终写入 sighand->sigpending 链表。若信号处理函数未及时消费(如未注册 handler 或 handler 长阻塞),sigqueue 节点将堆积于 shared_pending。
关键观测指标
/proc/[pid]/status中SigQ字段(<pending>/<max])runtime.Gosched()在信号 handler 中的响应延迟- goroutine 状态从
running→syscall→runnable的迁移耗时
| 指标 | 正常值 | 队列饱和时 |
|---|---|---|
SigQ |
0/32768 |
32767/32768 |
| 平均发送延迟 | >500μs(内核自旋等待) |
graph TD
A[goroutine 调用 Kill] --> B[内核 sigqueue_alloc]
B --> C{队列未满?}
C -->|是| D[插入 shared_pending]
C -->|否| E[阻塞于 __sigqueue_alloc 自旋]
E --> F[goroutine 状态变为 syscall]
第五章:Go signal机制演进与工程实践启示
Go 语言的 signal 处理机制并非一成不变,而是随 runtime 和标准库的迭代持续演进。从早期 os/signal 包的简单通道接收,到 Go 1.16 引入 signal.NotifyContext,再到 Go 1.21 对 runtime/trace 中信号事件的精细化观测支持,每一次变更都直指真实工程痛点。
信号注册的生命周期管理陷阱
早期常见错误是全局注册 signal.Notify 后未及时调用 signal.Stop,导致 goroutine 泄漏。某支付网关服务在升级 Go 1.15 后出现连接池耗尽,经 pprof 分析发现 37 个 goroutine 阻塞在 sigc <- s 的内部 channel 上——根源在于热更新时重复注册 SIGUSR2 而未清理旧监听器。修复方案采用 sync.Once + defer signal.Stop(c) 组合,确保每个信号通道仅注册一次且随 handler 退出自动注销。
NotifyContext 在微服务优雅停机中的落地
以下为生产环境验证的停机模板:
func runServer() {
ctx, cancel := signal.NotifyContext(context.Background(), syscall.SIGTERM, syscall.SIGINT)
defer cancel()
srv := &http.Server{Addr: ":8080"}
go func() {
if err := srv.ListenAndServe(); err != http.ErrServerClosed {
log.Fatal(err)
}
}()
<-ctx.Done() // 阻塞等待信号
log.Println("Shutting down server...")
shutdownCtx, shutdownCancel := context.WithTimeout(context.Background(), 10*time.Second)
defer shutdownCancel()
if err := srv.Shutdown(shutdownCtx); err != nil {
log.Fatal("Server shutdown failed:", err)
}
}
进程级信号与容器编排的协同问题
Kubernetes 中 kubectl delete pod 默认发送 SIGTERM,但某些 Go 应用因 os.Exit(0) 被提前调用导致无法执行 cleanup。通过 strace -e trace=kill,exit_group 抓取发现:当容器运行时(如 containerd)向进程组发送信号时,若主 goroutine 已退出而子 goroutine 仍在运行,SIGTERM 可能被忽略。解决方案是在 main() 函数末尾显式调用 syscall.Kill(syscall.Getpid(), syscall.SIGTERM) 触发信号重入,确保所有 goroutine 收到通知。
多信号优先级调度的实战案例
| 某实时风控系统需区分 SIGUSR1(热重载规则)和 SIGUSR2(强制 dump 状态)。通过构建信号优先队列实现: | 信号类型 | 处理延迟 | 是否阻塞主线程 | 关键依赖 |
|---|---|---|---|---|
| SIGTERM | 0ms | 是 | HTTP shutdown | |
| SIGUSR1 | ≤50ms | 否 | config watcher | |
| SIGUSR2 | ≤10ms | 否 | pprof.WriteHeapProfile |
该设计使规则热更新平均耗时从 320ms 降至 47ms,同时避免了状态 dump 期间请求处理中断。
信号调试工具链建设
团队构建了三类诊断能力:
- 编译期:启用
-gcflags="-m" -ldflags="-s -w"检查 signal 相关逃逸分析 - 运行时:
go tool trace中筛选signal-received事件,定位信号接收延迟 - 容器层:在 Dockerfile 中添加
STOPSIGNAL SIGTERM并验证docker stop -t 15行为一致性
某次线上故障复盘显示,92% 的信号丢失发生在容器启动后前 3 秒,最终确认为 init 容器未正确设置 proc/sys/kernel/core_pattern 导致子进程信号队列溢出。
