首页 / 资讯中心 / 文章详情

Linux内核挂死全解析:从现象到kdump实战排查

Linux内核挂死全解析:从现象到kdump实战排查 ★ FEATURED ARTICLE
干内核这一行的人最怕的其实不是进程崩溃也不是磁盘写满而是那种半夜被电话叫醒电话那头只说一句“机器连不上了ping 不通屏幕卡死按什么都没反应”。这种时候十有八九是 Linux 内核挂死。“linux 内核挂死”这个词听着挺硬核但它离普通用户并不远。你用的云主机、公司机房的物理服务器、路由器、甚至智能设备底层跑的都是 Linux 内核。一旦内核挂死不是某个 docker 容器挂了而是整台机器变成一块“砖头”网络断开、CPU 空转、磁盘不响应唯一的办法往往是强制断电重启。这个现象背后的原因、排查手段、以及怎么提前把“现场”留下来就是这篇文章想聊透的东西。文章适合谁看一个是做运维和 SRE 的另一个是写内核模块或者驱动开发的工程师还有那些被领导临时拉去处理“内核 panic 工单”的人。我会从现象分类、常见诱因、现场取证、实战复盘、工具清单这几个角度展开尽量用说人话的方式把技术细节讲清楚也把我自己踩过的坑一并交代了。1. 内核挂死到底是什么1.1 先给“挂死”画个像内核挂死本质上是内核失去了“继续推进”的能力。你可以把内核想象成一栋大楼的总控室里面有很多值班员各自负责调度 CPU、内存、硬盘、网络。如果其中一个值班员突然拿着钥匙卡死在一扇门前后面排队的人全部被堵住整栋大楼的所有业务就会停摆。更麻烦的是这个卡死往往发生在很底层的地方连报警电话都打不出来。它跟用户态进程崩溃有本质区别。普通程序崩了内核会清理它的资源其他进程照跑但内核自己挂死没有“更高优先级”的角色来收拾残局。硬件还在通电指示灯还亮着但操作系统已经没有“灵魂”了。从表现上看内核挂死有两种极端形态。第一种是“有声响”的挂死比如控制台不停打印类似Kernel panic - not syncing的堆栈信息然后系统自动重启或者永远卡在 panic 状态。第二种是“死寂式”挂死屏幕上什么都没有网络全断CPU 也不跑连硬件中断都没反应像极了断电但风扇还在转。这两种形态对应的取证手段完全不同后续会细说。注意有些被当成“内核挂死”的现象其实只是负载过高、内存耗尽、或者 disk I/O 卡住导致的系统“假死”。区分真假很重要假死往往还能通过 SSH 进去或者用 SysRq 键救回来真挂死通常只能重启。1.2 挂死的四种典型形态我在实际工作中见过大量内核挂死问题它们可以被归纳成四类每一类的内核机制、日志特征、检测方式都不一样。先看一张速查表类型典型表现内核日志特征检测机制内核 panic内核主动停机打印堆栈可能自动重启Kernel panic - not syncingpanic 处理路径soft lockup某 CPU 长时间无法调度其他任务但中断可能还开着watchdog: BUG: soft lockup - CPU#x stuck for 23s!soft lockup detectorhard lockup某 CPU 连中断都无法响应中断被长时间关闭Watchdog detected hard LOCKUP on cpu xNMI watchdog / hard lockup detectorhung task某个内核线程或用户进程长时间处于不可中断的 D 状态INFO: task xxx blocked for more than 120 secondshung task detector所谓 panic就是内核发现自身状态已经无法恢复主动触发“一键终止”。它能打印出调用栈通常还伴随Call Trace信息信息量最大也最好查。soft lockup 和 hard lockup 这两个词容易混淆。soft lockup 表示某个 CPU 在内核态里长时间处于禁止抢占的状态比如死循环或者长时间自旋其他用户态任务排不上队但是硬中断可能还能进来。hard lockup 更严重表示 CPU 在关中断状态下跑飞了连 NMI不可屏蔽中断都响应不了。检测 hard lockup 依赖 NMI watchdog没有硬件支持或者被虚拟化屏蔽时是检测不到的。hung task 则是一个任务在 D 状态不可中断睡眠下卡死超过默认的 120 秒。注意D 状态本身不一定是故障特别是存储系统抖动时进程经常短暂进入 D 状态但如果长时间不退出就一定有问题内核会主动打印出该任务的调用栈。理解这几种形态能帮你快速缩小排查范围。比如看到soft lockup优先怀疑锁竞争、死循环和 CPU 调度看到 hung task优先怀疑文件系统、块设备层和驱动层面的卡死看到 panic则要重点分析调用栈里的最后几个函数。2. 哪些原因会触发内核挂死2.1 锁与并发最隐蔽的元凶我在实际处理内核挂死的经验里锁相关的并发问题至少占了一半。Linux 内核里有大量的自旋锁、读写锁、互斥锁、信号量和 RCU 机制多核 CPU 同时访问共享资源时必须通过锁来保护。锁用得不好就会死锁。一个经典场景是“锁顺序反转”。假设 CPU0 持有锁 A想继续拿锁 BCPU1 持有锁 B想继续拿锁 A。两个 CPU 各自都不放手互相等待系统里的其他任务被越堵越多最终表现为 soft lockup 或者 hung task。这种问题在代码评审阶段很难看出来因为需要两个 CPU 同时踩到特定路径概率可能只有百万分之一。但一旦某些外部条件变化比如网络流量增大、磁盘吞吐升高、中断频率变化复现概率就会急剧上升。自旋锁还有一个更隐蔽的坑在中断上下文里尝试获取一个已经被占用的自旋锁。自旋锁的特性是“等不到就原地打转”如果持有锁的进程被其他 CPU 打断而新来的中断处理函数也去争抢同一把锁就会形成“ABBA 死锁”。这种情况连调用栈都很混乱经常看到两个 CPU 都停在_raw_spin_lock上。RCU 的问题也值得提。Linux 内核的 RCU 机制设计得再精巧在某些深度嵌套的场景下也可能出现“读者永远退出不了临界区”的情况导致宽限期无限延长后续的回收操作全部卡死。这类问题在普通日志里很难看出端倪只能靠 lockdep 和 RCU stall 检测日志提供线索。2.2 驱动和硬件现场最惨烈驱动代码质量参差不齐是最容易导致内核挂死的另一个大头。尤其是网卡、存储控制器、GPU 这类高性能设备驱动它们要跟硬件中断、DMA、内存屏障打交道任何一步写错都可能直接让系统崩掉。我遇到过一类很典型的问题驱动在设备热插拔时没有正确处理中断亲和性。设备移除后它申请的中断号被重新分配给了其他设备老的中断处理函数还在旧中断号上注册结果新的中断事件触发了一个“野指针”处理函数CPU 直接跑飞最终 hard lockup。这种问题往往只在切换硬件、热插拔操作后出现极难复现。还有一个高频元凶是中断风暴interrupt storm。某些硬件异常会导致设备不断产生中断CPU 被中断处理程序占满几乎没有时间运行其他任务。这不是严格意义上的“挂死”但用户感知就是系统完全卡住。中断风暴在日志里通常表现为/proc/interrupts里某个中断号计数飞速上涨或者 soft lockup 的调用栈反复落在中断处理函数里。硬件本身也可能背锅。内存的不可纠正错误会触发 Machine Check Exception轻则杀掉触发进程重则直接 panic。PCIe 设备的 AER 错误、NVMe 设备固件 bug、ACPI 固件实现不正确都可能导致内核在错误处理的路径上再次出错形成二次故障。所以排查内核挂死时不能只盯代码还要监控硬件报错日志。实操心得处理驱动导致的内核挂死最有效的动作不是现场 debug而是先收集完整的内核版本、驱动版本、设备型号和固件版本。很多驱动 bug 都有已知修复补丁只要版本对上答案基本就出来了。2.3 内存回收和文件系统慢得像挂死有些“内核挂死”并不是真的逻辑死锁而是内核内存管理或文件系统在极端情况下慢得离谱但没有彻底死透。这种问题更折磨人因为看起来是挂死但系统偶尔又有点反应。内存紧张时内核会进行直接内存回收direct reclaim。如果系统有大量 page cache 需要回写或者内存碎片严重直接回收路径可能在页表遍历和 LRU 链表操作上卡很长时间。特别是开启了 THP透明大页的虚拟机里khugepaged线程在合并页表时可能长时间持有锁让其他内存操作全部排队。文件系统也是重灾区。ext4 的日志提交线程jbd2在日志刷盘时如果遇到存储设备不响应可能让大量进程阻塞在 D 状态。当存储控制器出现故障或网络存储延迟异常会出现一整批进程卡在journal相关栈上最终触发 hung task。我在项目里查过很多次这种“存储挂了导致所有进程 D 状态”的情况根因往往在存储链路而不是内核代码。内存回收还有一个隐蔽问题cgroup 内存限制。当某个容器达到内存上限时内核会在容器内部进行回收如果该容器的 page cache 太多太碎回收线程可能在 memcg 的 LRU 锁上长时间自旋把容器所在节点的 CPU 拖死。这个现象在 Kubernetes 环境里尤其常见。2.4 误配置和内核版本回归除了代码和硬件配置和内核版本问题也会造成挂死。最常见的是内核启动参数里设置了不合适的 poll 机制、ACPI 休眠相关配置、或者 NUMA 相关参数导致在特定硬件上初始化失败。很多时候新装系统后首次重启就挂死大概率不是运气差而是内核和主板固件之间的兼容性问题。内核版本回归同样不可小觑。某个原本稳定的系统升级内核后突然频繁 soft lockup不要急着怀疑业务先查这个版本的内核变更日志。我遇到过一例某版本改动了块设备层的 IO 调度队列导致特定 SSD 型号的响应路径出现长时间自旋直到下一个补丁版本才修复。生产环境的“内核更新”必须作为重要变更管理不能看到有更新就顺手升级。3. 挂死现场信息采集没有数据一切免谈3.1 串口和 Netconsole让内核开口说话内核挂死往往伴随着网络、显示和输入设备全部失效这时候唯一可靠的“逃生通道”是串口。串口是独立于操作系统之外的物理设备内核只要能运行最低级的打印代码就能把日志输出到串口哪怕屏幕已经黑了、网卡已经死了。要开启串口输出你需要在内核启动参数里加上类似下面的配置consolettyS0,115200n8 earlyprintkserial,ttyS0,115200n8 ignore_loglevel其中console指定内核日志输出到串口earlyprintk让早期启动阶段的打印也走串口ignore_loglevel则是不按日志级别过滤把能打的都打出来。配置改完后需要重新生成引导配置并重启生效。在常见的 systemd 发行版上修改/etc/default/grub里的GRUB_CMDLINE_LINUX后执行update-grub或grub2-mkconfig -o /boot/grub2/grub.cfg。串口也有局限距离远、线缆多、不方便统一管理。另一个实用方案是 netconsole也就是通过网络把内核日志送到远端日志服务器。即使系统本身网络服务已经卡死内核的 netconsole 使用的是自己的协议栈很多时候仍然能输出日志。内核参数配置方式类似这样netconsole51410.0.0.10/eth0,51410.0.0.20/00:16:3e:00:00:01这个格式是“源端口源IP/网卡名,目标端口目标IP/目标MAC”。目标 MAC 最好显式指定否则可能依赖 ARP而内核挂死时 ARP 不一定会正常工作。远端用nc -u -l 514或者 rsyslog 接收即可。注意设置串口和 netconsole 后一定要在业务低峰期做一次强制 panic 测试看看远端到底能不能收到完整日志。很多系统配了日志通道但从未验证过真出事时才发现格式不对或目标不可达那就白白浪费了最重要的现场。3.2 kdump 和 crash留一份完整的尸体报告串口日志能告诉我们“最后发生了什么”但很多时候不够。想要知道内核挂死那一刻所有 CPU 的寄存器、栈、内存里的锁状态必须依靠 kdump。kdump 的机制是这套机制预留一部分内存crashkernel512M当内核 panic 时崩溃的内核不直接停机而是启动一个预先装载的“救援内核”把崩溃时刻的内存镜像vmcore转储到磁盘。这个 vmcore 就是内核的“尸体报告”内容极其完整。配置 kdump 的步骤大致如下安装kexec-tools并安装对应内核版本的 debuginfo 包至少要包含带符号表的vmlinux。在内核启动参数中预留内存crashkernel512M编辑/etc/kdump.conf指定 vmcore 保存路径并确保磁盘空间足够至少预留内存镜像大小 压缩后的空间。启动 kdump 服务并触发一次测试echo c /proc/sysrq-trigger如果配置正确系统会进入 panic几秒后救援内核起来把 vmcore 写到目标路径然后按配置重启。拿到 vmcore 后用 crash 工具分析。crash 是内核调试的瑞士军刀常用命令包括crash vmcore /usr/lib/debug/lib/modules/$(uname -r)/vmlinux进入 crash 交互环境后先bt查看当前崩溃线程的栈再用foreach bt遍历所有 CPU 的调用栈用ps查看当时进程状态用files查看打开的文件。如果怀疑锁问题还可以用lock查看锁依赖用dis反汇编关键函数。这些信息组合起来基本能还原挂死瞬间的完整画面。3.3 ramoops/pstore日志的最后一块遮羞布有些环境既没有串口也没有网络比如边缘设备、嵌入式开发板或者虚拟机里被禁用了一些输出通道。这时候还能指望 pstore 子系统。pstore 的原理是把内核崩溃前后的日志写到一块不会因重启而丢失的存储区域里常见实现是 ramoops用内存里的一段保留区域来保存日志。只要内核配置了CONFIG_PSTORE和CONFIG_PSTORE_RAM并通过设备树或内核参数指定保留内存地址和大小系统重启后挂载/sys/fs/pstore就能看到上次崩溃的dmesg-ramoops-0文件。ramoops 的好处是可读性强不需要复杂的 kdump 配置缺点是只能保存控制台打印的日志不能像 vmcore 那样保存完整的 CPU 现场。但对于很多驱动 bug 和 panic 问题日志信息已经足够定位了。嵌入式场景下我通常同时开启 pstore 和串口双保险。3.4 SysRq 魔法键人肉急救时的手动解剖如果系统还没完全死透只是 SSH 进不去网络断了但内核调度器还在运行SysRq 键是最后的现场取证手段。通过sysctl kernel.sysrq1开启后你可以直接往/proc/sysrq-trigger写字符来触发内核动作。常用的取证动作是echo t /proc/sysrq-trigger # 打印所有 CPU 的栈回溯 echo w /proc/sysrq-trigger # 打印 D 状态的任务堆栈 echo m /proc/sysrq-trigger # 打印内存状态 echo l /proc/sysrq-trigger # 打印所有在线 CPU 的栈回溯高级这些输出会打到内核日志里如果之前配置了 netconsole 或串口就能把堆栈远程抓下来。注意这一步不是让你救回系统而是趁它还活着赶紧把现场记录下来。如果完全不想要现场那当然可以直接echo b /proc/sysrq-trigger强制重启。常见误区有人看到内核卡了就立刻按电源键重启这等于销毁所有证据。除非已经明确知道是硬件故障且业务需要快速恢复否则先花两分钟尝试抓栈永远不亏。4. 一次内核挂死的实战排查复盘4.1 现象客户端大面积失控前面讲了一堆理论下面分享一个模拟项目里比较典型的复盘。某公司有一套跑在虚拟化平台上的工作节点集群某天凌晨监控开始疯狂告警所有节点几乎同时失联。物理控制台显示屏幕停留在登录界面鼠标键盘全部无效网络设备也收不到任何包。第一反应是硬件掉电但电源灯正常内存指示灯正常于是怀疑操作系统层面出了问题。当时没有任何串口日志因为新机房还没来得及布串口线kdump 倒是提前配了但只覆盖了 panic 场景。运维同事强制重启后系统恢复随后我去翻/var/log/messages发现内核日志里大量出现了这样的记录watchdog: BUG: soft lockup - CPU#3 stuck for 23s!同时还有多个进程的 hung task 记录像是某个宿主机上的存储 IO 全面停滞。这基本可以确定是内核挂了不是硬件掉电。4.2 时间线先有存储告警然后软锁定接下来要做的是把日志按时间线对齐。我先把 netconsole 配置到远端再结合/var/log/messages、内核日志和监控系统的 CPU/磁盘指标画时间线。发现挂死发生前 5 分钟存储侧已经开始频繁报 SCSI 错误和 NVMe 超时紧接着某个文件系统的日志线程出现异常然后才出现 CPU soft lockup。这个顺序很重要说明大概率不是 CPU 本身先出问题而是存储链路异常诱发了内核里的某种锁问题。另外日志里出现了一个细节不是所有 CPU 都卡死只有 CPU3 和 CPU7 两个核的日志特别密集其他核还在尝试处理网络中断。多核挂死经常表现为“部分 CPU 在死锁部分 CPU 被排挤”所以看到软锁定时一定不要只盯着发生软锁定的那个 CPU要把所有 CPU 的调用栈都捞出来。4.3 vmcore 剖析两个 CPU 在 spinlock 上死锁因为 kdump 覆盖了 panic但那次故障最终表现为 soft lockup没有触发 panic所以并没有自动产生 vmcore。后来我们复现故障时手动触发了 sysrq c成功抓到 vmcore。用 crash 打开 vmcore 后我执行了foreach bt输出显示 CPU3 停在一个文件系统日志提交线程的路径上栈里最后一层是spin_lockCPU7 则停在块设备层 IO 完成的路径上同样卡在_raw_spin_lock。再结合log命令里两个 CPU 的持锁栈可以确定这是一对典型的 ABBA 死锁文件系统日志提交路径持有一个日志锁等块设备层的完成锁块设备层完成路径持有了完成锁又想获取日志锁。两个 CPU 互相等待谁也跑不掉。这里要强调一个经验死锁问题只看单线程栈永远看不出问题。必须把相关 CPU 的栈放一起对比找到两个栈里互相引用的同一把锁。如果没有 vmcore你拿到手的只能是“CPU3 卡在 spin_lock”这种表面现象永远不知道背后还有一个 CPU7 也卡在另一把锁上。4.4 修复与验证一个补丁解决问题根因找到后问题就解决了一半。我翻看该内核版本在文件系统和块设备层相关子系统的补丁记录果然发现一个修复“日志提交路径中锁顺序错误”的补丁。这个补丁重新调整了锁的获取顺序避免在持有一个锁的同时去获取另一个锁从而打破 ABBA 循环。把内核升级到包含该补丁的版本后我们在同样环境里做了大量 IO 压力测试连续跑了一个星期没有再出现 soft lockup。这段复盘的教训是如果没有串口、没有 vmcore、没有远端日志几乎不可能定位到这种锁顺序问题。很多人把时间花在“重启后看 dmesg”上但 dmesg 在挂死那一刻之后的内容早就丢了重启后看到的只是幸存者偏差。5. 避坑经验与排查工具速查5.1 排查工具速查表根据我的实际操作经验面对内核挂死时下面这些工具最常派上用场工具用途常用命令dmesg / journalctl查看内核日志和系统日志dmesg -T、journalctl -k -b -0sysrq-trigger手动获取堆栈和内存信息echo t /proc/sysrq-triggerkdump crash崩溃时保存完整现场并分析crash vmcore vmlinuxperf lock分析锁竞争和死锁perf lock record、perf lock reporttrace-cmd / ftrace跟踪特定内核路径trace-cmd record -e sched_switchbpftrace动态追踪内核函数bpftrace -e kprobe:.../proc/interrupts判断中断风暴watch -n1 -d cat /proc/interruptssysctl vm/fs相关参数调整vm.dirty_ratio、kernel.hung_task_timeout_secs排查时我习惯先看/var/log/messages里有没有watchdog、hung task、kernel panic、BUG:关键字并列出来。如果没有现成日志再决定要不要主动复现并打开 trace。5.2 内核配置和生产调优建议在能控制内核编译参数的环境里建议和测试环境区分开。生产环境不要盲目追求把所有 debug 选项全开但几个跟挂死检测相关的选项必须确认打开CONFIG_SOFTLOCKUP_DETECTORy等于开启 soft lockup 检测。CONFIG_HARDLOCKUP_DETECTORy开启 hard lockup 检测依赖硬件 NMI 能力。CONFIG_HUNG_TASKy和CONFIG_DETECT_HUNG_TASKy开启 hung task 检测。CONFIG_KALLSYMSy在日志中打印可读的符号名否则调用栈全是十六进制地址。CONFIG_DEBUG_INFOy生成调试信息配合 crash 使用。CONFIG_LOCKDEP和CONFIG_PROVE_LOCKING是锁死锁的利器在测试环境一定要开生产环境要谨慎。lockdep 会维护锁依赖图性能开销明显但如果你正在排查一类随机出现的死锁问题短期内开启并伴随压测往往能直接抓到锁路径。我见过一个团队为了追一个偶发死锁生产环境开了 lockdep结果线上吞吐掉了几个百分点但最终成功定位问题值回票价。此外kernel.hung_task_timeout_secs默认是 120如果你觉得 120 秒太久可以适当调小比如 60 秒让内核更早打印出 D 状态任务。但不要调得太小否则正常的 IO 抖动也会触发误报。5.3 我踩过的那些坑排查内核挂死最大的坑是“没留现场”。我早期接手过一个集群某台节点平均两周挂一次每次重启后都正常无奈之下只能加了一个定时脚本每分钟把/proc/sysrq-trigger的w输出落盘。结果下一次挂死时脚本也跑了但磁盘里的日志只写到一半——因为系统在写入日志文件时刚好挂死文件没有 flush 到磁盘。后来我把脚本输出改成写到 pstore 区域才总算拿到了完整的 D 状态栈。第二个坑是“迷信 dmesg”。dmesg 只能显示当前运行内核的环形缓冲区挂死那一刻之后的内容会因缓冲区的覆盖机制而丢失。所以每次出问题后看到 dmesg 里干净如新不代表没有日志而是日志已经被后续打印冲掉了。正确做法是提前配置串口或 netconsole把日志实时同步到远端。第三个坑是“忽略 NMI watchdog 的硬件依赖”。在虚拟机里默认的 hard lockup 检测经常不生效因为虚拟 CPU 无法提供可靠的 NMI。这时候你认为自己在监控 hard lockup实际上什么都没监控安全问题就被漏过去。建议在虚拟化环境里至少打开 soft lockup 和 hung task 检测同时通过宿主机层面的监控辅助判断。最后再分享一个通用经验内核挂死排查是一个系统工程不能靠一个问题一个手段。串口、netconsole、kdump、pstore 四件套能配的都配上并且定期做故障演练。真正遇到问题的时候你才不会手忙脚乱。很多时候根因不是“代码写错”那么单纯而是四个条件刚好同时命中特定硬件、特定内核版本、特定负载、特定操作顺序。我们在生产里能做的就是尽量让“现场”完整留下来剩下的交给技术和一点耐心。
阅读完成 · 觉得有帮助?
咨询建站