15、信号与异常处理:跨平台信号处理、结构化异常(SEH)封装、崩溃捕获
说到信号和异常处理,我脑子里立刻浮现出几年前的一个深夜。服务器在线上突然挂了,查了半天日志,最后发现是一个野指针在 Linux 上触发了 SIGSEGV,而 Windows 上同样的逻辑却悄无声息地崩溃了。嗯,从那天起,我就下定决心要把跨平台的异常处理机制彻底搞透。
你想想看,一个 C++ 程序跑在不同的操作系统上,遇到非法内存访问、除零错误、Ctrl+C 中断,表现可能完全不一样。Windows 有它的结构化异常处理(SEH),Linux/Unix 用的是信号机制。作为跨平台开发者,我们得把这些差异统一封装起来。
15.1 信号处理基础:POSIX 信号
在 Linux 和 macOS 上,信号是操作系统通知进程的一种方式。常见的信号有:
| 信号 | 默认行为 | 典型场景 |
|---|---|---|
| SIGSEGV | 终止进程 + 核心转储 | 访问非法内存地址 |
| SIGFPE | 终止进程 + 核心转储 | 除零或浮点异常 |
| SIGABRT | 终止进程 + 核心转储 | 调用 abort() |
| SIGINT | 终止进程 | 用户按下 Ctrl+C |
| SIGTERM | 终止进程 | kill 命令默认发送 |
注册信号处理函数,用的是 signal() 或更推荐的 sigaction()。我个人习惯用 sigaction(),因为它更可控,行为也更一致。
#include <csignal>
#include <cstring>
#include <unistd.h>
void crash_handler(int sig, siginfo_t* info, void* context) {
// 注意:信号处理函数中能做的事非常有限
const char* msg = "捕获到崩溃信号!\n";
write(STDERR_FILENO, msg, strlen(msg));
// 这里可以尝试记录堆栈信息
// 但绝对不能调用 malloc、printf 等非异步安全函数
// 恢复默认处理并重新发送信号,让系统生成 core dump
signal(sig, SIG_DFL);
raise(sig);
}
void setup_signal_handlers() {
struct sigaction sa;
memset(&sa, 0, sizeof(sa));
sa.sa_sigaction = crash_handler;
sa.sa_flags = SA_SIGINFO | SA_NODEFER;
sigaction(SIGSEGV, &sa, nullptr);
sigaction(SIGFPE, &sa, nullptr);
sigaction(SIGABRT, &sa, nullptr);
}
free(),结果直接死锁了。能用的函数就那么几个:write()、sigaction()、_exit() 等。别想着打日志、写文件,那都是坑。
15.2 Windows 结构化异常处理(SEH)
Windows 的 SEH 是另一套体系。它用 __try/__except 和 __try/__finally 来捕获硬件异常。说白了,就是 Windows 版的 try-catch,但捕获的是硬件级别的异常。
#include <windows.h>
#include <stdio.h>
void test_seh() {
__try {
// 故意触发访问违规
int* p = nullptr;
*p = 42;
}
__except(EXCEPTION_EXECUTE_HANDLER) {
printf("捕获到异常!异常码: 0x%X\n",
GetExceptionCode());
}
}
// 更高级的用法:自定义异常过滤器
LONG WINAPI custom_filter(EXCEPTION_POINTERS* ep) {
printf("异常地址: 0x%p\n", ep->ExceptionRecord->ExceptionAddress);
printf("异常码: 0x%X\n", ep->ExceptionRecord->ExceptionCode);
// 返回 EXCEPTION_EXECUTE_HANDLER 表示由 __except 处理
return EXCEPTION_EXECUTE_HANDLER;
}
void test_with_filter() {
__try {
int arr[10];
arr[100] = 0; // 栈溢出
}
__except(custom_filter(GetExceptionInformation())) {
printf("异常已处理\n");
}
}
这里有个细节:__except 后面的表达式可以是三种值:EXCEPTION_EXECUTE_HANDLER(处理异常)、EXCEPTION_CONTINUE_SEARCH(继续向上传播)、EXCEPTION_CONTINUE_EXECUTION(忽略异常继续执行)。最后那个我建议你慎用,除非你真的知道自己在干什么。
15.3 跨平台封装:统一异常捕获接口
好了,现在我们要把这两套东西封装成一个统一的接口。我的思路是这样的:
- 定义一个回调函数类型,用于处理崩溃事件
- 在 Linux 上用
sigaction()注册信号处理 - 在 Windows 上用
SetUnhandledExceptionFilter()注册顶层异常过滤器 - 在回调中尽量收集有用的上下文信息
// crash_handler.hpp
#pragma once
#include <functional>
#include <string>
namespace crossplat {
// 崩溃信息结构体
struct CrashInfo {
uint64_t exception_code;
void* exception_address;
std::string thread_name;
// 可以扩展更多字段
};
// 崩溃回调类型
using CrashCallback = std::function<void(const CrashInfo&)>;
// 初始化崩溃捕获
void init_crash_handler(CrashCallback callback);
// 获取当前堆栈(跨平台实现)
std::string get_stack_trace();
} // namespace crossplat
// crash_handler_linux.cpp
#include "crash_handler.hpp"
#include <csignal>
#include <cstring>
#include <unistd.h>
#include <execinfo.h>
namespace crossplat {
static CrashCallback g_callback;
void signal_handler(int sig, siginfo_t* info, void* context) {
CrashInfo ci;
ci.exception_code = sig;
ci.exception_address = info->si_addr;
if (g_callback) {
g_callback(ci);
}
// 恢复默认处理
signal(sig, SIG_DFL);
raise(sig);
}
void init_crash_handler(CrashCallback callback) {
g_callback = callback;
struct sigaction sa;
memset(&sa, 0, sizeof(sa));
sa.sa_sigaction = signal_handler;
sa.sa_flags = SA_SIGINFO | SA_NODEFER;
sigaction(SIGSEGV, &sa, nullptr);
sigaction(SIGFPE, &sa, nullptr);
sigaction(SIGABRT, &sa, nullptr);
sigaction(SIGILL, &sa, nullptr);
}
std::string get_stack_trace() {
void* buffer[128];
int frames = backtrace(buffer, 128);
char** symbols = backtrace_symbols(buffer, frames);
std::string result;
for (int i = 0; i < frames; ++i) {
result += symbols[i];
result += '\n';
}
free(symbols);
return result;
}
} // namespace crossplat
// crash_handler_win.cpp
#include "crash_handler.hpp"
#include <windows.h>
#include <dbghelp.h>
namespace crossplat {
static CrashCallback g_callback;
LONG WINAPI unhandled_exception_filter(EXCEPTION_POINTERS* ep) {
CrashInfo ci;
ci.exception_code = ep->ExceptionRecord->ExceptionCode;
ci.exception_address = ep->ExceptionRecord->ExceptionAddress;
if (g_callback) {
g_callback(ci);
}
return EXCEPTION_EXECUTE_HANDLER;
}
void init_crash_handler(CrashCallback callback) {
g_callback = callback;
SetUnhandledExceptionFilter(unhandled_exception_filter);
// 同时处理 C++ 异常
std::set_terminate([]() {
CrashInfo ci;
ci.exception_code = 0xE06D7363; // MSVC C++ 异常码
ci.exception_address = nullptr;
if (g_callback) {
g_callback(ci);
}
abort();
});
}
std::string get_stack_trace() {
// 使用 StackWalk64 获取堆栈
// 代码略长,这里展示核心逻辑
HANDLE process = GetCurrentProcess();
HANDLE thread = GetCurrentThread();
SymInitialize(process, nullptr, TRUE);
// ... 堆栈遍历代码
return "stack trace (Windows)";
}
} // namespace crossplat
SetUnhandledExceptionFilter 只对未处理的异常生效。如果你的代码里已经有 __try/__except 捕获了异常,那这个过滤器就不会被调用。所以建议在程序入口处尽早注册,并且确保所有线程都使用统一的异常处理策略。
15.4 崩溃信息收集与日志
光捕获崩溃还不够,我们得把现场信息保存下来。我个人习惯在回调中做以下几件事:
- 记录异常码和异常地址
- 获取当前线程的堆栈
- 记录系统版本、CPU 信息
- 将信息写入一个独立的崩溃日志文件
void my_crash_callback(const CrashInfo& info) {
// 注意:这里不能分配内存、不能加锁
// 最好用预先分配好的缓冲区
static char buffer[4096];
int len = snprintf(buffer, sizeof(buffer),
"异常码: 0x%llX\n"
"异常地址: 0x%p\n"
"堆栈:\n%s\n",
info.exception_code,
info.exception_address,
get_stack_trace().c_str());
// 写入文件描述符(预先打开)
write(crash_log_fd, buffer, len);
// 也可以尝试用 mmap 写入共享内存
// 让另一个进程来读取
}
核心原则:崩溃处理函数中,能做的事极其有限。不要调用任何可能分配内存、加锁、或者访问复杂数据结构的函数。最好的做法是:
- 用
write()写入文件描述符 - 用
mmap写入共享内存 - 用
sigqueue()通知另一个监控进程
15.5 知识体系总览
下面这张图展示了本章的核心知识结构,从底层机制到上层封装,一目了然:
15.6 避坑指南与最佳实践
做了这么多年跨平台开发,我总结了几条关于异常处理的铁律:
- 在信号处理函数中调用
printf导致死锁——改用write() - 忘记恢复默认信号处理,导致递归调用信号处理函数——用
SA_NODEFER标志 - Windows 上
__except中访问无效指针——异常过滤器里也要小心 - 多线程环境下,一个线程崩溃其他线程还在跑——考虑用
pthread_kill通知所有线程
最后说一句:崩溃捕获是最后一道防线,别指望它能解决所有问题。真正靠谱的做法是:
- 写好单元测试,覆盖边界情况
- 用静态分析工具检查代码
- 在开发阶段启用 AddressSanitizer
- 上线前做充分的压力测试
嗯,这一章的内容就到这里。记住,异常处理不是为了掩盖 bug,而是为了在 bug 发生时,我们能拿到足够的信息去修复它。