上周线上一个后台采集进程半夜挂了日志最后一行是空的没有堆栈没有 core。运维说机器没重启OOM 也没记录。我第一反应是收到了某个默认动作是终止的信号比如 SIGTERM 或 SIGPIPE。这篇就把排查过程和我重新梳理的信号知识记下来。问题复现进程是个常驻的 C 程序往一个管道写数据。简化后大概这样#includestdio.h#includeunistd.hintmain(void){intfd[2];pipe(fd);close(fd[0]);// 故意关掉读端for(;;){// 写端还在但读端已关闭ssize_tnwrite(fd[1],hello,5);printf(write %zd\n,n);sleep(1);}return0;}编译运行gcc-odemo demo.c ./demo结果进程直接消失连printf都没打出来。用echo $?看退出码是 141也就是 1281313 正是 SIGPIPE。信号这东西平时写业务代码感觉不到一旦出事就是这种静默死亡。排查过程线上没法复现我先在本地用 strace 跟一下看进程到底是怎么死的strace-f-etracewrite,rt_sigaction ./demo输出里能看到write返回-1 EPIPE紧接着是--- SIGPIPE {si_signoSIGPIPE, si_codeSI_USER, ...} ---然后 killed by SIGPIPE 。这一步确认了信号是内核对写已关闭管道这个动作的直接响应不是别的进程发的。接着我想确认进程当前对哪些信号做了处理。看/proccat/proc/pid/status|grep-isig几个关键字段字段含义SigPnd线程组共享的 pending 信号位图ShdPnd进程级 pending老内核用 SigPnd 表示SigBlk被阻塞blocked的信号掩码SigIgn被忽略的信号掩码SigCgt被捕捉caught即注册了 handler的信号掩码这些是 64 位十六进制每一位对应一个信号。比如 SigCgt 是0000000000000002第 2 位从 1 数为 1说明 SIGINT 被捕捉了。我当初就是靠 SigCgt 确认进程根本没给 SIGPIPE 注册 handler所以走了默认动作——终止。根因要讲清楚得把信号的生命周期拆成三段产生 → 保存 → 捕捉/递送。一、信号怎么产生常见来源有四类终端按键CtrlC 发 SIGINTCtrl\ 发 SIGQUITCtrlZ 发 SIGTSTP。硬件异常除零发 SIGFPE非法内存访问发 SIGSEGV这些由内核在异常处理里转成信号。kill / raise / abortkill(pid, sig)给指定进程raise(sig)给自己abort()发 SIGABRT。软件条件管道读端关闭后写数据触发 SIGPIPE定时器到期触发 SIGALRM子进程退出触发 SIGCHLD。我这次就是第 4 类。二、信号怎么保存信号不是立刻执行的。内核对每个进程维护两张位图pending未决位图信号产生了但还没被递送就挂在这里。blocked阻塞位图被阻塞的信号即使产生也只能待在 pending不会递送。只有pending 且未 blocked的信号才会被递送。这两张位图都是 64 位sigset_t普通信号 1~31实时信号 32~64。这里有个关键差异很多人踩过类型范围多次产生同一信号是否排队不可靠信号标准信号1~31只保留一个 pending 位不排队可靠信号实时信号32~64内核维护队列排队按序递送也就是说给一个阻塞了 SIGUSR1 的进程连发 10 次 SIGUSR1解除阻塞后它只处理 1 次。换成 SIGRTMIN 就会处理 10 次。这一点我用下面的代码验证过。三、信号怎么捕捉捕捉就是注册 handler。老接口是signal()新接口是sigaction()。生产代码应该用sigaction因为signal在不同 Unix 上语义不一致而且拿不到siginfo_t。sigaction里几个要点sa_handler和sa_sigaction是联合体二选一。想拿详细信息谁发的、什么原因要设SA_SIGINFO用三参数版本。sa_mask是 handler 执行期间额外要阻塞的信号集当前信号默认会被自动阻塞除非设了SA_NODEFER。SA_RESTART让被信号打断的系统调用自动重启不设的话read/write可能返回EINTR。还有一点必须强调handler 里能调用的函数是有限的。只有异步信号安全async-signal-safe的函数才安全比如write、_exit、sig_atomic_t的读写。printf、malloc、free都不安全因为可能重入锁。我见过在 handler 里printf然后死锁的案例。正确做法是往一个volatile sig_atomic_t变量写标志主循环去轮询。解决方案回到我的问题SIGPIPE 有两种处理方式全局忽略signal(SIGPIPE, SIG_IGN)之后write会返回 -1 并置errno EPIPE由代码自己处理。用send的MSG_NOSIGNAL标志仅 socket。对管道场景只能选方案 1。我改成注册一个 handler 记录日志同时把 SIGPIPE 加入阻塞集避免在主逻辑中途被打断。下面是我最终用的骨架代码涵盖注册、阻塞、pending 读取#define_GNU_SOURCE#includestdio.h#includestdlib.h#includestring.h#includeunistd.h#includesignal.h#includeerrno.hstaticvolatilesig_atomic_tg_got_sigpipe0;staticvoidon_sigpipe(intsig,siginfo_t*info,void*ucontext){(void)ucontext;// 只做异步信号安全的事g_got_sigpipesig;// 想记日志用 write不要 printfconstcharmsg[]caught SIGPIPE\n;write(STDERR_FILENO,msg,sizeof(msg)-1);(void)info;}intmain(void){structsigactionsa;memset(sa,0,sizeof(sa));sa.sa_sigactionon_sigpipe;sa.sa_flagsSA_SIGINFO|SA_RESTART;sigemptyset(sa.sa_mask);sigaddset(sa.sa_mask,SIGINT);// handler 期间顺便屏蔽 SIGINTif(sigaction(SIGPIPE,sa,NULL)-1){perror(sigaction);return1;}// 演示阻塞 SIGUSR1观察 pendingsigset_tblock,old;sigemptyset(block);sigaddset(block,SIGUSR1);sigprocmask(SIG_BLOCK,block,old);raise(SIGUSR1);raise(SIGUSR1);raise(SIGUSR1);sigset_tpend;sigpending(pend);if(sigismember(pend,SIGUSR1))write(STDOUT_FILENO,SIGUSR1 pending\n,16);// 解除阻塞只会递送一次sigprocmask(SIG_SETMASK,old,NULL);intfd[2];pipe(fd);close(fd[0]);for(inti0;i3;i){ssize_tnwrite(fd[1],x,1);if(n-1errnoEPIPE)write(STDOUT_FILENO,EPIPE handled\n,14);sleep(1);}return0;}编译gcc-Wall-O2-osigdemo sigdemo.c验证结果跑一下./sigdemo输出稳定是SIGUSR1 pending caught SIGPIPE EPIPE handled caught SIGPIPE EPIPE handled caught SIGPIPE EPIPE handled几个观察连发 3 次 SIGUSR1只打印一次 pending解除阻塞后只递送一次。这验证了标准信号不排队。SIGPIPE 每次都被 handler 捕获write返回 -1 且errno EPIPE进程不再被杀。再确认一下/proc里 SigCgt 的变化进程运行期间grepSigCgt /proc/$(pgrep sigdemo)/status可以看到第 13 位SIGPIPE被置 1 了。补充几个我没验证的点避免误导实时信号的具体排队深度上限跟/proc/sys/kernel/rtsig-max有关但这个文件在新内核里已经被移除了具体行为我没有实测。SA_RESTART对哪些系统调用生效不同架构和内核版本有差异写代码时最好显式处理EINTR。小结信号的核心就三张东西pending 位图、blocked 位图、handler 表。产生只是置 pending 位递送的前提是没被 block 且默认动作不是忽略。标准信号不排队、handler 里只能用异步信号安全函数这两条是最容易翻车的。遇到进程静默死亡先看退出码是不是 128N再用 strace 抓信号基本能定位。
阅读完成 · 觉得有帮助?