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 知识体系总览

下面这张图总结了信号处理的核心逻辑:

信号处理跨平台知识体系 信号来源 注册方式 signal() sigaction() 平台行为差异 Linux (System V语义) BSD (旧语义) Windows (简化版) 用sigaction() 用sigaction() 只能用signal()

12.8 总结

信号处理这事儿,跨平台的关键就几点:

  • Unix上优先用sigaction(),别用signal()
  • Windows上只能用signal(),接受它的局限性
  • 信号处理函数里只做最安全的事——设置标志位
  • 用条件编译隔离平台差异

我当年第一次做跨平台信号处理时,就是因为没注意这些细节,导致程序在Linux上跑了两天突然崩溃。后来加上SA_RESTART标志,问题就解决了。你想想看,一个系统调用被信号中断后不自动重启,程序可能就卡在奇怪的状态里了。

嗯,信号处理看似简单,但坑不少。记住一条原则:能用sigaction就别用signal,信号处理函数里别干重活。这样你的代码在Unix和Windows上都能跑得稳。


公众号:蓝海资料掘金营,微信deep3321