半夜收到告警,进程退出了,日志里只有一行 Segmentation fault,但代码那么多,根本不知道是哪一行踩了坑。这种场景我遇到太多次了。后来接手任何服务器,我第一件事就是把 Linux Core Dump 配置好,崩溃瞬间内存里最真实的现场会被完整保存下来,排查效率完全不是一个量级。
这篇Linux Core Dump 配置与分析指南,我会从内核产生转储文件的机制讲起,把 ulimit、core_pattern、systemd 接管、GDB 堆栈分析这条链路完整走一遍,最后附上我踩过的那些坑。适合开发、运维、SRE,也适合想入门底层调试的同学,即使之前完全没碰过调试,照做一遍也能跑通。
1. Core Dump 是什么,为什么排查崩溃离不开它
1.1 崩溃现场从哪来
进程崩溃时,内核会给它发送信号,比如常见的 SIGSEGV、SIGABRT、SIGBUS、SIGILL。在进程被终止之前,内核会把那一刻的内存映像、寄存器状态、信号信息等写入一个文件,这就是 Core Dump,中文常翻译成“核心转储”。
很多人把它理解成“崩溃日志”,其实不完全对。日志是程序主动写的,很多崩溃场景里程序根本没机会写日志。Core Dump 是内核在进程死亡前代写的“现场快照”,包含的信息比日志全面得多,数据段、堆、栈、寄存器都能看到。我做个类比:日志相当于行车记录仪里你录的画外音,Core Dump 则是撞车那一瞬间整车传感器数据。要还原碰撞原因,光有画外音不够,必须看整车数据。
转储触发条件也不只是段错误,我用 gcore 命令对“还活着的进程”手动抓一份,在排查卡死问题时很有用。比如一个服务态,CPU 占满又不响应,用 gcore <pid> 就能把当时状态抓下来分析,不用杀掉进程,这一步很多朋友不知道。
1.2 三个核心开关:ulimit、core_pattern、core_uses_pid
要让内核乖乖吐文件,至少同时满足三件事。
第一,进程的 RLIMIT_CORE 资源限制。ulimit -c 查看,很多系统默认是 0,表示“拒绝写 core”,进程崩溃后只会留下一句 (core dumped) 提示,实际什么都没有。可以执行 ulimit -c unlimited 临时打开,但这个修改只在当前 shell 会话生效,退出就失效。永久生效要写在 /etc/security/limits.conf,或者 systemd 服务单元里的 LimitCORE=infinity。
第二,core_pattern 决定文件去哪里、叫什么。它在 /proc/sys/kernel/core_pattern,内容可以是路径模板,也可以是管道命令。模板里的 %e 表示可执行文件名,%p 是进程 PID,%t 是时间戳,%h 是主机名,%s 是信号编号,%g 和 %u 是进程 GID 和 UID。理解这些占位符特别重要,后面你要靠它避免文件互相覆盖。
第三,kernel.core_uses_pid 是老式内核的补充机制:如果 core_pattern 只是一个普通单词,没有包含任何 % 占位符,把它置 1 时系统会在文件名后面自动追加 PID。现在大家都用 %p 了,这个参数基本不需要管,但一些老教程还在提,容易混淆。
三个开关的关系我给个口诀:ulimit 管“能不能写”,core_pattern 管“写哪去、叫什么”,core_uses_pid 管“重名怎么办”。缺一个,Core Dump 都可能出不来或者乱放。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. Core Dump 配置:从临时改到永久生效
2.1 动手之前,先看当前环境
配置之前我习惯先摸一遍底,三条命令:
bash复制ulimit -c
cat /proc/sys/kernel/core_pattern
cat /proc/sys/kernel/core_uses_pid
我见过大量服务器,默认 ulimit -c 是 0,core_pattern 是 core 或者被 systemd 接管。所以配置前一定要记录当前值,后面排查时有对比。
还要注意顺序:先改 core_pattern,再改 ulimit。因为 ulimit 是会话级概念,新开终端要重新设置,而 core_pattern 是全局配置,先确认全局配置再调整进程限制,逻辑更清晰。
另外,现代发行版默认 core_pattern 经常是 |/usr/lib/systemd/systemd-coredump %P %u %g %s %t %c %h。这个写法意味着内核直接把崩溃进程“交给”systemd-coredump 处理,文件不会按传统方式直接落在某个目录。它自带压缩、管理和查询,但对只想“拿一个裸 core 文件”的人来说,可能找半天不知道东西在哪。这就是很多人说“明明崩溃了,但找不到 core 文件”的主要原因。
2.2 core_pattern 改法:临时、持久化与目录规划
最快的方式是直接往 /proc/sys/kernel/core_pattern 里写路径:
bash复制mkdir -p /data/cores
chmod 1777 /data/cores
echo '/data/cores/core.%e.%p.%t' > /proc/sys/kernel/core_pattern
注意,这种改动重启后就会丢失,只能用来救急或测试。作为运维习惯,我更推荐用 sysctl 持久化,把配置落到 /etc/sysctl.d/99-core.conf:
bash复制cat > /etc/sysctl.d/99-core.conf <<'EOF'
kernel.core_pattern=/data/cores/core.%e.%p.%t
kernel.core_uses_pid=0
EOF
sysctl --system
模板占位符我重点说一下:%e 和 %p 是我必用的,一个区分程序名,一个区分同一程序的多实例。%t 加时间戳可以避免覆盖,配合 %p 基本就能保证文件名唯一。千万别把 core_pattern 写成固定文件名,比如直接 core,那样多个进程崩溃会把同一个文件互相覆盖,最后你拿到的可能是“最后一个崩溃进程”的现场,前面几个的线索全丢了。
目录权限这里要非常谨慎。如果跑服务的用户不是 root,内核会以“崩溃进程的用户”身份去写 core 文件,目标目录没有写权限,文件就不会生成。最省事的方案是把目录做成 1777(带 sticky bit 的全局可写),但这是双刃剑,任何人都能往里扔文件,容易被塞满。更稳妥的做法是按服务用户分目录,比如 /data/cores/nginx 归 nginx 用户,权限 0700,谁的服务归谁的,避免互相干扰,也避免别的用户读取转储文件。
2.3 ulimit 永久配置与 systemd 服务的坑
shell 环境的 ulimit 修改,写入 /etc/security/limits.conf:
conf复制* soft core unlimited
* hard core unlimited
但这只影响通过 PAM 登录的会话,对 systemd 托管的服务不生效。如果你调的是 Java、Python 这类由 systemd 拉起的服务,需要在 service 单元里显式放开:
ini复制[Service]
LimitCORE=infinity
改完 systemctl daemon-reload && systemctl restart xxx 才生效。
这个坑我在真实环境里踩过:明明 limits.conf 配好了,手动跑程序能出 core,systemd 服务一崩,什么都看不到。查了半天,才发现 systemd 单元默认 LimitCORE 是 0,根本不继承 limits.conf。所以排查“为什么服务崩溃没 core”,第一步先查 systemctl show 你的服务 -p LimitCORE。
还有 shell 层:即使设置了 ulimit -c unlimited,如果 core_pattern 是管道给 systemd-coredump,systemd 侧也有自己的配额和策略,这个留在第 4 章细说。
3. 实战:构造崩溃、抓 Dump、用 GDB 定位
3.1 先造一个崩溃现场
理论说一堆,不如动手做一遍。我写一个注定段错误的 C 程序,用来生成 Core Dump:
c复制#include <stdio.h>
void bad_function(int *p) {
*p = 100; // 向空地址写数据,触发 SIGSEGV
}
int main() {
int *ptr = NULL;
bad_function(ptr);
return 0;
}
编译时务必加 -g,不然 GDB 看到的只有地址没有符号,分析效率差很多:
bash复制gcc -g -O0 -o crash crash.c
-O0 是防止优化把变量和调用结构改得面目全非。生产环境编译一般不会 -O0,但发布包最好保留带符号的调试版本,或者额外输出 build-id 对应的 debug symbol 文件,后续用 debuginfod 也能拉回来,这里先不展开。
设置好前两章那些开关后,运行:
bash复制./crash
终端会提示 Segmentation fault (core dumped),然后去配置目录看:
bash复制ls -l /data/cores/
你会看到一个名叫 core.crash.12345.1699999999 之类的文件,大小从几 KB 到几十 MB 不等,这就是这个进程崩溃瞬间的完整内存状态。
3.2 GDB 分析 Core Dump 的标准动作
拿到文件后,用 GDB 把它加载起来:
bash复制gdb ./crash /data/cores/core.crash.12345.1699999999
进去后第一条命令是 backtrace,看调用栈:
gdb复制(gdb) bt
输出大概是:
text复制#0 bad_function (p=0x0) at crash.c:4
#1 0x000000000040116a in main () at crash.c:9
看到 #0 在 crash.c:4 给空指针赋值,十秒钟就能定位下去。但真实项目栈比这深得多,可能有几十层,别急着看最底层,先整体扫一遍 bt,找“真正持有错误参数”的那一帧。
如果栈被打乱了,可以用 info registers 看寄存器,用 x/20i $pc 看当前指令附近的反汇编,判断是不是野指针跳飞了:
gdb复制(gdb) info registers
(gdb) x/20i $pc
很多时候崩溃点不在问题根源,而在“被连累”的地方。比如某个缓冲越界写坏了相邻结构体,崩溃点只是一个“受害者”。这时要看栈上变量的值和堆分配情况,用 frame N 跳到对应层,再 info locals 看局部变量:
gdb复制(gdb) frame 1
(gdb) info locals
我调试过一个内存被踩崩的服务,崩溃点永远在 free(),可真正凶手是另一个模块越界写。这种问题只看最顶层栈是找不出来的,必须配合对局部变量、堆内存布局的分析。
非交互场景下,比如要自动分析一批 core,可以这样写命令:
bash复制gdb -batch -ex "bt" -ex "info registers" ./crash /data/cores/core.crash.12345.1699999999
输出可以重定向到文件,批量处理崩溃时很好用。
3.3 从堆栈到修复线索:addr2line 与符号文件
如果只有 core 文件和没有 -g 编译的二进制,bt 可能显示的全是地址加 ??。这时候别慌,addr2line 能把地址翻译回源文件行号:
bash复制addr2line -e ./crash -f -C 0x40116a
输出:
text复制bad_function
crash.c:4
前提是二进制里至少保留了符号表。如果连符号表都被 strip 了,就需要提前保留 .gnu_debuglink 指向的 debug 文件,或者用 dump 工具导出调试信息。这也是我在第 1 章强调“发布包要留符号”的原因。
拿到源文件和行号,修复思路基本就出来了。像这个例子,函数入口对指针判空就行:
c复制void bad_function(int *p) {
if (p == NULL) {
fprintf(stderr, "bad_function: p is NULL\n");
return;
}
*p = 100;
}
实际项目里,崩溃点可能不是 bug 所在地,但 core 文件的价值在于:你有了一个完整的“案发现场”,GDB 可以反复查,不需要再去复现问题。这对那种几天才出现一次的偶发崩溃,是无可替代的调试材料。
我自己的习惯是:拿到 core 先 bt 和 thread apply all bt 全量栈,然后 info sharedlibrary 看加载了哪些动态库,再确认是不是依赖库版本不一致导致的问题。很多“线上崩、本地不崩”的案例,最后都是环境差异,core 文件里的动态库装载信息能直接给出线索。
4. Core Dump 不生成、文件膨胀、容器与安全:坑位大排查
4.1 Core Dump 一直不产生的排查清单
我把“崩溃了但没有 core 文件”的常见原因做成了速查表,按照从上到下的优先级排查:
| 现象 | 可能原因 | 排查/修复 |
|---|---|---|
| 有提示但没有文件 | ulimit -c 是 0 |
ulimit -c unlimited;修改 limits.conf;服务单元 LimitCORE=infinity |
core_pattern 是管道 |
systemd-coredump 接管 | coredumpctl list 查看;或改成裸路径 |
| 目录无写权限 | 进程用户不能写目标目录 | ls -ld 目标目录;按用户建独立目录 |
| 文件生成但为 0 字节 | RLIMIT_CORE 太小或磁盘满 |
df -h;加大限制 |
| 文件名互相覆盖 | 没用 %p/%t |
加上 pid 和时间戳 |
| 被系统自带崩溃收集器拦截 | 发行版自带 apport/abrt | 查对应守护进程日志 |
| 容器内设置无效 | /proc/sys 与宿主机共享或 namespace 限制 |
在宿主侧统一配置 |
排查时有个非常坑人的点:在 shell 里执行 echo 123 > /proc/sys/kernel/core_pattern 不一定报错,但系统可能根本没有生效。比如某些发行版用 systemd-sysctl 服务管理,你改了 /proc/sys 后重启,systemd 会把它拉回默认值。所以我的建议是:测试时用 echo 改完立刻测一场,生产环境一定要落到 /etc/sysctl.d/,并且用 sysctl --system 刷一遍,再用 cat /proc/sys/kernel/core_pattern 确认。
还有一点容易被忽略:如果进程是被父进程故意“吃”了崩溃信号,比如设置了 SIGCHLD 忽略、或者通过 setuid 运行,有时候内核会因为安全策略不生成 core。新版内核下这种情况较少见,但排查不通时还是值得查一下目标进程的 UID/GID 是否改变过。
4.2 转储文件膨胀与磁盘清理策略
Core 文件是进程内存的直接镜像,内存越大的进程,core 文件越夸张。我见过堆很大的服务,core 文件能到十几 GB,磁盘分分钟被塞满,然后引发连锁故障。
控制方法有几种。
第一,限制单个文件大小。ulimit -c 可以设具体值而不是 unlimited,比如 2048(KB):
bash复制ulimit -c 2048
但要注意:如果进程实际需要写超过这个量的 core,文件会被截断,截断的 core 往往没法用,所以这个值设小了等于白抓。我的习惯是对已知内存规模的服务,按内存的 1.5 到 2 倍设置,而不是拍脑袋。
第二,如果用了 systemd-coredump,它默认会把转储压缩存放,还支持设置保留周期和大小上限。相关配置在 /etc/systemd/coredump.conf:
ini复制[Coredump]
MaxUse=1G
KeepFree=2G
Compress=yes
MaxUse 控制所有 core 文件占用总大小,KeepFree 保证磁盘剩余空间,超过后会自动清理最旧的。这个机制非常省心,所以我强烈推荐在没有特殊需求时用 systemd-coredump,而不是自己把裸 core 写到目录里再写脚本清。
第三,自己写清理任务的话,我习惯用 systemd timer 每天清理超过 N 天的文件:
bash复制find /data/cores -type f -mtime +7 -delete
由于 core 文件权限可能是 0600 或 0644,普通用户不一定删得掉别人的,所以建议清理任务以 root 身份跑,别开了全局可写目录又不清,那样容易被塞爆。
4.3 管道方式、容器环境与安全权限
为什么把这几件事放一起说?因为它们都容易制造“你以为配置好了,其实没生效”的错觉。
core_pattern 的管道方式:如果写成 |/path/to/script %p %e,内核会 fork 一个进程去处理 core。管道方式有个老参数叫 kernel.core_pipe_limit,默认是 0,表示并发崩溃多个进程时,只有第一个会被送往管道处理,其余全部丢弃。在批量崩溃的场景下,这会导致你只能看到第一份,其它的都不见了。所以使用管道脚本收集 core 时,我习惯把 kernel.core_pipe_limit 调成 4 或更大。
容器环境更特殊。很多容器里的 /proc/sys/kernel/core_pattern 与宿主机是共享的,你在容器里改 core_pattern,宿主机整个都被改了;反过来,宿主机配置也会影响容器进程。而且容器内进程的 PID namespace 和宿主机不同,core_pattern 里的 %p 是容器的 pid,不是你宿主机看到的 pid,后面做对账会乱。
我的建议:容器环境的 Core Dump 统一在宿主机层配置,单容器调试时可以在容器里临时设置 ulimit -c unlimited,core_pattern 尽量别在容器里动。如果你真的需要每个容器独立收集,那就用独立内核参数空间的新内核特性,或者干脆让业务侧自己调用 gcore 自管,别指望优雅的全局方案。
安全方面,core 文件是进程内存快照,里面可能有明文密码、密钥、业务数据。如果目录权限是 1777,其他用户都能读你的 core,等于把生产环境的敏感内存信息白送出去。这也是我反复强调权限的原因。另外,很多进程 core 权限是 0600,属主是崩溃用户,所以如果建了全局可写目录,崩溃用户之间虽然不能互相删,但 root 能看到所有。管理上要对 core 目录做审计,别当成普通日志文件不管。
最后分享一个我一直在沿用的细节:把 core_pattern 指向 /var/lib/systemd/coredump 并统一用 coredumpctl gdb 分析,是省心程度最高的方案;但如果你更习惯裸 core 文件,一定要把 %e.%p.%t 模板和目录权限维护好。两者各有利弊,没有绝对正确。
再补一个实用习惯:每个项目交付时,我都会顺手写一条崩溃自检命令,把 ulimit、core_pattern、目录权限、磁盘剩余空间一次查出,放进运维巡检脚本里。崩溃这种事,真到出了事才想起来配置,往往已经迟了;而这条检查脚本,是花一分钟就能给未来省一天排查时间的投入,强烈建议你也放进自己的日常清单里。
