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] LimitCOREinfinity如果不知道当前系统的服务是怎么拉起来的可以用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里只能看到一堆地址没法映射到源码排查效率大打折扣。运行前先确认ulimitulimit -c unlimited然后直接运行./test_crash正常情况下会看到before crash Segmentation fault (core dumped)这时你检查当前目录或者你设置的core_pattern路径就能看到core文件。4.2 Java服务下的core dumpJava服务崩溃时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里加LimitCOREinfinity。JVM的core文件体积通常非常庞大因为JVM进程占用的虚拟内存动辄数GB生成一次core文件可能需要几十秒甚至几分钟且磁盘消耗惊人。建议给Java服务单独规划大容量磁盘同时coredump目录做好定时清理。4.3 用gdb分析core文件拿到core文件后就可以开始真正的排查了。执行gdb ./test_crash core_test_crash_12345_11进入gdb交互界面输入btbacktrace你就能看到类似这样的输出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设置了还是没有corecore_pattern是管道方式检查cat /proc/sys/kernel/core_pattern改回文件路径core文件生成到一半中断磁盘空间不足df -h检查磁盘给coredump单独分盘core文件很大写盘耗时太长进程占用内存太大在服务级别限制core大小或者使用压缩管道方式gdb加载core失败报错格式不对32位/64位架构不匹配用file命令确认换对应架构的gdbsystemd服务内不生成corelimits.conf不生效在service文件里加LimitCOREinfinity程序由root启动但降权运行不生成coresuid_dumpable是0按需修改fs.suid_dumpable1注意安全风险Java报Failed to write core dump容器ulimit限制启动容器时加--ulimit core-15.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] # LimitCOREinfinity7.2 Docker容器启动配置docker run \ --ulimit core-1 \ --mount typebind,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 tracefile跟踪进程看有没有尝试打开“core”文件路径的痕迹确认崩溃进程的架构和gdb架构一致确认路径所在磁盘还有剩余空间这套检查顺序我用了很多年每次排查速度都很快。另外还想提一句core dump不止服务于C/C和Java。在Qt应用、Python的c扩展、Go的cgo部分甚至是某些边缘计算设备里跑的原生程序core dump都是通用的兜底方案。它不像日志系统那样需要你埋点只要有崩溃就能记录属于“不带并发开销的审计员”。所以每个Linux工程师都值得花半天时间把这块彻底搞懂。后面遇到疑难杂症它就变成了你的第一把利器。
