1. Crash信号捕获机制与拦截点配置

Crash信号捕获是进程级异常拦截的第一道防线。当程序访问非法地址、执行非法指令或主动触发中止时,内核会向进程投递SIGSEGV、SIGABRT等信号。本章围绕信号注册、sigaction配置、关键信号拦截与拦截点优先级,拆解如何构建稳定、可调试的信号处理体系。

1.1 信号处理基础与注册入口

Posix信号处理提供两套API:旧的signal()和推荐的sigaction()。signal()行为因实现而异(SysV vs BSD),且不支持信号掩码、标志位精细控制。生产级Crash拦截必须统一使用sigaction()。

函数可移植性信号掩码控制标志位支持推荐场景
signal()弱(Unspecified语义)无无简单Demo
sigaction()强(Posix标准)通过sa_maskSA_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()返回安全点栈指针损坏时无法执行处理函数;递归信号
SIGABRTabort()、assert()失败、double free(glibc)冲刷日志、生成core dump、_exit()退出处理函数中再次调用abort()(如日志输出失败)形成连环abort
SIGBUS硬件错误(未对齐访问、物理地址不存在)与SIGSEGV类似,但si_code不同不常见,但同样需要拦截

对SIGABRT拦截时要特别小心:abort()会先尝试触发SIGABRT并允许处理函数执行;若处理函数返回(sighandler return),abort()会再次发送信号并强制终止。因此拦截SIGABRT的处理函数不能返回,应直接_exit()或siglongjmp()。

1.4 拦截点优先级与冲突解决

一个进程内可能存在多个模块(动态库、框架、用户代码)分别注册信号。拦截点的“最后安装者生效”原则导致不确定性。需建立明确的优先级策略:

  1. 系统核心层(coredump生成、守护进程框架) — 最后注册,拥有最高优先级。
  2. 中间件/第三方库 — 应当使用sigaction保存旧处理函数(oldact),并在自己处理完后酌情调用旧处理函数形成链式调用。
  3. 应用层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 信号拦截流程图

Crash信号捕获与拦截点配置流程 Crash信号拦截点配置流程 进程正常运行 内核投递Crash信号 sigaction结构检查 用户处理函数执行 _exit() / siglongjmp / 链式调用 处理函数必须异步信号安全,优先使用 SA_ONSTACK + 屏蔽递归信号

1.7 常见错误排查清单

错误典型表现根因修复
处理函数内部coredumpCrash日志未记录,进程直接终止处理函数调用了非异步信号安全函数(如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现场。