操作系统内功 · 第 58 篇|kexec、kdump 与 oops:内核崩溃的快速重启与现场尸检

kexec 与 kdump 崩溃现场链路(手绘信息图)

oops 与 panic:内核出事的两种姿势

内核代码出错的第一个动作往往不是死机,而是打 oops:非法解引用、空指针访问被页错误机制捕获,内核往日志里打出一整段现场——出错指令的地址、寄存器快照、调用栈(call trace)。如果错误发生在进程语境且没有伤到内核关键状态,内核杀掉肇事进程继续运行,系统表面还能跑;但 oops 的含义是「内核刚刚越过了自己的红线」,状态可信度已经存疑,后续连锁出事的概率陡增。运维上看到 oops 的正确反应不是「还能用就先不管」,而是当周排期查因。

panic 则是内核宣判不可恢复:出错点在中断或软中断语境、锁状态被破坏、关键路径显式调用 panic、或 init 进程死亡,内核停掉调度、打印 Panic 消息,之后按 panic_timeout 设定的秒数重启,或原地挂死等人工接手。两者的运维分界很清楚:oops 靠日志追(dmesg 里的调用栈是第一现场,地址要靠内核符号表翻译,装了带调试符号的内核包才能读出可读函数名);panic 是「整机完蛋」,必须提前布好死亡之后的机制——这正是 kdump 的职责。

一对开关值得记住:/proc/sys/kernel/panic_on_oops 置 1 后,任何 oops 都升级成 panic(kdump 环境通常这么配,保证任何内核错误都留下完整现场,而不是「先 oops 三天再突然真崩」);魔术键 SysRq 加 c 键(或往 /proc/sysrq-trigger 写 c)可以人为触发一次内核崩溃,用来演练整条 kdump 链路是否真的可用——没演练过的 kdump 等于没配。

概念层次再补一对:WARN_ON 与 BUG_ON。前者是「发现异常但尚可继续」,打日志不中断;后者直接触发 oops;panic 留给「继续跑只会更糟」的场景。内核开发者选哪个是风险评估,运维读日志时要据此分严重度——满屏 WARN 不等于系统在崩,但一条 BUG 加 oops 就要认真对待。另外提醒:现代内核开启了地址随机化(KASLR),oops 里的地址要做符号翻译才可读,这也是尸检必须配套精确版本调试符号包的原因之一。

还有一个实用视角:oops 日志的解读门槛其实不高。把日志按行拆开——错误类型行、出错指令地址行、寄存器组、调用栈,栈从下往上读就是事故经过;大多数时候不需要看懂全部寄存器,只要从调用栈里认出「哪个模块的哪个函数」就能定责。社区里大量事故报告的第一步都是这个动作,每个后端都值得亲手练一次。

kexec:跳过固件的快速重启

kexec 的原始卖点是快速重启。普通 reboot 要走固件自检、引导器装载、内核早期初始化的完整流程,物理机几十秒起步;kexec 则在当前内核还活着的时候,把新内核预先加载到内存里选好的空位,执行时直接跳过去——跳过固件与引导器,停机时间从分钟级缩到秒级。用法两步:kexec -l 装载新内核与 initrd 并给好命令行,kexec -e 执行跳跃。维护窗口里滚动重启一批内核的集群,这个加速是实打实的。

但 kexec 在本篇的真正身份是 kdump 的地基:panic 时不走固件重启,而是直接跳进一个「预先装载好的小内核」去采集现场。为什么不直接用崩溃内核自救?因为崩溃意味着内核的记账状态已经不可信——页表、锁、内存分配器随时可能说谎,任何依赖旧内核的采集动作都可能在半路再崩一次。唯一的可靠做法是换一个完全健康的内核来做这件事,kexec 恰好提供了「不经过固件、不经过旧内核的任何服务」的跳转能力,两者严丝合缝。

kexec 的两种用途在机制上也有差异,值得分开记。普通重启式装载的内核从常规内存启动,跳跃前旧内核会尽量把设备停干净;kdump 式装载(panic 触发)落进 crashkernel 预留区,命令行带精简参数,捕获内核对硬件采取最小假设策略——不依赖固件初始化、自己重新枚举必要设备。前者追求顺滑,后者追求在任何崩溃状态下都跑得起来,两者共用一套装载工具但配置语义不同。顺带一提,kexec 快速重启在集群运维的真实位置:它加速的只是「内核替换」这一步,滚动重启的总时长往往被服务启动、数据预热、流量切换主导——所以它对内核迭代频繁的基础设施团队价值最大,对普通业务集群只是锦上添花。

kdump 与 crashkernel:为尸检预留现场

kdump 的核心难点是一个优雅的死结:要采集崩溃现场,需要一个健康的内核在跑;但崩溃时旧内核已经不可信,不能指望它干任何活。解法是预留加预载:启动参数 crashkernel=256M(发行版也提供按内存分档的 auto 语法,大内存机器预留几百 MB)让内核在开机早期从物理内存里圈出一块固定区域,此后普通内核绝不使用这块内存;再用 kexec 把捕获内核(一个带精简 initrd 的小内核)预载进这块预留区待命。

崩溃发生时,panic 路径触发 kexec 跳跃:捕获内核带着特殊命令行启动——elfcorehdr 参数指向旧内核内存布局的描述表,maxcpus=1 之类的精简参数把启动开销压到最小。它眼里「旧内核的全部内存」就是一堆普通物理页,通过 /proc/vmcore 以 ELF 格式暴露出来。捕获内核的用户态脚本(发行版封装为 kdump-tools 或 kdumpctl,由 kdump 服务驱动)把 vmcore 压缩落盘到 /var/crash,然后重启回正常内核。全程不依赖崩溃内核的任何功能,这是它比「崩溃后抢救 dmesg」可靠得多的根本原因——日志缓冲区可能还没来得及落盘,而 kdump 拿到的是完整内存快照。

落盘体积是第一工程问题:vmcore 理论上约等于物理内存大小,512 GB 内存的机器裸 dump 就是 512 GB。makedumpfile 负责瘦身:丢弃零页、按级别裁掉用户态页(内核态占用为主时,产物常能压到内存的十分之一上下),再配合压缩算法,落盘时间从分钟级进到可接受的范围。验证闭环也要养成肌肉记忆:开机后确认 kdump 服务状态显示可用(发行版的 kdumpctl status 或 systemctl status kdump),再用 SysRq 触发演练一次,确认 /var/crash 里真的产出了 vmcore——预留区配小了捕获内核起不来、debuginfo 没装 vmcore 拿到也读不懂,这些坑只有演练能暴露。

链路的验证点也值得写进运维手册:/sys/kernel/kexec_crash_size 反映预留区实际大小,与内核命令行的 crashkernel 参数对得上才算生效;捕获内核的装载状态有对应接口可查,重装内核后必须重新装载捕获内核;落盘目标目录按最坏情况的 dump 体积规划容量并留余量,采集脚本还要处理磁盘满的分支(加大裁剪级别或转投远端)。kdump 是一条平时零流量、出事满负荷的链路,所有依赖项都要按出事时的峰值验收。

crash 工具入门:log、ps、bt

拿到 vmcore 后用 crash 工具分析:crash vmlinux vmcore,其中 vmlinux 是带调试符号的内核镜像(发行版 debuginfo 包提供,版本必须与崩溃内核精确一致)。进去后几个命令覆盖八成场景:log 打印崩溃内核的环形日志缓冲区,第一屏就是 panic 时的 oops 与调用栈——这是「死因陈述」;ps 列出崩溃时刻的全部进程与状态,处于运行态或长期持有的可疑进程一眼可见;bt 给指定进程打印完整内核栈(bt 后跟 pid,或直接看崩溃现场进程);files 看进程打开的文件,vm 看进程内存映射,mod 列已加载模块,kmem 分析内存分配器的状态。

一个典型尸检流程:log 看崩溃点在哪个函数;bt 看完整调用链,判断崩在驱动层、内存管理还是网络栈;ps 找出肇事进程与它当时在干什么;files 与 vm 补上下文;结合内核源码定位问题,最后决定是升级内核、回退驱动还是改配置。经验统计上,内核崩溃的多数根因落在第三方模块与设备驱动上,内核本体的 bug 反而少见——这也是生产机器不用最新内核、驱动升级前先过压测的朴素理由;同时 mod 命令的输出值得在尸检开始时先扫一眼,确认有没有版本可疑的第三方模块在场。

尸检时区分「软件崩」还是「硬件病」有个快速筛子:log 里若有机器检查(MCE)类记录、报错地址每次漂移、调用栈支离破碎,先怀疑内存或 CPU 故障,转硬件诊断;报错地址稳定复现、调用栈完整、总能归到同一个函数,则更像软件缺陷。crash 工具的 kmem 检查分配器状态、search 扫内存特征,都能辅助定性。先分硬件还是软件,再决定报修硬件还是提内核问题——顺序反了会浪费整个事故窗口。

kdump 与第 53 篇的 core dump 是一对镜像分工:用户态进程崩溃时,内核把它的地址空间快照成 ELF core,交给调试器做用户态尸检;内核自己崩溃时,kdump 把整个内核内存快照成 vmcore,交给 crash 做内核态尸检。层次不同、思路相同:死前留全尸,死后查现场。两套机制配齐的系统,从应用到内核的故障都有可回放的证据链,这是「玄学重启」与「根因归档」的分水岭。

还有一个与 kdump 互补的现代机制顺带一提:内核热修补。它解决「已知问题不想重启就能修」,与 kdump 解决的「崩了能查」方向相反,两者配合覆盖内核运维的两端——能不重启就不重启,真崩了必有现场。线上内核的完整治理策略通常两者都要:高危修复走热修,未知崩溃靠 kdump 留证据。

工具的形态也交代一句:crash 是交互式命令行,支持脚本化批处理,可以写脚本对多个 vmcore 做批量初筛(先 log 抓崩溃点、按崩溃函数聚类归组),人工只看筛出来的疑难样本——规模化运维把尸检的第一道工序自动化,是很划算的投入。各发行版的调试符号包命名规则不同,脚本要按发行版分别适配。

生产环境的成本与取舍

开 kdump 不是免费的,三笔账要算清。内存账:预留区平时完全不可用,256 MB 起步、大内存机器更多——对内存焦虑的机器这不是小数;IO 账:崩溃落盘要写几 GB 到几十 GB,系统盘写满或写慢都会拖垮采集,网络存储环境还涉及采集期间的组网配置(kdump 支持把 dump 传到远端,但链路复杂度上了一个台阶);运维账:内核升级后捕获内核要重新装载,debuginfo 要按版本配套下载,链路上的每一环都要定期演练——不演练的 kdump 在真崩溃时的失败率相当高。

取舍的朴素结论:生产环境值得开。内核崩溃概率低,但后果是整机全挂且往往无声无息(看门狗层面只看到失联);事后如果连现场都没有,事故复盘只能归档为「疑似硬件问题」或「玄学重启」,同类故障必然复发。存储与虚拟化基础设施、内核版本频繁变更的规模化集群、跑过 oops 的存量机器,是收益最高的三类场景;个人桌面与临时测试机可以不开,省下内存与维护成本。

接手新机器时的初始化检查清单里应该有一行 kdump:服务是否启用、crashkernel 参数是否在内核命令行、上次演练时间。发行版默认状态各异(不少服务器发行版装机即启用),别拿「装完就能用」当假设——kdump 的可靠性等于你上一次演练的可信度。

把这套机制放进工程文化里看更有意义:kdump 的价值不在于经常用上(健康的生产环境几年未必真崩一次),而在于它把「内核崩溃」从不可讨论的玄学变成有证据链的工程事件。预留几百 MB 内存,买到的是「每一次崩溃都有复盘能力」——这是运维基建里性价比最高的保单之一。

小结

oops 与 panic 是内核出事的两个级别:前者内核尚存一息但状态存疑,后者直接宣判整机不可恢复。运维动作的锚点是 panic_on_oops 与演练用魔术键,把「可能崩」变成「崩了必留痕」。

kexec 跳过固件的快速重启是 kdump 的技术地基:崩溃时跳进预载的捕获内核,绕开不可信的旧内核,用健康内核把 /proc/vmcore 采集落盘。crashkernel 预留、makedumpfile 瘦身、演练闭环,三件事决定这套机制在生产上的真实可用度。

crash 工具的 log、ps、bt 三连覆盖多数尸检场景,第三方模块是第一嫌疑人。与第 53 篇的用户态 core dump 合起来看:这套体系的哲学是「在一切崩溃路径上预埋健康执行体」,内核态如此,用户态如此,未来的分布式系统故障采集也是同一个思路。

下一篇回到安全语境:系统调用入口的最后一道闸——seccomp 的两种模式、BPF 过滤器的语义,以及它在容器安全纵深里与 Capabilities、LSM 的分工。

← 返回资讯列表

读者留言

COMMENTS 暂无
仅本站原创文章开放留言 · 请勿留下手机号、邮箱等个人信息

还没有留言,来说第一句?