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 知识体系总览
下面这张图帮你理清本章的核心逻辑:
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