Linux core dump 崩溃分析实战:coredumpctl 收集、gdb 读栈与段错误根因定位

进程莫名消失了,日志里什么都没留下

半夜收到一条告警说网站 502,你登录上去一看:PHP-FPM 主进程还在,但子进程数量不对;或者更典型的场景——那个每天定时跑的数据处理脚本昨晚应该跑完的,早上发现它根本没结束,日志停在半路;再或者,MySQL 偶尔重启一次,error log 里只有一行 mysqld got signal 11,然后什么都没有了。

这类「进程突然死了但没留下明确错误」的问题,靠看应用日志往往查不出根因,因为进程是被操作系统用信号杀掉的,应用来不及写日志。这正是 core dump(核心转储)要解决的场景:进程崩溃的瞬间,内核把该进程的完整内存镜像写到磁盘上,事后你可以用调试器回放崩溃现场,看到当时调用栈是什么、哪一行代码、哪个变量出了问题。

本文讲清楚三件事:core dump 的原理与开启方式、systemd 时代 coredumpctl 的现代用法、以及用 gdb 从转储里读出崩溃根因的实战流程。全程给可直接复制的命令,并标注个人站长最常踩的坑。

core dump 是怎么产生的

当一个进程收到某些信号(默认终止类)时,Linux 内核会做一件事:如果该进程的 resource limit 里 core file size 不为 0,就把进程的内存映像写成一个 core 文件。触发转储的典型信号包括:

  • SIGSEGV (11)——段错误,访问了非法内存地址,C/C++/Rust 程序崩溃最常见的原因。
  • SIGABRT (6)——程序主动调用 abort(),比如断言失败(assert)、glibc 检测到堆损坏、C++ 抛出了未捕获异常。
  • SIGFPE (8)——算术异常,例如整数除以零。
  • SIGILL (4)——非法指令,通常是二进制与 CPU 架构不匹配,或者内存中的代码被破坏。
  • SIGBUS (7)——总线错误,常见于内存映射文件被截断后继续访问。

注意 SIGKILL (9) 和 SIGTERM (15) 不会产生 core。所以如果进程是被 kill -9 或者 OOM Killer 杀掉的,你查 core dump 是查不到的——这种情况要去 dmesg 和 journal 里找 OOM 记录。先把「信号类型」这件事区分清楚,能省掉大量无效排查。

第一步:确认当前系统是否在收集 core

很多人以为 core dump 是默认开着的,实际上绝大多数现代发行版默认关闭。先做体检:

# 1. 看当前 shell 的 core 大小限制(0 表示不产生 core)
ulimit -c

# 2. 看内核的 core 输出模式与路径
cat /proc/sys/kernel/core_pattern
cat /proc/sys/kernel/core_uses_pid

# 3. 若启用了 systemd-coredump,看它收到了多少转储
coredumpctl list | tail -20

三种可能的输出对应三条处理路线:

  • core_pattern 是 |/usr/lib/systemd/systemd-coredump ...(或 |/lib/systemd/...)——说明由 systemd-coredump 统一收集,用 coredumpctl 管理,这是最省心的方式。
  • core_pattern 是 core 或 /var/crash/core.%p 之类——说明内核直接写文件到当前工作目录或指定路径。
  • ulimit -c 输出 0 且 coredumpctl list 为空——说明压根没在收集,需要开启。

第二步:开启 core dump 收集(systemd 环境)

Debian 11/12、Ubuntu 20.04 及以后的系统,推荐直接用 systemd-coredump。安装与配置:

# 安装(多数系统已预装,没有则补上)
apt install -y systemd-coredump

# 查看当前配置
cat /etc/systemd/coredump.conf

# 关键项说明
# [Coredump]
# Storage=external   # 存到 /var/lib/systemd/coredump(推荐,默认 journal 只存元数据)
# Compress=yes       # 压缩存储,通常能压到 20%~30%
# ProcessSizeMax=2G  # 超过这个大小的进程不做转储,防止把磁盘写满
# ExternalSizeMax=2G # 单个外部 core 文件上限
# MaxUse=           # 全部 core 占用的磁盘上限,为空表示不限制(建议设置)

修改后要重载并让 systemd 重新读取:

# 写入配置
mkdir -p /etc/systemd/coredump.conf.d
cat > /etc/systemd/coredump.conf.d/zz-custom.conf <<'EOF'
[Coredump]
Storage=external
Compress=yes
ProcessSizeMax=4G
ExternalSizeMax=4G
MaxUse=2G
EOF

systemctl daemon-reload
systemctl restart systemd-coredump.socket

# 放开 ulimit(对当前会话生效)
ulimit -c unlimited

关键坑位一:ulimit -c 的作用域。ulimit 只影响执行它的 shell 及其子进程,不能全局生效。要让它对所有服务生效,必须在 systemd unit 里写 LimitCORE=infinity,或者通过 systemctl edit 覆盖:

systemctl edit php8.2-fpm.service
# 在打开的编辑器里写入:
[Service]
LimitCORE=infinity

# 保存后
systemctl daemon-reload
systemctl restart php8.2-fpm

关键坑位二:Storage=journal 会让 core 塞进 systemd journal。journal 有大小限制(默认通常是磁盘的 10% 或 4G 封顶),一个大 core 进去可能把历史日志全挤掉。所以生产环境应该用 Storage=external,并给 MaxUse 设个上限,同时确认 /var/lib/systemd/coredump 所在分区的剩余空间——转储一个 2G 内存的 PHP-FPM 进程会实打实吃掉几百 MB 到 2G 磁盘。

第三步:用 coredumpctl 找到并查看崩溃

收集到转储后,日常操作几乎都围绕 coredumpctl:

# 列出所有转储(按时间倒序)
coredumpctl list

# 只看某个程序的(支持通配)
coredumpctl list /usr/sbin/php-fpm*
coredumpctl list mysqld

# 看最近一次崩溃的概要信息:信号、时间、PID、命令行
coredumpctl info

# 看指定 PID 的崩溃信息
coredumpctl info 12345

# 导出转储文件,便于拷到别的机器分析
coredumpctl dump -o /tmp/php-fpm.core 12345

coredumpctl info 的输出通常已经能给出非常有价值的信息,例如:

           PID: 12345 (php-fpm)
        Signal: 11 (SEGV)
     Timestamp: Tue 2026-09-23 03:14:07 CST (1 day ago)
  Command Line: php-fpm: pool www
    Executable: /usr/sbin/php-fpm8.2
       Message: Process 12345 (php-fpm) of user www-data dumped core.

Stack trace of thread 12345:
#0  0x00007f3c8a1b2d31 n/a (n/a)
#1  0x0000559c9f1234ab n/a (n/a)
#2  0x0000559c9f10ff02 n/a (n/a)

看到 n/a (n/a) 这种没有符号的栈,说明系统里没有对应的调试符号。想让它显示出函数名和行号,需要装 debug 包:

# Debian/Ubuntu:开启 debug 源并安装符号包
echo "deb http://deb.debian.org/debian-debug/ bookworm-debug main" \
  > /etc/apt/sources.list.d/debug.list
apt update
apt install -y php8.2-fpm-dbgsym libc6-dbg

# 之后重新输出栈
coredumpctl info 12345

关键坑位三:coredumpctl info 显示的栈是「事后回放的近似结果」。它用当时进程的二进制加上符号表来解析,如果二进制已经被 apt upgrade 覆盖过(同一路径但内容变了),符号就对不上,栈会显示错误甚至完全乱掉。所以排查崩溃的铁律是:崩溃发生后,先不要升级或重启相关服务,先把 core 导出并保存好。coredumpctl dump -o 导出到持久目录,是每次排查的第一动作。

第四步:用 gdb 深挖,读到具体是哪一行

coredumpctl info 给出的是概要,要真正定位根因(比如哪个变量是 NULL、内存从哪来的),用 gdb 加载转储:

apt install -y gdb

# 方式一:直接让 gdb 从 coredumpctl 读取(最方便)
coredumpctl gdb 12345

# 方式二:手动加载导出的 core
gdb /usr/sbin/php-fpm8.2 /tmp/php-fpm.core

进入 gdb 后,常用命令按这个顺序执行:

# 看崩溃时的调用栈,带完整参数
bt full

# 切到指定帧,看该帧的源码上下文
frame 2
list

# 打印变量(找到可疑指针)
print *ptr
print variable_name

# 看所有线程的栈(多线程程序必做,真正崩的线程可能不是 #0)
thread apply all bt

# 看寄存器,判断是读还是写非法地址
info registers

# 看崩溃地址附近的内存映射
info proc mappings

一个真实的排查思路示范:MySQL 报 got signal 11,用 thread apply all bt 发现崩溃发生在某个存储引擎的读取路径,bt full 显示一个指针为 0x0,那么结论大概率是「触发了引擎的一个已知空指针缺陷」,接下来就能去搜对应的 bug 编号或者比对版本变更记录。如果没有 core dump,这个判断只能靠猜。

第五步:把 core 数量控制住,别让它写满磁盘

core dump 是有代价的:一个反复崩溃的进程可能在几分钟内写出几十个转储,把磁盘打满,然后引发更严重的事故(磁盘满导致 MySQL 拒绝写入、日志写不进去)。所以开启 core 的同时必须做三件控制:

  1. 设置 MaxUse:MaxUse=2G 表示所有转储加起来不超过 2G,超出后 systemd 会清理最旧的。这是最重要的一道闸。
  2. 设置 ProcessSizeMax:避免一个吃了 30G 内存的进程把磁盘直接撑爆。
  3. 加监控与定期清理:把 /var/lib/systemd/coredump 的磁盘占用纳入监控,并加一条定时清理旧转储的脚本:
# 清理 7 天前的转储
coredumpctl vacuum --keep=7d

# 或按体积保留
coredumpctl vacuum --max-use=1G

# 定期任务:每天凌晨清理一次
cat > /etc/cron.daily/coredump-vacuum <<'EOF'
#!/bin/sh
/usr/bin/coredumpctl vacuum --keep=7d >/dev/null 2>&1
EOF
bash -c 'chmod +x /etc/cron.daily/coredump-vacuum'

没有 core dump 时的替代排查手段

如果崩溃是 OOM Killer 干的(收不到 core),或者你还没来得及开 core 服务又崩了,可以用这些手段补足信息:

# 1. 内核有没有杀掉进程
dmesg -T | grep -iE 'killed process|out of memory'

# 2. 完整 OOM 记录(带进程名和内存占用)
journalctl -k --since "1 day ago" | grep -i oom

# 3. 进程为什么收到信号:用 bpftrace 跟踪(需内核支持)
apt install -y bpftrace
bpftrace -e 'tracepoint:signal:signal_generate { printf("%s -> %d sig=%d\n", comm, pid, args->sig); }'

# 4. PHP-FPM 的段错误可以打开它的独立 coredump 目录
grep -rE 'rlimit_core|core' /etc/php/*/fpm/pool.d/www.conf

其中 bpftrace 那条对「谁给进程发了信号」这类问题特别有效,能在不重启服务的情况下抓到实时事件。而 dmesg -T 带 -T 参数会把人人都看不懂的内核时间戳转成可读时间,排查时优先用它。

常见问答

Q:core dump 会不会影响服务性能?
A:不在崩溃时完全没有影响。转储只在进程收到终止信号的那一刻发生,代价是一次磁盘写。真正需要担心的是崩溃频繁导致反复写大文件,所以要用 MaxUse 和监控兜住。日常可以只在排查期对特定服务开 LimitCORE=infinity,查清后关掉。

Q:为什么我 ulimit -c unlimited 之后还是没 core?
A:三个常见原因:一是服务由 systemd 启动,unit 里没有 LimitCORE,shell 的 ulimit 管不到它;二是 core_pattern 指向一个不存在的目录,内核静默失败;三是进程工作目录不可写且 core_pattern 是相对路径。逐个排查即可。

Q:core 文件能拷到别的机器分析吗?
A:可以,但需要同版本的二进制和匹配的调试符号。coredumpctl dump -o 导出后用同版本环境的 gdb 打开,否则栈会失真。

Q:PHP 脚本崩了也归 core dump 管吗?
A:PHP 层的致命错误(Fatal error)不会产生 core,它只是脚本终止,日志写在 error_log 里。会产生 core 的是 PHP 解释器本身(php-fpm 进程)发生段错误的情况,两者要分开排查。

Q:磁盘快满了能不能直接关掉 core?
A:能,但建议先导出最有价值的那一两个转储再关。systemctl stop systemd-coredump.socket 可以临时停掉,把 Storage 改回 none 则是永久关闭。

小结

core dump 的价值在于把「进程莫名其妙死了」这个黑盒问题变成可回放的白盒问题。落地只需要四步:确认 core_pattern 与 ulimit 状态,用 systemd-coredump 统一收集并设置容量上限,崩溃后用 coredumpctl dump 第一时间保存转储,最后用 gdb 的 bt full 与 thread apply all bt 读出根因。三条纪律请务必记住:SIGKILL 和 OOM 不会有 core,要去 dmesg 找;崩溃后先保存转储再动二进制;开启收集必须同时设 MaxUse,否则磁盘会比你更早崩溃。

Last modification:September 25th, 2026 at 08:30 pm

Leave a Comment