1. 从一次深夜排查说起:core 文件为什么凭空消失
线上服务在凌晨两点突然挂掉,进程没了,日志只留下一行“Segmentation fault”。你第一反应是去看 core 文件,结果目录里空空如也。这不是玄学,是 Linux 系统里一个非常经典、又极其容易被忽略的配置链路问题。core dump 机制涉及内核参数、进程资源限制、可执行文件格式、systemd 接管方式、容器隔离等多个层面,任何一环没打通,core 文件都不会落地。
这篇文章面向的是经常和 Linux 服务打交道的后端开发、运维工程师、SRE,以及正在学习系统编程的同学。我会把“程序崩溃却没有 core 文件”这件事拆成一条完整的排查链路,从内核参数到进程限制,从 systemd 到容器环境,逐层解释每个环节的作用和常见坑点。读完你不仅能定位当前问题,还能建立一套自己的排查顺序,下次遇到类似情况不用再靠猜。
需要先明确一个概念:core dump 是进程在收到某些信号(比如 SIGSEGV、SIGABRT、SIGFPE)而异常终止时,内核把该进程当时的内存镜像、寄存器状态、调用栈等信息写入磁盘形成的文件。它本质上是进程死亡瞬间的“现场快照”,对定位崩溃原因极其关键。没有它,很多问题只能靠日志和代码审查硬推,效率差一个数量级。
但 core 文件不会无缘无故出现,也不会无缘无故消失。它需要满足一系列条件才会被写入。下面我按排查优先级从高到低,把整条链路讲清楚。
2. 第一道闸门:ulimit 与 core 文件大小限制
2.1 core 文件大小限制到底限制了什么
很多人知道ulimit -c可以查看 core 文件大小限制,但不太清楚这个限制具体作用在哪个环节。它其实是内核在决定是否写 core 文件时检查的第一个用户态资源限制。当进程崩溃时,内核会先看当前进程的RLIMIT_CORE值:
- 如果值为 0,内核直接放弃写 core 文件,什么都不会发生。
- 如果值为一个有限正整数,内核只会写入不超过该大小的 core 文件,超出部分被截断。
- 如果值为
unlimited,内核会尝试写入完整的内存镜像。
这里有个容易误解的点:ulimit -c显示的是当前 shell 的限制,但真正起作用的是崩溃进程自身的限制。也就是说,如果你在 shell 里设置了ulimit -c unlimited,但服务是通过 systemd 启动的,那这个设置对服务进程无效。这是最常见的“我明明设了 unlimited 还是没有 core”的原因。
2.2 临时设置与永久设置的区别
临时设置很简单,在当前 shell 执行:
ulimit -c unlimited然后在这个 shell 里启动的程序就会继承这个限制。但一旦 shell 关闭,设置就没了。永久设置需要区分不同场景:
对于通过 PAM 登录的用户会话,可以修改/etc/security/limits.conf:
# 在文件末尾添加 * soft core unlimited * hard core unlimited注意*表示对所有用户生效,也可以指定具体用户名。修改后需要重新登录才生效,因为 limits 是在会话建立时应用的。
对于 systemd 管理的服务,limits.conf通常不生效,因为 systemd 不经过 PAM。需要在 unit 文件里单独配置:
[Service] LimitCORE=infinity改完后执行systemctl daemon-reload再重启服务。这一步很多人会漏掉,导致以为设置了 unlimited 但实际服务进程的限制仍然是 0。
2.3 验证限制是否真正生效
不要只看 shell 里的ulimit -c,要直接查看目标进程的实际限制:
cat /proc/<pid>/limits | grep core输出类似:
Max core file size unlimited unlimited bytes如果这里显示 0,那不管你怎么设置其他参数,core 文件都不会产生。这是排查的第一站,也是最容易被跳过的一站。
提示:有些发行版默认把 core 文件大小限制设为 0,尤其是面向生产环境的镜像。部署新服务时建议把这一项纳入检查清单。
3. 第二道闸门:core_pattern 决定文件去哪了
3.1 core_pattern 的默认行为与常见取值
即使进程的 core 限制是 unlimited,core 文件也不一定出现在你期望的目录。内核通过/proc/sys/kernel/core_pattern决定 core 文件的命名和存放位置。常见取值有几种:
| core_pattern 值 | 行为说明 |
|---|---|
core | 在当前工作目录生成名为 core 的文件 |
/var/crash/core.%e.%p | 在指定目录生成带可执行文件名和 PID 的文件 |
|/usr/lib/systemd/systemd-coredump %P %u %g %s %t %c %h | 交给 systemd-coredump 处理 |
|/bin/false | 直接丢弃,不生成任何文件 |
最后一种情况就是典型的“配置看起来正常但 core 文件永远不出现”。有些系统或容器镜像会故意把 core_pattern 设成管道到某个不存在的程序,或者直接丢弃,目的是避免 core 文件占满磁盘。
3.2 管道模式下的 core 文件去向
现代 Linux 发行版越来越多地使用管道模式,把 core 文件交给systemd-coredump处理。这种情况下 core 文件不会以普通文件形式出现在当前目录,而是被 systemd 收集并存储到/var/lib/systemd/coredump/目录下,同时可以通过coredumpctl命令查看和管理。
查看当前 core_pattern:
cat /proc/sys/kernel/core_pattern如果输出以|开头,说明是管道模式。此时你需要用:
coredumpctl list来查看是否有 core 记录。如果有记录但你没找到文件,可以用:
coredumpctl info <PID>查看详细信息,或者用:
coredumpctl dump <PID> -o /tmp/core.dump把 core 文件导出到指定位置。
3.3 修改 core_pattern 的注意事项
如果你想改回传统的文件模式,可以执行:
echo '/var/crash/core.%e.%p' > /proc/sys/kernel/core_pattern但要注意几点:第一,这个修改是临时的,重启后失效,永久修改需要写/etc/sysctl.conf或/etc/sysctl.d/下的配置文件;第二,如果系统已经启用了 systemd-coredump,直接改 core_pattern 可能会和 systemd 的管理逻辑冲突;第三,在容器环境中,core_pattern 是宿主机级别的参数,容器内修改通常无效。
%e表示可执行文件名,%p表示 PID,%t表示崩溃时间戳,%h表示主机名。合理使用这些占位符可以避免 core 文件互相覆盖,尤其是在多进程服务中非常有用。
4. 第三道闸门:目录权限与磁盘空间
4.1 工作目录是否可写
当 core_pattern 是相对路径(比如默认的core)时,core 文件会写入进程的当前工作目录。如果这个目录对进程用户不可写,core 文件就写不进去。这种情况在服务以非 root 用户运行时特别常见。
比如一个服务以appuser运行,工作目录是/opt/app,但/opt/app的属主是 root,权限是 755。那appuser就没有写权限,core 文件自然无法生成。解决办法要么是修改目录权限,要么是把 core_pattern 改成绝对路径且该路径对进程用户可写。
查看进程工作目录:
ls -l /proc/<pid>/cwd然后检查该目录的权限和属主是否匹配进程的运行用户。
4.2 磁盘空间与 inode 是否充足
这个原因看起来很低级,但在生产环境里真实发生过很多次。core 文件可能非常大,尤其是内存占用高的服务,一个 core 文件几个 GB 很正常。如果目标分区空间不足,内核写 core 文件会失败,而且通常不会在应用日志里留下明显提示。
检查磁盘空间:
df -h df -idf -i查看 inode 使用情况,有时候磁盘空间够但 inode 耗尽,同样写不了新文件。
另外要注意,如果 core_pattern 指向的目录所在分区挂载时带了noexec或nosuid等选项,一般不影响写入,但如果分区本身是只读挂载,那肯定写不了。检查挂载选项:
mount | grep <分区>4.3 安全模块的限制
某些安全加固配置会限制 core 文件的生成。比如 SELinux 或 AppArmor 可能对特定进程的 core dump 行为进行约束。如果你确认前面所有配置都正确,但 core 文件仍然不出现,可以临时把 SELinux 设为 permissive 模式测试:
setenforce 0如果此时 core 文件能正常生成,说明是 SELinux 策略问题,需要调整相应策略而不是直接关闭。查看 SELinux 审计日志:
ausearch -m avc -ts recentAppArmor 类似,可以查看/var/log/syslog或dmesg中是否有相关拒绝记录。
5. 第四道闸门:信号与进程状态的特殊情况
5.1 哪些信号会触发 core dump
不是所有导致进程终止的信号都会产生 core 文件。默认情况下,以下信号会触发 core dump:
| 信号 | 数值 | 说明 |
|---|---|---|
| SIGQUIT | 3 | 终端退出信号 |
| SIGILL | 4 | 非法指令 |
| SIGABRT | 6 | 进程主动 abort |
| SIGFPE | 8 | 浮点异常 |
| SIGSEGV | 11 | 段错误 |
| SIGBUS | 7 | 总线错误 |
| SIGSYS | 31 | 系统调用错误 |
而 SIGTERM、SIGKILL、SIGINT 等信号默认不会触发 core dump。所以如果你的程序是被kill -9杀掉的,那不会有 core 文件,这是正常行为。排查时要先确认进程到底收到了什么信号。
查看进程退出状态:
echo $?如果返回值大于 128,说明进程是被信号终止的,返回值减去 128 就是信号编号。比如返回 139,139 - 128 = 11,说明是 SIGSEGV。
5.2 多线程程序的 core dump 行为
多线程程序崩溃时,core 文件记录的是整个进程的内存镜像,包括所有线程的栈和寄存器状态。但有一个细节:如果崩溃线程不是主线程,某些调试器在分析时可能需要额外指定线程。另外,如果程序在崩溃前自己安装了信号处理函数,并且在处理函数里调用了exit()而不是重新触发默认行为,那 core dump 就不会发生。
这是一个非常隐蔽的坑。有些程序框架会捕获 SIGSEGV 做自定义处理,比如打印堆栈后调用exit(1)。这种情况下内核认为信号已经被处理,不会走默认的 core dump 流程。解决办法是在信号处理函数里恢复默认处理并重新触发信号:
void handler(int sig) { signal(sig, SIG_DFL); raise(sig); }这样既打印了自定义信息,又能让内核正常生成 core 文件。
5.3 进程处于不可中断状态时的表现
如果进程处于 D 状态(不可中断睡眠),通常是卡在某个内核态操作上,比如磁盘 IO。这种情况下进程不会响应信号,也就不会崩溃,自然不会有 core 文件。这种问题需要从 IO 层面排查,而不是 core dump 配置。
查看进程状态:
ps -eo pid,stat,comm | grep ' D'如果发现大量 D 状态进程,说明系统 IO 可能有问题,需要检查磁盘健康状态和 IO 负载。
6. 容器环境下的特殊处理
6.1 容器内 core_pattern 的继承关系
在容器里跑服务时,core dump 的行为和物理机有明显差异。核心原因是core_pattern是内核级别的参数,容器共享宿主机的内核,所以容器内看到的core_pattern就是宿主机的设置。你在容器里执行echo 'core' > /proc/sys/kernel/core_pattern通常会失败,因为这个文件在容器内是只读的。
这意味着如果宿主机的 core_pattern 是管道模式,容器里的进程崩溃后 core 文件会被宿主机的 systemd-coredump 接管,而不是落在容器内的目录。排查时需要到宿主机上查看。
6.2 容器内 ulimit 的设置方式
Docker 启动容器时可以通过--ulimit core=<软限制>:<硬限制>来设置:
docker run --ulimit core=unlimited:unlimited ...在 Kubernetes 中,可以通过 Pod 的 securityContext 或者容器的ulimits字段配置。但要注意,即使容器内 ulimit 设了 unlimited,如果宿主机的 core_pattern 指向一个容器内不存在的路径,core 文件仍然写不出来。
6.3 推荐的做法:挂载可写目录并调整 core_pattern
比较稳妥的方案是在宿主机上把 core_pattern 设置为一个绝对路径,比如/var/crash/core.%e.%p,然后把这个目录以 volume 形式挂载到容器内。这样容器内进程崩溃时,core 文件会写到宿主机的/var/crash目录,你在容器内外都能访问。
如果无法修改宿主机 core_pattern,另一个办法是在容器启动脚本里检查并尝试调整,但成功率取决于容器的权限配置。最保险的还是从宿主机层面统一管理 core dump 策略。
注意:在生产环境调整 core_pattern 前,务必确认磁盘容量和清理策略,避免 core 文件把磁盘写满导致更严重的故障。
7. 一套可复用的排查顺序与实战命令
7.1 从进程视角倒推的排查链路
遇到“程序崩溃没有 core 文件”时,我习惯按以下顺序排查,基本能覆盖 95% 以上的情况:
- 确认进程收到了什么信号,是否属于会触发 core dump 的信号。
- 查看
/proc/<pid>/limits中 core 文件大小限制是否为 0。 - 查看
/proc/sys/kernel/core_pattern,确认 core 文件去向。 - 检查目标目录的权限、磁盘空间和 inode。
- 检查程序是否自定义了信号处理函数并阻止了默认行为。
- 如果是容器环境,检查宿主机 core_pattern 和容器挂载配置。
- 检查 SELinux/AppArmor 是否有拒绝记录。
这个顺序的原则是:从最可能、最容易检查的地方开始,逐步深入到系统级配置。不要一上来就改内核参数,先确认基础限制和路径。
7.2 常用命令速查表
| 目的 | 命令 |
|---|---|
| 查看进程 core 限制 | cat /proc/<pid>/limits | grep -i core |
| 查看 core_pattern | cat /proc/sys/kernel/core_pattern |
| 查看进程工作目录 | ls -l /proc/<pid>/cwd |
| 查看磁盘空间 | df -h和df -i |
| 查看 systemd core 记录 | coredumpctl list |
| 导出 systemd core 文件 | coredumpctl dump <PID> -o /tmp/core |
| 查看 SELinux 拒绝 | ausearch -m avc -ts recent |
| 查看进程退出信号 | echo $?(大于 128 时减去 128) |
7.3 一个真实的排查案例复盘
之前遇到过一个服务,配置了LimitCORE=infinity,core_pattern也是传统的core,但崩溃后就是没有文件。按顺序排查发现:进程的工作目录是/,而/对运行用户不可写。core_pattern 是相对路径,所以内核尝试往/写 core 文件,权限不足直接失败。
解决办法是把 core_pattern 改成绝对路径/var/crash/core.%e.%p,并确保/var/crash对服务用户可写。改完后再次触发崩溃,core 文件正常生成。这个案例说明,即使 ulimit 和 core_pattern 看起来都正常,工作目录权限这个细节仍然可能让整个机制失效。
另一个案例是容器环境,宿主机 core_pattern 是管道模式,容器内进程崩溃后 core 文件被宿主机的 systemd-coredump 收集。开发同学在容器里找了半天没找到,最后通过coredumpctl在宿主机上找到了记录。这种情况不是配置错误,而是对机制理解不到位导致的排查方向偏差。
8. 让 core 文件真正可用的几个进阶技巧
8.1 控制 core 文件大小避免磁盘爆炸
core 文件大小等于进程的内存镜像,对于内存占用大的服务,一个 core 文件可能几十 GB。如果频繁崩溃,磁盘很快会被写满。可以通过设置RLIMIT_CORE为一个合理值来限制单个 core 文件大小,比如 2GB:
ulimit -c 2097152单位是 KB,2097152 KB 等于 2GB。这样即使进程内存很大,core 文件也不会超过这个大小,虽然可能丢失部分内存信息,但通常调用栈和关键数据结构都在前面,足够定位问题。
8.2 使用 coredumpctl 管理 core 文件生命周期
如果系统使用 systemd-coredump,可以通过/etc/systemd/coredump.conf配置保留策略:
[Coredump] Storage=external Compress=yes ProcessSizeMax=2G ExternalSizeMax=2G MaxUse=1G KeepFree=1GMaxUse限制 coredump 目录总大小,KeepFree保留最小空闲空间。这样 systemd 会自动清理旧的 core 文件,避免磁盘被占满。修改后执行:
systemctl restart systemd-coredump8.3 配合调试工具快速定位
有了 core 文件之后,用 gdb 加载:
gdb /path/to/binary /path/to/core进入 gdb 后常用命令:
bt 查看完整调用栈 thread apply all bt 查看所有线程调用栈 info registers 查看寄存器状态 frame <n> 切换到指定栈帧 print <variable> 打印变量值如果是 C++ 程序,还可以加载符号表后查看更详细的类型信息。对于 Go 程序,core 文件的分析方式略有不同,通常用dlv core配合二进制文件进行调试。
提示:生产环境的二进制文件建议保留带符号表的版本,否则 core 文件分析时只能看到地址,定位难度会大幅增加。
8.4 在开发阶段主动触发 core dump 验证配置
不要等到线上崩溃才发现 core 配置有问题。开发阶段可以写一个简单的测试程序主动触发段错误:
#include <stdlib.h> int main() { int *p = NULL; *p = 42; return 0; }编译运行后如果 core 文件正常生成,说明配置链路是通的。这个测试建议纳入部署检查清单,尤其是新环境上线前。
9. 几个容易被忽略的边界情况
9.1 程序自身调用了 setrlimit 降低限制
有些程序在启动时会主动调用setrlimit(RLIMIT_CORE, ...)把 core 限制设为 0,目的是避免生成 core 文件。这种情况下,即使外部 ulimit 设了 unlimited,程序自身的限制仍然是 0。排查时要以/proc/<pid>/limits为准,而不是 shell 的 ulimit。
9.2 动态链接库崩溃与主程序 core 的关系
如果崩溃发生在动态链接库中,core 文件仍然会生成,但 gdb 分析时需要确保能加载对应的库符号。如果库文件在崩溃后发生了更新或删除,分析时可能看不到完整的调用栈。建议在部署时保留与二进制匹配的库文件版本。
9.3 内核参数 core_uses_pid 的影响
/proc/sys/kernel/core_uses_pid控制 core 文件名是否自动附加 PID。当 core_pattern 中没有%p时,如果这个参数为 1,内核会在文件名后加上.PID。多进程服务中建议显式在 core_pattern 里使用%p,避免文件覆盖。
9.4 文件系统类型对 core 写入的影响
某些网络文件系统或特殊文件系统可能不支持 core 文件的写入方式。比如 NFS 挂载的目录在某些配置下写入 core 文件会失败。如果 core_pattern 指向的是网络挂载目录,建议先测试确认可写性。
10. 我个人的排查习惯与建议
排查 core 文件不生成的问题,最忌讳的就是一上来就改各种参数。我自己的习惯是先做三件事:确认信号类型、查看进程实际 limits、查看 core_pattern。这三步通常五分钟内就能完成,而且能覆盖大部分常见原因。
另外,建议把 core dump 配置纳入服务部署的标准检查项。新服务上线前,用一个测试程序验证 core 文件能否正常生成,比出事后手忙脚乱要高效得多。对于容器环境,更要提前确认宿主机的 core_pattern 和挂载配置,不要假设容器内外的行为一致。
还有一点经验:core 文件虽然重要,但不要让它成为唯一的排查手段。完善的日志、指标监控、分布式追踪同样关键。core 文件解决的是“崩溃瞬间发生了什么”,而日志和监控解决的是“崩溃前系统处于什么状态”。两者结合,才能真正快速定位问题。
最后分享一个小技巧:如果生产环境不方便直接生成 core 文件,可以在测试环境复现问题后生成 core,再用 gdb 分析。测试环境的配置可以更宽松,磁盘空间也更充裕,分析起来没有后顾之忧。