☰
Linux进程信号详解:从产生、保存到捕捉的完整排查记录
2026/9/29 22:10:39 网站建设 项目流程

上周线上一个后台采集进程半夜挂了,日志最后一行是空的,没有堆栈,没有 core。运维说机器没重启,OOM 也没记录。我第一反应是收到了某个默认动作是终止的信号,比如 SIGTERM 或 SIGPIPE。这篇就把排查过程和我重新梳理的信号知识记下来。

问题复现

进程是个常驻的 C 程序,往一个管道写数据。简化后大概这样:

#include<stdio.h>#include<unistd.h>intmain(void){intfd[2];pipe(fd);close(fd[0]);// 故意关掉读端for(;;){// 写端还在,但读端已关闭ssize_tn=write(fd[1],"hello",5);printf("write = %zd\n",n);sleep(1);}return0;}

编译运行:

gcc-odemo demo.c ./demo

结果进程直接消失,连printf都没打出来。用echo $?看退出码是 141,也就是 128+13,13 正是 SIGPIPE。

信号这东西平时写业务代码感觉不到,一旦出事就是这种"静默死亡"。

排查过程

线上没法复现,我先在本地用 strace 跟一下,看进程到底是怎么死的:

strace-f-etrace=write,rt_sigaction ./demo

输出里能看到write返回-1 EPIPE,紧接着是--- SIGPIPE {si_signo=SIGPIPE, si_code=SI_USER, ...} ---,然后+++ killed by SIGPIPE +++。

这一步确认了:信号是内核对写已关闭管道这个动作的直接响应,不是别的进程发的。

接着我想确认进程当前对哪些信号做了处理。看/proc:

cat/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,所以走了默认动作——终止。

根因

要讲清楚,得把信号的生命周期拆成三段:产生 → 保存 → 捕捉/递送。

一、信号怎么产生

常见来源有四类:

  1. 终端按键:Ctrl+C 发 SIGINT,Ctrl+\ 发 SIGQUIT,Ctrl+Z 发 SIGTSTP。
  2. 硬件异常:除零发 SIGFPE,非法内存访问发 SIGSEGV,这些由内核在异常处理里转成信号。
  3. kill / raise / abort:kill(pid, sig)给指定进程,raise(sig)给自己,abort()发 SIGABRT。
  4. 软件条件:管道读端关闭后写数据触发 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 有两种处理方式:

  1. 全局忽略:signal(SIGPIPE, SIG_IGN),之后write会返回 -1 并置errno = EPIPE,由代码自己处理。
  2. 用send的MSG_NOSIGNAL标志(仅 socket)。

对管道场景,只能选方案 1。我改成注册一个 handler 记录日志,同时把 SIGPIPE 加入阻塞集,避免在主逻辑中途被打断。

下面是我最终用的骨架代码,涵盖注册、阻塞、pending 读取:

#define_GNU_SOURCE#include<stdio.h>#include<stdlib.h>#include<string.h>#include<unistd.h>#include<signal.h>#include<errno.h>staticvolatilesig_atomic_tg_got_sigpipe=0;staticvoidon_sigpipe(intsig,siginfo_t*info,void*ucontext){(void)ucontext;// 只做异步信号安全的事g_got_sigpipe=sig;// 想记日志用 write,不要 printfconstcharmsg[]="caught SIGPIPE\n";write(STDERR_FILENO,msg,sizeof(msg)-1);(void)info;}intmain(void){structsigactionsa;memset(&sa,0,sizeof(sa));sa.sa_sigaction=on_sigpipe;sa.sa_flags=SA_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(inti=0;i<3;i++){ssize_tn=write(fd[1],"x",1);if(n==-1&&errno==EPIPE)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 里只能用异步信号安全函数,这两条是最容易翻车的。遇到进程静默死亡,先看退出码是不是 128+N,再用 strace 抓信号,基本能定位。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询