程序崩了不可怕,可怕的是崩了之后什么都没留下。
线上环境排查崩溃,核心就一个问题:崩溃现场能不能保住? 这篇文章聊两种主流方案 —— Google Breakpad 和 Kernel Coredump,它们各自怎么干活、什么时候会掉链子、该怎么选。
Breakpad:在进程咽气前抢救现场
Breakpad 的思路很直接:程序崩溃时内核会发信号(SIGSEGV、SIGABRT 等),Breakpad 提前注册了信号处理函数,在进程真正死掉之前,抓紧把调用栈、寄存器、关键内存区域写进一个 minidump 文件。
整个过程大致是这样的:
程序启动 → 注册信号处理函数
↓
程序崩溃 → 内核发送信号(如 SIGSEGV)
↓
Breakpad 信号处理函数接管
├─ 进程暂停但尚未终止,内存状态还在
├─ 遍历调用栈、收集寄存器状态
└─ 写入 .dmp 文件(通常只有几 MB)
↓
恢复默认信号处理,重新 raise 信号 → 进程退出
一个最小的接入示例:
#include "client/linux/handler/exception_handler.h"
bool DumpCallback(const google_breakpad::MinidumpDescriptor& descriptor,
void* context, bool succeeded) {
// 注意:这里只能用 async-signal-safe 的函数
const char msg[] = "Crash dump saved\n";
write(STDERR_FILENO, msg, sizeof(msg) - 1);
return succeeded;
}
int main() {
google_breakpad::MinidumpDescriptor descriptor("/var/crash");
google_breakpad::ExceptionHandler eh(descriptor, NULL,
DumpCallback, NULL,
true, -1);
// 你的业务代码
run_application();
return 0;
}Breakpad 的优点很明显:dump 文件小(几 MB vs. 动辄 GB 的 core 文件)、跨平台、可以过滤敏感数据、方便离线分析。但它有一个本质弱点 —— 它是在”患者”体内做手术。
Breakpad 靠不住的时候
既然 Breakpad 跑在用户态、依赖进程自身的环境,那进程状态一旦烂透了,它也就无能为力了。常见的翻车场景:
栈溢出 —
信号处理函数也需要栈空间来执行。栈都爆了,处理函数根本跑不起来。可以通过
sigaltstack 设置备用栈来缓解:
stack_t ss;
ss.ss_sp = malloc(SIGSTKSZ);
ss.ss_size = SIGSTKSZ;
ss.ss_flags = 0;
sigaltstack(&ss, NULL);回调中二次崩溃 — 如果 DumpCallback
里不小心触发了新的段错误,系统不允许嵌套处理同一信号,进程直接死亡,dump
文件可能写到一半就废了。所以回调里绝对不能用
malloc、printf、std::string
这些东西,只能用
write、open、close、_exit
这类 async-signal-safe 的函数。
SIGKILL / OOM Killer — kill -9 和 OOM
Killer 发出的是 SIGKILL,压根不经过信号处理函数,Breakpad
连执行的机会都没有。
内存严重损坏 — double free 破坏了堆的元数据、buffer overflow 覆盖了栈帧链表… 这些情况下 Breakpad 遍历调用栈时拿到的数据本身就是错的,生成的 dump 要么不完整,要么有误导性。
多线程竞态 — 多个线程同时崩溃,竞争写 minidump,可能产出损坏的文件。
还有一些边缘情况:dump 目录没权限、磁盘满了、容器沙箱禁用了某些系统调用… 总之,Breakpad 需要一个”还算健康”的进程环境,才能完成它的工作。
Kernel Coredump:从外面给进程拍遗照
Kernel Coredump 的思路完全不同:内核捕获到异常后,自己从外部读取进程的整个内存空间,写成 core 文件。进程的代码完全不参与这个过程。
用前面的比喻来说:
Breakpad 是让溺水者自己写遗书 —— 如果他已经失去意识,就没戏了。
Kernel Coredump 是法医做尸检 —— 不管遗体什么状态,都能检查。
这解释了为什么 Kernel Coredump 在很多极端场景下更可靠:
- 栈溢出? 内核跑在自己的内核栈上,用户栈爆了不影响它。
- 内存损坏?
内核只是遍历进程的虚拟内存区域(VMA)逐页
dump,不关心堆的元数据是不是完好。遇到损坏的页面,
copy_from_user返回错误,跳过继续。 - 二次崩溃? 不存在的。内核代码跑在保护模式下,和用户进程的状态完全隔离。
- 权限? 内核以最高权限运行,不受 ulimit、文件权限的限制。
// 内核 do_coredump 的核心逻辑(极度简化)
do_coredump() {
// 跑在内核栈上,不依赖用户栈
struct mm_struct *mm = current->mm;
// 遍历所有虚拟内存区域,逐页 dump
for (vma = mm->mmap; vma; vma = vma->vm_next) {
dump_range(vma->vm_start, vma->vm_end);
}
// 就是给整个内存空间"拍照"
}但 Kernel Coredump 也有自己的硬伤:
| 问题 | 说明 |
|---|---|
| 文件巨大 | 进程用了 10GB 内存,core 文件就是 10GB |
| 生成慢 | 写一个大 core 文件可能要几十秒 |
| 隐私风险 | 完整内存 dump,strings core \| grep password
就能翻出敏感数据 |
| 分析不便 | 需要原始可执行文件 + 调试符号,还得在同架构机器上用 gdb |
而 SIGKILL 这种信号,Kernel Coredump 同样无法处理 —— 这是设计如此,SIGKILL 的语义就是”立即终止,不留后路”。
两者对比一览
| 维度 | Breakpad | Kernel Coredump |
|---|---|---|
| 执行环境 | 用户态,进程内 | 内核态,进程外 |
| 栈溢出 | 通常失败(除非配了备用栈) | 正常工作 |
| 内存损坏 | 可能失败或产出错误数据 | 不受影响 |
| 二次崩溃 | 致命 | 不存在此问题 |
| SIGKILL | 无法捕获 | 同样无法处理 |
| 文件大小 | 几 MB | 可达 GB 级别 |
| 生成速度 | 快 | 慢 |
| 跨平台 | 支持 | 平台相关 |
| 隐私控制 | 可过滤敏感数据 | 全量 dump |
| 离线分析 | 方便,有专用工具链 | 需要匹配环境 |
退出码:读懂进程的死因
当进程被信号终止时,退出码 = 128 + 信号编号。这个规则很实用,直接从退出码就能判断死因:
| 退出码 | 信号 | 含义 | Breakpad 能捕获? |
|---|---|---|---|
| 132 | SIGILL | 非法指令 | 能 |
| 133 | SIGTRAP | 断点陷阱 | 能 |
| 134 | SIGABRT | 主动 abort | 能 |
| 135 | SIGBUS | 总线错误 | 能 |
| 136 | SIGFPE | 浮点异常 | 能 |
| 137 | SIGKILL | 强制终止 | 不能 |
| 139 | SIGSEGV | 段错误 | 能 |
Breakpad 在生成 minidump 后,会恢复默认信号处理再
raise(sig),让进程以原始信号退出——所以外部看到的退出码和没装
Breakpad 时是一样的,监控脚本不用做特殊适配。
一个简单的监控示例:
#!/bin/bash
./my_program
EXIT_CODE=$?
case $EXIT_CODE in
0) echo "正常退出" ;;
134) echo "SIGABRT - 检查 assert 或主动 abort" ;;
137) echo "SIGKILL - 被强杀,无 minidump" ;;
139) echo "SIGSEGV - 段错误,查看 minidump" ;;
*)
if [ $EXIT_CODE -ge 128 ]; then
echo "被信号 $((EXIT_CODE - 128)) 终止"
else
echo "异常退出,退出码:$EXIT_CODE"
fi ;;
esac生产环境怎么选?
大多数情况下,Breakpad 是首选。 文件小、传输快、隐私可控,适合线上大规模部署。
但最稳妥的做法是两者都开:Breakpad 作为主力生成小巧的 minidump 用于快速分析,Kernel Coredump 作为兜底——Breakpad 翻车的时候,至少还有 core 文件可以捞。
int main() {
// 兜底:启用 kernel coredump
struct rlimit rl = { RLIM_INFINITY, RLIM_INFINITY };
setrlimit(RLIMIT_CORE, &rl);
// 主力:初始化 Breakpad
setup_alternate_stack(); // 别忘了备用栈
google_breakpad::MinidumpDescriptor descriptor("/var/crash");
google_breakpad::ExceptionHandler eh(descriptor, NULL,
DumpCallback, NULL,
true, -1);
return run_application();
}配套的运维工作也别忘了:
# dump 目录权限
mkdir -p /var/crash && chown myapp:myapp /var/crash
# kernel coredump 路径配置
echo "/var/crash/core.%e.%p.%t" | sudo tee /proc/sys/kernel/core_pattern
# 自动清理(只保留 7 天)
find /var/crash -name "*.dmp" -mtime +7 -delete
# 构建时生成符号文件,供离线分析
dump_syms ./myapp > myapp.sym最后一条建议:定期测试 dump 功能。
不要等到线上真崩了才发现 dump 目录没权限、磁盘满了、符号文件没更新。用
eh.WriteMinidump() 主动触发一次,确认整条链路是通的。