线上环境排查崩溃,核心问题是:崩溃现场能不能保住? 本文介绍两种主流方案,Google Breakpad 和 Kernel Coredump,分别说明它们的工作原理、局限性,以及如何选型。


Breakpad:用户态信号处理方案

Breakpad 的工作方式是:在程序启动时注册信号处理函数,当进程收到致命信号(SIGSEGV、SIGABRT 等)时,在进程终止之前,将调用栈、寄存器状态、关键内存区域写入一个 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 运行在用户态,依赖进程自身的执行环境。当进程状态已经严重损坏时,Breakpad 无法正常工作。具体有这些场景:

  • 回调中二次崩溃

    如果 DumpCallback 内部触发了新的段错误,内核不允许嵌套处理同一信号,进程立即终止,dump 文件可能不完整。因此回调中禁止使用 mallocprintfstd::string 等非信号安全函数,只能使用 writeopenclose_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 由内核直接完成:内核捕获到致命信号后,从内核态读取进程的整个虚拟地址空间,写成 ELF 格式的 core 文件。进程自身的代码不参与这个过程。

这种设计上的差异决定了它在极端场景下的可靠性:

栈溢出。 内核使用独立的内核栈执行 dump 操作,不依赖用户栈。

内存损坏。 内核逐页遍历进程的虚拟内存区域(VMA),不依赖堆的元数据结构。遇到无法读取的页面,copy_from_user 返回错误后跳过,继续处理剩余区域。

二次崩溃。 不存在此问题。内核代码运行在独立的地址空间,与用户进程完全隔离。

权限。 内核以特权模式运行,不受 ulimit 和文件系统权限的约束。

// 内核 do_coredump 的核心逻辑(极度简化)
do_coredump() {
    struct mm_struct *mm = current->mm;

    // 遍历所有虚拟内存区域,逐页写入
    for (vma = mm->mmap; vma; vma = vma->vm_next) {
        dump_range(vma->vm_start, vma->vm_end);
    }
}

Kernel Coredump 的代价:

问题 说明
文件巨大 进程使用 10GB 内存,core 文件就是 10GB
生成慢 写入大型 core 文件可能耗时数十秒
隐私风险 完整内存转储,strings core \| grep password 可提取敏感数据
分析门槛高 需要原始可执行文件 + 调试符号,且必须在同架构环境下用 gdb 分析

需要注意的是,SIGKILL 信号 Kernel Coredump 同样无法处理。SIGKILL 的语义是”立即终止,不可捕获、不可忽略”,内核不会为其生成 core 文件。


对比

维度 Breakpad Kernel Coredump
执行环境 用户态,进程内 内核态,进程外
栈溢出 通常失败(除非配置了备用栈) 正常工作
内存损坏 可能失败或产出错误数据 不受影响
二次崩溃 致命 不存在此问题
SIGKILL 无法捕获 同样无法处理
文件大小 几 MB 可达 GB 级别
生成速度
跨平台 支持 平台相关
隐私控制 可过滤敏感数据 全量转储
离线分析 有专用工具链(minidump_stackwalk) 需要匹配的调试环境

退出码与信号的对应关系

进程被信号终止时,退出码 = 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 失效的场景。

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 功能是否正常。 可以通过 eh.WriteMinidump() 主动触发一次 dump,确认目录权限、磁盘空间、符号文件等整条链路都是可用的。


References