一、信号是什么?—— 从一次崩溃说起
我记得刚入行那会儿,写了一个多线程下载程序。跑了一整天,突然进程就没了。没有报错,没有日志,就像人间蒸发。后来查了半天,才发现是用户按了 Ctrl+C,系统发了个 SIGINT 过来,而我的程序根本没处理它。
信号,说白了就是软件层面的中断。它告诉进程:「嘿,出事了!」或者「嘿,该停下了!」。进程可以选择忽略、处理,或者按默认方式响应。
你想想看,操作系统怎么通知你程序出问题了?总不能发个微信吧?信号就是那个「紧急通知」机制。
二、常见信号:你迟早会遇到的几个
Linux 下信号有好几十种,但日常开发中,你真正需要记住的其实就这几个:
| 信号 | 编号 | 默认行为 | 我遇到的场景 |
|---|---|---|---|
| SIGINT | 2 | 终止进程 | 用户按 Ctrl+C,想优雅退出 |
| SIGKILL | 9 | 强制终止 | kill -9 杀不掉的进程?不存在的 |
| SIGSEGV | 11 | 终止+core dump | 空指针解引用,段错误,老熟人了 |
| SIGTERM | 15 | 终止进程 | kill 默认发的信号,可以捕获 |
| SIGCHLD | 17 | 忽略 | 子进程退出时通知父进程 |
| SIGPIPE | 13 | 终止进程 | 写管道时读端已关闭 |
我曾经在线上环境遇到过一个问题:某个服务进程突然不响应了,kill 发 SIGTERM 没用,因为它自己写了个信号处理函数卡在死循环里。最后只能 kill -9 强制干掉。嗯,从那以后我写信号处理函数都格外小心。
三、signal 函数:注册你的信号处理
处理信号最传统的方式就是用 signal() 函数。它的原型很简单:
#include <signal.h>
typedef void (*sighandler_t)(int);
sighandler_t signal(int signum, sighandler_t handler);
第一个参数是信号编号,第二个参数可以是:
SIG_IGN—— 忽略该信号SIG_DFL—— 恢复默认行为- 自定义函数指针 —— 你写的处理函数
来个例子:
#include <stdio.h>
#include <signal.h>
#include <unistd.h>
void sigint_handler(int sig) {
// 注意:信号处理函数里尽量别做复杂操作
write(STDOUT_FILENO, "收到 SIGINT,但我偏不退出!\n", 28);
}
int main() {
// 注册 SIGINT 的处理函数
signal(SIGINT, sigint_handler);
while(1) {
printf("程序运行中...\n");
sleep(1);
}
return 0;
}
运行这个程序,你按 Ctrl+C 试试?程序不会退出,只会打印一条消息。想退出?只能开另一个终端 kill 掉它。
四、信号的不可靠性 —— 一个历史遗留问题
早期的 Unix 信号机制有个大坑:信号是不可靠的。什么意思呢?
举个例子:你注册了一个 SIGINT 处理函数,当信号到达时,内核会先把这个信号的处理函数重置为默认行为,然后再调用你的处理函数。也就是说,如果你在处理函数执行期间又来了一个同样的信号,它就会按默认方式处理 —— 很可能直接终止进程。
为什么会这样?因为早期的设计者觉得信号处理函数应该尽快执行完,重置是为了防止递归调用。但现实是,你的处理函数可能还没跑完,第二个信号就来了。
看个代码感受一下:
#include <stdio.h>
#include <signal.h>
#include <unistd.h>
void handler(int sig) {
printf("开始处理信号...\n");
sleep(3); // 模拟耗时操作
printf("处理完毕\n");
}
int main() {
signal(SIGINT, handler);
while(1) {
pause(); // 等待信号
}
return 0;
}
运行后快速按两次 Ctrl+C,你会发现第二次按的时候,程序可能直接退出了。因为第一次信号处理还没结束,信号处理函数被重置了,第二次信号触发了默认的终止行为。
我曾经在一个嵌入式项目里踩过这个坑。设备运行在野外,偶尔会收到重复的信号,结果程序莫名其妙就挂了。查了两天才定位到是 signal() 的不可靠性问题。后来全部换成 sigaction(),世界清净了。
五、信号的知识体系
下面这张图帮你理清信号相关的核心概念:
六、避坑指南
最后分享几个我这些年积累的经验:
- 信号处理函数要短小精悍 —— 别在里面 malloc、printf、甚至加锁。这些函数大多不是异步信号安全的。我一般只设一个标志位。
- 别用 signal() 写生产代码 —— 它的行为在不同 Unix 系统上不一样。用 sigaction() 替代,它更可控、更可靠。
- SIGKILL 不能被捕获 —— 别费劲去写它的处理函数,编译器会报错,运行时也不会生效。
- 多线程环境要小心 —— 信号在多线程中的行为更复杂。默认情况下,信号会发给任意一个不阻塞该信号的线程。
信号是 Linux 系统编程的基础,也是理解进程间通信的第一步。虽然它看起来简单,但用好了能解决很多实际问题。下次你的程序需要优雅退出、处理异常、或者响应外部事件时,记得想想信号这个老朋友。
公众号:蓝海资料掘金营,微信deep3321