Linux hung_task故障分析:实战排查方法与Dependency Chain

第6章:hung_task实战排查方法——Waiter到Owner再到Dependency Chain

0. 源码基线与本章边界

本文以 Linux v6.1 为源码基线,对应上游标签提交830b3c68c1fb1e9176028d02ef86f3cf76aa2476。源码坐标包括:

本章不是继续扩展机制,而是把前五章压缩为一套可执行SOP。核心原则是:

1
每一条依赖边都必须写清Claim、Evidence、Confidence和Missing Evidence。

6.1 为什么需要固定SOP

仅凭经验容易形成“看到栈、猜一个模块、改一点代码、问题暂时不再出现”的闭环,却没有证明因果关系。固定SOP要求把分析过程外显:

1
2
Observation -> Hypothesis -> Evidence -> Falsification
-> Conclusion -> Fix -> Regression

这样别人能够复核,你也能在下一次复现时继续补证,而不是重新猜测。

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
2
3
4
5
6
Name/comm:
PID/TID/PPID:
State:
Reported blocked time:
CPU(若日志提供):
Task type: user thread / kthread / worker / IRQ thread

Waiter是症状承载者,不等于根因责任者。任务身份只用于决定从哪个业务入口和对象生命周期开始追踪。

6.4 Step 3:识别Wait Primitive

从Call Trace的公共调度帧向业务方向寻找:

1
2
3
4
5
6
wait_for_completion*()
mutex_lock*() / rwsem slowpath
wait_event*()对应的等待helper
io_schedule()
folio/page wait
schedule_timeout*()

输出应是“任务通过哪类同步机制睡眠”,而不是“该同步原语有Bug”。

6.5 Step 4:定位Business Wait Point

Wait Primitive下方第一个带有子系统语义的函数是Business Wait Point。例如:

1
2
3
4
schedule_timeout()
wait_for_completion_timeout()
spi_sync_locked()
sensor_ioctl()

可翻译为“该ioctl正在同步等待SPI message完成”。这个函数帮助找到真实源码调用点、请求对象和所属子系统。

6.6 Step 5:定位Wait Object

回到匹配版本源码查看调用实参:

1
2
3
wait_for_completion(&xfer->done);   // xfer->done
mutex_lock(&dev->lock); // dev->lock
wait_event(dev->wq, dev->ready); // dev->wq + dev->ready

同时记录对象类型、容器、初始化和销毁位置、请求ID/地址以及生命周期。只知道函数名而不知道实例,无法区分同一路径上的多个并发请求。

6.7 Step 6:定位Owner或Producer

按对象类型选择目标:

1
2
3
4
5
6
mutex/rwsem -> 当前Owner或Holders
Completion -> complete()/complete_all()的Producer
wait_event -> condition writer + wake_up caller
Block I/O -> request完成路径
DMA -> callback/IRQ/poll完成路径
NFS/RPC -> RPC响应与网络/服务端

静态搜索只能列出候选路径;实际Owner和本次Producer必须用运行时证据确认。

6.8 Step 7:获取Owner或Producer状态

对Task Owner/worker记录其状态和stack:

1
2
3
ps -eLo pid,tid,ppid,stat,wchan:32,comm
cat /proc/<tid>/stack
cat /proc/<tid>/wchan

对IRQ Producer检查中断计数、handler trace、mask/routing和设备状态;对hardware/firmware检查ring、mailbox、心跳、寄存器、DMA状态和错误日志。

需要区分:Running、Runnable、Sleeping、从未触发和已经执行但走了错误分支。

6.9 Step 8:继续追下一跳Dependency

找到Owner不代表结束。如果Owner也在等:

1
2
3
Task A -> Mutex M -> Task B
Task B -> Completion C -> IRQ X
IRQ X -> Device Command D

就继续用同一SOP分析Task B与Completion C,直到到达正在推进的节点、缺失的事件、外部边界或形成闭环。

6.10 Step 9:构建Dependency Graph

统一节点和边:

1
2
节点:Task、Lock、Completion、Condition、Work、I/O Request、IRQ、FW Queue、Hardware、Server
边:waits-for、owned-by、completed-by、scheduled-on、depends-on

示例:

1
2
3
Task A --waits-for--> Mutex M --owned-by--> Task B
Task B --waits-for--> Completion C --completed-by--> IRQ 45
IRQ 45 --generated-by--> DMA Engine

对象地址、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
2
3
4
Claim: Task A waits for Mutex M
Evidence: A stack + 对应源码调用点 + 运行时对象地址
Confidence: Confirmed / Strongly supported / Suspected
Missing: Mutex owner现场

四类证据各自回答不同问题:

类型 回答的问题 不能单独证明什么
Static Source 代码设计上谁应该做什么 现场是否真实执行
Runtime State 某一时刻对象和任务是什么状态 完整时间顺序
Temporal Trace 事件以什么顺序发生/缺失 未被trace覆盖的内部状态
Reproduction 故障是否稳定、修复是否相关 单独解释底层机制

6.13 Step 12:形成Root Cause结论

结论严格分层:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
Symptom:
Task A在D状态被报告超过184秒。

Immediate Cause:
Task A等待Completion C。

Dependency:
C只应由IRQ X中的complete()产生。

Root Cause:
命令提交后设备IRQ X未产生,因为中断mask配置错误。

Evidence:
栈、源码、寄存器、IRQ trace、修复前后复现结果。

Root Cause应解释“为什么不推进”,而不仅是重新描述“在哪里等待”。

6.14 Step 13:提出Fix方向

修复分三类,避免混写:

1
2
3
4
5
6
7
8
Root Cause Fix
例:修正IRQ mask、补齐错误路径complete、统一锁顺序

Fault Containment
例:超时、取消、reset、降级、返回错误

Observability Improvement
例:新增tracepoint、request ID、状态快照、超时日志

外部设备可能失效时,通常既需要根因修复,也需要containment防止永久D状态。

6.15 Step 14:设计Regression Test

回归测试至少覆盖:

  • 原始触发条件能否复现、修复后是否消失;
  • 并发和长时间压力;
  • 成功、失败、取消、超时、reset、remove等控制流;
  • 异常注入:丢IRQ、延迟Completion、I/O失败、firmware无响应;
  • 修复后是否存在泄漏、UAF、重复完成和迟到回调;
  • 关键延迟与吞吐是否退化。

测试应验证依赖链重新推进,而不只是“没有再次打印hung_task”。

6.16 如何判断证据足够

一个可审查的高置信度结论通常具备:

1
2
3
4
5
6
7
Waiter与Wait Object明确
Owner/Producer明确
依赖边有对象级而非仅函数级证据
停止推进节点有运行时或trace证据
替代假设已被检查
修复改变了对应机制
回归或异常注入验证成功

只有单条Waiter栈时,最多确认直接等待关系,不应宣布底层硬件根因。

6.17 如何处理证据冲突

把冲突当作更新模型的机会。例如trace显示IRQ确实到达,就不能继续坚持“IRQ未发生”,而应拆分:

1
2
3
4
5
6
IRQ进入了吗?
状态位匹配吗?
是否走到complete分支?
Completion实例相同吗?
complete后Waiter被唤醒了吗?
醒来后condition是否仍为false?

记录被否定的假设,避免团队重复调查。

6.18 如何处理现场已经消失

系统恢复后,当前/proc/<pid>/stack不再代表故障时刻。此时优先使用带时间戳的dmesg/串口、pstore、vmcore、保存的trace buffer、设备/firmware日志和业务请求ID。

若证据不足,只能输出已确认事实和下一次复现需要增加的观测点。不要用恢复后的现场替代历史现场。

6.19 如何处理多个hung_task

建立表格后聚类:

1
Task | Primitive | Business Point | Object | Next Dependency

先找共享锁、Completion、workqueue、block queue、设备或远端服务。多个Waiter常是一个Common Dependency的扇出结果,逐个修改Waiter通常无效。

6.20 如何处理锁类问题

1
2
3
4
5
6
确认具体Lock实例
-> 确认Owner/Holders
-> 获取Owner stack和state
-> 检查持锁时长与获取/释放路径
-> 若Owner等待,继续追下一对象
-> 检查是否形成Cycle

静态源码中的可能Owner列表不能代替现场Owner证据;lockdep也不能代替对象级依赖图。

6.21 如何处理Completion类问题

1
2
3
4
5
6
确认Completion实例与请求生命周期
-> 找所有complete/complete_all路径
-> 确认预期Producer
-> 检查Producer是否被触发
-> 检查错误/取消/reset路径
-> 检查reinit、迟到complete和对象复用

若Producer是IRQ,就转入IRQ/设备链;若是worker,就检查work入队、执行和workqueue阻塞。

6.22 如何处理IO类问题

按请求生命周期逐段验证:

1
2
3
4
5
6
7
8
Request created?
Submitted?
Queued?
Dispatched?
Accepted by device/server?
Completed?
Completion propagated upward?
Waiter awakened?

使用对象ID把issue和complete配对。只看全局吞吐或/proc/diskstats无法确认某个请求是否停滞。

6.23 如何处理Driver或Firmware类问题

以命令为主键串联:

1
2
CMD_ALLOC -> CMD_SUBMIT -> FW_RECEIVE -> HW_START
-> HW_DONE -> IRQ_ENTER -> CMD_DONE -> WAKE

为每个节点记录request/sequence ID和时间戳。若只能确认host submit而没有firmware receive,结论应停在接口边界,不能直接声称hardware损坏。

6.24 如何处理Memory类问题

从分配约束和压力开始:order、GFP、zone水位、可回收页面、dirty/writeback、slab和OOM状态。若栈进入writeback或folio等待,继续追文件系统与块I/O:

1
Allocation -> Reclaim -> Writeback/Folio -> Block I/O -> Device

“卡在reclaim”只是路径描述,不是完整根因。

6.25 如何处理Dependency Cycle

对每条边验证同一时刻的持有和等待关系:

1
2
Task A owns M1 and waits M2
Task B owns M2 and waits M1

只有图回到既有节点且边全部成立,才确认Cycle。若末端是外部设备或远端服务,通常是线性依赖停滞而不是死锁环。

6.26 如何写Bug分析报告

1
2
3
4
5
6
7
8
9
10
11
12
13
# 1. Problem Summary
# 2. Environment(release/commit/config/modules/taint/hardware)
# 3. Trigger and Reproduction
# 4. Raw hung_task Evidence
# 5. Waiter and Stack Layering
# 6. Wait Object
# 7. Owner / Producer
# 8. Dependency Graph
# 9. Claim-Evidence Table
# 10. Root Cause and Confidence
# 11. Fix(Root Cause / Containment / Observability)
# 12. Regression and Fault Injection
# 13. Remaining Risks

Raw evidence应保留原始时间戳和完整上下文,不要只贴经过筛选的几行。

6.27 如何写“无法完全确认”的结论

使用证据强度而不是含糊措辞:

1
2
3
4
Confirmed:对象与事件由现场/trace直接确认。
Strongly supported:多项独立证据一致,但缺少最后一层状态。
Suspected:模型合理,只有有限证据。
Unknown:现有数据无法区分候选原因。

示例:“已确认Task A等待Completion C;源码显示C只能由IRQ X完成,trace在提交后未记录IRQ X。证据强烈支持设备/IRQ完成链停滞,但未保存设备寄存器,因此无法区分中断mask、firmware或hardware状态。”

6.28 常用命令

现场仍存活时可进行只读采集:

1
2
3
4
5
6
7
8
9
10
11
uname -a
cat /proc/sys/kernel/tainted
ps -eLo pid,tid,ppid,stat,wchan:32,comm
cat /proc/<tid>/status
cat /proc/<tid>/stack
cat /proc/<tid>/wchan
cat /proc/interrupts
cat /proc/diskstats
cat /proc/meminfo
cat /proc/vmstat
cat /proc/sys/kernel/hung_task_timeout_secs

权限、配置和内核安全策略可能限制部分接口。SysRq抓取全任务状态具有大量日志和系统影响,只在受控环境、确认配置和操作风险后使用。

6.29 Trace补证据策略

先写要验证的Claim,再选最小事件集:

1
2
3
4
5
6
7
任务何时睡/醒:sched:sched_switch、sched:sched_waking、sched:sched_wakeup
IRQ是否进入:irq:irq_handler_entry、irq:irq_handler_exit
Work是否执行:workqueue:workqueue_execute_start/end
Block请求:block子系统中实际可用的issue/complete事件
内存回收:vmscan事件
回写:writeback事件
驱动协议:自定义tracepoint或谨慎使用动态探针

先查/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
2
3
4
5
6
Hop 1 Claim:
Static Evidence:
Runtime Evidence:
Trace Evidence:
Confidence:
Missing Evidence:

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 学完检查题

  1. 固定SOP相对“看栈猜Bug”解决了什么问题?
  2. 如何判断一份hung现场是否完整可信?
  3. Waiter记录为什么需要TID而不仅是进程PID?
  4. Wait Primitive与Business Wait Point分别回答什么?
  5. 如何从源码实参确认Wait Object?
  6. 为什么对象生命周期也必须记录?
  7. mutex、Completion和wait_event分别寻找谁?
  8. 静态搜索为什么不能确认现场Owner?
  9. 如何区分Producer从未触发与触发后走错分支?
  10. 找到Owner后为什么要继续追?
  11. Dependency Graph可包含哪些节点与边?
  12. 为什么节点旁需要记录对象地址或request ID?
  13. First Non-progress Node是什么?
  14. 它为什么不一定等于最终物理根因?
  15. Claim-Evidence卡至少包含哪些字段?
  16. Static Source Evidence能证明什么?
  17. Runtime State Evidence缺少什么维度?
  18. Temporal Trace为何适合证明顺序?
  19. Reproduction Evidence为什么不能单独解释机制?
  20. Root Cause结论应怎样分层?
  21. Root Cause Fix、Fault Containment和Observability Improvement有什么区别?
  22. 为什么加timeout必须考虑迟到Completion?
  23. Regression Test为何需要异常注入?
  24. 什么条件下可把结论标为Confirmed?
  25. 只有Waiter栈时最多能确认到哪一层?
  26. 证据与原假设冲突时应该怎么做?
  27. 系统恢复后为什么当前/proc/<pid>/stack不能代表历史现场?
  28. 多个hung任务如何聚类?
  29. 锁类问题的完整SOP是什么?
  30. Completion类应特别检查哪些生命周期问题?
  31. I/O类如何逐段验证request?
  32. Driver/Firmware类为什么需要统一request ID?
  33. Memory类为什么不能停在reclaim?
  34. 证明Dependency Cycle需要什么证据?
  35. Bug报告为何要保留Raw Evidence?
  36. 如何严谨表达“高度怀疑但尚未完全确认”?
  37. 使用SysRq前要考虑什么?
  38. 如何根据Claim选择最小trace集合?
  39. 为什么必须记录trace丢失和内核配置?
  40. 你能否不看本文,从一条hung日志完成SOP、画出Dependency Graph,并为每条边标注Claim、Evidence和Confidence?

下一章将进入hung_task Flight Recorder:让ftrace或trace-cmd在故障发生前持续保留有限历史,在告警时冻结ring buffer,从而获得Dependency Chain所需的调度、IRQ、I/O和驱动时序证据。


Linux hung_task故障分析:实战排查方法与Dependency Chain
https://cj0510.github.io/2026/10/11/Linux Kernel/Debug/Linux-hung-task故障分析-第6章-实战排查方法-Dependency-Chain/
作者
CJ1018
发布于
2026年10月11日
许可协议
CC BY-NC-SA 4.0