Linux hung_task故障分析:实战排查方法与Dependency Chain
第6章:hung_task实战排查方法——Waiter到Owner再到Dependency Chain
0. 源码基线与本章边界
本文以 Linux v6.1 为源码基线,对应上游标签提交830b3c68c1fb1e9176028d02ef86f3cf76aa2476。源码坐标包括:
kernel/hung_task.c:检测和现场输出;kernel/sched/core.c:任务状态、栈和调度路径;kernel/sched/completion.c:Completion依赖;kernel/locking/mutex.c与kernel/locking/rwsem.c:锁依赖;include/trace/events/:调度、IRQ、block、workqueue、vmscan与writeback等tracepoint定义。
本章不是继续扩展机制,而是把前五章压缩为一套可执行SOP。核心原则是:
1 | |
6.1 为什么需要固定SOP
仅凭经验容易形成“看到栈、猜一个模块、改一点代码、问题暂时不再出现”的闭环,却没有证明因果关系。固定SOP要求把分析过程外显:
1 | |
这样别人能够复核,你也能在下一次复现时继续补证,而不是重新猜测。
6.2 Step 1:确认现场是否可信
先记录现场质量:
- 日志是否包含完整告警头和Call Trace;
- 是否有多条hung任务、soft lockup、RCU stall或设备错误;
- kernel release、构建号、配置、模块和taint;
- 时间戳是否来自同一时钟域,日志是否丢失或乱序;
- 系统是否仍存活,可否读取
/proc与tracefs; - 是否有vmcore、pstore、串口日志或常驻trace。
现场不完整时应先标注能力边界,而不是用假设填空。
6.3 Step 2:识别Waiter
为每个被报告任务建立身份证:
1 | |
Waiter是症状承载者,不等于根因责任者。任务身份只用于决定从哪个业务入口和对象生命周期开始追踪。
6.4 Step 3:识别Wait Primitive
从Call Trace的公共调度帧向业务方向寻找:
1 | |
输出应是“任务通过哪类同步机制睡眠”,而不是“该同步原语有Bug”。
6.5 Step 4:定位Business Wait Point
Wait Primitive下方第一个带有子系统语义的函数是Business Wait Point。例如:
1 | |
可翻译为“该ioctl正在同步等待SPI message完成”。这个函数帮助找到真实源码调用点、请求对象和所属子系统。
6.6 Step 5:定位Wait Object
回到匹配版本源码查看调用实参:
1 | |
同时记录对象类型、容器、初始化和销毁位置、请求ID/地址以及生命周期。只知道函数名而不知道实例,无法区分同一路径上的多个并发请求。
6.7 Step 6:定位Owner或Producer
按对象类型选择目标:
1 | |
静态搜索只能列出候选路径;实际Owner和本次Producer必须用运行时证据确认。
6.8 Step 7:获取Owner或Producer状态
对Task Owner/worker记录其状态和stack:
1 | |
对IRQ Producer检查中断计数、handler trace、mask/routing和设备状态;对hardware/firmware检查ring、mailbox、心跳、寄存器、DMA状态和错误日志。
需要区分:Running、Runnable、Sleeping、从未触发和已经执行但走了错误分支。
6.9 Step 8:继续追下一跳Dependency
找到Owner不代表结束。如果Owner也在等:
1 | |
就继续用同一SOP分析Task B与Completion C,直到到达正在推进的节点、缺失的事件、外部边界或形成闭环。
6.10 Step 9:构建Dependency Graph
统一节点和边:
1 | |
示例:
1 | |
对象地址、PID、request ID等应直接写在节点旁,避免把不同实例错误合并。
6.11 Step 10:识别First Non-progress Node
First Non-progress Node是沿依赖方向,第一个有证据显示应该推进却没有推进的节点。比如命令已提交,但之后没有设备完成状态和IRQ:停止点位于设备/IRQ完成链附近,而不是最外层mutex Waiter。
它不一定就是最终物理根因。IRQ缺失还可能由命令未真正启动、中断mask、firmware、时钟或硬件错误造成,需继续细化。
6.12 Step 11:补证据
为每条边建立证据卡:
1 | |
四类证据各自回答不同问题:
| 类型 | 回答的问题 | 不能单独证明什么 |
|---|---|---|
| Static Source | 代码设计上谁应该做什么 | 现场是否真实执行 |
| Runtime State | 某一时刻对象和任务是什么状态 | 完整时间顺序 |
| Temporal Trace | 事件以什么顺序发生/缺失 | 未被trace覆盖的内部状态 |
| Reproduction | 故障是否稳定、修复是否相关 | 单独解释底层机制 |
6.13 Step 12:形成Root Cause结论
结论严格分层:
1 | |
Root Cause应解释“为什么不推进”,而不仅是重新描述“在哪里等待”。
6.14 Step 13:提出Fix方向
修复分三类,避免混写:
1 | |
外部设备可能失效时,通常既需要根因修复,也需要containment防止永久D状态。
6.15 Step 14:设计Regression Test
回归测试至少覆盖:
- 原始触发条件能否复现、修复后是否消失;
- 并发和长时间压力;
- 成功、失败、取消、超时、reset、remove等控制流;
- 异常注入:丢IRQ、延迟Completion、I/O失败、firmware无响应;
- 修复后是否存在泄漏、UAF、重复完成和迟到回调;
- 关键延迟与吞吐是否退化。
测试应验证依赖链重新推进,而不只是“没有再次打印hung_task”。
6.16 如何判断证据足够
一个可审查的高置信度结论通常具备:
1 | |
只有单条Waiter栈时,最多确认直接等待关系,不应宣布底层硬件根因。
6.17 如何处理证据冲突
把冲突当作更新模型的机会。例如trace显示IRQ确实到达,就不能继续坚持“IRQ未发生”,而应拆分:
1 | |
记录被否定的假设,避免团队重复调查。
6.18 如何处理现场已经消失
系统恢复后,当前/proc/<pid>/stack不再代表故障时刻。此时优先使用带时间戳的dmesg/串口、pstore、vmcore、保存的trace buffer、设备/firmware日志和业务请求ID。
若证据不足,只能输出已确认事实和下一次复现需要增加的观测点。不要用恢复后的现场替代历史现场。
6.19 如何处理多个hung_task
建立表格后聚类:
1 | |
先找共享锁、Completion、workqueue、block queue、设备或远端服务。多个Waiter常是一个Common Dependency的扇出结果,逐个修改Waiter通常无效。
6.20 如何处理锁类问题
1 | |
静态源码中的可能Owner列表不能代替现场Owner证据;lockdep也不能代替对象级依赖图。
6.21 如何处理Completion类问题
1 | |
若Producer是IRQ,就转入IRQ/设备链;若是worker,就检查work入队、执行和workqueue阻塞。
6.22 如何处理IO类问题
按请求生命周期逐段验证:
1 | |
使用对象ID把issue和complete配对。只看全局吞吐或/proc/diskstats无法确认某个请求是否停滞。
6.23 如何处理Driver或Firmware类问题
以命令为主键串联:
1 | |
为每个节点记录request/sequence ID和时间戳。若只能确认host submit而没有firmware receive,结论应停在接口边界,不能直接声称hardware损坏。
6.24 如何处理Memory类问题
从分配约束和压力开始:order、GFP、zone水位、可回收页面、dirty/writeback、slab和OOM状态。若栈进入writeback或folio等待,继续追文件系统与块I/O:
1 | |
“卡在reclaim”只是路径描述,不是完整根因。
6.25 如何处理Dependency Cycle
对每条边验证同一时刻的持有和等待关系:
1 | |
只有图回到既有节点且边全部成立,才确认Cycle。若末端是外部设备或远端服务,通常是线性依赖停滞而不是死锁环。
6.26 如何写Bug分析报告
1 | |
Raw evidence应保留原始时间戳和完整上下文,不要只贴经过筛选的几行。
6.27 如何写“无法完全确认”的结论
使用证据强度而不是含糊措辞:
1 | |
示例:“已确认Task A等待Completion C;源码显示C只能由IRQ X完成,trace在提交后未记录IRQ X。证据强烈支持设备/IRQ完成链停滞,但未保存设备寄存器,因此无法区分中断mask、firmware或hardware状态。”
6.28 常用命令
现场仍存活时可进行只读采集:
1 | |
权限、配置和内核安全策略可能限制部分接口。SysRq抓取全任务状态具有大量日志和系统影响,只在受控环境、确认配置和操作风险后使用。
6.29 Trace补证据策略
先写要验证的Claim,再选最小事件集:
1 | |
先查/sys/kernel/tracing/available_events。设置合适buffer、过滤PID/设备/request ID,记录trace丢失情况;不要默认所有发行版和配置都具备相同事件。
6.30 本章实验
6.30.1 对历史日志跑完整SOP
禁止一步写Root Cause。依次填写Waiter、Primitive、Business Point、Object、Owner/Producer、Next Dependency,并为每一步标注证据强度。
6.30.2 每一跳写Claim与Evidence
1 | |
6.30.3 构造两级依赖
在可恢复测试环境中让Task A等待由Task B持有的mutex,让B等待Completion,再由delayed work完成。观察正常链;随后跳过完成动作,比较A和B的栈与依赖图。模块必须具备安全退出和超时清理。
6.30.4 证据冲突练习
假设已观察到IRQ entry但Waiter仍未醒,列出至少五个替代假设,并为每个假设设计一个最小观测点。
6.30.5 写Bug Report
将一次分析整理为本章模板,邀请他人只根据报告复画依赖图。如果无法复画,说明对象、边或证据表达仍不够清楚。
6.31 工程意义
SOP把个人经验转换成团队可Review的工程资产。它帮助区分“代码设计上应该发生”和“故障现场实际发生”,减少误修和无效trace。
同一结构也适合自动化:解析栈、识别Wait Primitive、关联trace与请求ID、生成依赖图。但Wait Object实例解析和跨子系统语义仍需要版本匹配、调试符号和人工验证,不能把自动报告当最终结论。
6.32 常见误区
误区1:找到Wait Object就结束
还必须找Owner/Producer和下一层依赖。
误区2:找到Owner就结束
Owner可能也是Waiter。
误区3:源码有complete()就证明现场调用过
源码是Static Evidence,执行事实需要runtime/trace证据。
误区4:没有IRQ就是硬件损坏
命令、mask、routing、firmware、电源和时钟都可能造成相同现象。
误区5:稳定复现等于已经解释机制
复现提高可信度,但仍要证明停止节点和因果边。
误区6:timeout等于修复
timeout通常是containment,还要处理取消和迟到事件。
误区7:日志天然完整且按真实发生顺序输出
ring buffer覆盖、console延迟和多CPU交错都可能影响可见日志。
误区8:证据不足就不能写报告
可以明确写Confirmed、Strongly supported、Suspected或Unknown,并列出缺失证据。
误区9:trace越多越可靠
无目标的大量事件会增加开销、丢失和分析噪声,应围绕Claim采集。
6.33 博客总结
hung_task实战排查是一项依赖关系重建工作。先确认现场可信度和Waiter身份,从Call Trace识别Wait Primitive与Business Wait Point,再回到匹配版本源码确定具体Wait Object。锁对象寻找Owner,Completion、condition、I/O和设备事务寻找Producer;得到Owner/Producer后继续检查它的状态和下一层依赖,直到建立完整Dependency Graph并定位First Non-progress Node。
每条依赖边都应包含Claim、静态源码证据、运行时状态、时序trace、置信度和缺失证据。结论区分Symptom、Immediate Cause与Root Cause,修复区分Root Cause Fix、Fault Containment与Observability Improvement,回归测试则验证机制恢复、异常路径和迟到事件安全。证据不足时明确表达不确定性,比把合理推测包装成事实更专业。
6.34 学完检查题
- 固定SOP相对“看栈猜Bug”解决了什么问题?
- 如何判断一份hung现场是否完整可信?
- Waiter记录为什么需要TID而不仅是进程PID?
- Wait Primitive与Business Wait Point分别回答什么?
- 如何从源码实参确认Wait Object?
- 为什么对象生命周期也必须记录?
- mutex、Completion和wait_event分别寻找谁?
- 静态搜索为什么不能确认现场Owner?
- 如何区分Producer从未触发与触发后走错分支?
- 找到Owner后为什么要继续追?
- Dependency Graph可包含哪些节点与边?
- 为什么节点旁需要记录对象地址或request ID?
- First Non-progress Node是什么?
- 它为什么不一定等于最终物理根因?
- Claim-Evidence卡至少包含哪些字段?
- Static Source Evidence能证明什么?
- Runtime State Evidence缺少什么维度?
- Temporal Trace为何适合证明顺序?
- Reproduction Evidence为什么不能单独解释机制?
- Root Cause结论应怎样分层?
- Root Cause Fix、Fault Containment和Observability Improvement有什么区别?
- 为什么加timeout必须考虑迟到Completion?
- Regression Test为何需要异常注入?
- 什么条件下可把结论标为Confirmed?
- 只有Waiter栈时最多能确认到哪一层?
- 证据与原假设冲突时应该怎么做?
- 系统恢复后为什么当前
/proc/<pid>/stack不能代表历史现场? - 多个hung任务如何聚类?
- 锁类问题的完整SOP是什么?
- Completion类应特别检查哪些生命周期问题?
- I/O类如何逐段验证request?
- Driver/Firmware类为什么需要统一request ID?
- Memory类为什么不能停在reclaim?
- 证明Dependency Cycle需要什么证据?
- Bug报告为何要保留Raw Evidence?
- 如何严谨表达“高度怀疑但尚未完全确认”?
- 使用SysRq前要考虑什么?
- 如何根据Claim选择最小trace集合?
- 为什么必须记录trace丢失和内核配置?
- 你能否不看本文,从一条hung日志完成SOP、画出Dependency Graph,并为每条边标注Claim、Evidence和Confidence?
下一章将进入hung_task Flight Recorder:让ftrace或trace-cmd在故障发生前持续保留有限历史,在告警时冻结ring buffer,从而获得Dependency Chain所需的调度、IRQ、I/O和驱动时序证据。