11、信号处理:POSIX 信号与 Windows 结构化异常处理 (SEH) 的对比与封装

信号处理这事儿,说实话,是跨平台开发里最让我头疼的模块之一。你想想看,在 Linux 下跑得好好的程序,一挪到 Windows 上,遇到个段错误就直接崩溃,连个抢救的机会都没有。我早年做嵌入式服务器移植时,就吃过这个亏。

今天咱们就把 POSIX 信号和 Windows 的 SEH 掰开揉碎了讲。我会告诉你它们各自的设计哲学,以及如何用一套代码优雅地兼容两者。

11.1 POSIX 信号:Unix 世界的“软中断”

POSIX 信号,本质上是一种异步事件通知机制。进程收到信号后,可以忽略、捕获并执行自定义函数,或者执行默认动作(比如终止进程)。

常用的信号就那么几个:

  • SIGINT:终端中断,Ctrl+C 触发
  • SIGTERM:终止信号,kill 命令默认发送
  • SIGSEGV:段错误,非法内存访问
  • SIGPIPE:向无读取端的管道写数据

注册信号处理函数,用 signal()sigaction()。我个人强烈建议用 sigaction(),因为它更可控,行为也更一致。

#include <stdio.h>
#include <signal.h>
#include <stdlib.h>

void sigint_handler(int sig) {
    // 注意:信号处理函数里能做的事非常有限
    // 我一般只设置一个 volatile 标志
    write(STDOUT_FILENO, "Caught SIGINT, cleaning up...\n", 30);
    exit(0);
}

int main() {
    struct sigaction sa;
    sa.sa_handler = sigint_handler;
    sigemptyset(&sa.sa_mask);
    sa.sa_flags = 0;

    if (sigaction(SIGINT, &sa, NULL) == -1) {
        perror("sigaction");
        return 1;
    }

    // 模拟主循环
    while (1) {
        pause(); // 等待信号
    }
    return 0;
}
避坑指南:信号处理函数必须是可重入的。我曾经在信号处理函数里调用了 printf(),结果在极端情况下导致了死锁。因为 printf 内部用了锁,而主程序可能正好也在调用 printf。记住,信号处理函数里只做最原子的事:设置标志位、写管道、或者直接退出。

11.2 Windows SEH:结构化的“异常”而非“信号”

Windows 的思路完全不同。它不叫“信号”,叫“结构化异常处理”(SEH)。SEH 是操作系统和编译器共同支持的一套机制,专门用来处理硬件异常(比如除零、非法内存访问)和软件异常。

SEH 的核心是 __try / __except / __finally 这三个关键字。它们看起来像 C++ 的 try-catch,但底层实现完全是两码事。

#include <windows.h>
#include <stdio.h>

int main() {
    __try {
        // 故意触发一个访问违规
        int* p = NULL;
        *p = 42;
    }
    __except (EXCEPTION_EXECUTE_HANDLER) {
        // 过滤器表达式返回 EXCEPTION_EXECUTE_HANDLER
        // 表示我们处理这个异常
        printf("Caught access violation!\n");
    }

    printf("Program continues...\n");
    return 0;
}

这里有个关键点:__except 后面跟的是一个“过滤器表达式”。这个表达式可以非常复杂,甚至是一个函数调用。它决定是否处理当前异常。如果返回 EXCEPTION_CONTINUE_SEARCH,异常会继续向上传播。

核心区别:POSIX 信号是异步的,你永远不知道它什么时候来。而 SEH 是同步的,异常发生在当前指令流中,处理完异常后,程序可以继续执行(前提是你修复了问题)。

11.3 对比:设计哲学与使用场景

我把两者的关键差异整理成了表格,方便你对照:

维度 POSIX 信号 Windows SEH
触发方式 异步(硬件/软件/用户) 同步(硬件/软件异常)
处理模型 回调函数(信号处理函数) 栈展开 + 过滤器表达式
可重入性 必须保证,否则有死锁风险 由操作系统保证,相对安全
恢复执行 信号处理函数返回后继续 异常处理块执行后继续
典型用途 进程间通信、优雅退出 崩溃保护、资源清理

说白了,POSIX 信号更适合做“通知”,而 SEH 更适合做“保护”。

11.4 封装:用一套 API 覆盖两个世界

跨平台开发嘛,最终目标就是写一次代码,到处编译。我常用的做法是抽象出一个“异常信号处理层”。

先定义统一的接口:

// signal_handler.h
#ifndef SIGNAL_HANDLER_H
#define SIGNAL_HANDLER_H

#ifdef _WIN32
    #include <windows.h>
#else
    #include <signal.h>
#endif

// 统一异常类型
typedef enum {
    EX_SIGINT,   // Ctrl+C
    EX_SIGTERM,  // 终止
    EX_SIGSEGV,  // 段错误
    EX_SIGFPE    // 浮点异常
} ExceptionType;

// 回调函数类型
typedef void (*ExceptionCallback)(ExceptionType type, void* context);

// 初始化异常处理
int exception_init(ExceptionCallback cb, void* context);

// 触发异常(用于测试)
void exception_raise(ExceptionType type);

#endif

然后分别实现 Linux 和 Windows 版本。Linux 版本用 sigaction

// signal_handler_linux.c
#include "signal_handler.h"
#include <stdio.h>
#include <stdlib.h>

static ExceptionCallback g_callback = NULL;
static void* g_context = NULL;

static void handler(int sig) {
    ExceptionType type;
    switch (sig) {
        case SIGINT:  type = EX_SIGINT;  break;
        case SIGTERM: type = EX_SIGTERM; break;
        case SIGSEGV: type = EX_SIGSEGV; break;
        case SIGFPE:  type = EX_SIGFPE;  break;
        default: return;
    }
    if (g_callback) {
        g_callback(type, g_context);
    }
}

int exception_init(ExceptionCallback cb, void* context) {
    g_callback = cb;
    g_context = context;

    struct sigaction sa;
    sa.sa_handler = handler;
    sigemptyset(&sa.sa_mask);
    sa.sa_flags = SA_RESTART; // 自动重启被中断的系统调用

    if (sigaction(SIGINT, &sa, NULL) == -1) return -1;
    if (sigaction(SIGTERM, &sa, NULL) == -1) return -1;
    if (sigaction(SIGSEGV, &sa, NULL) == -1) return -1;
    if (sigaction(SIGFPE, &sa, NULL) == -1) return -1;

    return 0;
}

void exception_raise(ExceptionType type) {
    int sig;
    switch (type) {
        case EX_SIGINT:  sig = SIGINT;  break;
        case EX_SIGTERM: sig = SIGTERM; break;
        case EX_SIGSEGV: sig = SIGSEGV; break;
        case EX_SIGFPE:  sig = SIGFPE;  break;
        default: return;
    }
    raise(sig);
}

Windows 版本用 SEH 的 __try/__except 包裹主线程:

// signal_handler_win.c
#include "signal_handler.h"
#include <stdio.h>
#include <stdlib.h>

static ExceptionCallback g_callback = NULL;
static void* g_context = NULL;

// 将 Windows 异常码映射到我们的统一类型
static ExceptionType map_exception(DWORD code) {
    switch (code) {
        case EXCEPTION_ACCESS_VIOLATION: return EX_SIGSEGV;
        case EXCEPTION_INT_DIVIDE_BY_ZERO:
        case EXCEPTION_FLT_DIVIDE_BY_ZERO: return EX_SIGFPE;
        default: return EX_SIGTERM;
    }
}

// 这个函数作为 SEH 的过滤器
static DWORD seh_filter(PEXCEPTION_POINTERS ep, void* context) {
    ExceptionType type = map_exception(ep->ExceptionRecord->ExceptionCode);
    if (g_callback) {
        g_callback(type, context);
    }
    // 返回 EXCEPTION_EXECUTE_HANDLER 表示我们处理了
    return EXCEPTION_EXECUTE_HANDLER;
}

// 注意:这个函数需要在 __try 块中调用
void exception_raise(ExceptionType type) {
    // Windows 下用 RaiseException 模拟
    DWORD code;
    switch (type) {
        case EX_SIGSEGV: code = EXCEPTION_ACCESS_VIOLATION; break;
        case EX_SIGFPE:  code = EXCEPTION_INT_DIVIDE_BY_ZERO; break;
        default: code = 0xE0000001; break; // 自定义异常码
    }
    RaiseException(code, 0, 0, NULL);
}

// 初始化:注册控制台事件(Ctrl+C)
int exception_init(ExceptionCallback cb, void* context) {
    g_callback = cb;
    g_context = context;

    // 注册 Ctrl+C 处理
    if (!SetConsoleCtrlHandler((PHANDLER_ROUTINE)handler, TRUE)) {
        return -1;
    }
    return 0;
}

// 控制台事件处理
static BOOL WINAPI handler(DWORD dwCtrlType) {
    ExceptionType type;
    switch (dwCtrlType) {
        case CTRL_C_EVENT:      type = EX_SIGINT;  break;
        case CTRL_BREAK_EVENT:  type = EX_SIGTERM; break;
        default: return FALSE;
    }
    if (g_callback) {
        g_callback(type, g_context);
    }
    return TRUE;
}

我的经验:Windows 下用 SEH 包裹整个主循环是个好习惯。我曾经在一个图像处理服务器里,用 SEH 捕获了所有未处理的异常,然后记录崩溃现场并重启工作线程。这样即使某个模块挂了,整个服务也不会完全宕掉。

11.5 知识体系总览

下面这张图帮你理清本章的核心逻辑:

信号处理跨平台封装架构 POSIX 信号 • 异步通知机制 • sigaction() 注册处理函数 • 处理函数必须可重入 • 典型信号:SIGINT, SIGSEGV Linux 实现 struct sigaction sa; sa.sa_handler = handler; sigaction(SIGINT, &sa, NULL); 统一 API Windows SEH • 结构化异常处理 • __try/__except 块包裹 • 过滤器表达式决定处理 • 同步异常,可恢复执行 Windows 实现 __try { ... } __except (filter()) { ... } SetConsoleCtrlHandler()

11.6 使用示例:跨平台优雅退出

最后,给你看一个实际的使用场景。假设我们有一个服务器程序,希望用户按 Ctrl+C 时能优雅退出:

#include "signal_handler.h"
#include <stdio.h>
#include <stdlib.h>

static volatile int g_running = 1;

void on_exception(ExceptionType type, void* context) {
    switch (type) {
        case EX_SIGINT:
            printf("\nReceived Ctrl+C, shutting down...\n");
            g_running = 0;
            break;
        case EX_SIGTERM:
            printf("Received termination signal.\n");
            g_running = 0;
            break;
        case EX_SIGSEGV:
            printf("Oops! Segmentation fault. Saving state...\n");
            // 这里可以做最后的资源清理
            exit(1);
        default:
            break;
    }
}

int main() {
    if (exception_init(on_exception, NULL) != 0) {
        fprintf(stderr, "Failed to init exception handler\n");
        return 1;
    }

    printf("Server running. Press Ctrl+C to stop.\n");

    while (g_running) {
        // 模拟服务器主循环
        printf(".");
        fflush(stdout);
        sleep(1);
    }

    printf("\nClean shutdown complete.\n");
    return 0;
}

这段代码在 Linux 和 Windows 上都能编译运行。你不需要关心底层是信号还是 SEH,只需要实现一个回调函数就行。

总结一下:POSIX 信号和 Windows SEH 虽然长得不一样,但抽象到“异常回调”这个层面后,它们完全可以被统一封装。我建议你在项目初期就把这个封装层写好,后面所有模块都通过它来处理异常。这样不仅代码干净,而且移植时只需要改底层实现,上层逻辑纹丝不动。


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