Linux hung_task故障分析:Root Cause分类

第5章:hung_task Root Cause分类——Lock、Completion、IO、Driver、Memory与Dependency Cycle

0. 源码基线与分类边界

本文以 Linux v6.1 为源码基线,对应上游标签提交830b3c68c1fb1e9176028d02ef86f3cf76aa2476。本章主要使用以下源码坐标建立分类:

分类只负责选择下一步调查路线,不替代证据。真实故障可能跨越多个类别:表面是mutex,下一层是Completion,最终却是设备固件没有响应。

5.1 为什么要做Root Cause分类

没有分类时,调查很容易变成“看到函数名、搜索源码、凭经验猜测”。分类把开放式搜索变成有限决策:

1
2
3
4
5
Wait Object
-> 判断依赖类型
-> 选择应该寻找的Owner或Producer
-> 选择匹配的运行时证据
-> 验证依赖链是否推进

这样可以减少无关trace和无目标源码阅读。

5.2 分类不是为了贴标签

说“这是I/O类hung”并没有完成诊断。分类真正决定下一步动作:

1
2
3
4
5
6
7
Lock          -> 找Owner与释放路径
Completion -> 找Producer与complete路径
Condition -> 找condition writer与wake路径
Block I/O -> 找request生命周期与完成事件
NFS/RPC -> 找网络与远端响应
Memory -> 找reclaim/writeback下游依赖
Cycle -> 证明依赖图闭环

类别必须随依赖链向下更新,不能被第一层栈永久锁定。

5.3 Lock类hung_task

典型形态包含mutex_lock()、__mutex_lock_common()、down_read()、down_write()或rwsem slowpath。模型是:

1
Waiter -> Lock Object -> Owner/Holders

第一层事实只是Waiter没有获得锁。需要证明具体锁实例、当前持有者、持有者栈和未释放原因。

5.4 Mutex类问题

mutex通常有单一Owner。常见故障模式包括:

  • Owner自身等待另一把锁、Completion或I/O;
  • 异常/早退路径遗漏mutex_unlock();
  • 持锁期间执行不可控的慢操作;
  • Owner不再得到调度或陷入其他故障;
  • 锁顺序冲突形成环。

mutex竞争本身可能只是负载现象;只有长时间无进展并影响关键路径时才构成hung问题。

5.5 RWSEM类问题

rwsem允许多个Reader或一个Writer,因此“Owner”可能是Reader集合或单个Writer。一个长期Writer能阻塞大量Reader;长期Reader也可能使Writer持续等待,具体公平和唤醒行为应以v6.1实现及现场为准。

多个任务停在同一rwsem_down_*_slowpath()时,应优先确认是否共享同一个struct rw_semaphore,再找持有路径。

5.6 锁Owner不推进

Owner不推进通常有三类原因:

1
2
3
Owner正在运行但临界区极慢
Owner可运行但长期得不到CPU
Owner也在睡眠,依赖另一个对象

因此要查看Owner的task state、stack、调度轨迹和下一层Wait Object。只看Waiter栈无法区分这三种情况。

5.7 Lock Ordering与Deadlock

如果两个路径以不同顺序获得相同锁集合,可能形成AB-BA:

1
2
Task A:lock(M1) -> wait M2
Task B:lock(M2) -> wait M1

lockdep能够发现许多潜在和实际锁依赖问题,但受配置、注解和运行覆盖影响。没有lockdep输出不等于排除死锁,仍需用现场依赖图证明或否定闭环。

5.8 Completion与Wait类hung_task

典型Wait Primitive是wait_for_completion*()、子系统条件等待helper或wait_event*()。模型是:

1
Waiter -> Completion/Condition -> Producer

这类对象通常没有传统锁Owner。关键是找谁应该改变done或condition,谁负责执行complete()或wake_up()。

5.9 Missing Complete

正常链路:

1
submit request -> Producer处理 -> complete(&req->done) -> Waiter继续

Missing Complete常发生在错误、取消、复位、设备移除或罕见竞态分支:状态已经终止,但没有通知Waiter。静态审查应覆盖Producer的所有出口,运行时应同时验证请求状态和complete()是否发生。

5.10 Lost Wakeup

Lost Wakeup是等待登记、条件检查和唤醒之间的竞态:事件发生并唤醒时Waiter尚未正确登记,随后Waiter又进入睡眠,且再无新事件。

标准wait_event*()封装了必要的状态设置、队列操作和复查。风险常见于手写等待协议、错误的锁/屏障配对或对象生命周期混乱;不能仅凭“没有醒来”就断言Lost Wakeup。

5.11 Condition Never True

Producer可能确实调用了wake_up(),但condition始终为false:

1
wake -> Waiter重新运行 -> condition=false -> 再次睡眠

调查时必须同时追踪condition的所有写入点、状态机合法迁移、对象实例和wake顺序。只记录wake事件不足以证明等待应该结束。

5.12 Timeout Missing

对可能永久失败的外部依赖使用无限等待,会把设备或远端故障放大为永久D状态。改用带超时等待、取消和恢复路径可限制故障影响。

但必须区分:

1
2
Timeout/Reset = Fault Containment与恢复机制
硬件为何不完成 = Root Cause问题

盲目加超时还可能引入对象释放、迟到Completion和重复完成等新竞态。

5.13 IO类hung_task

I/O类的共同模型是:

1
2
Task -> I/O Object -> Subsystem Queue -> Driver/Network -> Device/Server
<- completion一路返回

需要定位请求在哪一层停止:尚未提交、已经排队、已经dispatch、设备处理中,还是已经完成但上层未被通知。

5.14 Block IO

块I/O典型路径:

1
2
Filesystem -> bio -> request/blk-mq -> host driver -> storage
<- end_io/complete request <- IRQ/polling

关键证据包括request issue/complete配对、队列freeze/quiesce、超时与reset、设备错误和中断变化。相同io_schedule()栈可以对应完全不同的磁盘和request,必须确认实例。

5.15 Filesystem

文件系统任务可能等待inode锁、journal事务、页/folio解锁、writeback或块I/O。文件系统函数位于栈上只说明请求经过该层,不证明根因就在文件系统。

应把Wait Object继续映射到journal、folio、writeback control或bio,确定下游是否推进。

5.16 NFS与RPC

NFS等待链跨越本机内核、网络和远端服务器:

1
Task -> NFS -> RPC task -> socket/network -> remote service/storage

需要检查RPC状态和重传、挂载策略、网络收发、服务端响应及远端存储。硬挂载的长期等待可能符合配置语义,但仍需判断是否超过业务可接受范围。

5.17 Device IO

SPI、USB、eMMC、摄像头和加速器等设备I/O通常表现为:

1
submit -> wait -> IRQ/poll/worker -> completion

它位于I/O类和Driver类的交界处。先确认请求对象及生命周期,再进入寄存器、DMA、IRQ、firmware等设备专属证据。

5.18 Driver、Firmware与Hardware类hung_task

嵌入式设备常见完整链路:

1
2
3
Task -> Kernel Driver -> command/ring/mailbox
-> Firmware -> Hardware
-> IRQ/doorbell -> Driver completion -> Task

任意一段停止都可能让最上层Waiter进入D状态。调查必须检查请求和完成两个方向,不能只看提交成功日志。

5.19 IRQ Missing

“没有看到IRQ增长”只是现象,可能原因包括:

  • 硬件操作根本没有完成;
  • 中断未使能或被mask;
  • 路由、触发类型或亲和性配置错误;
  • 设备、控制器或firmware未发出中断;
  • ISR进入但未识别正确状态位;
  • 日志/采样窗口遗漏。

应联合设备状态寄存器、/proc/interrupts和IRQ trace验证。

5.20 DMA Never Complete

DMA不完成可能来自descriptor、长度、地址、IOMMU映射、缓存一致性、engine状态、通道复位或完成中断问题。证据链至少应包含:

1
2
3
4
5
6
描述符已提交
DMA engine已启动
地址映射有效
状态寄存器/错误位
IRQ或poll完成路径
上层complete/end_io

不要仅靠Waiter栈区分这些原因。

5.21 Firmware Hang

Firmware可能停止心跳、不再消费ring、卡在状态机或等待另一个硬件单元。内核通常只看到命令长期未完成。

需要把host侧队列读写指针、mailbox/doorbell、firmware日志、心跳和reset结果按时间对齐,确认命令是否到达以及响应停在哪一步。

5.22 Hardware No Response

硬件无响应调查通常涉及电源域、时钟、复位、总线错误、寄存器状态和设备内部队列。若设备处于错误电源状态,即使驱动逻辑正确也不会产生Completion。

“Hardware No Response”仍是中间结论;高质量根因还要说明为何无响应,例如时钟未开启、非法descriptor或firmware异常。

5.23 Memory类hung_task

内存压力路径常出现try_to_free_pages()、direct reclaim、dirty throttling、writeback和folio wait。这里最容易犯的错误是把栈中mm/函数直接定性为内存管理Bug。

内存回收可能同步依赖文件系统和存储,因此需要继续沿回写/I/O链向下追。

5.24 Direct Reclaim

进程分配内存时若无法从快速路径获得页面,可能直接参与回收:

1
2
Allocation -> Direct Reclaim -> 扫描/回收
-> 脏页处理/文件系统/I/O

需要检查内存水位、分配order和GFP约束、可回收页面、slab、回收效率以及是否被下游writeback拖住。

5.25 Writeback

大量dirty pages可能触发节流,任务等待回写取得进展。若存储设备慢、队列冻结或完成中断丢失,writeback无法降低脏页压力,上游分配和写入任务会成批阻塞。

应结合dirty/writeback计数、writeback trace、块I/O issue/complete和设备错误判断瓶颈层。

5.26 Page Lock与Folio Wait

v6.1许多页缓存路径已经使用folio抽象。等待函数可能在等folio解锁、writeback位清除或I/O完成。下一步要找谁设置该状态、谁应该清除,以及对应end_io或unlock路径是否运行。

folio_wait*()只是等待原语,具体folio和mapping才是Wait Object上下文。

5.27 Allocation Dependency

表面上的内存分配hung可能形成长链:

1
2
Task -> allocation -> reclaim -> filesystem -> writeback
-> block request -> driver -> device

若设备不完成,最终报告栈可能停在reclaim。分类必须沿依赖链动态从Memory转向I/O再转向Driver/Hardware。

5.28 Dependency Cycle

Dependency Cycle表示依赖图存在有向闭环:每个节点都等待下一个节点,而最后一个又依赖前面的节点。闭环中没有外部事件能自然打破等待时,系统形成死锁。

长期线性等待不是Cycle:例如Task一直等待已经失效的硬件,也会hung,但依赖链不回到自身。

5.29 AB-BA Deadlock

最小锁环:

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

证明AB-BA需要确认相同锁实例和同时成立的持有/等待关系。仅从静态源码看到两种锁顺序,说明存在潜在风险,不等于现场已经形成该环。

5.30 多级Dependency Chain

真实环可能跨越同步原语与执行上下文:

1
2
3
Task A -> Mutex M1 -> Task B
Task B -> Completion C -> Worker W
Worker W -> Mutex M2 -> Task A

必须逐边记录对象、Owner/Producer和证据。只要其中一条边是假设,Cycle结论就仍需验证。

5.31 多个hung_task如何聚类

为每个Waiter建立最小记录:

1
Task/PID | Wait Primitive | Business Point | Object | Subsystem

先按Wait Primitive和业务路径粗分,再用对象地址、设备、挂载点、队列或Owner细分。相同栈不保证相同对象,不同栈也可能汇聚到同一个下游设备。

5.32 Common Dependency分析

将每组任务画成有向边:

1
2
3
A/B/C -> Mutex M -> Owner X
D/E -> RPC requests -> Server S
F/G -> writeback -> Block Queue Q -> Device Z

入边最多、同时又不推进的Owner、Producer、队列、设备或远端服务,是优先验证的Common Dependency。

5.33 Decision Tree

1
2
3
4
5
6
7
8
9
10
11
12
13
hung_task
|
+-- mutex/rwsem? --------> 定位Lock实例 -> Owner/Holders -> Owner下一依赖
|
+-- completion/event? ---> 定位对象/condition -> Producer -> 完成或唤醒发生?
|
+-- I/O/filesystem? -----> 定位page/bio/request/RPC -> pipeline停在哪层?
|
+-- reclaim/writeback? --> 检查压力与进展 -> 是否阻塞在FS/I/O?
|
+-- driver helper? ------> command -> DMA/FW/HW -> IRQ -> completion
|
`-- 依赖回到旧节点? ----> 验证Cycle;否则继续线性下钻

Decision Tree的输出不是“答案”,而是下一批最小证据。

5.34 每类问题需要补什么证据

类别 首要对象 关键补充证据
Mutex/RWSEM 锁实例 Owner/Holders、Owner栈、获取释放路径、lockdep
Completion completion实例 complete()路径、IRQ/worker、对象状态与生命周期
Wait Event queue + condition condition写入、wake调用、同步与顺序
Block I/O bio/request/queue issue/complete、timeout/reset、设备和IRQ日志
NFS/RPC RPC request/server 重传、网络、挂载策略、服务端状态
Driver command/DMA对象 ring、descriptor、firmware、寄存器、IRQ
Memory allocation/folio/writeback meminfo、vmstat、vmscan、writeback、下游I/O
Cycle 图中所有节点 每条持有/等待边的同时态证据

5.35 每类问题适合什么Tracing

可按类别选择最小trace集合:

1
2
3
4
5
6
Lock       : sched_switch/sched_wakeup + 可用的锁事件或自定义tracepoint
Completion : sched + irq + Producer附近自定义事件/kprobe
Block I/O : block:block_rq_issue、block:block_rq_complete等实际可用事件
Driver : irq:irq_handler_entry/exit + 驱动tracepoint
NFS/RPC : nfs/rpc相关可用tracepoint + 网络证据
Memory : vmscan、writeback、block事件

事件名称和字段受内核配置影响,先检查/sys/kernel/tracing/available_events,不要假设目标系统全部提供。

5.36 本章实验

5.36.1 历史日志分类

选取若干真实日志,只填写Wait Object、第一层类别、下一步证据,不急于写根因。随后检查第一层类别是否沿依赖链发生变化。

5.36.2 Completion缺失

在可恢复测试环境中,让内核线程等待Completion而Producer故意不调用complete()。预测栈、D状态和hung告警,再加入超时/取消路径比较故障影响。模块必须设计安全退出,不能在生产机实验。

5.36.3 锁Owner

让Task A持有mutex后阻塞在可控事件上,Task B等待该mutex。不要只观察B,必须记录A的栈并画出B -> M -> A -> Event。

5.36.4 多Waiter聚类

让多个任务等待同一mutex或Completion,比较“按栈分类”与“按对象分类”的结果,验证为什么对象实例比函数名更接近Common Dependency。

5.36.5 有选择地跟踪

先预测所需事件,再查看:

1
cat /sys/kernel/tracing/available_events

只启用能验证当前依赖边的sched、irq、block、vmscan或writeback事件,记录观测问题、预期、结果与解释。

5.37 工程意义

分类把排障从“全系统撒网”变成“围绕Wait Object验证最小依赖链”。它降低trace数据量和观测扰动,也让团队在Bug单中共享统一语言。

修复也应分层:先用timeout、reset或降级限制故障传播,再修复未释放、未完成、状态机或硬件配置等根因,最后做异常注入与回归验证。

5.38 常见误区

误区1:一个hung_task只能属于一个类别

类别会沿依赖链变化,第一层不等于最终根因。

误区2:mutex栈证明Lock实现有Bug

它只证明Waiter在等锁,Owner可能卡在完全不同的子系统。

误区3:wait_for_completion()证明Driver有Bug

Producer也可能是worker、网络或其他任务;必须确认具体对象与Producer。

误区4:io_schedule()证明Block Layer有Bug

故障可能位于设备、driver、filesystem或远端。

误区5:reclaim栈证明内存管理有Bug

reclaim可能被writeback和存储完成链拖住。

误区6:多个hung任务代表多个独立Bug

它们可能共享同一个Owner、队列、设备或服务端。

误区7:加timeout就是完成修复

timeout主要限制影响,还必须处理取消、迟到完成和真正根因。

误区8:长期等待就是Deadlock

只有证据证明依赖图闭环,才能得出Cycle结论。

误区9:一开始打开所有trace最保险

数据洪流会增加开销并淹没因果关系,应先分类再选事件。

5.39 博客总结

hung_task Root Cause分类的价值在于选择下一步证据,而不是给日志贴标签。Lock类问题沿锁实例寻找Owner及其下一层依赖;Completion与条件等待寻找Producer、状态写入和通知路径;I/O类沿page、bio、request或RPC定位pipeline停止位置;Driver类检查command、DMA、firmware、hardware、IRQ与Completion;Memory类必须确认reclaim和writeback是否被文件系统及存储拖住;只有依赖图回到已有节点并且每条边都有同时态证据时,才能确认Dependency Cycle。

真实问题常跨越多类:表面mutex_lock(),Owner等待Completion,Producer依赖设备IRQ,最终根因是firmware或hardware。多个hung任务应按Wait Object和下游依赖聚类,寻找Common Dependency。最终分析过程应始终遵循:

1
2
症状模式 -> 初始分类 -> 补充证据 -> 更新分类
-> 建立依赖图 -> 验证停止点 -> Root Cause

5.40 学完检查题

  1. Root Cause分类真正要解决什么问题?
  2. 为什么第一层分类不是最终结论?
  3. Lock类问题的首要运行时对象是什么?
  4. 如何区分Owner正在运行、Runnable和Sleeping?
  5. mutex遗漏unlock与Owner下游阻塞需要哪些不同证据?
  6. rwsem为什么可能有多个Holders?
  7. 静态锁顺序能否单独证明现场AB-BA?
  8. Completion类为什么通常寻找Producer而非Owner?
  9. Missing Complete最容易遗漏哪些控制流?
  10. 证明Lost Wakeup需要观察哪些顺序关系?
  11. 为什么记录到wake_up()仍不能证明condition成立?
  12. Timeout Missing属于根因还是鲁棒性问题?
  13. 加超时为什么可能引入迟到Completion竞态?
  14. 块I/O请求生命周期有哪些关键阶段?
  15. 为什么文件系统栈可能只是中间层?
  16. NFS类需要补充哪些远端证据?
  17. Device I/O为何横跨I/O类和Driver类?
  18. 调查IRQ Missing时为什么要同时看硬件状态?
  19. DMA Never Complete至少需要哪些证据?
  20. 如何证明firmware已经收到命令?
  21. “Hardware No Response”为什么仍可能不是最终根因?
  22. Direct Reclaim如何依赖存储I/O?
  23. Writeback停滞会怎样向上游传播?
  24. Folio Wait的真正Wait Object上下文是什么?
  25. Allocation Dependency为何会使Memory分类转向Driver分类?
  26. 线性长期等待与Dependency Cycle的区别是什么?
  27. 证明AB-BA需要哪些同时态关系?
  28. 多级Cycle为什么必须逐边验证?
  29. 多个hung任务应按哪些字段聚类?
  30. 相同栈是否说明同一对象?
  31. 如何识别Common Dependency?
  32. Decision Tree的输出应该是什么?
  33. Lock、Completion、I/O和Memory分别需要补什么核心证据?
  34. 为什么应先检查available_events?
  35. 哪些trace可验证块请求issue/complete?
  36. 为什么不要一开始全开trace?
  37. Fault Containment与Root Cause Fix有什么区别?
  38. 请画出“Memory reclaim -> writeback -> block I/O -> driver -> device”的依赖链。
  39. 请把“Task A等mutex,Owner B等Completion,firmware未响应”分成表面、中间和最终类别。
  40. 你能否不看本文画出完整Root Cause Decision Tree,并为每个分支写出下一条必须收集的证据?

下一章将把分类方法收敛为一套可直接执行的实战流程:从Waiter定位Owner/Producer,递归追踪下一层依赖,为每条边补证据,最终形成可以写入Bug单的Root Cause结论与回归方案。


Linux hung_task故障分析:Root Cause分类
https://cj0510.github.io/2026/10/11/Linux Kernel/Debug/Linux-hung-task故障分析-第5章-Root-Cause分类/
作者
CJ1018
发布于
2026年10月11日
许可协议
CC BY-NC-SA 4.0