崩溃检测与信号捕获:信号处理注册、核心转储触发、Watchdog 机制
程序在运行态发生非法内存访问、除零、栈溢出等严重错误时,操作系统内核会向进程投递 POSIX 信号。本章围绕三个核心环节展开:信号处理注册(捕获信号并执行自定义恢复逻辑)、核心转储触发(保存崩溃现场)、Watchdog 机制(超时检测与自愈)。三者在工程中形成“检测→快照→恢复”的基础闭环。
1.1 原理概述
崩溃检测与信号捕获的底层依赖是 Linux/POSIX 信号模型。信号由内核或进程发送,内核在目标进程的上下文中调用对应的处理函数。下表列出与程序崩溃强相关的关键信号:
| 信号 | 默认行为 | 典型触发场景 |
|---|---|---|
| SIGSEGV | 终止 + core dump | 访问非法内存地址、空指针解引用 |
| SIGABRT | 终止 + core dump | abort() 主动调用、断言失败 |
| SIGFPE | 终止 + core dump | 整数除零、浮点异常 |
| SIGBUS | 终止 + core dump | 未对齐内存访问、映射文件访问越界 |
| SIGILL | 终止 + core dump | 非法指令、二进制损坏 |
| SIGTERM | 终止 | 外部 kill 请求 |
Watchdog 机制则独立于信号模型,分为硬件 Watchdog(如 SoC 内置定时器)和软件 Watchdog(如用户态心跳线程)。当进程或系统无响应超时后,Watchdog 通过复位或重启完成边界恢复。
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–30s | abort / 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;
}
clock_gettime(CLOCK_MONOTONIC) 可获取毫秒级精度。
1.5 崩溃检测与信号捕获完整流程
以下 SVG 图展示了从程序崩溃到信号捕获、核心转储、Watchdog 恢复的工程闭环流程。图中包含 5 个核心阶段和 6 条关键跳转关系。
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=-1 | Kubernetes: 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 | 避免原子性窗口导致野指针 |