3. 信号处理方式
信号处理,说白了就是进程收到信号后怎么办。这个问题我早年踩过不少坑,今天跟你好好聊聊。
3.1 三种基本处理方式
每个信号都有三种处理方式可选。嗯,咱们一个一个看。
| 处理方式 | 说明 | 典型场景 |
|---|---|---|
| 默认处理 | 执行系统预定义的动作 | 大多数信号的默认行为 |
| 忽略信号 | 内核直接丢弃该信号 | SIGCHLD、SIGURG 等 |
| 自定义捕捉 | 调用用户注册的处理函数 | 优雅退出、资源清理 |
默认处理又分几种:终止进程、终止+core dump、忽略、暂停、继续。比如 SIGINT 默认终止进程,SIGQUIT 默认终止+core dump。我刚开始学的时候老记混,后来发现用 man 7 signal 查一下最靠谱。
忽略信号要注意——SIGKILL 和 SIGSTOP 你不能忽略,这是内核的底线。我曾经在项目里想屏蔽 SIGKILL 保护守护进程,结果编译都过不了,哈哈。
3.2 signal 函数——最朴素的信号捕捉
signal 函数是 POSIX 标准最早提供的接口。用法很简单:
#include <signal.h>
typedef void (*sighandler_t)(int);
sighandler_t signal(int signum, sighandler_t handler);
第二个参数有三种取值:
SIG_DFL:恢复默认处理SIG_IGN:忽略信号- 函数指针:自定义处理函数
来个实际例子:
#include <stdio.h>
#include <signal.h>
#include <unistd.h>
void sigint_handler(int sig) {
// 注意:信号处理函数中只能调用异步信号安全函数
write(STDOUT_FILENO, "Caught SIGINT!\n", 15);
}
int main() {
// 捕捉 Ctrl+C
signal(SIGINT, sigint_handler);
while(1) {
pause(); // 等待信号
}
return 0;
}
3.3 信号集——管理信号的集合
信号集说白了就是一个位图,每个信号占一位。内核用 sigset_t 类型表示。我习惯把它想象成一个「信号清单」,你可以往里面加信号、删信号、查信号。
操作信号集的函数:
#include <signal.h>
int sigemptyset(sigset_t *set); // 清空集合
int sigfillset(sigset_t *set); // 填满所有信号
int sigaddset(sigset_t *set, int sig); // 添加信号
int sigdelset(sigset_t *set, int sig); // 删除信号
int sigismember(const sigset_t *set, int sig); // 检查是否存在
使用前一定要先初始化,要么 sigemptyset 要么 sigfillset。我见过有人直接定义 sigset_t set; 就用,结果里面是随机值,排查了半天。
3.4 信号屏蔽字——进程的「信号黑名单」
信号屏蔽字,就是告诉内核:这些信号先别递送给进程,给我挂着。每个进程都有一个信号屏蔽字,子进程会继承父进程的屏蔽字。
核心函数是 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 完全替换当前屏蔽字 |
来看一个经典用法——保护临界区:
sigset_t newmask, oldmask;
// 初始化信号集,加入 SIGINT
sigemptyset(&newmask);
sigaddset(&newmask, SIGINT);
// 阻塞 SIGINT,保存旧屏蔽字
sigprocmask(SIG_BLOCK, &newmask, &oldmask);
// 临界区代码——此时 SIGINT 被挂起
// ... 操作共享数据 ...
// 恢复旧屏蔽字,挂起的 SIGINT 会递送
sigprocmask(SIG_SETMASK, &oldmask, NULL);
3.5 知识体系总览
下面这张图帮你理清信号处理的整体脉络:
3.6 避坑指南
我这些年做信号处理,踩过的坑不少。挑几个典型的说说:
- 信号处理函数要尽量简单——只能调用异步信号安全函数(async-signal-safe),比如
write可以,printf不行。我曾经在信号处理里调malloc,结果死锁了,查了一整天。 - signal 函数在不同系统上行为不一致——System V 上 signal 会重置处理函数为 SIG_DFL,BSD 上不会。跨平台项目建议用 sigaction。
- 屏蔽字要记得恢复——用 SIG_SETMASK 恢复旧屏蔽字时,挂起的信号会立即递送。如果你不想在临界区里处理信号,可以用 sigsuspend 原子地等待。
- 子进程继承屏蔽字——fork 之后子进程的屏蔽字和父进程一样。如果你在父进程里屏蔽了 SIGINT,子进程里也是屏蔽的,别忘了在子进程里重新设置。
信号处理这块,说白了就是「什么时候做什么事」。默认处理省心,忽略信号省事,自定义捕捉灵活但要注意安全。信号集和屏蔽字是精细控制信号递送的工具,多线程编程里尤其重要。嗯,先聊到这儿,你先把这些基础打牢,后面讲 sigaction 和实时信号时咱们再深入。