资讯详情 Linux信号处理函数全解析:从signal到sigaction的实战指南
📅 2026/10/15 4:36:47
1. 项目概述1.1 为什么Linux开发者绕不开信号这关做Linux服务端开发的朋友早晚都要跟信号打交道。你写了一个后台守护进程领导要求重启时平滑退出别把正在处理的请求打断你写了一个多线程异地登录监控工具老板一句话内存占用怎么这么高赶紧优化你在排查线上问题的时候发现进程僵死kill -9都杀不掉——这些场景背后全是信号相关函数的问题。信号是Linux/Unix系统提供的一种异步事件通知机制。你可以把它理解成内核给进程发消息进程不用主动去问事情发生的时候会被动收到通知。进程什么时候会收到信号用户按下了CtrlC、某个定时器到期、内存访问出错、另一个进程显式调用kill都会触发信号。如果进程自己不处理默认动作往往很粗暴要么直接终止要么生成core dump要么直接把IO操作打断报错。你要是只会kill -9 pid这种操作那说明还没真正进入Linux系统编程的大门。信号相关函数是Linux系统编程的必修课从signal到sigaction从kill到sigqueue从sigprocmask到sigsuspend每一个函数都有它存在的理由也有它对应的坑。这篇文章不仅讲清楚每个函数怎么用还会讲清楚为什么这么用什么时候该用哪个以及我在实际项目里踩过的那些坑和总结的排查方法。1.2 这篇文章能给你什么这篇文章适合这么几类读者刚学完APUE想深入信号机制的初学者工作中被信号问题折腾过但没时间系统梳理的工程师以及准备面试想快速掌握信号函数体系的求职者。我会围绕Linux信号相关函数从核心概念、信号集操作、实时信号、异步安全到常见问题排查一层层拆开讲。阅读完你至少能解决这几个问题为什么signal函数已经过时了、sigaction到底强在哪里、怎么阻塞一组信号、怎么安全地在信号处理函数里做事情、实时信号和标准信号的区别是什么、EINTR错误到底是怎么回事。这些都是实战里高频出现的问题。2. 信号机制核心概念与函数全景图2.1 信号的本质内核给进程发的异步通知要理解信号相关函数先得理解信号本身是怎么工作的。信号的本质是软中断也就是内核在某个事件发生时主动通知进程你有事情需要处理一下然后进程在合适的时机从内核态返回用户态时去执行对应的处理动作。这里有个关键点信号的到达是异步的。进程正在执行任何位置的用户代码信号随时可能插入进来。这也意味着如果你的用户代码正在修改某个全局变量信号处理函数也去修改这个同一个变量就会出现数据竞争的问题。我一会儿讲异步信号安全的时候还要展开说。信号从产生到递达中间还要经过一个待处理pending的状态。如果信号被阻塞它就一直停留在待处理状态直到阻塞被解除才会真正递达。理解这个流程是你掌握sigprocmask和sigpending的基础。2.2 信号相关函数总览一张图看懂全家族Linux信号相关函数主要分成几个家族信号注册/设定处理函数signal()、sigaction()信号发送kill()、raise()、sigqueue()、abort()、alarm()信号集操作sigemptyset()、sigfillset()、sigaddset()、sigdelset()、sigismember()信号阻塞/等待sigprocmask()、sigpending()、sigsuspend()信号等待同步获取sigwait()、sigwaitinfo()、sigtimedwait()其他辅助sigaltstack()信号处理栈、pthread_sigmask()线程版信号阻塞这篇文章的主角是前四类。我先给一张总表把每个函数的职责和核心C头文件列出来函数族核心函数职责头文件信号注册signal/sigaction设置信号处理函数signal.h信号发送kill/raise/sigqueue给进程或线程发送信号signal.h信号集sigemptyset/sigaddset等构造、修改、检查信号集signal.h阻塞控制sigprocmask/sigpending查看和修改阻塞信号集signal.h等待挂起sigsuspend原子地解除阻塞并等待信号signal.h同步获取sigwaitinfo/sigtimedwait同步获取信号信息signal.h这些函数你在做守护进程、网络服务、IPC、多线程协作、故障恢复模块的时候都会碰到。2.3 关键概念铺垫信号生命周期与三种处理方式一个信号从产生到处理完整走一遍包含这几个阶段产生内核或进程主动产生信号阻塞进程可以设置阻塞集合把某些信号放在等待区待处理信号处于pending状态等待递达递达内核强制进程去处理从内核态返回用户态时处理执行默认动作、捕获动作或忽略要注意标准信号处理有一个合并的坑如果同一个标准信号在待处理期间连续到达多次内核只记录一次递达时也只处理一次。也就是说标准信号不排队。这也是后面sigqueue和实时信号存在的意义。信号的三种处理方式分别是默认动作SIG_DFL、忽略SIG_IGN、自定义处理函数SIG_DFL以外指定回调函数。这里提醒一句SIGKILL和SIGSTOP这两个信号不能捕获也不能忽略它们是内核的最后手段。3. 信号注册与发送函数实操对比3.1 signal与sigaction为什么老接口只适合简单玩signal()是最早的接口它的问题很多。在Linux系统上signal()的行为跟Unix System V和BSD都有差异可移植性差而且注册一次后在部分系统上会把处理动作重置回默认值你得在回调里重新调用signal()注册这种重入注册容易引入竞态问题。更关键的是signal()没办法精确控制信号处理时的其他属性比如是否需要设置SA_RESTART标志来让被中断的系统调用自动重启比如是否需要设置SA_SIGINFO来获取信号携带的详细信息。这些在现代网络服务里都很刚需。我的建议很简单不管你是刚入门还是老手新代码一律用sigaction()。它虽然接口复杂一点但是所有行为都是明确的、可控的。3.2 sigaction函数参数详解一个结构体搞定全部控制sigaction()的原型是int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact);参数含义signum要捕获的信号编号act新的处理配置oldact旧的处理配置如果是NULL则不需要保留关键在于struct sigaction这个结构体struct sigaction { void (*sa_handler)(int); // 简单信号处理函数 void (*sa_sigaction)(int, siginfo_t *, void *); // 扩展信号处理函数 sigset_t sa_mask; // 处理时额外阻塞的信号集合 int sa_flags; // 行为控制标志 void (*sa_restorer)(void); // 不用管它 };设计这个结构体的时候sa_handler和sa_sigaction是共用一块内存的所以你在设置的时候只能用其中一个千万别两个都填。使用sa_sigaction时必须把sa_flags里面加上SA_SIGINFO否则sa_sigaction不会生效。sa_mask字段表示当本次信号的处理函数正在执行时自动把这组信号也阻塞掉防止处理过程中被打断。这不是进程全局的阻塞集合而是这个处理函数执行期间的临时屏蔽集合处理函数返回后自动恢复。sa_flags常用值有这么几个SA_SIGINFO使用扩展处理函数能拿到信号来源、发送者PID、用户ID、具体数值SA_RESTART被此信号中断的系统调用自动重启比如read()被信号打断后不会返回EINTR而是自动重新调用SA_NOCLDSTOP仅用于SIGCHLD表示子进程停止或继续时不通知父进程SA_NOCLDWAIT仅用于SIGCHLD表示子进程终止时自动回收不产生僵尸进程我前一阵处理一个网络代理的代码连接很多常规signal()注册的信号每次read()被打断都要判EINTR代码量上去不少。后来全部换成sigaction加SA_RESTART中断之后系统自动重新发起该次读取代码干净了很多。这就是sigaction的实际价值。3.3 kill、raise、sigqueue发信号的三兄弟怎么选kill()是发信号最经典的函数int kill(pid_t pid, int sig);注意这个函数名字叫kill但它不是杀死进程的意思它是给进程发信号发给谁由pid参数决定发什么信号由sig参数决定。比如kill(pid, SIGUSR1)就是给指定进程发用户自定义信号。参数pid有多种取值等于0发给同进程组所有进程等于-1发给有权限的所有进程小于-1发给进程组ID为|pid|的所有进程。实际项目中我用到最多的还是直接传指定PID。raise()则简单许多它只给当前进程自己发信号int raise(int sig);它等价于kill(getpid(), sig)。sigqueue()是后面讲实时信号时的主角它的优势是可以带数据int sigqueue(pid_t pid, int sig, const union sigval value);这里的value可以传一个整数或者指针配合SA_SIGINFO接收方可以在siginfo_t结构体里拿到这个数据。这等于说信号不再是只知道发生了什么的干巴巴事件而是知道带上了一份数据的信息载体。在后面做进程间简单消息传递时代价低、效果直接。4. 信号集操作与阻塞机制从入门到精通4.1 信号集函数实操sigemptyset到sigismember信号集sigset_t是一个通用的信号集合类型。你不能直接给sigset_t赋值必须用专门函数操作因为不同平台的底层实现不一样有些是数组有些是位图直接赋值会导致未定义行为。基本操作顺序一般是先sigemptyset()清空再用sigaddset()添加需要的信号最后可以用sigismember()查询某个信号是否在集合里。sigset_t set; sigemptyset(set); sigaddset(set, SIGINT); sigaddset(set, SIGTERM); if (sigismember(set, SIGINT)) { printf(SIGINT is in set\n); } sigdelset(set, SIGINT); // 把SIGINT移除你没看错确实要先清空再用这看起来像初始化一个变量一样基础但我见过不少同事直接定义sigset_t set; sigaddset(set, SIGINT);结果因为栈上的垃圾数据导致莫名其妙把别的信号也加到集合里。这不是脏数据无所谓的问题这是必须规范化的操作流程。4.2 sigprocmask阻塞信号如何保护临界区sigprocmask()函数用来查看和修改进程的阻塞信号集合。原型int sigprocmask(int how, const sigset_t *set, sigset_t *oldset);how参数有三种选择SIG_BLOCK把set里的信号加到当前阻塞集合里并集操作SIG_UNBLOCK把set里的信号从当前阻塞集合里移除差集操作SIG_SETMASK直接用set替换当前阻塞集合oldset非空时会返回设置前的旧阻塞集合这样后面方便恢复现场。什么时候需要阻塞信号典型场景就是临界区保护。比如你维护一个全局链表正在做插入操作这个操作还没做完的时候来了一个SIGTERM处理函数中如果也操作了同一个链表那就完蛋了。做法sigset_t block_set, old_set; sigemptyset(block_set); sigaddset(block_set, SIGTERM); sigprocmask(SIG_BLOCK, block_set, old_set); // 修改共享数据的临界区操作 insert_data(list, node); sigprocmask(SIG_SETMASK, old_set, NULL); // 恢复原阻塞集合这里最重要的一点是恢复阻塞集合之后如果信号已经在等待状态它会立刻被递达。你不需要担心信号会丢失它只是被延迟处理了。所以做临界区保护时阻塞信号是安全手段不是信号清零手段。另外要说清楚sigprocmask在多线程程序里是有问题的它操作的是整个进程的阻塞掩码。多线程环境下请用pthread_sigmask()它只影响当前调用线程。这个坑我当初在线程池程序里踩过sigprocmask改了主线程的信号掩码其他线程却还在响应信号排查了半天发现是文档理解不深的问题。4.3 sigpending查看待处理信号不会丢的信号sigpending()用来查看当前进程有哪些信号正处于待处理状态也就是已经被阻塞、还没递达的信号。它的原型很简单int sigpending(sigset_t *set);注意这个函数返回的是待处理信号的集合不是收到过多少个信号。如果你想确定某个信号是否已经被触发但还在等待就调用sigismember检查返回的集合。我当时写过一个监控模块主逻辑里挡住了SIGTERM但是想知道我到底有没有被要求退出然后在某个合适的检查点去调用sigpending查看SIGTERM是否在待处理集合里。这样可以做到优雅退出不是在信号来的瞬间立刻退出而是处理完手头的一批任务后自己主动退出。这种写法比信号处理函数里直接调用exit更平滑。4.4 sigsuspend原子挂起等待信号的标准姿势sigsuspend()是这几个函数里最容易理解错的。它的原型是int sigsuspend(const sigset_t *mask);它做的事情一句话概括把进程的阻塞集合临时替换成mask然后挂起进程等待信号递达。一旦有信号被处理函数返回-1并设置errno为EINTR同时把阻塞集合恢复成调用前的样子。这里要注意的是临时替换不是先解除阻塞再等待这么简单它们是原子操作。为什么要原子因为如果分两步可能出现信号在两个步骤之间到达并被处理了这个极短窗口导致该信号没有被挂起等待到。这在做进程间同步时需要极其小心。比如你想先解除SIGUSR1的阻塞然后挂起等待SIGUSR1如果写成先sigprocmask(SIG_UNBLOCK, ...)再sleep()或pause()这中间信号就会丢掉。sigsuspend设计出来就是解决这个竞态问题的。代码模板大致是sigset_t new_mask, old_mask; sigemptyset(new_mask); sigaddset(new_mask, SIGUSR1); // 第一步设置阻塞集里包含SIGUSR1进入临界区等待前防止遗漏 sigprocmask(SIG_BLOCK, new_mask, old_mask); // 业务检查如果条件不满足就挂起等待 if (ready_flag 0) { sigsuspend(old_mask); // 临时把阻塞集合恢复为old_mask即解除SIGUSR1阻塞并挂起 } // 唤醒后恢复阻塞 sigprocmask(SIG_SETMASK, old_mask, NULL);这里sigsuspend(old_mask)传的是旧阻塞集合也就是把除了SIGUSR1以外的其他信号照旧阻塞只放开SIGUSR1等待它实现精准等待。等待期间如果SIGUSR1到达处理完返回后进程继续往下走。4.5 信号集综合应用守护进程优雅退出方案把上面这些函数组合起来就能写一个守护进程的优雅退出框架。我的做法是这样的用sigaction注册SIGTERM和SIGINT的处理函数处理函数里只设置一个全局的volatile sig_atomic_t stop_flag 1不做任何复杂事情主循环里不阻塞这两个信号正常干活主循环每次检查stop_flag如果为1就退出循环然后做清理工作、释放资源、退出这个方案的好处是信号处理函数足够简单没有安全风险主程序的退出时机自己可控不会出现在malloc过程中被打断然后处理函数里你去调free这种危险场景。5. 实时信号与sigqueue处理更多信息量的选择5.1 标准信号的三大短板排队、信息和精度标准信号1~31号在实战中经常不够用。第一个短板是不排队就像前面说的多次触发会合并成一次处理第二个短板是携带信息少标准信号处理函数只能知道来了哪个信号来源是谁、为什么产生、带着什么数据一概不知第三个短板是精度有限标准定时信号粗糙高精度场景扛不住。为了解决这些问题POSIX引入了实时信号Linux上是SIGRTMIN到SIGRTMAX通常是34~64号。实时信号的行为有保证信号排队不合并每个信号都能带着一个联合体值整数或指针递达顺序按发送顺序保证。这对于你做简单的进程间发送小消息非常可靠。5.2 sigqueue配套sigaction的SA_SIGINFO用法要使用实时信号接收数据必须在sigaction注册时设置SA_SIGINFO并用sa_sigaction回调。看这个例子发送端union sigval value; value.sival_int 666; // 想传递的整数消息 sigqueue(target_pid, SIGRTMIN 1, value);接收端void handler(int sig, siginfo_t *info, void *context) { // info-si_value.sival_int 即为666 // info-si_pid 是发送者的PID // info-si_uid 是发送者用户ID printf(received signal %d, value %d, from pid %d\n, sig, info-si_value.sival_int, info-si_pid); } struct sigaction sa; sa.sa_sigaction handler; sigemptyset(sa.sa_mask); sa.sa_flags SA_SIGINFO; sigaction(SIGRTMIN 1, sa, NULL);这里值得注意的细节很多。第一个SIGRTMIN不是一个固定值不同平台定义不同Linux上并且没有glibc内部占用时通常是34但是你在逻辑里不要写死数字写成SIGRTMINn是标准做法。第二个实时信号也要求发送和接收方属于同一用户或有权限权限检查和kill一样。第三个如果实时信号因为放不下排队数量上限RLIMIT_SIGPENDING发不出去sigqueue会返回EAGAIN这在极端场景里要处理。5.3 实时信号排队验证实验我建议刚上手的同学做个小验证发送端循环100次sigqueue给接收进程发SIGRTMIN1接收端每次收到信号就把计数器加一。结果你会发现100次信号全都会递达一个都不丢。用标准信号做同样测试最终只会收到一次回调这就是排队和不排队的直观差别。我在实际项目里用实时信号做过一个轻量级的热配置刷新机制监控进程收到外部通知时通过sigqueue把新的配置版本号发送给工作进程工作进程在信号处理函数里只把版本号记录下来再设置一个标志位主循环发现标志位后再重新读配置。整个流程不需要引入复杂的消息队列组件轻量可靠。6. 信号处理中的异步安全问题与实用技巧6.1 可重入函数与异步信号安全信号处理函数的红线这是信号处理函数里最重要的一条规则信号处理函数里只能调用异步信号安全的函数。因为这些函数可能会在任意时刻插入到你的主程序执行流的任意位置如果调用的函数本身不是可重入的或不是异步信号安全的轻则数据错乱重则死锁崩溃。什么叫可重入简单说一个函数在执行过程中被中断然后再次进入执行两次执行不会互相干涉结果依然正确。比如strtok这种用静态内部变量的函数就不是可重入的如果信号处理函数里也调用了strtok就会把主程序正在解析的字符串状态打乱。printf在信号处理函数里就是高频踩坑点。printf内部有缓冲区操作不是异步信号安全的。很多人写Demo的时候习惯在信号处理函数里printf调试看起来能用但在高并发、多线程、高压力场景下迟早要出事。POSIX标准明确列出了一批异步信号安全函数包括write、read、open、close、kill、sigaction、_exit等等。你在写信号处理函数时最稳妥的做法是只设置volatile sig_atomic_t全局变量如果必须做更多事情用self-pipe trick自管道技巧绝不在处理函数里调用printf、malloc、free、strtok这类非安全函数补充一个细节errno也是需要注意的。信号处理函数可能改变errno的值如果你主程序正在判断errno返回后可能发现errno被改了。所以信号处理函数里如果调用了会设置errno的函数比如write应该先保存旧的errno退出前恢复。6.2 volatile sig_atomic_t跨信号安全的变量类型我前后两次做信号处理都在变量可见性这个坑上跌过跟头。信号处理函数里修改的标志位主循环里读取不到或者读到了旧值。这里有两层问题第一层编译器优化。如果编译器认为主循环里的某个变量在循环中没有被修改它可能把变量优化到寄存器每次循环不去内存读取。信号处理函数改了内存里的值主循环却还在用寄存器里的旧值。解决办法是给变量加volatile告诉编译器这个变量每次使用都必须从内存里读。第二层多字节访问的原子性。int在32位平台上是4字节如果信号处理函数在主程序读取int的一半时插进来修改这个int主程序可能读到撕裂值。POSIX提供了sig_atomic_t这个类型保证读或写是原子的配合volatile使用static volatile sig_atomic_t stop_flag 0; void stop_handler(int sig) { stop_flag 1; }注意volatile sig_atomic_t只保证单个变量的读写原子性如果你要同时修改多个变量那还需要信号阻塞机制来配合。6.3 自管道技巧在信号处理函数里做大事自管道技巧self-pipe trick是我很推荐的一种模式。它解决的问题是信号处理函数里只能做简单事情这个限制太严了我想在收到信号后执行复杂的业务逻辑怎么办思路是这样在信号处理函数里只往一个管道pipe里写入一个字节用write是异步信号安全的主程序在一个epoll或者select循环里监听这个管道的读端管道可读时说明有信号来过了这时候在主程序里做复杂的处理什么都可以做完全不受异步信号安全的限制。int pipe_fds[2]; pipe(pipe_fds); // 非阻塞设置可以按需加 void generic_handler(int sig) { int save_errno errno; char ch (char)sig; write(pipe_fds[1], ch, 1); // 把信号值当字节写进管道 errno save_errno; }主程序里添加pipe_fds[0]到epoll集合可读事件到达后读出一个字节根据字节值判断是哪个信号然后执行处理逻辑。这个模式我用了好几年稳定可靠。还有一个好处是它可以自然融合到现有的IO多路复用框架中不用打破主循环结构去等待信号。6.4 信号与多线程别在错误的地方处理信号多线程程序里信号的处理和阻塞规则更复杂。三个核心规则第一signal处理函数是进程级的进程里的任意线程收到信号会由某个不阻塞该信号的线程来处理具体是哪个不确定。第二sigprocmask在多线程里不要用要用pthread_sigmask它只设置当前线程的阻塞状态。第三专门等待和处理信号的线程可以长期阻塞一组信号然后用sigwaitinfo或sigtimedwait同步获取信号。这种模式下信号处理完全在常规线程上下文里执行不需要担心异步安全问题代码读起来也流畅。我的建议是多线程程序想要可靠处理信号最好的方式是启动时在main线程里阻塞掉所有感兴趣的信号然后专门创建一个信号处理线程用sigwaitinfo阻塞等待。这样信号处理逻辑是同步的和其他线程之间用互斥锁或队列交互一切都在掌控之中。7. 常见问题与排查技巧实录7.1 信号处理函数里为什么不能用printf和malloc前面已经讲了原理这里再补充一个实际排查案例。我这边的某监控采集程序上线初期信号处理函数里用了printf打日志压测时行为正常跑到第三天突然偶发崩溃。崩溃栈指向的是动态库里的内存分配函数但主流程当时根本没有在做内存操作。后来用gdb带核心转储一查发现崩溃点是在信号处理函数执行过程中printf内部调用了mallocmalloc与主程序正在进行的malloc竞争内部锁导致死锁和崩溃。最终全部改成自管道主循环epoll处理日志输出问题消失。还有一个更隐蔽的坑信号处理函数里调用syslog或log4cplus这类日志库即便是线程安全的库也无法保证异步信号安全。原因很简单这些库的底层用到了锁和内存分配信号随时可能打断正在持锁的代码。7.2 EINTR错误被信号打断的系统调用怎么恢复当你调用read、write、open、select、poll这类阻塞型系统调用时如果进程收到了信号且信号处理函数执行完毕这些系统调用可能会返回-1并且errno被设为EINTR意思是被信号中断了。很多人第一次看到EINTR会愣住这好好的read怎么失败了实际上它不算失败你的文件描述符还是好的只是这次调用被打断了没有读完数据没有超时一切都还可以重新发起。处理方式有两种。第一种简单粗暴在循环里手动处理EINTR。ssize_t n; do { n read(fd, buf, sizeof(buf)); } while (n 0 errno EINTR);第二种是更推荐的方式注册信号时加上SA_RESTART标志让内核自动重启被中断的系统调用。但注意SA_RESTART不是所有系统调用都能自动重启比如poll、select、epoll_wait、sem_wait这些即便设置了SA_RESTART也有部分情况不会重启不同系统行为有差异。所以最稳妥的是对关键系统调用手动封装一个重试EINTR的辅助函数。我个人的经验是网络编程里对所有socket相关的read/write/recv/send都要写循环容错EINTR哪怕注册时用了SA_RESTART因为你在项目里遇到的底层场景未必都支持SA_RESTART。7.3 SIGCHLD和僵户级联如何正确处理子进程退出通知父进程想要知道子进程什么时候结束就要等待SIGCHLD信号。但真实项目里常见的误区是信号来了之后父进程只在回调里嗝屁一声哦子进程结束了却没调用waitpid()结果子进程就一直挂在那儿变成了僵尸进程。正确的做法是在SIGCHLD处理函数里调用waitpid(-1, status, WNOHANG)循环尽量多地回收已经退出的子进程直到返回0或-1。加了WNOHANG是为了避免阻塞因为信号处理函数里绝对不能做阻塞等待。当然也可以用sigaction的SA_NOCLDWAIT标志让内核自动不产生僵尸但这会丧失获取子进程退出状态的能力按需选择。实测一下如果在SIGCHLD回调里用waitpid一定要用while而不是单次if。为什么因为SIGCHLD标准信号不排队可能多个子进程同时退出却只触发一次信号你在回调里只waitpid一次会漏回收僵尸就留下了。这个循环waitpid是很经典的经验也是我自己运维过的一个坑。7.4 信号相关疑难杂症速查表现象可能原因排查思路进程kill不掉信号被阻塞grep SigBlk /proc/pid/status查看阻塞位图信号回调被执行了两次标准信号被合并/注册回调里重复注册在回调里不要重新调用sigactionread莫名其妙返回-1被信号中断errnoEINTR打印errno循环重试或用SA_RESTART信号处理函数崩溃使用了非异步安全函数printf/malloc改用write和sig_atomic_t或自管道sigqueue返回EAGAIN实时信号队列满检查RLIMIT_SIGPENDING消费不及时子进程变僵尸父进程没waitpidSIGCHLD回调里循环waitpid多线程程序的信号乱套用sigprocmask影响全局改用pthread_sigmaskCtrlC没反应SIGINT被阻塞或被某个线程吞掉检查信号掩码用pthread_sigmask查看线程掩码7.5 排查信号问题的两个利器排查信号相关问题我最依赖两个工具。第一个是strace可以直接看到进程收到了哪些信号以及进程调用了哪些和信号相关的系统调用。比如strace -p pid挂上进程后按CtrlC你能在跟踪输出里看到--- SIGINT ---字样就能立刻确认信号有没有到达进程。第二个是/proc/pid/status文件里面有SigBlk、SigIgn、SigCgt三个字段分别显示当前信号阻塞位图、忽略位图和捕获位图。配合sigismember能很快判断某个信号是被阻塞了还是被忽略了。比如你发现进程收不到SIGTERM第一件事就是看SigBlk里是不是有SIGTERM那一项。我遇到过一例线上问题运维反馈进程杀不死kill -TERM没反应kill -KILL也杀不掉最后查出来是有人把进程丢进了某个设置了极端阻塞掩码的启动环境里连SIGKILL的阻塞位都被设置过了理论上SIGKILL不能被阻塞但某些调试工具或奇葩框架会在内核里做特殊处理现实中极其罕见。后来用gdb附加调试修改信号掩码后才恢复。这类情况百分百要从阻塞位图查起。8. 进阶组合技信号在其他系统中的跨界运用这部分内容虽然不是常规教材里的重点但在真实项目里价值很高。很多开发者以为信号只是进程控制的工具其实它还可以作为事件通知机制和跨平台协作的基础设施。第一个组合是信号 自管道 epoll这个前面讲过了。把信号事件统一转成IO事件配合epoll做事件循环可以让一个程序同时处理高并发的网络IO和系统信号这套模式在网关、代理、服务框架中非常常见。第二个组合是定时器信号SIGALRMsetitimer或timer_create。高精度定时器的实现往往是通过timer_create配合实时信号来完成的。理解信号是理解定时器后续知识的基础。第三个组合是信号在跨平台系统Windows/Linux/macOS上的抽象层。Windows没有POSIX信号体系很多跨平台库网络库、游戏引擎在Windows上模拟了一套信号语义但两者行为差异大。如果你的项目需要跨平台建议尽早把信号抽象成统一的退出通知接口和异步通知接口而不是直接暴露kill和sigaction。这些扩展用法恰恰也是Linux服务端工程师从会写业务逻辑进阶到能设计可靠系统的必修课中比较关键的一环。9. 项目总结与个人经验心得写到这里Linux信号相关函数的主干内容基本覆盖完了。要是让我用一句话总结这套体系那就是信号是机制函数是接口而真正体现水平的是你在何种场景下、用何种组合、规避何种风险来处理异步事件。我自己的信号处理经验心得大概有这么几条值得你参考。第一新项目请统一用sigaction。把signal这个函数当作历史遗留物看待不管你是在什么平台上开发。理由前面已经详细说了可移植性差、语义不统一、缺少精细控制。第二信号处理函数只做记录和通知。坚持用volatile sig_atomic_t记录状态用自管道通知主循环遇到复杂逻辑一律回到主循环去处理。这条原则贯穿了我这些年写过的大部分服务端程序大大减少了信号处理函数崩溃这类问题的出现概率。第三把信号相关的代码集中封装。把信号注册、信号发送、信号处理统一封装成一套接口对上层业务只暴露监听退出事件监听配置刷新事件发送通知这样的高层接口。这样到了后期排查问题时你只需要看一个模块不用在项目里到处翻信号相关的代码。第四线上排查信号问题不要慌。先看/proc/pid/status里的SigBlk和SigCgt再用strace跟踪确认信号是否到达最后看处理函数里是否干了不该干的事。这套排查顺序几乎能覆盖百分之九十的信号疑难案例。信号相关函数学习曲线不陡峭但涉及的概念密度很大。建议你上手写几个小实验验证一下一个阻塞-挂起的小程序一个实时信号排队的实验一个自管道处理信号的程序。跑完这三个实验你对Linux信号这套体系的理解会非常通透。如果你在实际项目中遇到具体的信号问题欢迎带着调用栈和排查记录来交流。信号这门功夫真的是用到的时候才觉得哎当初应该学深一点。现在把基础打扎实后面做高并发、做守护进程、做优雅退出都会省很多事。