信号处理:signal/sigaction,信号集,可重入函数
信号处理这个话题,说实话,很多C语言开发者写了三五年代码都不一定真正搞明白。我早年做嵌入式Linux驱动时,就被信号坑过——一个异步信号进来,全局变量被改得乱七八糟,查了两天才定位到问题。从那以后,我对信号处理就格外谨慎了。
信号本质上是一种软件中断。它告诉进程:嘿,有事情发生了!可能是用户按了Ctrl+C,可能是子进程退出了,也可能是定时器到期了。处理信号的方式,直接决定了你的程序是否健壮。
signal vs sigaction:老接口与新接口
先说说最基础的 signal()。这个函数很简单,注册一个信号处理函数就行:
#include <signal.h>
void handler(int sig) {
printf("收到信号 %d\n", sig);
}
int main() {
signal(SIGINT, handler); // 注册Ctrl+C处理
while(1);
return 0;
}
看起来挺简单对吧?但问题来了——signal() 的行为在不同Unix系统上不一致。有些系统在处理完信号后会重置为默认行为,有些不会。这就导致了一个经典的竞态条件:
我曾经踩过的坑:在旧版Linux上,signal注册的处理函数执行一次后就被重置了。第二次收到同样的信号,程序直接默认退出。后来我改用 sigaction,再也没出过这种问题。
sigaction() 是POSIX标准推荐的接口。它更强大,也更可控:
#include <signal.h>
void handler(int sig, siginfo_t *info, void *context) {
printf("信号 %d 来自进程 %d\n", sig, info->si_pid);
}
int main() {
struct sigaction sa;
sa.sa_sigaction = handler;
sigemptyset(&sa.sa_mask);
sa.sa_flags = SA_SIGINFO; // 获取额外信息
sigaction(SIGINT, &sa, NULL);
while(1);
return 0;
}
看到区别了吗?sigaction 可以获取信号的来源、发送者的PID、用户态时间等详细信息。我个人习惯,只要项目不是玩具代码,一律用 sigaction。
信号集:管理多个信号的利器
信号集说白了就是一个位图,每个bit代表一个信号。你想想看,如果程序要同时屏蔽十几个信号,总不能一个个去设置吧?
信号集的操作有标准API:
sigset_t set;
sigemptyset(&set); // 清空所有信号
sigfillset(&set); // 设置所有信号
sigaddset(&set, SIGINT); // 添加SIGINT
sigdelset(&set, SIGINT); // 删除SIGINT
sigismember(&set, SIGINT); // 检查是否包含
实际项目中,信号集最常用的场景是:在处理某个信号时,临时屏蔽其他信号。比如:
struct sigaction sa;
sigset_t block_mask;
sigemptyset(&block_mask);
sigaddset(&block_mask, SIGTERM);
sigaddset(&block_mask, SIGHUP);
sa.sa_mask = block_mask; // 处理SIGINT时,自动屏蔽SIGTERM和SIGHUP
sa.sa_handler = my_handler;
sigaction(SIGINT, &sa, NULL);
嗯,这里要注意:sa_mask 是在处理当前信号期间临时屏蔽的信号集。处理完当前信号后,这些屏蔽会自动解除。这个机制能有效防止信号嵌套导致的混乱。
可重入函数:信号处理中的雷区
这是信号处理里最容易被忽视的问题。什么叫可重入?说白了,就是一个函数在被中断后,再次进入执行,不会出问题。
我举个例子:
int global_flag = 0;
void handler(int sig) {
global_flag = 1; // 危险!可能正在被主程序修改
}
int main() {
signal(SIGINT, handler);
while(1) {
if (global_flag) {
// 处理逻辑
global_flag = 0;
}
}
}
这段代码有什么问题?如果主程序刚检查完 global_flag == 1,还没来得及处理,信号又来了,处理函数又把 global_flag 改成1。等回到主程序时,逻辑就乱了。
可重入函数的核心原则:
- 不使用全局变量或静态变量
- 不调用不可重入的函数(如malloc、printf)
- 不修改自身的代码段
- 使用原子操作或锁保护共享数据
POSIX标准明确列出了可重入函数列表。我建议你把这个列表记在脑子里:read()、write()、open()、close()、sigaction() 等是安全的。而 malloc()、free()、printf()、fprintf() 这些,千万别在信号处理函数里用。
我的个人经验:信号处理函数里只做一件事——设置一个volatile sig_atomic_t类型的标志位。主程序轮询这个标志位,再做实际处理。这样既简单又安全。
知识体系总览
下面这张图,把信号处理的几个核心概念串起来了:
避坑指南:我踩过的几个雷
做嵌入式这些年,信号处理相关的bug我见过不少。挑几个典型的说说:
- 信号处理函数里调用printf——printf内部用了锁,信号中断时可能死锁。我曾经在调试时加了个printf,结果程序随机崩溃,查了两天。
- 忽略SIGCHLD信号——子进程退出后变成僵尸进程。我建议所有创建子进程的程序,都注册SIGCHLD处理函数,调用waitpid回收。
- 信号屏蔽字没处理好——多线程环境下,每个线程有自己的信号屏蔽字。主线程屏蔽了SIGINT,子线程却收到了。这个坑我印象很深。
一句话总结:信号处理不是玄学,是工程。用对接口、管好信号集、保证可重入,你的程序就能稳稳地跑下去。