在Linux下写过多进程或者服务类程序的人,大概率都碰过这种场景:一个后台进程莫名消失,或者某个定时任务不准时,又或者向一个进程发了“终止信号”它却无动于衷。这些问题的根源,往往都落在信号机制那几个核心函数上——kill、raise、abort、alarm、setitimer。很多人对这些函数的理解停留在“kill就是杀进程、alarm就是定时器”的层面,但真到排查问题时,又会因为不知道它们的语义细节而卡住。
这篇文章不打算做什么“从入门到精通”的宏观综述,而是直接聚焦这五个函数,把它们的原型、行为边界、使用场景和常见坑点逐一拆开讲透。我会先从信号机制本身说起,再逐个函数实战解析,最后分享一些我在实际项目里踩过的坑和被信号坑过的排查记录。如果你正在做Linux服务端开发、嵌入式开发,或者只是想在C/C++层面对信号有个更扎实的认识,这篇文章应该能帮你省下不少走弯路的时间。
1. 信号机制初识:进程之间最原始的沟通方式
1.1 信号到底是什么
进程间的通信方式有很多,管道、消息队列、共享内存、Socket,各有各的用途。但信号这东西和它们都不太一样,它更像一个“门铃”——门铃响了,只是通知你“有人来了”,至于来的是谁、你要不要开门、开门之后说什么,那是另外一回事。
信号本质上是一种软件中断,也是Unix/Linux系统里最古老的异步事件通知机制。当某个事件发生时,内核可以主动向进程发送信号,进程收到信号后会暂停当前正在执行的指令流,转而去执行对应信号的处理逻辑,处理完之后再回到原来中断的地方继续运行。这个“中断”的过程完全是异步的,进程根本不知道信号什么时候会来,所以信号也常被用来实现超时控制、心跳检测、进程退出通知等场景。
一个常见的例子是你在终端里按Ctrl+C,内核会向前台进程组发送SIGINT信号,默认动作就是把进程杀掉。但如果你在代码里捕获了SIGINT,就可以在进程退出之前做一些清理工作。类似的情况还包括Ctrl+Z发送SIGTSTP、非法内存访问触发SIGSEGV、除零触发SIGFPE等。
1.2 常见信号速查
信号虽然数量不少,但我们日常工作里高频接触的其实就那么十几个。下面我整理了一张速查表,方便你随时查阅:
| 信号 | 编号 | 默认动作 | 常见触发方式 |
|---|---|---|---|
| SIGHUP | 1 | 终止进程 | 终端挂断或控制终端关闭 |
| SIGINT | 2 | 终止进程 | 键盘Ctrl+C |
| SIGQUIT | 3 | 终止进程并核心转储 | 键盘Ctrl+\ |
| SIGKILL | 9 | 强制终止进程 | kill -9 |
| SIGSEGV | 11 | 终止进程并核心转储 | 非法内存访问 |
| SIGPIPE | 13 | 终止进程 | 向无读端的管道写数据 |
| SIGALRM | 14 | 终止进程 | alarm/setitimer超时 |
| SIGTERM | 15 | 终止进程 | kill命令默认发送 |
| SIGCHLD | 17 | 忽略 | 子进程退出或暂停 |
| SIGABRT | 6 | 终止进程并核心转储 | abort函数调用 |
需要多说一句的是,SIGKILL和SIGSTOP这两个信号很特殊,它们不能被捕获、不能被阻塞、也不能被忽略。换句话说,无论你在程序里怎么折腾,进程收到SIGKILL都只能被强制终止,这是内核为管理员保留的最后一招。这个特性既是优点也是缺点——优点是系统管理员总有办法干死一个失控进程,缺点是你没办法在SIGKILL到来时做任何资源清理。
1.3 信号的三种处理方式
进程收到信号后,实际处理方式有三种:系统默认动作、忽略信号、或执行自定义的信号处理函数。在代码里可以通过signal或sigaction函数来设置。
用signal函数时,标准写法是这样的:
#include <signal.h> void my_handler(int sig) { // 收到信号后要做的事情 } signal(SIGINT, my_handler); // 设置自定义处理 signal(SIGINT, SIG_IGN); // 忽略该信号 signal(SIGINT, SIG_DFL); // 恢复默认行为这里有一个经常踩坑的点:signal函数在部分Unix系统上,信号处理函数执行完毕后,该信号的处置方式会被重置回默认值,也就是“一次性”行为。为了保证可靠行为,现代Linux开发中更推荐用sigaction函数来注册信号处理函数,因为它可以显式指定是否重置处理方式、是否阻塞其他信号等。下面是一个sigaction的基本示例:
#include <signal.h> struct sigaction sa; sa.sa_handler = my_handler; sigemptyset(&sa.sa_mask); // 不屏蔽其他信号 sa.sa_flags = 0; // 不设置额外标志位 sigaction(SIGINT, &sa, NULL);从可移植性和稳定性角度考虑,我在实际项目中基本都是用sigaction替代signal,除非只是临时写个演示小程序。理解了信号的这些基础属性,后面再看kill、raise、abort、alarm、setitimer这些函数时,你会觉得非常顺理成章。
2. 一发入魂:kill、raise、abort三兄弟详解
2.1 kill:不止是“杀进程”那么简单
很多人一看函数名就叫它“杀进程函数”,但其实kill在Linux里是个通用信号发送函数,它只是历史上从“终止进程”的场景发展而来,名字沿用了老传统。你完全可以用kill把SIGUSR1、SIGTERM、SIGALRM等任意信号发送给指定进程。
kill函数的原型是:
#include <sys/types.h> #include <signal.h> int kill(pid_t pid, int sig);这个函数看上去简单,但pid的语义却有一堆罗嗦的细节,这也是大部分人用出错的地方。整理如下:
- pid > 0:信号发送给进程ID等于pid的那个进程。这是最常用的用法,比如kill(12345, SIGTERM)就是给PID为12345的进程发终止信号。
- pid == 0:信号发送给当前调用进程所在进程组的所有进程。比如你在一个程序里调kill(0, SIGUSR1),该程序自己所在进程组里的所有进程都会收到SIGUSR1。
- pid == -1:信号发送给调用进程有权限发送的所有进程,系统进程和内核进程除外。这个用法要格外小心,搞不好会把别的服务也干掉。
- pid < -1:信号发送给进程组ID为|pid|的所有进程。比如kill(-1000, SIGTERM)就是给进程组ID为1000的所有进程发信号。
返回值方面,成功返回0,失败返回-1并设置errno。常见的errno有EINVAL(sig参数不合法)、EPERM(没有权限向目标进程发送信号)、ESRCH(目标进程不存在)。需要注意的是,发送信号和“目标进程是否还活着”是两个概念,如果目标进程已经变成僵尸进程,kill调用仍然可能成功,因为僵尸进程虽然已死但它的进程表项还在,只是无法再处理信号而已。
下面是一个实用的例子:假设我们要向某个正在运行的守护进程发送SIGTERM,让它优雅退出,而不是直接用SIGKILL强杀。
#include <stdio.h> #include <signal.h> #include <sys/types.h> #include <unistd.h> #include <errno.h> #include <string.h> int stop_process(pid_t target_pid) { if (target_pid <= 0) { fprintf(stderr, "非法PID: %d\n", target_pid); return -1; } if (kill(target_pid, SIGTERM) == -1) { fprintf(stderr, "发送SIGTERM失败: %s\n", strerror(errno)); return -1; } return 0; }每次调用kill之前,尽可能明确目标进程的PID是否合法,发送什么信号。如果发送的是SIGKILL,目标进程没有机会做任何清理,这应该是你最后的选择。
2.2 raise:给自己补一刀
raise函数和kill有点“亲密关系”,它是用来向当前进程自身发送信号的。原型如下:
#include <signal.h> int raise(int sig);在单线程环境下,raise(sig)其实等价于kill(getpid(), sig)。之所以说“单线程环境”,是因为多线程程序里raise的行为会有差异。raise向调用线程发送信号,而kill(getpid(), sig)在有些实现下可能会把信号发给进程内的某个其他线程,这在线程不安全的场景里会带来非常隐蔽的问题。
raise的经典使用场景有三个:一是进程主动向自己发送自定义信号,比如业务逻辑里需要触发某个信号处理流程;二是在检测到内存越界、数据异常等不可恢复错误时,先给自己发一个SIGABRT,让程序带着核心转储信息退出,方便事后分析;三是配合信号处理函数模拟某些事件,比如测试信号处理逻辑是否正常。
下面是一个简单的raise示例:
#include <stdio.h> #include <signal.h> #include <unistd.h> void sigusr1_handler(int sig) { printf("收到SIGUSR1信号,进程继续执行清理逻辑\n"); } int main(void) { struct sigaction sa; sa.sa_handler = sigusr1_handler; sigemptyset(&sa.sa_mask); sa.sa_flags = 0; sigaction(SIGUSR1, &sa, NULL); printf("主流程执行中,马上触发SIGUSR1\n"); raise(SIGUSR1); printf("信号处理完成,回到主流程\n"); return 0; }从代码语义来看,raise比kill(getpid(), sig)更明确、更安全,因为你不必自己处理getpid()的返回值,也省了一层对进程基本概念的依赖。
2.3 abort:让进程以SIGABRT退出
abort函数的标准声明特别“绝”,因为它根本没有返回值:
#include <stdlib.h> void abort(void);abort的作用是向当前进程发送SIGABRT信号。如果SIGABRT被阻塞或者被忽略,abort并不会就此罢休——为了保证进程一定能够异常终止,它还会尝试采用另外的方式强制终止进程。换句话说,abort的设计目的就是“让进程必定死掉”,并且最好还能留下一个核心转储,方便事后分析。
来看一个实际场景:你在写一个数据结构库,发现传入的链表头指针非法,而且后续操作极有可能导致更严重的内存损坏。这时候所有错误恢复手段都不管用了,最佳实践就是直接abort。因为与其让一个坏掉的程序继续跑下去,不如立刻终止并留下现场,让开发和运维从核心转储里找到问题根源。
#include <stdio.h> #include <stdlib.h> typedef struct Node { int value; struct Node *next; } Node; void process_list(Node *head) { if (head == NULL) { fprintf(stderr, "链表头指针为空,程序状态不可信,中止执行\n"); abort(); } // 正常处理逻辑 }关于core dump,有一点必须说明:很多Linux发行版默认不生成core文件,或者被AppArmor、SELinux等机制限制。如果abort后想拿到core dump,需要提前用ulimit -c unlimited放开限制,并且确认核心转储路径可以被写入。否则abort只是让进程退出,事后调试价值会大打折扣。
2.4 kill、raise、abort三者的对比
这三个函数虽然都能发送信号,但面向的对象、返回值设计和应用场景差异很大。整理成表格一目了然:
| 函数 | 发送目标 | 是否有返回值 | 不可忽略时结局 | 典型场景 |
|---|---|---|---|---|
| kill | 任意进程/进程组 | 有,0或-1 | 目标进程可能忽略信号 | 进程间通信、终止、信号通知 |
| raise | 当前进程自身 | 有,0或-1 | 自己忽略信号,则继续运行 | 主动触发自身信号流程 |
| abort | 当前进程自身 | 无返回 | 强制异常终止 | 不可恢复错误、主动崩溃 |
这里的“不可忽略时结局”是区分abort和raise的一个重要维度:raise发送SIGABRT后,如果程序里把SIGABRT捕获了,那么进程可以继续运行;而abort即使遇到SIGABRT被忽略,也仍然会走强制终止流程。所以需要“真正的崩溃”时,用abort;只是“通知自己处理一件事”时,用raise。
2.5 一个容易被忽略的“信号继承”问题
这里要特别提醒一下:信号处理方式是“子承父业”的。如果你在一个进程里用sigaction设置了某些信号的自定义处理函数,然后这个进程fork了一个子进程,那么子进程同样会继承这些信号处理设置。这在写多进程服务时是个大坑——子进程如果不小心继承了父进程的SIGALRM处理函数,却在业务逻辑里根本没想到信号会来,那就会导致随机且诡异的行为。
我在一个项目里遇到过类似情况:父进程里用alarm做了超时控制,但没在fork子进程前清理掉定时器,结果子进程继承了还在倒计时的alarm,没过多久子进程就收到SIGALRM退出了。排查了很久才发现这个原因。遇到涉及fork的代码,务必考虑信号设置和定时器是否也需要重置。
3. 定时炸弹:alarm与setitimer实战对比
3.1 alarm:简单、单一、秒级的一次性闹钟
如果说kill是“给别人发信号”的利器,那alarm就是“给自己设闹钟”的入门级函数。原型如下:
#include <unistd.h> unsigned int alarm(unsigned int seconds);它的语义非常直白:让内核在seconds秒之后,向当前进程发送SIGALRM信号。如果之前已经设置了一个alarm定时器,那么再次调用alarm会覆盖之前的设置,也就是旧的闹钟会被取消,返回值是旧闹钟剩余的秒数。如果没有旧闹钟,返回0。
需要注意的是alarm的单位是秒,精度粗糙。它更像一个“软超时”,而不是精确的高精度定时器。而且,SIGALRM的默认动作是终止进程,所以如果你只调用alarm(3)却没有安装任何SIGALRM处理函数,3秒后你的进程就会直接退出。这在实际开发中是个最常见的低级失误。
一个完整的alarm超时控制示例:
#include <stdio.h> #include <unistd.h> #include <signal.h> #include <errno.h> static void alarm_handler(int sig) { // 这里可以做超时处理,也可以只是用来打断阻塞的系统调用 } int main(void) { struct sigaction sa; sa.sa_handler = alarm_handler; sigemptyset(&sa.sa_mask); sa.sa_flags = 0; sigaction(SIGALRM, &sa, NULL); printf("设置5秒超时闹钟\n"); alarm(5); // 假设这里是一个可能长时间阻塞的IO操作 // 真正的场景里,系统调用会被SIGALRM打断并返回EINTR sleep(10); printf("正常结束\n"); return 0; }这里的关键点是:当进程阻塞在某个系统调用(比如read、write、sleep)时收到信号,内核会让该系统调用以EINTR错误返回,这时你需要自行判断是“继续等”还是“放弃”。这个机制在后面的常见问题部分还会深入展开。
3.2 setitimer:更精细的间隔定时器
setitimer函数比alarm功能强大得多,它支持微秒级精度,还可以周期性触发,不过使用门槛也更高。原型如下:
#include <sys/time.h> int setitimer(int which, const struct itimerval *new_value, struct itimerval *old_value);它有三个计时维度可选,这是最容易混淆的地方:
- ITIMER_REAL:以系统真实时间计时,超时后发送SIGALRM。和alarm模式类似,但精度更高。
- ITIMER_VIRTUAL:只统计进程在用户态运行的时间,程序一旦进入系统调用或睡眠,计时就暂停。超时后发送SIGVTALRM。
- ITIMER_PROF:统计进程在用户态和内核态运行的总时间,通常用于性能分析。超时后发送SIGPROF。
需要配套的itimerval结构体定义如下:
struct timeval { time_t tv_sec; /* 秒 */ suseconds_t tv_usec; /* 微秒 */ }; struct itimerval { struct timeval it_interval; /* 周期性定时器的间隔 */ struct timeval it_value; /* 首次触发的时间,为0则停止定时器 */ };它不像alarm那样每次调用都重置并设置一次。setitimer是先设置“首次触发时间”it_value,首次触发后,再用it_interval作为间隔周期性触发。如果it_value全为0,则定时器关闭;如果it_interval全为0,则定时器只触发一次,相当于一个高精度的alarm。
来一个周期性定时的实战例子:
#include <stdio.h> #include <sys/time.h> #include <signal.h> static void timer_handler(int sig) { printf("定时器触发了,执行周期任务\n"); } int main(void) { struct sigaction sa; sa.sa_handler = timer_handler; sigemptyset(&sa.sa_mask); sa.sa_flags = 0; sigaction(SIGALRM, &sa, NULL); struct itimerval timer; timer.it_value.tv_sec = 1; // 1秒后首次触发 timer.it_value.tv_usec = 0; timer.it_interval.tv_sec = 2; // 之后每2秒触发一次 timer.it_interval.tv_usec = 0; if (setitimer(ITIMER_REAL, &timer, NULL) == -1) { perror("setitimer"); return 1; } while (1) { pause(); // 等待信号 } return 0; }这段代码会每2秒打印一次“定时器触发了”。pause()用来挂起进程,直到有信号到达并处理完毕后返回。
3.3 setitimer与alarm如何取舍
根据我的经验,选alarm还是setitimer,核心看三点:是否需要周期性触发、精度要求是否高于秒级、是否需要区分不同类型的时间。
如果只是做一个秒级的超时控制,alarm完全够用,API简单,不易出错。如果需要周期性执行任务,或者需要微秒级精度,甚至需要精确到进程运行时间,那必须用setitimer。
还有一点要注意,alarm和ITIMER_REAL共享同一个定时器。也就是说,你不能在一个进程里同时使用alarm和setitimer(ITIMER_REAL)来设置两个独立的闹钟,后者会覆盖前者。如果业务上同时需要多个超时控制,应该考虑采用其他机制(比如事件循环、多路复用),或者接受一个排队信号模型的实现,而不是依赖多个alarm。这一点是很多新手最先撞上的墙。
3.4 定时器的精度与系统负载
高精度定时器很容易给人“精确定时”的错觉。实际上setitimer依赖内核的定时机制,它并不保证每次触发都在精确的微秒时间点。如果系统负载很高,时钟粒度很粗,实际触发时间可能会有数毫秒到数十毫秒的偏差。对时间精度要求特别苛刻的领域,应该考虑POSIX定时器或者高精度时钟加忙等,而不是把setitimer当作万金油。
我在一个音视频同步项目里就用过setitimer控制采集节奏,结果因为系统调度抖动导致音视频不同步,后来改成了基于单调时钟的主动等待才算稳定。项目里对时间敏感的部分尽量别依赖信号定时器,因为你永远不知道调度器在忙什么。
4. 常见问题与排查技巧实录
4.1 信号处理函数不生效的原因
很多人写好信号处理函数后,发现程序收到信号时并没有执行预期逻辑。排查这个问题时,建议按这个顺序来:
- 先确认你注册的确实是目标信号。信号编号和名字对应错是很常见的事,比如SIGALRM和SIGVTALRM名字相近,但触发条件完全不同。
- 再确认信号是否被阻塞。调用sigprocmask可以查看和修改进程的信号屏蔽字,如果某个信号长期被阻塞,那它永远不会被唤醒处理。
- 确认是否用错了注册函数。signal函数在部分场景下行为不可靠,特别是信号处理函数执行期间,如果又被相同信号打扰,容易产生难以预测的结果。尽量换成sigaction。
- 确认代码里有没有人“篡改”了你的处理函数。如果有第三方库或者子进程继承链,半路把信号处理函数重置成SIG_DFL或SIG_IGN,你的设置就会被覆盖。
我之前遇到过一个非常隐蔽的问题:某个服务初始化时调用了系统的命令行工具,那个工具内部会将SIGCHLD设置重设,导致我们后来注册的SIGCHLD处理函数失效。这种“信号处理被外部改动”的情况在复杂项目里很常见,排查时一定要留心。
4.2 EINTR:系统调用被信号打断的经典坑
信号异步打断阻塞系统调用,是Linux服务端编程最经典的坑之一。当一个进程阻塞在read、write、sleep、accept这类系统调用时,一旦有信号到达,内核会尝试唤醒进程,让这个系统调用返回-1并把errno设置为EINTR,表示“我没完成任务,我是被信号打断的”。
很多程序员看到read返回-1就直接当作出错处理,结果程序跑得好好的,突然开始疯狂报错。正确的做法是主动判断errno是否等于EINTR,如果是,应该重新发起这次系统调用,或者根据业务需要决定是否继续。
下面是一段常见的处理模式:
#include <errno.h> #include <unistd.h> ssize_t robust_read(int fd, void *buf, size_t count) { ssize_t ret; while ((ret = read(fd, buf, count)) == -1 && errno == EINTR) { // 被信号打断,重新调用 } return ret; }还有一种办法是在注册信号处理函数时设置sa_flags |= SA_RESTART,大多数系统会尝试自动重新启动被信号打断的系统调用。但不是所有系统调用都支持SA_RESTART,比如poll、select、epoll_wait在一些平台上仍然会返回EINTR。所以,完全依赖SA_RESTART是不可靠的,还是得在业务代码里对EINTR做防御。
4.3 多线程程序里能不能依然随便用这些信号函数
多线程程序中的信号处理比单线程复杂得多。信号既可以发送给进程,也可以发送给特定线程。raise在单线程中等价于kill(getpid(), sig),但在多线程下,它向调用线程发送信号;而kill(getpid(), sig)可能将信号投递给进程中的任意线程,这取决于内核的调度策略。这会导致你自以为捕获了信号,结果处理函数在一个完全不同的线程里跑,线程局部变量和锁状态完全对不上。
在对线程安全要求很高的代码里,我建议遵循几个原则:
- 尽量不要在多线程程序里用raise、alarm、setitimer这类向进程自身发送信号的机制去控制线程逻辑。
- 如果必须使用,最好让线程用pthread_kill向指定线程发送信号,不要让信号随机投递。
- 对SIGALRM这类可能导致进程退出的信号,要确保至少有一个线程安装了处理函数,否则默认行为会终止整个进程。
- 在多线程服务里,用epoll或eventfd等机制替代信号来实现超时和事件通知,通常更可控。
说实话,如果你在写一个现代的多线程网络服务,能用事件循环解决的场景就尽量别引入信号。信号机制在复杂并发模型下带来的不确定性,会远超它带来的代码简洁性。
4.4 被SIGPIPE杀死:一个隐秘的进程“自杀”事件
和标题里的几个函数不太相关,但很多人在用kill、alarm时都会顺带撞上SIGPIPE问题,我个人觉得有必要提一笔。当一个进程向对端已经关闭的Socket或管道写数据时,内核会向进程发送SIGPIPE信号,默认动作是终止进程。这个行为经常让服务端程序莫名崩溃。
解决方案有两个方向:要么忽略SIGPIPE信号(signal(SIGPIPE, SIG_IGN);),让write/read函数通过错误码返回EPIPE,你自己决定怎么处理;要么在套接字层面设置MSG_NOSIGNAL标志(send函数),避免内核生成SIGPIPE。很多服务框架会在启动时默认把SIGPIPE忽略掉,原因就在这里。
4.5 排查信号问题的实用工具
排查信号相关问题,几个工具非常趁手:
- strace:可以跟踪进程收到的所有信号和系统调用。比如strace -p PID,能看到进程何时收到SIGALRM、SIGTERM,非常直观。
- gdb:可以通过handle SIGALRM print等方式调试信号处理流程,也可以直接看信号是否被阻塞。
- kill -l:查看当前系统所有信号名称和编号的对应关系。
- cat /proc/PID/status:查看进程当前的信号屏蔽字、未决信号集,比如SigBlk、SigCgt、SigPnd这些字段。
有一次线上服务反复重启,我用strace挂上去一看,发现进程每次都在收到SIGTERM后才退出,马上顺着发送方查到了守护进程的监控脚本,问题一下就定位了。所以工具熟练的人排查问题效率会高很多。
5. 写在最后的几个细节
5.1 信号处理的权衡:用默认还是自定义
经常有人问我:是不是每个信号都应该注册自定义处理函数?我的答案是:不一定。很多信号的默认行为就已经很合理,没必要也没办法去自定义,比如SIGKILL。而像SIGCHLD这种信号,如果你用waitpid在线程里做循环回收,可能根本不需要注册SIGCHLD处理函数。
适合自定义处理的信号通常就那几种:SIGINT用于优雅退出、SIGTERM用于守护进程停止、SIGALRM用于超时、SIGUSR1/SIGUSR2用于业务自定义事件。信号不是越多处理越好,处理函数越复杂,越容易踩到异步安全的问题。信号处理函数里只能调用异步信号安全函数,像printf、malloc这类函数在信号上下文里调用都可能带来致命风险。
我自己的习惯是,信号处理函数里只做最简单的事情:设置一个标志位,或者写入一个管道(self-pipe trick),然后让主循环去处理真正的逻辑。这样既响应及时,又不会在信号上下文里做危险操作。
5.2 从信号函数到系统设计的延伸
如果你把kill、raise、abort、alarm、setitimer整个看下来,会发现它们其实对应着两类系统设计:通知事件和定时控制。信号机制最强大的地方在于它让进程具备了处理异步事件的能力,而不是只能埋头顺序执行。但也是因为异步,它才成为了无数诡异Bug的来源。
在做一个稍微复杂一点的系统时,我会建议你把信号当作“最后一公里的通知手段”,而不是核心业务逻辑的载体。核心业务逻辑尽量显式化、可预期,信号只在特定边界场景里发挥作用。
5.3 个人实操体会
用了这么多年Linux,我自己最深刻的体会是:信号的坑大多数不是由函数本身带来的,而是由“对异步行为的误判”带来的。你总觉得信号下一刻才会来,结果它可能已经来了好几次。你总觉得一个回调信号处理完就结束了,结果它可能覆盖了你在别处置好的标志位。
如果你刚接触这些函数,不妨先写几个小Demo,用strace观察每一个信号从发起到处理的过程。等你对SIGALRM、SIGTERM、SIGCHLD的来龙去脉有了手感,再面对复杂的服务治理、优雅重启、超时控制时,会格外有底气。最后再分享一个小技巧:排查信号相关问题前,先看一遍进程状态、再看一遍系统调用记录、最后才看业务代码,这个顺序能帮你节省大量排查时间。