干运维和内核调试这些年,接到“linux 内核挂死”的工单次数不少。最直观的体验就是:机房灯还亮着,风扇还在转,可系统像被点了穴一样——网络不通、控制台无响应、业务连不进去,ssh 也毫无反应。很多朋友第一反应是赶紧重启,但只要你按下复位键,最关键的现场就没了。内核挂死不是普通崩溃,它往往没有一条完整的报错日志等着你。这篇文章会把内核挂死的常见形态、前期该做的准备、现场怎么取证、根因怎么定位,按我自己的排查习惯一步步讲清楚。不管是刚接触内核问题的新手,还是被“卡死”折磨过的资深运维,都能拿这套思路去复现排查流程。
1. 先搞明白“内核挂死”到底是哪种死法
1.1 内核挂死不是普通崩溃,别急着按重启
内核挂死(kernel hang)和内核崩溃(kernel panic / oops)是两类完全不同的问题。panic 发生时,内核会主动打印寄存器、栈回溯、异常模块等一大段信息,还会根据 panic 参数决定是否立即重启;而 hang 往往是“再也没动静了”,不会主动输出有效日志,也不会自动恢复。用生活里的例子说,panic 是“人突然大喊‘我出事了你快看’”,hang 则是“握着拳头僵在原地,意识还在但说不出话”。
挂死现场最常见的错误动作,是业务同事在等待超时后直接断电重启。我理解这种压力,但请记住一个重要原则:如果能扛住业务压力,先别重启。因为像进程状态、锁等待关系、中断状态、CPU 栈回溯这些关键证据,重启后一切归零。你真想定位根因,至少要先保证现场被“固定”下来——把控制台照片拍下来、把串口日志保存、把非易失性存储中的日志数据导出。慌归慌,证据不能丢。
1.2 四种常见挂死形态:软锁、硬锁、死锁、静默卡死
我习惯把内核挂死按“内核能不能自己报告异常”分成四种:
软锁(soft lockup)。某个 CPU 上发生长时间不调度,内核的软锁检测机制会报警,打印消息类似watchdog: BUG: soft lockup - CPU#0 stuck for 22s!。它不是系统 100% 不可用,只是这颗 CPU 卡死,其它核可能还能还魂。如果能收到这种输出,已经是挂死问题里最“友好”的一种了。
硬锁(hard lockup)。CPU 进入中断被长期关闭的状态,连 NMI watchdog 都无法打断,内核可能只输出极简的警告,甚至完全无输出。这类现场大多要靠外部带外控制和串口日志来辅助还原。
死锁(deadlock)。两个或多个任务各自持有一把锁,在等对方手里的资源,进入谁也动不了的循环。死锁的第一步通常锁在内核态线程或者驱动回调上,表现为整个系统或某个核心进程永久挂起。
静默卡死(silent hang)。这是实战里最头疼的一类。任务集体进入不可中断睡眠(D 状态),等待某个 IO 或设备操作永远不返回,系统没有 lockup 警告,也没有 panic。往往是存储盘阵掉线、网络存储超时、或者某块设备驱动无法完成请求造成的。
区分这几种形态,直接决定你下一步要看什么:软锁优先查 CPU 栈和自旋锁;硬锁优先查关中断路径和平台时钟;死锁要看任务栈与锁依赖;静默卡死则要把重点放到 IO 等待链路上。
1.3 哪些场景更容易中招
根据我见过的现场,内核挂死高发场景集中在这么几类:虚拟化平台上的 CPU 时钟与 tick 配置异常;新装驱动或升级内核后没经过充分压测;存储设备链路抖动引发文件系统长期等待;数据库、备份、批量计算这类高并发高 IO 业务把某些内核路径推到极限。内核调度、文件系统、设备驱动的代码固然复杂,但绝大多数挂死问题并非无迹可寻,只是你需要一套系统的取证流程。
2. 排查前的准备动作:把探针提前埋好
内核挂死排查最尴尬的情况,是问题发生后你才发现系统什么日志都没留。所以真正有效的排查,从“平时就把环境配好”开始。以下是我维护关键机器时的标准配置清单。
2.1 内核参数:让系统具备自爆能力
内核有些参数是“事故预防”用的,建议在 /etc/sysctl.conf 或内核命令行里明确设置:
kernel.panic = 60 kernel.panic_on_oops = 1 kernel.softlockup_panic = 1 kernel.hardlockup_panic = 1 kernel.panic_on_io_nmi = 1简单解释一下每个参数的意义:
kernel.panic = 60:内核 panic 后 60 秒自动重启。生产环境不可能一直停在那里,给 60 秒是为了留出时间让带外管理或串口把最后的屏障打出去。kernel.panic_on_oops = 1:普通 oops 也触发 panic。oops 本身就是内核走到错误路径的信号,与其让系统带伤继续跑下去,不如直接让它定格,方便取证。kernel.softlockup_panic / hardlockup_panic:检测到 CPU 锁死时立即 panic。注意这会有个权衡——系统会自动重启,你可能会丢失一部分现场,但你会得到一张明确的“诊断书”。愿意监控而不是自动重启的环境,可以保留默认值,用 watchdog 日志作为告警依据。kernel.panic_on_io_nmi:遇到 IO 相关的 NMI 错误时 panic,用于捕获硬件和 IO 路径异常。
提示:这些参数不是所有发行版都默认开启,尤其是 panic_on_oops 和 lockup_panic,不少系统默认是 0。没有开启时,遇到软锁可能只 printk 不动作,日志稍纵即逝。
内核启动参数里还要考虑串口叠加。比如在某发行版常见的 GRUB 配置:
GRUB_CMDLINE_LINUX="console=tty0 console=ttyS0,115200n8"console=ttyS0的意义在于把内核日志导到串口。哪怕系统网络完全挂了,只要带外串口还通,你就还有眼睛。注意调试时最好去掉quiet参数,不然大量 printk 会被抑制,关键信息可能直接被吞掉。
2.2 kdump 和 crash 工具:出事以后有“尸体”可验
只靠串口日志,拿到的是文字线索;想进一步分析内存现场,就要靠 kdump。kdump 在内核崩溃时启动一个捕获内核,把崩溃内存转储成 vmcore 文件,之后可以用 crash 工具离线分析。
基础安装大体是这几个组件:kdump、crash、kernel-debuginfo(不同发行版包名略有差异)。配置上确保启动参数里有crashkernel=auto或指定预留内存,比如:
crashkernel=512M预留多大要看你机器的内存规格,通常 512M 到 1G 足够。装好之后可以用 sysrq 触发一次测试性崩溃来验证 dump 流程:
echo c > /proc/sysrq-trigger这会故意制造一次内核 panic。在测试环境做,别在生产上玩。如果 vmcore 能正常落盘,说明你的救援通道是通的。
实战心得:很多公司机器上 kdump 装了但从来没验证过,出了事才发现 dump 路径不通,比如预留内存不足、存储挂载没就绪、或者磁盘空间不够。建议每次内核升级、每次存储变更后,都在变更窗口主动测一次 dump。
2.3 现场采集清单:动作要快,步骤要有顺序
真的发生挂死,时间窗口可能只有几十秒到几分钟。我的顺序是:
- 打开带外管理界面,截屏或录像,记录控制台最后输出。
- 确认串口日志是否在实时记录中。如果已经接入串口,马上把日志文件副本取出来。
- 如果机器还响应 SysRq 键,按顺序执行
Alt+SysRq+w打印所有任务栈、Alt+SysRq+t打印任务列表、Alt+SysRq+l打印 CPU 栈回溯。 - 手动触发 kdump(如果配置了
echo c > /proc/sysrq-trigger),或者等待自动 panic 转储。 - 记录环境信息:内核版本(
uname -r)、发行版版本、最近做过的变更、硬件型号、BIOS 版本、驱动版本。
第 5 步常常被忽略,但它对根因定位非常关键。“内核挂死”往往不是无缘无故的,升级内核、替换硬件、上线驱动,这些时间点的记录就是排查的锚点。
3. 现场分析:从日志到任务栈的逐步推理
假设你已经提前把串口开着、kdump 也通了,机器挂死时你手上有一批线索。接下来要做的是把线索串成证据链。
3.1 内核日志:watchdog 消息的潜台词
开机之后第一步看 dmesg:
dmesg -T | tail -n 500如果系统已经异常,或者在串口日志里,先找时间点附近的输出。典型软锁消息长这样:
watchdog: BUG: soft lockup - CPU#3 stuck for 22s! [kworker/u16:1:17629] Modules linked in: ... CPU: 3 PID: 17629 Comm: kworker/u16:1 Not tainted ... Call Trace: schedule_timeout ...很多新手只看“CPU#3 stuck”,但其实更重要的几个信息是:
- Comm 字段:卡住的进程/线程名,明确是内核线程、软中断,还是某个用户态进程通过某系统调用陷入内核。
- Call Trace:栈回溯里的最后几个函数,锁定卡在哪条路径。
- Modules linked in:看看加载的哪些模块可能与问题相关。
如果日志里是BUG: hard lockup,一般伴随IP和RIP指向 CPU 当前执行位置。由于硬锁时中断被屏蔽,输出通常更短,有时只有一条函数名和寄存器片段。这种时候我多半会转向外部证据:串口、带外、硬件监控日志。
3.2 sysrq:把现场抠出来的急救工具
先确保 sysrq 可用:
cat /proc/sys/kernel/sysrq通常 1 表示启用全部功能;如果不满足,内核命令行加sysrq=1后重启。
挂死时如果网络已经不通,但有键盘或带外控制台,最经典的几个键是:
| 组合键 | 作用 | 什么时候用 |
|---|---|---|
| Alt+SysRq+w | 显示所有任务的内核栈回溯 | 判断是否存在死锁或卡死路径 |
| Alt+SysRq+t | 显示所有任务的状态和 CPU 列表 | 判断是否有大量 D 状态进程 |
| Alt+SysRq+l | 显示当前 CPU 的栈回溯 | 定位当前执行位置 |
| Alt+SysRq+c | 触发 panic 并转储 | kdump 测试或主动收尾 |
远程命令行触发:
echo w > /proc/sysrq-trigger echo t > /proc/sysrq-trigger输出会打印到内核日志,串口日志和 dmesg 都能读到。一次挂死现场,我通常按 w、t、l 的顺序来,先看全貌再看局部。
3.3 一个典型现场的逐步推理
假设某次挂死的日志长这样:
INFO: task mysqld: 1234 blocked for more than 120 seconds. "echo 0 > /proc/sys/kernel/hung_task_timeout_secs" disables this message. task:mysqld state:D stack: 0 pid: 1234 ppid: 1 ... Call Trace: __switch_to submit_bio_wait blk_execute_rq ...看到blocked for more than 120 seconds,说明这是典型 D 状态卡死。第一步是看它在等什么:栈回溯显示submit_bio_wait,意味着进程在向底层块设备 IO 链路发起了同步等待。此时我会并行看几样东西:
- 系统里所有 D 状态任务是否都集中在同一设备。执行
ps -eo pid,stat,wchan:30,cmd | grep D。 - 查看
/proc/diskstats中对应设备是否有 IO 持续增长,确认是设备完全无响应还是链路抖动。 - 查看 dmesg 有没有设备错误、SCSI 错误、文件系统错误的相关记录。
如果所有 D 状态任务都挂在同一个 sdxx 或 nvme 设备上,优先级最高的排查方向就是设备与驱动;如果 D 状态任务分散,那要回到锁分析和 CPU 栈上。这种“同设备聚集”的特征,我在存储挂死现场见过太多次。
4. 根因定位的常见方向与实用方法
把现场看完,接下来要回答“为什么会挂死”。以下是我排查根因时最常走的四个方向。
4.1 驱动与内核模块:上线变更的高危区
驱动是内核挂死的头号嫌疑。尤其是网络、存储、GPU 这类复杂设备驱动,只要中断处理、自旋锁、IO 完成路径里出现一个笔误,就能让系统僵住。
常见的问题模式有:
- 在中断上下文里调用了可能睡眠的函数,导致中断路径阻塞。
- 自旋锁(spinlock)使用不当,没有在最坏情况下释放,或者同一个锁被重复获取。
- 驱动的定时器/工作队列与卸载路径竞争,导致 use-after-free。
- 固件交互超时后,驱动没有正确恢复设备状态,设备进入假死。
排查时我通常会先查驱动版本和内核版本是否匹配,然后看最近一次变更记录:有没有升级内核、换过驱动包、改过设备参数。如果是在新驱动上线后开始挂死,回退驱动或者用旧内核验证是成本最低的止损方式。
查看当前驱动信息和关联的设备:
lsmod | grep -E 'nvme|scsi|ixgbe|bond|virtio' ethtool -i eth0 # 网卡驱动与固件信息 lspci -vvv # 设备能力与状态4.2 存储与 IO 路径:静默卡死的第一现场
存储链路故障造成的挂死,我在生产环境里见得非常多。现象上常常是数据库或大规模 IO 任务先卡住,然后系统资源被耗尽,最终演变成整体无响应。
初步判断思路:
- 看任务状态:大量 D 状态进程。
- 看是否集中在同一块设备上:
cat /proc/diskstats,对比 io_ms 等字段。 - 看文件系统层:dmesg 是否出现 “INFO: task X blocked” 或有 “EXT4-fs error”、“XFS ... I/O error” 之类的日志。
- 看底层设备:如果是远程盘、分布式存储、SAN 存储,还要联系存储侧看链路丢包与延迟。
如果确认是 IO 路径问题,紧急恢复优先考虑两个方向:一是让受影响的服务快速恢复,比如杀掉相关 D 状态进程并重建,或者切换存储链路;二是保留证据后重启,再对存储系统的固件、多路径配置、超时参数做彻底检查。内核的 IO 超时参数也要看一眼,避免超时时间过长导致任务长时间挂起。
4.3 硬件与虚拟化平台:别忘了看环境信号
有些挂死与业务代码和驱动没关系,根子在硬件或虚拟化平台上。下面几个线索很有价值:
- 带外日志显示硬件 watchdog 重启,但内核日志却没有任何 panic。
- dmesg 中反复出现硬件错误事件,如
EDAC、PCIe Bus Error、MCE(Machine Check Exception)。 - 虚拟机上出现时钟跳变、CPU 热插拔、或者底层宿主机负载过高。
对于虚拟化环境,特别值得怀疑的有两类:一类是虚拟 CPU 的时钟源漂移,导致内核 tick 异常,watchdog 误报或者真的挂死;一类是底层宿主机的 IO 排队,让虚拟机的块设备请求迟迟不能完成,表现为 guest 内大量 D 状态进程。
提示:怀疑硬件问题时,跑一遍内存/磁盘自检,观察温度、电源状态,并记录固件版本。硬件问题不一定每次复现,但往往有规律,比如特定负载、特定温度才出现。
4.4 用户态负载与资源耗尽:看似无关的因素
某次排查,内核挂死出现在凌晨定时任务窗口。一开始所有人都在查驱动和内核代码,后来发现是用户态脚本在短时间内创建海量线程,触发了内核任务调度路径上的长时间遍历,进而和 CPU hotplug 产生了竞争。
这类问题虽然是“业务引爆”的,但最终卡死点在内核代码。遇到高并发、资源上限被反复打满的场景,多留个心眼:看 CGroup 限制、线程上限、文件句柄上限、内存和 swap 水位,这些外部条件可能是压垮内核路径的最后一根稻草。
5. 速查表与实践心得
5.1 常见挂死场景速查表
我把这些年排查过的挂死典型场景整理成一张表,便于大家在现场快速对照。
| 典型症状 | 首要怀疑方向 | 最快取证动作 | 止损手段 |
|---|---|---|---|
| 网络中断、进程全卡,无日志 | 硬锁/时钟问题/驱动 | 串口日志 + SysRq+w | panic_on_oops 重启或 dump |
| 大量 D 状态进程集中在某设备 | 存储/IO 路径 | 查 diskstats、设备错误日志 | 切链路/重启后验证设备 |
| 只有单个 CPU 报警且可恢复 | 软锁/驱动死循环 | dmesg + 栈回溯 | 升级/回退驱动 |
| 内核线程互相等待,崩溃前有锁痕迹 | 死锁 | crash 分析 vmcore | 记录调用栈后重启 |
| 带外日志出现硬件 watchdog 重启 | 硬件故障 | 检查 MCE/EDAC/PCIe 日志 | 替换硬件/更新固件 |
| 升级内核后首次出现 | 回归/兼容性 | 对比新旧内核日志 | 回退内核版本 |
表格不是万能答案,但它能帮你快速锁定“下一步该往哪看”,节省最宝贵的黄金排查窗口。
5.2 我踩过的坑和现在坚持的习惯
讲几个真实踩过的坑,希望对你有用。
第一个坑是“不验证 kdump 就当它有”。有次业务机器真正挂死,kdump 配置看着齐全,但预留内存被驱动加载后占掉,导致 dump 失败,最后连 vmcore 都没有。后来我在每个变更窗口都坚持做一次echo c > /proc/sysrq-trigger的验证,确认 dump 能正常完成才收工。
第二个坑是“只看 dmesg 不看时间关系”。内核挂死前往往还有一堆普通日志,你必须把日志时间和变更时间对齐。曾经一台机器在升级某网卡驱动后 40 分钟出现挂死,日志里却只有一段看起来无关的“link up”记录,后来发现正是驱动在 link up 事件后触发了错误的队列初始化。找到时间关联,根因就到了眼前。
第三个习惯是“平时就把调试通道打通”。我维护的每台关键机器,都要求满足三个基本条件:串口控制台可访问、kdump 已验证、sysrq 可用。这三条通道平时看着多余,真出事时每一条都可能成为唯一的救命稻草。
虽然内核挂死问题的表现千奇百怪,且每个现场都有自己的特殊性,但只要你能做到平时把三件事准备好——串口、kdump、sysrq,事中按“固定现场、采集日志、分析栈、关联变更”的顺序走,大部分根因都能落到具体模块或设备上。我个人现在对每台关键机器的要求是变更前后各做一次 dump 验证,新驱动的灰度范围不超过 10%。这些习惯看着不起眼,却是我这些年从一次次现场里换来的最实在的结论。