
做Linux下的服务端开发这一行几乎没有人能躲开信号处理。你可能写过kill -9杀进程也可能被SIGSEGV段错误折磨到深夜但真正把signal、sigaction、sigprocmask、sigsuspend这些核心函数挨个吃透的人却不算多。原因也不难理解信号机制不是“学会了就能天天用”的东西很多时候程序跑得好好的你根本不关心它可一旦线上出现“进程莫名退出”“子进程变僵尸”“信号处理函数里调 printf 导致死锁”这类问题再回头翻手册就有点来不及了。这篇文章就是我整理的 Linux 信号核心函数速查表把发送、捕获、阻塞、等待、与多路复用结合这几个最关键的环节拆开讲清楚。重点放在函数语义、参数区别、以及新手最容易踩的坑上适合正在学系统编程的同学也适合写过一段时间 C/C 但总感觉信号这块不成体系的开发者。先给一张总览表后面逐个展开。函数头文件核心作用kill/raisesignal.h向进程/线程发送信号signal/sigactionsignal.h注册信号处理函数sigemptyset/sigaddsetsignal.h操作信号集位图sigprocmasksignal.h阻塞/解除阻塞信号sigpendingsignal.h查询未决信号sigsuspendsignal.h原子替换掩码并等待信号sigwaitsignal.h同步等待信号的到来signalfdsys/signalfd.h把信号变成文件描述符timer_createsignal.h/time.h创建定时器到期发信号下面进入正题。1. 信号机制速览为什么不能只靠 kill 和 sleep1.1 信号是什么内核到底做了什么信号本质上是一种软件中断。进程收到信号内核就把这个事件记到进程的pending位图上等进程从内核态返回用户态之前检查有没有需要处理的信号。有就触发对应的处理动作忽略、捕获调用用户函数、执行默认行为或者终止进程。这个机制有几个特点值得注意。第一信号是异步的。它不像函数调用那样由你主动发起而是随时可能发生。所以信号处理函数不能假设“当前执行到哪一行”它可能发生在malloc内部、可能在printf途中、也可能在一条普通赋值语句中间。这个特性直接决定了信号处理函数能调用什么、不能调用什么。第二信号有“标准信号”和“实时信号”的区别。1~31 号是标准信号不排队多个相同的信号合并成一个34~64 号是实时信号支持排队和携带数据。绝大多数业务场景用的都是标准信号但你要知道 SIGUSR1、SIGUSR2 这类信号如果短时间内到多个可能只触发一次别拿它们当精确计数器。第三信号的默认行为并不相同。整理成表格方便记忆信号默认动作典型触发场景SIGINT终止进程CtrlC终端中断SIGQUIT终止并产生核心转储Ctrl\SIGKILL强制终止不可捕获kill -9SIGTERM终止进程可捕获kill默认发送SIGSEGV终止并核心转储非法内存访问SIGCHLD忽略子进程停止或结束SIGPIPE终止进程写已关闭的管道/socketSIGALRM终止进程alarm()定时器到期SIGUSR1/SIGUSR2终止进程用户自定义事件SIGSTOP/SIGCONT停止 / 继续作业控制1.2 为什么“速查”比“精通”更现实很多人刚接触信号时有个误区以为signal函数够用了其他函数以后再看。结果真到项目里遇到“屏蔽 SIGINT 再恢复”“安全地等待子进程退出”“在 epoll 里监听信号”这些场景才发现signal一个都解决不了。信号这块内容的特点是“知道有哪些函数、每个函数适合什么场景”远比“背诵函数签名”重要。所以我把速查表拆成三个层次发送与捕获、信号集与阻塞、同步等待与多路复用。每个层次解决一类问题你手上的需求属于哪一类直接跳过去看对应的函数就行。2. 发送与捕获kill、raise、signal、sigaction 到底怎么选2.1 kill 与 raise发送信号的两个入口先说发送。kill的原型是#include sys/types.h #include signal.h int kill(pid_t pid, int sig);pid参数有几个特殊取值很多人容易忽略pid 0发给指定进程。pid 0发给同进程组的所有进程。写服务端程序时如果不小心对调用方的进程组发起信号可能把同一组里的多个进程一并干掉慎用。pid -1发给有权限发送的所有进程不包括init和自己实际上会排除一些系统进程但效果仍然很“猛”日常代码几乎不该用。pid -1发给进程组abs(pid)。返回值方面成功返回 0失败返回 -1常见错误码有EINVAL信号编号非法、EPERM无权限、ESRCH进程不存在。这里有个实用技巧用kill(pid, 0)可以检测进程是否存在。注意这个 0 不是“发空信号”的意思而是“不发送实际信号只做权限和存在性检查”。如果返回 0 说明进程存在且你有权限返回 -1 且errno ESRCH说明进程已经没了。但测完到真正操作之间进程可能退出存在竞态不能把这个当成绝对可靠的判断。raise就简单了#include signal.h int raise(int sig);等价于在自己的进程里执行kill(getpid(), sig)但区别在于raise发送成功后信号可能被当前线程直接处理而不是放到进程级 pending 里。实际开发中如果用不上“向其他进程发信号”的能力raise更安全、更直观。2.2 signal经典但坑多新代码不推荐signal的签名是typedef void (*sighandler_t)(int); sighandler_t signal(int signum, sighandler_t handler);用法一看就懂给某个信号挂一个处理函数。但坑就在这里——不同操作系统对signal的语义不统一。System V 下信号处理完后恢复默认行为如果你想接着处理就需要在回调里再次调用signal中间存在一个时间窗信号来了可能直接按默认行为终止进程。BSD 下又是另一种语义自动保持已设置的处理器。可移植性极差。更麻烦的是历史上signal不支持设置SA_RESTART。很多系统调用如read、write、accept、poll被信号打断后返回EINTR错误。如果你的进程对EINTR没做处理业务逻辑就会莫名其妙地中断做了处理又得处处加判断代码很难看。所以我个人的观点是新代码统一用sigaction。signal可以留着维护老项目时看写新功能不要再用。2.3 sigaction完整参数与每个 flag 的真实含义sigaction是 POSIX 提供的能力完整版#include signal.h int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact); 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_handler为SIG_IGN表示忽略设为SIG_DFL恢复默认行为。如果你想拿到信号的具体来源发送者 pid、信号值、是否来自定时器等就要用sa_sigaction并且把sa_flags里加上SA_SIGINFO。sa_mask是非常关键的一个字段。它表示“当这个信号的处理函数正在执行时额外阻塞哪些信号”。注意正在处理的信号本身通常也会被阻塞防止嵌套触发。举个例子你捕获SIGINT在处理函数里又收到了SIGTERM如果没设置sa_mask包含SIGTERM那么SIGTERM可以立刻打断SIGINT处理器。要避免这种情况就在信号处理函数入口处把SIGTERM加入sa_mask内核会在进入处理器前临时加锁等处理器返回后再恢复。实际的编程中常用struct sigaction sa; memset(sa, 0, sizeof(sa)); sa.sa_handler handle_sigint; sigemptyset(sa.sa_mask); sa.sa_flags SA_RESTART; // 重要自动重启被中断的系统调用 sigaction(SIGINT, sa, NULL);这里SA_RESTART一定要解释清楚。它不是“所有系统调用都自动重启”而是针对“受支持”的调用比如read、write、accept、ioctl这些内核在信号处理返回后自动重试一次。poll、select、epoll_wait这类有超时语义的调用不一定会自动重启尤其是epoll_wait信号打断就返回EINTR。很多人在生产环境里发现“epoll 明明注册了事件代码却偶尔卡一下”查到最后就是没对EINTR做处理。把sa_flags里常见的几个拎出来flag作用SA_RESTART系统调用被打断后自动重启部分调用SA_SIGINFO使用sa_sigaction回调附带siginfo_tSA_NOCLDWAIT仅用于SIGCHLD子进程退出后自动回收不产生僵尸SA_NODEFER处理函数执行期间不阻塞当前信号允许递归触发SA_RESETHAND信号处理完后恢复默认行为相当于 System V 的signaloldact参数也值得一说。传入非空oldact内核会把“旧的信号配置”完整保存。有些项目会用“先捕获旧处理器最后再恢复”的方式实现优雅插件化这个参数就是干这个用的。2.4 实例用 sigaction 做一个可优雅退出的进程我写一个最小的示例演示捕获SIGINT和SIGTERM并做清理#include stdio.h #include string.h #include signal.h #include unistd.h #include stdlib.h static volatile sig_atomic_t g_running 1; static void handle_term(int sig) { // 只置标志位绝不在信号处理里做复杂操作 g_running 0; } int main(void) { struct sigaction sa; memset(sa, 0, sizeof(sa)); sa.sa_handler handle_term; sigemptyset(sa.sa_mask); sa.sa_flags SA_RESTART; if (sigaction(SIGINT, sa, NULL) 0 || sigaction(SIGTERM, sa, NULL) 0) { perror(sigaction); exit(EXIT_FAILURE); } while (g_running) { printf(working...\n); sleep(1); } printf(cleaned, bye\n); return 0; }这里g_running要用volatile sig_atomic_t因为它在信号处理函数里被修改、在主循环里被读取两者之间没有同步机制。sig_atomic_t是 ISO C 规定的一个原子整数类型加上volatile是为了防止编译器把变量优化进寄存器后信号处理函数改了内存里的值主循环却读不到新值。3. 信号集与阻塞sigprocmask / sigpending / sigsuspend 的原子操作逻辑3.1 信号集一张位图三个基础函数信号集算得上信号处理的基础设施。它的本质就是一张位图每一位代表一个信号是否在集合里。标准流程是sigset_t set; sigemptyset(set); // 先清空才能保证没有脏数据 sigaddset(set, SIGINT); sigaddset(set, SIGTERM);为什么一定要先sigemptyset因为sigset_t在不同平台上实现不一样把栈上未初始化的局部变量直接传给sigaddset可能残留其他 bit。这个顺序不能省。对应的还有sigdelset从集合中移除一个信号sigismember判断信号是否在集合中。这三个函数都是纯内存操作不需要系统调用。注意区分“阻塞”和“忽略”的区别。阻塞一个信号意味着内核把信号记录为 pending但不会执行处理函数忽略一个信号意味着信号到达后直接扔掉。前者可以用sigprocmask撤销阻塞后继续处理这个信号后者扔掉就真没了。这两者经常混用实际语义差别很大。3.2 sigprocmask进程级阻塞与解除sigprocmask的原型#include signal.h int sigprocmask(int how, const sigset_t *set, sigset_t *oldset);how有三种取值SIG_BLOCK把set里的信号加入当前阻塞集合。SIG_UNBLOCK把set里的信号从当前阻塞集合中移除。SIG_SETMASK直接用set替换当前阻塞集合。oldset用来取出调用之前的旧掩码以便后边恢复。常见写法sigset_t block_set, old_set; sigemptyset(block_set); sigaddset(block_set, SIGINT); sigprocmask(SIG_BLOCK, block_set, old_set); // 临界区不希望被 SIGINT 打断 sigprocmask(SIG_SETMASK, old_set, NULL); // 恢复有个开发上的小细节在多线程进程里sigprocmask的行为是未定义的实际上 POSIX 规定它是进程级的但 Linux 内核将“阻塞掩码”保存在每个线程自己的 TCB 里。pthread_sigmask才是线程级别的接口。所以在多线程程序里不要用sigprocmask要用#include pthread.h int pthread_sigmask(int how, const sigset_t *set, sigset_t *oldset);参数一模一样。这也是一个非常容易踩坑的点你在一个线程里想屏蔽某个信号结果用sigprocmask只影响了调用线程还是影响了进程行为跟平台相关在 glibc 下实际影响调用线程但手册并不推荐这样用。与其赌平台实现不如直接写成pthread_sigmask。3.3 sigpending检查未决信号如果某个信号被阻塞了内核不会立刻执行它的处理动作而是放到 pending 集合里。sigpending就是用来查这个集合的int sigpending(sigset_t *set);成功返回 0set里得到所有“被阻塞且尚未处理”的信号。这个函数常见的用途是实现“信号是否曾经到达过”的检查。举例你屏蔽了SIGUSR1程序跑到某个里程碑点想确认有没有人来过消息就可以查 pending。不过要提醒一点sigpending查到的信号不代表它一直没有被处理过。如果一个信号到达后已经被处理了它就不会留在 pending 里。所以它只反映“当前积压着的、来不及处理的信号”不是“历史记录”。3.4 sigsuspend等待信号时的原子操作sigsuspend是我认为理解门槛最高的一个函数。它的原型很简单#include signal.h int sigsuspend(const sigset_t *mask);执行过程分三步先把当前阻塞集合替换成mask然后挂起进程等待信号最后收到信号并处理后恢复原来的阻塞集合并返回 -1同时errno设为EINTR。这个函数解决了一个经典的竞态问题你要“先解除某个信号的阻塞再等待它发生”如果分两步做中间可能错失信号。比如sigprocmask(SIG_UNBLOCK, set, NULL); // 第一步 pause(); // 第二步如果信号在第一步和第二步之间到达且你没在阻塞它信号就被正常处理并把 pending 状态置空然后pause()继续挂起永远等不到后续信号。对于超时重试、事件同步这类逻辑这是致命的。而sigsuspend把“替换掩码”和“等待信号”合并成一个原子操作信号到了才能从挂起中返回不会丢。实际用的时候要注意因为sigsuspend总会返回 -1所以正常写法是对errno EINTR做特殊处理不要当成失败打印错误。sigset_t mask; sigemptyset(mask); // 假设之前阻塞了 SIGUSR1现在只等待它 sigprocmask(SIG_SETMASK, mask, NULL); // 上面这样是错的有竞态 sigsuspend(mask); // 正确 if (errno EINTR) { // 说明确实被信号打断了继续业务 }4. 线程里的信号sigwait、signalfd 与 self-pipe 的现代做法4.1 信号处理函数能调用哪些函数先说清楚“异步信号安全”讨论线程里的信号必须先回答一个基础问题信号处理函数里到底能做什么答案是只能调用 async-signal-safe 函数。这个概念比“线程安全”更严格。线程安全只保证多线程并发执行时不产生数据竞争但信号处理函数可能中断主线程的任意一条指令哪怕主线程正持着锁。如果信号处理函数里调用了一个同样需要拿锁的函数而主线程恰好在锁里就死锁了。所以最稳妥的做法是信号处理函数里只做两件事——设置一个volatile sig_atomic_t标志位或向self-pipe写入一个字节。其他事情全部放到主循环里做。这就是为什么下面的sigwait和signalfd路径逐渐流行它们把“信号处理”从异步上下文搬到普通上下文让你可以正常地调用malloc、printf、加锁解锁。4.2 sigwait阻塞等待信号而非打断主流程sigwait的用法是把信号集中到一个地方同步处理#include signal.h int sigwait(const sigset_t *set, int *sig);调用线程会阻塞直到set里的某个信号到达。注意它需要配合pthread_sigmask先把这些信号阻塞掉否则信号可能会走默认处理逻辑而不是被sigwait捕获。严格说不同平台的行为不完全一致但正确姿势就是“先阻塞再 wait”。这个模式特别适合“信号处理逻辑比较重”的场景。比如你收到SIGTERM要做服务摘流量、持久化状态、等业务线程收尾这些操作可能在信号处理函数里做会导致各种诡异问题放到sigwait里就成了一次普通函数调用可以放心用锁、用条件变量、用日志库。还有一个隐藏收益sigwait返回值里能知道具体收到了哪个信号。这样在一个线程里可以统一处理多个不同的信号不用为每个信号单独写一个处理函数。一个注意点给sigwait设置信号集时同样的规则也适用。比如set里的信号必须先用pthread_sigmask阻塞不然信号可能会被其他线程按默认行为处理掉。原因在于“进程发送信号”时选哪个线程来处理规则比较复杂但总的来说被阻塞的信号更有可能被sigwait的线程拿到。4.3 signalfd把信号伪装成普通 IO 事件sigwait适合“专门起一个线程等信号”的模型但有些服务本来就基于 epoll 事件循环再额外开线程会让整个架构变重。这种情况下signalfd就很有价值#include sys/signalfd.h int signalfd(int fd, const sigset_t *mask, int flags);第一次调用时fd传-1内核返回一个新的文件描述符后续read这个 fd 就能读到signalfd_siginfo结构体里面包含信号的编号、发送者 pid、时间戳等信息。常见组合是先用sigprocmask或pthread_sigmask阻塞感兴趣的信号然后创建signalfd再把 fd 丢给epoll_wait监听可读事件。收到信号后 epoll 会告诉你该 fd 可读你正常read出来处理。这样信号处理就彻底融入了 IO 多路复用的流程里处理函数里可以自由使用堆内存、锁、日志等资源。要注意的是创建signalfd后信号并不是自动被“消费”掉你必须认真 read 直到返回EAGAIN否则会反复触发可读事件导致 busy loop。4.4 self-pipe trick老牌方案背后的原理在signalfd出现之前经典的self-pipe trick是在信号处理函数里write一个字节到管道主循环read这个管道。那样信号处理函数里仍然有一个系统调用但write本身是 async-signal-safe 的可以安全使用。这个方案今天并没有完全过时。如果你用的是比较老的内核或者代码需要在 BSD、macOS 等多平台编译signalfd可能不可用self-pipe依然是最稳妥的思路。它唯一的缺点是全局不能有多个实例否则管道会被多处写、随处读数据容易错配。实现时要给每个事件循环单独分配管道并且注意管道缓冲区可能写满导致write阻塞——信号处理函数里阻塞是很严重的问题。所以写的时候要做非阻塞处理或者在信号处理函数里只用write的一个字节基本不会撑满。5. 实战整合定时信号、子进程回收与 epoll 融合5.1 timer_create SIGEV_SIGNAL定时器的信号化很多人知道alarm()可以发SIGALRM但alarm只有秒级精度而且同一时刻只能有一个。如果要做高精度、可区分多个定时器的场景要用 POSIX 定时器#include signal.h #include time.h int timer_create(clockid_t clockid, struct sigevent *sevp, timer_t *timerid);sevp里可以设置sigev_notify SIGEV_SIGNAL这样定时器到期时会给进程发指定信号。更细的配置还能通过sigev_value.sival_ptr带一个指针在siginfo_t的si_value里拿到。不过要强调即使使用了SIGEV_THREAD让内核起线程执行回调回调函数里依然要小心锁的问题因为定时器线程和你自己的业务线程并发锁的粒度如果控制不好照样会死锁。真正常见组合是timer_createsignalfd定时器到期发信号信号进入 signalfdepoll 读取后执行对应回调。这样定时事件和 IO 事件统一进事件循环代码结构很干净。5.2 SIGCHLD 与 waitpid别让子进程变僵尸开发者刚开始写多进程服务时最容易遗漏的是SIGCHLD。子进程退出后父进程没调用waitpid子进程就变成僵尸。僵尸进程不占 CPU但占pid积累多了fork会失败。捕获SIGCHLD时不要只waitpid一次就完事。因为同一时刻可能多个子进程一起退出SIGCHLD是一个标准信号可能合并成一次触发。正确做法是在处理函数里循环while (waitpid(-1, status, WNOHANG) 0) ;这里WNOHANG是必要的否则进程可能阻塞在waitpid上。不过很多人也发现了在信号处理函数里循环waitpid没问题因为waitpid是 async-signal-safe 函数如果你用sigwait或signalfd集中处理那更轻松可以在主业务线程里随便写逻辑。另一种更省心的方案是sigaction的SA_NOCLDWAITflag。设置了它之后子进程退出自动被内核回收父进程无需调用waitpid。代价是你再也没法获取子进程的退出码。如果只是“不想留僵尸”这个 flag 很省事如果需要拿到退出状态做处理就老老实实写SIGCHLD处理。5.3 把信号接进 epoll 事件循环一个值得直接抄的骨架这里给出一个可直接运行的骨架把signalfd、epoll、timer_create整合到一起。为了简洁省略了错误检查但实际代码里这段不能省。#include sys/epoll.h #include sys/signalfd.h #include signal.h #include stdio.h #include unistd.h #include string.h #include stdint.h #include stdlib.h static int make_signalfd(void) { sigset_t mask; sigemptyset(mask); sigaddset(mask, SIGINT); sigaddset(mask, SIGTERM); sigaddset(mask, SIGCHLD); sigprocmask(SIG_BLOCK, mask, NULL); return signalfd(-1, mask, SFD_NONBLOCK); } int main(void) { int sfd make_signalfd(); int epfd epoll_create1(0); struct epoll_event ev; ev.events EPOLLIN; ev.data.fd sfd; epoll_ctl(epfd, EPOLL_CTL_ADD, sfd, ev); for (;;) { struct epoll_event events[8]; int n epoll_wait(epfd, events, 8, -1); if (n 0 errno EINTR) continue; for (int i 0; i n; i) { if (events[i].data.fd sfd) { struct signalfd_siginfo fsi; ssize_t r; while ((r read(sfd, fsi, sizeof(fsi))) 0) { if (fsi.ssi_signo SIGINT || fsi.ssi_signo SIGTERM) { printf(got exit signal\n); return 0; } else if (fsi.ssi_signo SIGCHLD) { while (waitpid(-1, NULL, WNOHANG) 0) ; } } } } } return 0; }注意这段代码用了SFD_NONBLOCK读取时只要还有数据就一直读直到返回 -1 且errno EAGAIN。这样才能保证 epoll 不会因为事件没消费完而频繁触发可读事件。另外signalfd_siginfo里的ssi_signo字段是信号编号读取时一定要检查read返回的长度是否等于sizeof(fsi)如果短了说明数据结构版本不匹配就不能继续解析。5.4 高频信号下的性能与正确性权衡信号机制本身并不慢慢的是“处理动作”。如果你在信号处理函数里做过多操作每次信号到来都会打断主线程的指令流水线轻则缓存污染重则引入死锁。高频场景我总结了几条经验只在信号处理函数里置标志位或写self-pipe真正的业务逻辑放主循环。使用signalfd后信号就从异步上下文解脱出来可以放心批量读取、批量处理。如果信号量特别大比如每秒几万个SIGUSR1优先考虑别的 IPC 机制比如消息队列、socketpair。标准信号本身可能合并、可能丢指望它不丢本来就是错的方向。实时信号虽然排队但排队有上限超过队列长度照样丢信号高频场景不可依赖它做可靠传输。6. 高频问题与现场排查方法6.1 段错误信号把问题掩盖了程序崩溃时很多时候会显示SIGSEGV但如果你在程序里注册过SIGSEGV的处理器就可能导致正常的段错误没能触发 core dump反而在信号处理函数里死循环或二次崩溃。排查问题时第一件事就是看ulimit -c和/proc/sys/kernel/core_pattern确认 core dump 是否打开。如果项目里有对SIGSEGV做清理的逻辑调试时可以先临时注释掉方便拿到原始崩溃现场。6.2 信号处理函数卡死主流程我曾碰到一个特诡异的现象重启服务时偶尔能正常退出偶尔直接 hang 住。后来定位到是SIGTERM的处理函数里调用了free而主线程当时正在malloc内部两个线程同时操作堆管理的锁直接死锁。这种现象现场非常难查因为崩溃时的调用栈可能完全看不出“信号处理函数在等待锁”。排查建议是在信号处理函数里绝对不要调用malloc、printf、syslog这类常规函数如果确实需要做这些事就把信号处理挪到sigwait或signalfd里。6.3 EINTR 被忽略的隐患系统调用被信号打断返回EINTR很多新手的代码不检查这个错误直接往下走结果就是明明accept没有新连接却返回了一个无效 fd或者read明明没数据却被当成读到 0 字节处理导致业务提前结束。排查方法其实很简单在所有可能返回EINTR的系统调用处对errno做一次判断必要时重新调用。用sigaction加SA_RESTART能覆盖一部分调用但epoll_wait、poll这类超时语义明确的调用大概率还是要手动处理。6.4 子进程退出却没有收到 SIGCHLD如果父进程里已经用sigaction设置了SIGCHLD为SIG_IGN或者设置了SA_NOCLDWAIT那么内核会直接回收子进程父进程自然收不到信号。另一个情况是在信号处理函数里waitpid只执行了一次而多个子进程同时退出导致剩余子进程仍然留在僵尸状态。处理办法就是前面说的循环回收。6.5 快速定位信号的现场技巧调试信号问题我最常用的三件套kill -l查看当前系统支持的信号列表。gdb里用handle SIGUSR1 nostop noprint控制信号是否让程序停住避免调试时信号干扰。strace -e signal查看进程实际收到的信号和系统调用之间的时序很多“信号太早到达”“信号没送达”的问题能一眼看出来。/proc 文件系统里也藏着不少信息/proc/pid/status里的SigBlk、SigIgn、SigCgt分别是阻塞、忽略、捕获的信号位图。/proc/pid/stat里的信号相关字段可以辅助判断。排查时先用这些数据确认“信号到底到没到进程”再确认是不是被阻塞、被忽略就能避免在错误方向浪费时间。6.6 信号相关错误码速查错误码含义常见场景EINTR系统调用被信号打断read、write、epoll_wait返回 -1EINVAL信号编号非法 / 参数有误sigaction传入无效sigEPERM无权限发送信号对不属于自己的进程调用killESRCH目标进程不存在kill(pid, 0)探测失败回头看信号其实不是一个“难”的知识点但它的坑在于细节太多、触发时机不固定平时不显山露水出问题就是线上事故。如果把sigaction、sigprocmask、sigsuspend、sigwait、signalfd这几个函数用熟再把“信号处理函数要短、要原子、不碰锁”的原则刻在脑子里大部分日常开发里的信号问题都能在五分钟内想明白。我个人在做服务端框架时最省心的组合就是“信号全部阻塞 signalfd epoll”再配合sigaction处理个别无法用 signalfd 覆盖的边缘情况。这套方案既绕过了异步信号安全函数的长长列表又让信号处理逻辑能按正常业务代码的方式去写调试时还能直接打日志。建议你也找一个小工具项目练一练比如写一个支持优雅退出的定时任务服务把signalfd和timer_create接进同一个事件循环。跑通一次比死记十篇文档都管用。