崩溃检测与信号捕获:信号处理注册、核心转储触发、Watchdog 机制

程序在运行态发生非法内存访问、除零、栈溢出等严重错误时,操作系统内核会向进程投递 POSIX 信号。本章围绕三个核心环节展开:信号处理注册(捕获信号并执行自定义恢复逻辑)、核心转储触发(保存崩溃现场)、Watchdog 机制(超时检测与自愈)。三者在工程中形成“检测→快照→恢复”的基础闭环。

1.1 原理概述

崩溃检测与信号捕获的底层依赖是 Linux/POSIX 信号模型。信号由内核或进程发送,内核在目标进程的上下文中调用对应的处理函数。下表列出与程序崩溃强相关的关键信号:

信号默认行为典型触发场景
SIGSEGV终止 + core dump访问非法内存地址、空指针解引用
SIGABRT终止 + core dumpabort() 主动调用、断言失败
SIGFPE终止 + core dump整数除零、浮点异常
SIGBUS终止 + core dump未对齐内存访问、映射文件访问越界
SIGILL终止 + core dump非法指令、二进制损坏
SIGTERM终止外部 kill 请求

Watchdog 机制则独立于信号模型,分为硬件 Watchdog(如 SoC 内置定时器)和软件 Watchdog(如用户态心跳线程)。当进程或系统无响应超时后,Watchdog 通过复位或重启完成边界恢复。

工程经验:生产环境中,SIGSEGV 和 SIGABRT 占到崩溃捕获需求的 80% 以上。优先为这两个信号注册恢复处理函数,并在其中完成日志刷新、指标上报、资源清理等轻量操作。

1.2 信号处理注册:signal 与 sigaction 对比

POSIX 提供两代 API。传统 signal() 语义因历史实现差异(System V 与 BSD 行为不同)在可移植场景下不推荐;现代工程选用 sigaction() 以获得精确控制和可移植性。

特性signal()sigaction()
标准化ANSI C,语义随平台变化POSIX.1,行为明确
信号掩码控制不支持支持设置临时掩码,避免嵌套
标志位无SA_RESTART、SA_NODEFER、SA_SIGINFO 等
获取旧处理函数返回旧函数指针通过 oldact 参数获取
多线程安全不可靠线程级掩码,配合 pthread_sigmask

以下代码展示使用 sigaction 注册 SIGSEGV 和 SIGABRT 捕获的工程模板:

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

static void crash_handler(int sig, siginfo_t *info, void *context) {
    // 1. 立即恢复默认行为,防止递归崩溃
    signal(sig, SIG_DFL);

    // 2. 记录崩溃原因
    fprintf(stderr, "[CRASH] Signal %d (%s) at addr %p\n",
            sig, sig == SIGSEGV ? "SIGSEGV" : "SIGABRT", info->si_addr);

    // 3. 触发核心转储(重新发送自身信号)
    raise(sig);
}

void register_crash_handlers(void) {
    struct sigaction sa;
    sa.sa_sigaction = crash_handler;
    sigemptyset(&sa.sa_mask);
    sa.sa_flags = SA_SIGINFO | SA_NODEFER;

    sigaction(SIGSEGV, &sa, NULL);
    sigaction(SIGABRT, &sa, NULL);
}

int main() {
    register_crash_handlers();
    // 业务逻辑...
    return 0;
}
⚠ 信号处理函数内危险操作:严禁调用 printf(非异步信号安全)、malloc、free、mutex_lock。应使用 write()、sigqueue()、siginfo_t 等异步信号安全函数。

1.3 核心转储触发与控制

核心转储(core dump)是崩溃现场的内存快照,包含栈、堆、寄存器状态。生产环境需精细控制转储策略以避免磁盘爆满和敏感数据泄露。

1.3.1 配置核心转储

# 设置 core 文件大小上限(单位:字节;unlimited 表示不限制)
ulimit -c unlimited

# 查看当前限制
ulimit -c

# 持久化配置:/etc/security/limits.conf
*               soft    core            unlimited
*               hard    core            unlimited

1.3.2 自定义转储路径与文件名

通过 /proc/sys/kernel/core_pattern 控制核心文件的输出路径和命名格式:

# 格式示例:/var/crash/core-%e-%p-%t
# %e 可执行文件名,%p PID,%t 时间戳(UNIX秒)
echo "/var/crash/core-%e-%p-%t" > /proc/sys/kernel/core_pattern

# 若核心文件需要被管道交给处理程序(如 systemd-coredump)
# |/usr/lib/systemd/systemd-coredump %P %u %g %s %t %c %h

1.3.3 编程触发核心转储

#include <sys/resource.h>
#include <unistd.h>

void enable_core_dump(void) {
    struct rlimit rl;
    rl.rlim_cur = RLIM_INFINITY;
    rl.rlim_max = RLIM_INFINITY;
    setrlimit(RLIMIT_CORE, &rl);
}

// 在信号处理函数中调用 abort() 或 re-raise 信号以生成 core
void force_core_dump(void) {
    abort();  // 生成 SIGABRT + core dump
}

1.4 Watchdog 机制

Watchdog 分为硬件级(SoC 内部定时器)和软件级(进程内心跳线程)。工程实践中,软件 Watchdog 更灵活,广泛应用于服务框架和嵌入式用户态。

类型粒度依赖典型超时恢复动作
硬件 Watchdog系统级内核驱动 & 设备树1–60s硬件复位
软件 Watchdog(基于信号)进程级定时器 + 信号100ms–30sabort / restart
软件 Watchdog(基于心跳)线程级独立监视线程用户自定义回调 + graceful shutdown

1.4.1 软件 Watchdog 实现模式

#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <unistd.h>

#define WATCHDOG_TIMEOUT_SEC 5

static volatile int g_heartbeat = 0;

// 监视线程——定时检查心跳
void *watchdog_thread(void *arg) {
    while (1) {
        sleep(1);
        if (--g_heartbeat < 0) {
            fprintf(stderr, "[WATCHDOG] Heartbeat lost, triggering recovery\n");
            // 执行恢复策略:重启进程 / 通知管理面 / 记录事件
            exit(EXIT_FAILURE);
        }
    }
    return NULL;
}

// 业务线程——定期“喂狗”
void *worker_thread(void *arg) {
    while (1) {
        // 业务逻辑...
        g_heartbeat = WATCHDOG_TIMEOUT_SEC;  // 重置计数器
        sleep(2);
    }
    return NULL;
}

void start_watchdog(void) {
    pthread_t tid;
    pthread_create(&tid, NULL, watchdog_thread, NULL);
    pthread_detach(tid);
}

int main() {
    start_watchdog();
    // 启动 worker 线程...
    return 0;
}
优化要点:软件 Watchdog 的心跳计数器建议设为连续倒计数(每 tick 减一)而非布尔标志,可更精确地检测“喂狗延迟堆积”。搭配 clock_gettime(CLOCK_MONOTONIC) 可获取毫秒级精度。

1.5 崩溃检测与信号捕获完整流程

以下 SVG 图展示了从程序崩溃到信号捕获、核心转储、Watchdog 恢复的工程闭环流程。图中包含 5 个核心阶段和 6 条关键跳转关系。

崩溃检测与信号捕获工程闭环 程序运行时崩溃 (SIGSEGV/SIGABRT) 内核信号投递 (sigaction 注册) 用户处理函数 (crash_handler) 核心转储 core (保存现场) Watchdog 恢复 (重启/自愈) 内核捕获异常 调用注册句柄 raise(sig) 进程终止 Watchdog 超时 ⚠ 递归崩溃风险 处理函数内需立即恢复默认

1.6 调试与验证

信号处理函数注册后,必须通过实际触发来验证行为是否符合预期。推荐使用以下调试手段:

  • 注入测试信号:kill -SEGV <PID> / kill -ABRT <PID> 验证处理函数是否被调用。
  • GDB 捕获信号:使用 handle SIGSEGV nostop noprint 让信号传递给程序,观察处理流程。
  • strace 跟踪系统调用:strace -e signal -p <PID> 实时观察信号投递和处理。
  • 核心转储测试:执行 ulimit -c unlimited && ./your_app,触发崩溃后检查 core 文件是否生成。
# 注入 SIGSEGV 测试
pid=$(pgrep -x your_app)
kill -SEGV $pid

# 验证核心文件是否生成(若配置 core_pattern)
ls -lh /var/crash/core-*

# 使用 GDB 分析 core 文件
gdb ./your_app /var/crash/core-your_app-12345-1700000000
(gdb) bt full
(gdb) info registers

1.7 部署注意事项

环境配置项建议值原因
裸金属 / 虚拟机/proc/sys/kernel/core_pattern/var/crash/core-%e-%p-%t隔离 core 文件,避免覆盖
容器--ulimit core=-1Kubernetes: securityContext: procMount: Default容器内需显式允许 core dump
嵌入式Watchdog 超时实际业务 SLA × 1.5避免正常慢路径被误杀
高安全环境core dump 内容限制设置 /proc/sys/kernel/core_pipe_limit防止敏感内存数据泄露

1.8 常见工程错误

  • 信号处理函数中使用全局锁:若锁已被崩溃线程持有,会导致死锁。改用自旋锁 + atomic 标志或异步信号安全队列。
  • 未恢复默认信号行为:递归崩溃时处理函数又被调一次,极易栈溢出。
  • core_pattern 指向不可写路径:容器内未挂载 /var/crash 或权限不足,导致核心转储静默丢失。
  • Watchdog 心跳在 IO 阻塞中停止:需要使用独立的监视线程,避免业务 IO 阻塞影响喂狗。
  • 多线程环境下仅注册主线程信号掩码:子线程需通过 pthread_sigmask 统一管理信号掩码。

1.9 性能与稳定性优化

优化方向方法收益
减少信号处理开销处理函数内只做必要记录,通过共享环形缓冲区将日志外抛降低对业务线程的冲击
避免 core dump IO 冲击限制 core 文件大小;使用 core_pipe_limit 控制并发转储数防止磁盘 IO 打满导致级联故障
Watchdog 自愈降级首次超时尝试 graceful restart,连续超时触发 full reset减少频繁重启造成的可用性损失
信号注册幂等性初始化阶段一次性注册,运行时不再重复调用 sigaction避免原子性窗口导致野指针
生产级建议:在服务框架初始化早期(加载配置后、启动业务线程前)完成信号处理和 Watchdog 的注册。核心转储路径建议与监控系统联动,当 core 文件产生时自动触发告警和归档,避免残留在磁盘上消耗空间。