1. Crash信号捕获机制与拦截点配置
Crash信号捕获是进程级异常拦截的第一道防线。当程序访问非法地址、执行非法指令或主动触发中止时,内核会向进程投递SIGSEGV、SIGABRT等信号。本章围绕信号注册、sigaction配置、关键信号拦截与拦截点优先级,拆解如何构建稳定、可调试的信号处理体系。
1.1 信号处理基础与注册入口
Posix信号处理提供两套API:旧的signal()和推荐的sigaction()。signal()行为因实现而异(SysV vs BSD),且不支持信号掩码、标志位精细控制。生产级Crash拦截必须统一使用sigaction()。
| 函数 | 可移植性 | 信号掩码控制 | 标志位支持 | 推荐场景 |
|---|---|---|---|---|
signal() | 弱(Unspecified语义) | 无 | 无 | 简单Demo |
sigaction() | 强(Posix标准) | 通过sa_mask | SA_SIGINFO、SA_NODEFER等 | 生产系统 |
核心原则:任何Crash信号处理函数必须遵守“异步信号安全”约束,只允许调用异步信号安全函数(如
write()、sigprocmask()、原子操作),禁止使用malloc()、printf()、互斥锁等。
1.2 sigaction结构与配置要点
sigaction结构体控制处理行为的全部细节:
struct sigaction {
void (*sa_handler)(int); // 无SIGINFO时使用
void (*sa_sigaction)(int, siginfo_t *, void *);
sigset_t sa_mask; // 处理期间阻塞的信号集
int sa_flags; // 标志位组合
void (*sa_restorer)(void); // 已废弃,置NULL
};
关键标志位:
SA_SIGINFO— 使用sa_sigaction获取额外信息(信号编号、发信号进程PID、用户态/内核态地址)。Crash拦截强烈建议开启,便于获取触发异常的内存地址。SA_NODEFER— 处理函数执行期间不自动阻塞相同信号。默认行为:进入处理函数时自动阻塞该信号(防止递归),但SA_NODEFER允许递归信号——对Crash信号极不推荐,可能造成无限递归。SA_RESTART— 被信号中断的系统调用自动重启动。对Crash拦截通常无需设置,因为Crash信号后线程不会再恢复。SA_ONSTACK— 使用替代信号栈(sigaltstack)。当进程栈溢出时,此标志保证信号处理函数有独立栈可用,是SIGSEGV拦截的关键配置之一。
1.2.1 典型SIGSEGV拦截配置
struct sigaction sa;
memset(&sa, 0, sizeof(sa));
sa.sa_sigaction = crash_signal_handler;
sa.sa_flags = SA_SIGINFO | SA_ONSTACK | SA_NODEFER; // 注意:SA_NODEFER需谨慎
sigemptyset(&sa.sa_mask);
if (sigaction(SIGSEGV, &sa, NULL) == -1) {
perror("sigaction SIGSEGV");
exit(1);
}
危险配置:若同时设置
SA_NODEFER且处理函数中出现新的SIGSEGV(如访问空指针),将导致递归死循环。建议Crash信号不设置SA_NODEFER,让内核自动屏蔽该信号,保障至少一次处理安全。
1.3 SIGSEGV / SIGABRT 拦截差异
| 信号 | 触发条件 | 拦截后典型操作 | 特殊隐患 |
|---|---|---|---|
| SIGSEGV | 无效内存访问(空指针、写只读页) | 记录siginfo_t->si_addr,调用_exit()或siglongjmp()返回安全点 | 栈指针损坏时无法执行处理函数;递归信号 |
| SIGABRT | abort()、assert()失败、double free(glibc) | 冲刷日志、生成core dump、_exit()退出 | 处理函数中再次调用abort()(如日志输出失败)形成连环abort |
| SIGBUS | 硬件错误(未对齐访问、物理地址不存在) | 与SIGSEGV类似,但si_code不同 | 不常见,但同样需要拦截 |
对SIGABRT拦截时要特别小心:abort()会先尝试触发SIGABRT并允许处理函数执行;若处理函数返回(sighandler return),abort()会再次发送信号并强制终止。因此拦截SIGABRT的处理函数不能返回,应直接_exit()或siglongjmp()。
1.4 拦截点优先级与冲突解决
一个进程内可能存在多个模块(动态库、框架、用户代码)分别注册信号。拦截点的“最后安装者生效”原则导致不确定性。需建立明确的优先级策略:
- 系统核心层(coredump生成、守护进程框架) — 最后注册,拥有最高优先级。
- 中间件/第三方库 — 应当使用
sigaction保存旧处理函数(oldact),并在自己处理完后酌情调用旧处理函数形成链式调用。 - 应用层Crash拦截 — 通常使用
SA_NODEFER+替代栈,避免被其他处理函数干扰。
实现链式调用的关键:
static struct sigaction old_sa;
static void my_handler(int sig, siginfo_t *info, void *context) {
// 自己的Crash记录逻辑
// ...
// 调用旧处理(若有)
if (old_sa.sa_flags & SA_SIGINFO) {
if (old_sa.sa_sigaction)
old_sa.sa_sigaction(sig, info, context);
} else if (old_sa.sa_handler) {
old_sa.sa_handler(sig);
}
}
// 注册时保存旧动作
sigaction(SIGSEGV, &my_sa, &old_sa);
调试技巧:在GDB中设置
handle SIGSEGV nostop noprint可以观察Crash处理行为而不被调试器拦截;使用info signals查看当前信号配置。运行时通过/proc/<pid>/status中的SigCgt字段验证哪些信号已被用户态注册。
1.5 性能与稳定性优化要点
- 最小化处理函数代码:只做原子写入(如日志文件描述符)、设置状态标志、调用
_exit()。避免任何可能触发内存分配或锁的操作。 - 使用替代信号栈:通过
sigaltstack()分配专用栈,避免进程栈耗尽导致处理函数无法执行。 - 避免信号嵌套:在
sa_mask中阻塞本信号及其他Crash相关信号,防止处理期间二次中断。 - 考虑线程安全:多线程环境下使用
pthread_sigmask()控制线程级信号掩码,确保Crash处理只在一个特定线程(如监控线程)中执行。
1.6 信号拦截流程图
1.7 常见错误排查清单
| 错误 | 典型表现 | 根因 | 修复 |
|---|---|---|---|
| 处理函数内部coredump | Crash日志未记录,进程直接终止 | 处理函数调用了非异步信号安全函数(如printf)导致二次Crash | 只使用write()、原子操作;使用sigaction的sa_mask屏蔽信号 |
| 递归信号无限循环 | CPU 100%,日志重复 | 设置了SA_NODEFER且处理函数再次触发相同信号 | 移除SA_NODEFER或增加递归计数后_exit() |
| SIGABRT处理后仍被终止 | 总是最后退出码为139+ | 处理函数return导致abort()再次发送信号 | 处理函数中直接_exit()或siglongjmp |
| 多线程SIGSEGV仅主线程捕获 | 子线程Crash不触发处理函数 | 未使用pthread_sigmask设置线程信号掩码,信号被默认发送到主线程 | 在Crash处理线程中统一设置sigaction,其他线程阻塞该信号 |
1.8 本章小结
Crash信号拦截的核心在于:正确使用sigaction配置、理解信号掩码与标志位的相互作用、严格遵循异步信号安全编码规范。下一阶段将深入替代信号栈的实现与栈回溯采集,确保在栈损坏时仍能可靠捕获Crash现场。