12、信号处理:signal()与sigaction()在Unix与Windows下的行为差异
信号处理这事儿,说白了就是操作系统给程序发“小纸条”。
进程正跑着呢,突然收到一个信号——可能是用户按了Ctrl+C,也可能是某个定时器到期了。这时候程序该怎么反应?是优雅地退出,还是直接崩溃?
我早年做跨平台项目时,就被信号处理坑过好几次。Windows上跑得好好的代码,一放到Linux上就莫名其妙挂掉。查了半天,原来是信号注册方式不一样。
12.1 信号的基本概念
信号是一种软件中断。它通知进程:有事件发生了。
常见的信号有:
- SIGINT(2):终端中断,Ctrl+C触发
- SIGTERM(15):终止信号,kill默认发送
- SIGKILL(9):强制终止,不可捕获
- SIGSEGV(11):段错误,非法内存访问
嗯,这里要注意:SIGKILL和SIGSTOP这两个信号,你注册了也没用。系统直接无视你的处理函数。
核心区别一句话:
signal()是原始接口,行为因平台而异。sigaction()是POSIX标准接口,行为可预测。
12.2 signal():简单但不可靠
signal()的声明很简单:
#include <signal.h>
void (*signal(int sig, void (*func)(int)))(int);
看着有点绕对吧?其实就是:传入信号编号和处理函数,返回旧的处理函数指针。
我个人的习惯是,在Unix上尽量不用signal()。为什么?因为它的行为在System V和BSD两派之间不一样。
| 行为 | System V(Linux默认) | BSD(早期) |
|---|---|---|
| 信号处理完后 | 重置为默认处理 | 保持用户处理函数 |
| 系统调用中断 | 不自动重启 | 自动重启 |
你想想看,如果信号处理完后被重置成默认行为,那第二次收到信号时,程序可能就直接退出了。这谁受得了?
我曾经踩过的坑:
在Linux上用signal()注册了SIGINT处理函数。第一次按Ctrl+C正常,第二次再按,程序直接退出。查了半天,才发现Linux的signal()是System V语义,处理完后自动重置了。
12.3 sigaction():可靠且可控
sigaction()是POSIX标准推荐的接口。它用结构体传参,控制粒度更细。
#include <signal.h>
int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact);
struct sigaction长这样:
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_flags里常用的标志:
- SA_RESTART:被信号中断的系统调用自动重启
- SA_SIGINFO:使用sa_sigaction而不是sa_handler
- SA_NOCLDSTOP:子进程停止时不发送SIGCHLD
我的建议:
在Unix/Linux上,一律用sigaction()。它行为明确,跨平台兼容性好。signal()就当它不存在吧。
12.4 Windows下的信号处理
Windows的信号机制……嗯,怎么说呢,它更像是一个“模拟器”。
Windows本身没有Unix那种完整的信号模型。它用控制台事件(Console Events)来处理Ctrl+C这类操作。
Windows下也有signal()函数:
#include <signal.h>
void (*signal(int sig, void (*func)(int)))(int);
但支持的信号非常有限:
- SIGABRT
- SIGFPE
- SIGILL
- SIGINT
- SIGSEGV
- SIGTERM
而且,Windows的signal()行为是固定的——处理完后重置为默认。没有SA_RESTART,没有sa_mask,没有sigaction()。
说白了:
Windows的signal()就是个简化版。它只保证基本功能,别指望它能做精细控制。
12.5 跨平台信号处理的实战方案
那怎么办?总不能写两套代码吧?
我一般这么做:
#ifdef _WIN32
// Windows: 只能用signal()
signal(SIGINT, my_handler);
signal(SIGTERM, my_handler);
#else
// Unix/Linux: 用sigaction()
struct sigaction sa;
sa.sa_handler = my_handler;
sigemptyset(&sa.sa_mask);
sa.sa_flags = SA_RESTART; // 自动重启系统调用
sigaction(SIGINT, &sa, NULL);
sigaction(SIGTERM, &sa, NULL);
#endif
这样写,两边都能跑。Windows上虽然简陋点,但基本功能没问题。
注意:
Windows下不要用SIGUSR1、SIGUSR2这类信号。它们根本不存在。编译时不会报错,但运行时信号永远不会触发。
12.6 信号处理函数里能做什么?
这个问题很关键。信号处理函数是在中断上下文中执行的,能做的事情非常有限。
安全操作(异步信号安全):
- 读写volatile sig_atomic_t类型的变量
- 调用signal()重新注册信号
- 调用_exit()立即退出
危险操作(不要做):
- 调用malloc()、free()等内存管理函数
- 调用printf()、fprintf()等I/O函数
- 操作全局数据结构(除非用锁保护,但锁本身可能不安全)
我个人的做法是:信号处理函数里只设置一个标志位,主循环里检查这个标志位再做处理。
volatile sig_atomic_t g_signal_received = 0;
void signal_handler(int sig) {
g_signal_received = sig; // 只做这一件事
}
int main() {
// 注册信号...
while (1) {
if (g_signal_received) {
// 处理信号
printf("收到信号: %d\n", g_signal_received);
g_signal_received = 0;
}
// 正常业务逻辑...
}
}
这样既安全又清晰。信号处理函数只负责“通知”,主循环负责“处理”。
12.7 知识体系总览
下面这张图总结了信号处理的核心逻辑:
12.8 总结
信号处理这事儿,跨平台的关键就几点:
- Unix上优先用sigaction(),别用signal()
- Windows上只能用signal(),接受它的局限性
- 信号处理函数里只做最安全的事——设置标志位
- 用条件编译隔离平台差异
我当年第一次做跨平台信号处理时,就是因为没注意这些细节,导致程序在Linux上跑了两天突然崩溃。后来加上SA_RESTART标志,问题就解决了。你想想看,一个系统调用被信号中断后不自动重启,程序可能就卡在奇怪的状态里了。
嗯,信号处理看似简单,但坑不少。记住一条原则:能用sigaction就别用signal,信号处理函数里别干重活。这样你的代码在Unix和Windows上都能跑得稳。
公众号:蓝海资料掘金营,微信deep3321