信号进阶:sigaction、信号集与阻塞
信号这东西,说白了就是软件层面的中断。你写个程序跑得好好的,突然按个 Ctrl+C,SIGINT 就来了。但真实项目里,信号处理远没这么简单。我早年做网络服务器时,就被信号坑过好几次——不是丢信号,就是死锁。嗯,今天咱们就把这些坑填上。
一、sigaction:比 signal 更靠谱的选择
很多人刚学信号时,都用 signal() 函数。但说实话,signal() 的行为在不同 Unix 系统上是有差异的。我个人习惯直接用 sigaction(),它更底层、更可控。
#include <signal.h>
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_mask 字段。它指定了在信号处理函数执行期间,哪些信号应该被阻塞。我当年写一个日志守护进程时,就靠这个避免了信号嵌套导致的死锁。
核心区别:
signal()行为因系统而异,有些系统会重置处理函数为默认值sigaction()行为由 POSIX 标准明确定义,跨平台一致sigaction()支持SA_SIGINFO标志,能获取发送者的 PID、UID 等信息
// 一个典型的 sigaction 用法
#include <stdio.h>
#include <signal.h>
#include <unistd.h>
void handler(int sig) {
write(STDOUT_FILENO, "Got signal!\n", 12);
}
int main() {
struct sigaction sa;
sa.sa_handler = handler;
sigemptyset(&sa.sa_mask); // 不额外阻塞任何信号
sa.sa_flags = 0; // 无特殊标志
if (sigaction(SIGINT, &sa, NULL) == -1) {
perror("sigaction");
return 1;
}
while(1) pause();
return 0;
}
我的习惯:只要涉及信号处理,一律用 sigaction() 而不是 signal()。虽然多写几行代码,但心里踏实。
二、信号集与 sigprocmask:阻塞的艺术
信号集 sigset_t 本质上是一个位图,每个 bit 对应一个信号。你想想看,系统里有几十种信号,总不能一个个去操作吧?所以 POSIX 提供了一组操作函数。
sigemptyset(sigset_t *set); // 清空信号集
sigfillset(sigset_t *set); // 填满信号集
sigaddset(sigset_t *set, int signo); // 添加一个信号
sigdelset(sigset_t *set, int signo); // 删除一个信号
sigismember(const sigset_t *set, int signo); // 判断是否在集中
sigprocmask() 是阻塞信号的核心函数。它有三种操作模式:
| 参数 how | 含义 | 实际场景 |
|---|---|---|
| SIG_BLOCK | 将 set 中的信号加入阻塞集 | 临时屏蔽某些信号,保护临界区 |
| SIG_UNBLOCK | 从阻塞集中移除 set 中的信号 | 恢复之前屏蔽的信号 |
| SIG_SETMASK | 用 set 完全替换当前阻塞集 | 彻底改变信号屏蔽策略 |
我曾经踩过的坑:在信号处理函数里调用 printf()。printf 内部使用了全局锁,如果信号恰好中断了另一个正在 printf 的线程,就会死锁。后来我改用 write() 系统调用,才彻底解决。
三、sigpending:看看哪些信号在排队
当一个信号被阻塞时,它不会丢失,而是变成「待决」状态。你可以用 sigpending() 查看当前进程有哪些信号在排队等待处理。
#include <signal.h>
int sigpending(sigset_t *set); // 返回当前待决信号集
这个函数在调试时特别有用。我记得有一次排查一个诡异的信号丢失问题,就是用 sigpending 发现信号其实没丢,只是被阻塞了,一直没机会处理。
// 演示 sigpending 的用法
sigset_t pending;
sigpending(&pending);
if (sigismember(&pending, SIGINT)) {
printf("SIGINT is pending!\n");
}
四、可重入函数与异步信号安全
这是信号处理中最容易出问题的地方。什么叫可重入?简单说,就是一个函数在被中断后,再次进入执行,不会出问题。
为什么信号处理要关心这个?因为信号处理函数是异步执行的——它可能在程序的任何位置被触发。如果信号处理函数里调用了不可重入的函数,而主程序恰好也在执行同一个函数,那数据就可能乱掉。
异步信号安全函数列表(部分):
- 安全:
write(),read(),open(),close(),exit(),_exit() - 不安全:
printf(),malloc(),free(),pthread_*系列
你可能会问:为什么 malloc 不安全?因为它内部维护了一个全局的堆管理链表。如果信号处理函数里调 malloc,而主程序刚好也在 malloc 里操作那个链表,链表结构就可能被破坏。嗯,这就是典型的不可重入问题。
// 安全的信号处理函数写法
void safe_handler(int sig) {
// 用 write 代替 printf
const char *msg = "Caught signal, exiting...\n";
write(STDOUT_FILENO, msg, strlen(msg));
_exit(0); // 用 _exit 而不是 exit
}
我的原则:信号处理函数里只做最必要的事——设置一个 volatile sig_atomic_t 标志变量,然后尽快返回。真正的处理逻辑放到主循环里去做。这样既安全又清晰。
五、知识体系总览
下面这张图把本章的核心知识点串起来了。你可以看到,信号处理从注册(sigaction)到阻塞(sigprocmask),再到查询待决信号(sigpending),最后到安全处理,是一条完整链路。
信号处理看起来简单,但实际项目中稍不注意就会出问题。记住几个关键点:用 sigaction 代替 signal、信号处理函数里别调 printf 和 malloc、用 sig_atomic_t 做标志位。做到这几点,大部分信号相关的坑你都能避开。