1. core dump到底是什么,为什么关键时刻它能救命
先聊点实在的。做Linux下C/C++开发或者运维的朋友,大概率都见过类似这样的输出:
Segmentation fault (core dumped)或者是Java服务崩溃时日志里的那句:
Failed to write core dump. Core dumps have been disabled. To enable core dumping, try "ulimit -c unlimited" before starting Java again看到这句话,恭喜你,你的程序“猝死”了。而core dump,就是系统在进程异常终止的那一刻,自动把进程在内存里的完整状态——包括寄存器、堆栈、已加载的共享库信息、所有线程的上下文——打包写到一个文件里。这个文件就是core文件,俗称“核心转储”。
为什么要强调它重要?因为程序崩溃是一个瞬间发生的事情,等你想起来去查的时候,进程已经没了,内存里的变量、调用栈全部烟消云散。而core文件相当于给案发现场按了个快门,把崩溃那一帧的完整画面保留下来。后面你随时可以用gdb去还原这个现场,精确地看到程序到底死在哪一行、谁调用了谁、关键参数的值是什么。
一个真实的场景:银行或者支付系统的某个后台服务,线上运行了几个月都好好的,突然某个凌晨开始反复崩溃,每次都发生在凌晨3点左右。你不可能蹲在服务器前等它复现,更不可能用调试器去连生产环境。但如果系统开了core dump,崩溃瞬间的core文件就躺在磁盘上,你第二天早上来上班,直接gdb 程序 core.xxx,输入bt,一分钟就能看到死活的调用栈。这种效率,远比对着日志猜来猜去要高得多。
往下我会把core dump这块常用的知识体系全部拆开:从内核参数、系统配置、工具链,到实际调试案例,每个细节都按我在真实项目里用过的方案来讲,希望能帮你少踩几个坑。
2. 从零开始开启core dump:临时生效和永久生效的完整操作
2.1 临时开启:一行命令立刻见效
最简单的方案,是在当前shell里执行:
ulimit -c unlimited然后验证一下:
ulimit -c如果输出的是unlimited,说明当前这个终端会话已经允许生成core文件了。这里要注意,ulimit是一个shell内建命令,它只影响当前shell以及从这个shell启动的所有子进程。所以如果你关掉这个终端,再开一个新的,这个配置就失效了,需要重新设置。
有的系统里,你可能会看到:
ulimit -c 204800这说明系统限制core文件最大为200MB左右,204800的单位是KB。生产环境里如果限了大小,遇到大内存进程崩溃,core文件写到一半可能就被截断了,gdb加载会报错,非常尴尬。所以我个人强烈建议,调试环境下直接设成unlimited,不要手软。尤其是Java进程或者内存占用几GB的C++服务,你那个core文件轻轻松松就能到几个GB,设个几百MB基本等于白设。
2.2 永久生效:配置limits.conf
如果想要系统重启之后依然生效,需要改/etc/security/limits.conf文件。常见的写法有两种:
* soft core unlimited * hard core unlimited或者按用户来指定:
root soft core unlimited root hard core unlimited testuser soft core unlimited testuser hard core unlimited这里有两个字段容易搞混:soft和hard。soft limit是当前值,用户可以在不高于hard limit的前提下自己调大或者调小;hard limit是上限,只有root可以改。如果你只写了soft没写hard,那soft一旦设置失败就会有问题。稳妥起见,两个都配上。
需要特别提醒:这个方法在传统的登录方式下是生效的,但如果你使用的是systemd管理的系统,而且进程是通过systemd service方式启动的(比如写了一个xxx.service单元文件),那么/etc/security/limits.conf不一定能覆盖到。systemd的服务需要在service文件里单独配置:
[Service] LimitCORE=infinity如果不知道当前系统的服务是怎么拉起来的,可以用systemctl status 服务名去看一下,确认是ExecStart还是别的形式,再决定配置放哪个文件。这个坑我踩过好几次,一直以为limits.conf是万能的,结果服务由systemd托管时根本不吃这一套。
改完limits.conf后,一般不需要重启机器,重新登录一下当前用户,让PAM重新加载配置即可生效。
2.3 还在被“Permission denied”卡住?检查文件路径权限
有一种很常见的情况:ulimit -c unlimited已经设了,进程也崩溃了,却看不到core文件生成。这时多半是写文件的目录权限不够。举个例子,如果程序的工作目录是/app/logs,而这个目录的所有者是root,程序以普通用户身份运行,那操作系统在尝试写core文件时会直接丢弃,甚至不会给出任何提示。
排查方法很简单:
cat /proc/sys/kernel/core_pattern先看清楚core文件会被写到哪个路径。如果这个路径对应的目录当前用户没有写权限,那就是permission denied。解决方式是把core_pattern指向一个允许写入的目录,或者把程序的工作目录权限调整好,这个我们下面细说。
3. 深入理解core_pattern:这个参数决定core文件去哪、叫什么名
3.1 查看和修改内核参数
core文件往哪里写、文件名长什么样,都是由内核参数kernel.core_pattern控制的。查看方法:
cat /proc/sys/kernel/core_pattern绝大多数Linux发行版上,默认返回值是:
core或者是:
|/usr/share/apport/apport %p %s %c %d %P如果是后面这个,说明发行版(比如Ubuntu)用apport接管了core dump的生成,系统不会直接在当前目录生成core文件,而是把崩溃的信息交给apport处理。这种情况下你常常会觉得“明明开了core dump,怎么就没有core文件”,排查半天发现是apport把文件劫走了。
修改这个参数有两种方式:
临时修改:
echo "core_%e_%p" > /proc/sys/kernel/core_pattern永久修改:在/etc/sysctl.conf里加一行:
kernel.core_pattern = core_%e_%p然后执行:
sysctl -p让配置生效。
我建议在生产服务器上,把core_pattern统一改成带进程名和PID的格式,后面找文件方便很多。
3.2 core_pattern的格式化标识符详解
core_pattern支持若干个%符号类型的占位符,比较常用的有:
| 格式符 | 含义 |
|---|---|
| %p | 崩溃进程的PID,防止重名 |
| %e | 崩溃进程的可执行文件名 |
| %u | 崩溃进程的真实用户ID |
| %g | 崩溃进程的真实组ID |
| %s | 导致崩溃的信号编号 |
| %t | 崩溃时间,从epoch开始的秒数 |
| %h | 主机名 |
| %% | 字面意义的百分号 |
举个例子,假设我设置:
echo "core_%e_%p_%s" > /proc/sys/kernel/core_pattern然后一个名为test_crash的进程因为段错误(信号11)崩溃,PID为12345,那生成的core文件名就是core_test_crash_12345_11。
这里有一个容易踩的细节:%e拿到的可执行文件名是截断过的,不同的内核版本对长度限制不一样,一般在15个字符左右,如果你的二进制的名字很长,文件名里显示出来的可能是截短后的版本。做脚本计算文件名时要小心,不要想当然地以为basename和你设置的名称完全一致。
3.3 绝对路径与相对路径的差异
core_pattern里如果写的是竖线开头,说明用管道把core交给外部程序处理。
|/usr/share/apport/apport %p %s %c %d %P这种情况下,内核把core dump的内容写到apport进程的标准输入,由apport决定最终放到哪里。这种设计的优点是方便实现自动压缩、自动归档,缺点也很明显:如果接管程序本身有bug,或者依赖库不完整,core就直接丢了,连原始文件都不会落盘。
不想用系统自带管道的,可以改成绝对路径:
echo "/data/coredump/core_%e_%p" > /proc/sys/kernel/core_pattern这样core文件会固定写到/data/coredump/目录下,不受当前工作目录影响。我强烈建议生产环境这么做,因为如果你的服务是通过systemd启动的,工作目录可能是/,如果/目录空间不足,core写一半就会失败。而单独规划一个/data/coredump分区或目录,既好找文件,也方便做清理策略。
3.4 别忘了sysctl里还有个fs.suid_dumpable
还有一个隐藏开关:fs.suid_dumpable。如果程序是以setuid方式运行的(比如某些特权命令),或者由root启动后降权,内核默认不允许生成core,这个参数默认值是0。如果需要为这类程序生成core,就要改:
echo 1 > /proc/sys/fs/suid_dumpable永久配置同样写在sysctl.conf里:
fs.suid_dumpable = 1需要注意,这个参数设置为1会有安全风险,因为core文件里包含进程内存,可能有敏感数据。非必要不要在生产环境开启。我在做嵌入式设备调试时偶尔会用到,普通服务器上一般不用动它。
4. 如何用Java和C++分别复现并抓取core文件
4.1 写一个必然崩溃的C++程序
光说不练假把式,这里我们用一段非常经典的“空指针解引用”代码来复现崩溃:
#include <cstdio> int main() { int* p = nullptr; printf("before crash\n"); *p = 42; // 往地址0写入数据,触发段错误 printf("after crash\n"); return 0; }编译的时候建议加上调试信息和-O0来防止编译优化把代码结构改掉:
g++ -g -O0 -o test_crash test_crash.cpp加上-g很重要,这样core文件里才能直接看到源码行号和变量名。如果没加调试信息,gdb里只能看到一堆地址,没法映射到源码,排查效率大打折扣。
运行前先确认ulimit:
ulimit -c unlimited然后直接运行:
./test_crash正常情况下会看到:
before crash Segmentation fault (core dumped)这时你检查当前目录,或者你设置的core_pattern路径,就能看到core文件。
4.2 Java服务下的core dump
Java服务崩溃时,JVM也会尝试把崩溃前的堆信息、线程快照写到文件中。很多情况下报错信息是:
Failed to write core dump. Core dumps have been disabled. To enable core dumping, try "ulimit -c unlimited" before starting Java again这种情况大多发生在docker容器里。因为容器默认的ulimit是继承自dockerd进程的,而很多发行版默认ulimit -c是0。解决办法有两种:
第一种,在启动容器时指定:
docker run --ulimit core=-1 ...第二种,在容器内执行ulimit -c unlimited后再启动Java进程,或者在systemd service里加LimitCORE=infinity。
JVM的core文件体积通常非常庞大,因为JVM进程占用的虚拟内存动辄数GB,生成一次core文件可能需要几十秒甚至几分钟,且磁盘消耗惊人。建议给Java服务单独规划大容量磁盘,同时coredump目录做好定时清理。
4.3 用gdb分析core文件
拿到core文件后,就可以开始真正的排查了。执行:
gdb ./test_crash core_test_crash_12345_11进入gdb交互界面,输入bt(backtrace),你就能看到类似这样的输出:
Program terminated with signal SIGSEGV, Segmentation fault. #0 0x00000000004005c0 in main () at test_crash.cpp:5 5 *p = 42;看,崩溃点清晰明了:test_crash.cpp第5行,对空指针p赋值。如果你只是看终端里那行Segmentation fault,可能根本不知道发生了什么。而有了core文件和gdb,整个调用栈、崩溃位置一览无遗。
如果进程是多线程的,gdb里输入thread apply all bt可以查看所有线程的调用栈,在多线程并发问题排查时,这个命令几乎是必用的。
还有一个日常很实用的组合:用gdb查看core文件里某个变量的值:
gdb> frame 0 gdb> info locals gdb> p *p通过这些基础操作,能快速定位程序崩溃时的实时状态。
4.4 core文件不能用?别忘了算好文件类型和系统位数
有朋友会遇到core文件生成了,但是gdb打不开,报"not a core file"或格式错误。这种情况第一优先检查文件类型:
file core_test_crash_12345_11正常情况下输出类似于:
ELF 64-bit LSB core file, x86-64, version 1 (SYSV)如果你的core文件是32位的,但gdb跑在64位系统上,需要用file确认架构,然后安装对应的gdb-multiarch或者32位gdb。另外,如果系统的core_pattern配置了管道方式,而接管脚本写出的不是二进制core文件而是一段文本日志,那同样没法用gdb直接读。
这些点看起来很小,但真遇到问题的时候,会浪费不少时间。我当初调试一个ARM交叉编译的程序时,就是因为拿64位x86的gdb去读ARM的core文件,折腾了半天才意识到架构不匹配。
5. 常见的core dump相关问题和排查技巧
5.1 典型问题速查表
根据这几年的经验和社区里的高频问题,整理成表格,方便你对照排查:
| 现象 | 可能原因 | 排查/解决办法 |
|---|---|---|
| 提示core dumped,但当前目录没有core文件 | 当前目录无写权限 | 配置绝对路径core_pattern,指向可写目录 |
| ulimit -c unlimited设置了,还是没有core | core_pattern是管道方式 | 检查cat /proc/sys/kernel/core_pattern,改回文件路径 |
| core文件生成到一半中断 | 磁盘空间不足 | df -h检查磁盘,给coredump单独分盘 |
| core文件很大,写盘耗时太长 | 进程占用内存太大 | 在服务级别限制core大小,或者使用压缩管道方式 |
| gdb加载core失败,报错格式不对 | 32位/64位架构不匹配 | 用file命令确认,换对应架构的gdb |
| systemd服务内不生成core | limits.conf不生效 | 在service文件里加LimitCORE=infinity |
| 程序由root启动但降权运行,不生成core | suid_dumpable是0 | 按需修改fs.suid_dumpable=1,注意安全风险 |
| Java报Failed to write core dump | 容器ulimit限制 | 启动容器时加--ulimit core=-1 |
5.2 实战心得一:优先配置core_pattern的绝对路径
我曾经在一次生产故障排查中,发现一个C++交易程序会随机崩溃,但现场没有任何core文件可查。后来排查才发现,程序的工作目录是/,虽然root用户在/目录有写权限,但core_pattern默认值是core,生成的core文件直接写在根目录下,根本不在程序启动的当前目录,也不是程序指定目录。后来我把core_pattern改成/var/log/coredump/core_%e_%p,问题立刻解决,后续再遇到崩溃,core文件稳稳地躺在固定目录里。
这里额外建议:给coredump目录做定时清理,因为大流量服务如果在凌晨崩溃,一晚上可能生成好几GB的core文件。写一个简单的crontab脚本,删掉3天前的core文件,很管用。
5.3 实战心得二:生产环境别开“无限”太久
生产服务上长期设置ulimit -c unlimited其实是有风险的。假如服务有内存泄漏,崩溃时dump出来的core文件可能是几十GB,瞬间把磁盘塞满,导致其他服务间接受影响。我这里一条经验是:
- 核心交易服务和无人值守的常驻进程:设置一个明确的core上限,比如2GB或4GB,够让gdb还原现场就行。
- 磁盘充足并且对稳定性要求极高的服务:可以开unlimited,但必须有监控和自动清理任务。
- 开发测试环境:随便开,怎么方便怎么来。
5.4 实战心得三:不会写复杂脚本,先学会这三条命令
很多时候,你不一定需要立刻进gdb。用下面这三条命令,可以在极端情况下先拿到关键信息:
# 1. 查看core文件是由哪个程序、哪个信号造成的 file core.xxx # 2. 直接用strings提取二进制里的可读字符串,有时候能看到崩溃前打印的日志 strings core.xxx | grep -i "error\|fatal" # 3. 用gdb只执行一条bt命令后就退出,适合写进自动化脚本 gdb -batch -ex bt -ex quit ./your_program core.xxx第一条命令帮你确认文件是否完整;第二条用于快速筛查core里有没有留下线索;第三条适合批量处理多个core文件时用,可以在循环里一条条跑。
5.5 关于核心转储的一个“附加题”:容器环境
现在大量服务跑在docker或k8s容器里,容器内的core dump除了ulimit之外,还要注意宿主机的/proc/sys/kernel/core_pattern。因为core_pattern是宿主机内核的全局参数,不是每个容器独立维护的。如果你在容器内改了core_pattern,实际改动的是宿主机的内核参数,会影响宿主机上所有进程的行为。所以在容器环境里做core dump调试时,尽量把core_pattern保持在一种“通用”状态,比如core_%e_%p这种不涉及绝对路径的写法,然后让容器内的进程在当前工作目录写core。这样至少不会污染宿主机的其他服务。
还有一种做法是给core_pattern设置成管道模式,把core转交给外部收集系统,比如用|/opt/collect_script.sh %e %p来自动压缩归档。但脚本本身的稳定性一定要高,否则会成为新的故障点。
6. 内核转储相关的安全边界与性能开销
开了core dump之后,并不是万事大吉,有两个问题需要你提前想清楚。
第一个是安全问题。core文件是进程内存的完整镜像,如果程序里处理了密钥、密码、用户隐私数据,那这些数据在崩溃时会原封不动地出现在core文件里。如果core文件权限设置过宽,或者落在了一个所有人都能读的目录下,那就是变相的数据泄露。操作建议:core文件目录权限设为700,并且最好由专门的用户管理;涉及敏感数据的服务,在崩溃时可以考虑不生成core,而是通过日志输出手动解决。
第二个是性能开销。生成core文件的过程是同步的,内核需要把整个进程地址空间写盘,期间进程已经停止,但磁盘I/O占用会非常高。如果这个服务所在磁盘同时还有数据库在跑,写core的尖峰很可能把数据库的IOPS打满。我经历过一次MySQL和业务进程同盘部署,业务进程崩溃触发了一个8GB的core dump,结果MySQL的查询延迟飙升到几十秒。后来做了磁盘隔离,core文件写到独立的物理盘上,问题才缓解。
所以,开启core dump前一定要想清楚:“这个文件到底要写到哪块盘上”“磁盘空间够不够”“有没有权限限制”。
7. 几条能直接复制的配置模板
最后给大家一份我在生产环境常用的core dump配置模板,可直接参考。
7.1 宿主机Linux服务器通用配置
# 1. 创建目录(权限按需调整) mkdir -p /var/log/coredump chmod 1777 /var/log/coredump # 2. 修改内核参数 cat >> /etc/sysctl.conf <<'EOF' kernel.core_pattern = /var/log/coredump/core_%e_%p_%s_%t fs.suid_dumpable = 0 EOF sysctl -p # 3. 修改limits cat >> /etc/security/limits.conf <<'EOF' * soft core unlimited * hard core unlimited EOF # 4. 如果服务由systemd启动,在service文件中追加 # [Service] # LimitCORE=infinity7.2 Docker容器启动配置
docker run \ --ulimit core=-1 \ --mount type=bind,source=/var/log/coredump,target=/coredump \ your_image容器内的服务如果需要生成core,可以设置环境变量或启动脚本中指向/coredump目录,同时core_pattern用相对形式:
echo "core_%e_%p" > /proc/sys/kernel/core_pattern7.3 压缩core文件的思路
如果你的存储紧张,可以通过管道方式让内核把core压缩后落盘:
echo "|/usr/local/bin/compress_core %e %p" > /proc/sys/kernel/core_patterncompress_core脚本核心逻辑很简单:
#!/bin/bash exec gzip -9 > /var/log/coredump/core_$1_$2.gz好处是压缩比很高,坏处是内核dump的实时性会受影响,因为gzip压缩需要消耗CPU,对性能要求极高的服务要谨慎。这个方案我在嵌入式存储受限的设备上用过,效果很不错,但真要在每秒百万级QPS的服务上跑,还是老老实实分盘存储更靠谱。
8. 我最后想多强调几句
很多人觉得core dump就是个“ulimit -c unlimited”的事,其实真正用起来,牵扯到内核参数、系统服务、磁盘规划、权限控制、架构匹配,甚至跨容器、跨平台的问题。我在实际调试中,发现最有价值的往往不是一条命令,而是“遇到问题知道去哪里查”的思路。对照下面的检查链走一遍,大概率能解决九成以上的core dump不生效问题:
ulimit -c确认进程资源限制cat /proc/sys/kernel/core_pattern确认写入路径ls -ld 目标目录确认目录权限systemctl cat 服务名确认systemd是否覆盖了限制- 如果还是不行,用
strace -f -e trace=file跟踪进程,看有没有尝试打开“core”文件路径的痕迹 - 确认崩溃进程的架构和gdb架构一致
- 确认路径所在磁盘还有剩余空间
这套检查顺序我用了很多年,每次排查速度都很快。
另外还想提一句,core dump不止服务于C/C++和Java。在Qt应用、Python的c扩展、Go的cgo部分,甚至是某些边缘计算设备里跑的原生程序,core dump都是通用的兜底方案。它不像日志系统那样需要你埋点,只要有崩溃就能记录,属于“不带并发开销的审计员”。所以每个Linux工程师,都值得花半天时间把这块彻底搞懂。后面遇到疑难杂症,它就变成了你的第一把利器。