Linux hung_task故障分析:Root Cause分类
第5章:hung_task Root Cause分类——Lock、Completion、IO、Driver、Memory与Dependency Cycle
0. 源码基线与分类边界
本文以 Linux v6.1 为源码基线,对应上游标签提交830b3c68c1fb1e9176028d02ef86f3cf76aa2476。本章主要使用以下源码坐标建立分类:
kernel/locking/mutex.c与kernel/locking/rwsem.c:睡眠锁等待;kernel/sched/completion.c与kernel/sched/wait.c:Completion和等待队列;block/与include/trace/events/block.h:块I/O请求生命周期;mm/vmscan.c、mm/page-writeback.c与mm/filemap.c:回收、回写与folio等待;kernel/hung_task.c:hung任务检测与现场输出。
分类只负责选择下一步调查路线,不替代证据。真实故障可能跨越多个类别:表面是mutex,下一层是Completion,最终却是设备固件没有响应。
5.1 为什么要做Root Cause分类
没有分类时,调查很容易变成“看到函数名、搜索源码、凭经验猜测”。分类把开放式搜索变成有限决策:
1 | |
这样可以减少无关trace和无目标源码阅读。
5.2 分类不是为了贴标签
说“这是I/O类hung”并没有完成诊断。分类真正决定下一步动作:
1 | |
类别必须随依赖链向下更新,不能被第一层栈永久锁定。
5.3 Lock类hung_task
典型形态包含mutex_lock()、__mutex_lock_common()、down_read()、down_write()或rwsem slowpath。模型是:
1 | |
第一层事实只是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 | |
因此要查看Owner的task state、stack、调度轨迹和下一层Wait Object。只看Waiter栈无法区分这三种情况。
5.7 Lock Ordering与Deadlock
如果两个路径以不同顺序获得相同锁集合,可能形成AB-BA:
1 | |
lockdep能够发现许多潜在和实际锁依赖问题,但受配置、注解和运行覆盖影响。没有lockdep输出不等于排除死锁,仍需用现场依赖图证明或否定闭环。
5.8 Completion与Wait类hung_task
典型Wait Primitive是wait_for_completion*()、子系统条件等待helper或wait_event*()。模型是:
1 | |
这类对象通常没有传统锁Owner。关键是找谁应该改变done或condition,谁负责执行complete()或wake_up()。
5.9 Missing Complete
正常链路:
1 | |
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 | |
调查时必须同时追踪condition的所有写入点、状态机合法迁移、对象实例和wake顺序。只记录wake事件不足以证明等待应该结束。
5.12 Timeout Missing
对可能永久失败的外部依赖使用无限等待,会把设备或远端故障放大为永久D状态。改用带超时等待、取消和恢复路径可限制故障影响。
但必须区分:
1 | |
盲目加超时还可能引入对象释放、迟到Completion和重复完成等新竞态。
5.13 IO类hung_task
I/O类的共同模型是:
1 | |
需要定位请求在哪一层停止:尚未提交、已经排队、已经dispatch、设备处理中,还是已经完成但上层未被通知。
5.14 Block IO
块I/O典型路径:
1 | |
关键证据包括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 | |
需要检查RPC状态和重传、挂载策略、网络收发、服务端响应及远端存储。硬挂载的长期等待可能符合配置语义,但仍需判断是否超过业务可接受范围。
5.17 Device IO
SPI、USB、eMMC、摄像头和加速器等设备I/O通常表现为:
1 | |
它位于I/O类和Driver类的交界处。先确认请求对象及生命周期,再进入寄存器、DMA、IRQ、firmware等设备专属证据。
5.18 Driver、Firmware与Hardware类hung_task
嵌入式设备常见完整链路:
1 | |
任意一段停止都可能让最上层Waiter进入D状态。调查必须检查请求和完成两个方向,不能只看提交成功日志。
5.19 IRQ Missing
“没有看到IRQ增长”只是现象,可能原因包括:
- 硬件操作根本没有完成;
- 中断未使能或被mask;
- 路由、触发类型或亲和性配置错误;
- 设备、控制器或firmware未发出中断;
- ISR进入但未识别正确状态位;
- 日志/采样窗口遗漏。
应联合设备状态寄存器、/proc/interrupts和IRQ trace验证。
5.20 DMA Never Complete
DMA不完成可能来自descriptor、长度、地址、IOMMU映射、缓存一致性、engine状态、通道复位或完成中断问题。证据链至少应包含:
1 | |
不要仅靠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 | |
需要检查内存水位、分配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 | |
若设备不完成,最终报告栈可能停在reclaim。分类必须沿依赖链动态从Memory转向I/O再转向Driver/Hardware。
5.28 Dependency Cycle
Dependency Cycle表示依赖图存在有向闭环:每个节点都等待下一个节点,而最后一个又依赖前面的节点。闭环中没有外部事件能自然打破等待时,系统形成死锁。
长期线性等待不是Cycle:例如Task一直等待已经失效的硬件,也会hung,但依赖链不回到自身。
5.29 AB-BA Deadlock
最小锁环:
1 | |
证明AB-BA需要确认相同锁实例和同时成立的持有/等待关系。仅从静态源码看到两种锁顺序,说明存在潜在风险,不等于现场已经形成该环。
5.30 多级Dependency Chain
真实环可能跨越同步原语与执行上下文:
1 | |
必须逐边记录对象、Owner/Producer和证据。只要其中一条边是假设,Cycle结论就仍需验证。
5.31 多个hung_task如何聚类
为每个Waiter建立最小记录:
1 | |
先按Wait Primitive和业务路径粗分,再用对象地址、设备、挂载点、队列或Owner细分。相同栈不保证相同对象,不同栈也可能汇聚到同一个下游设备。
5.32 Common Dependency分析
将每组任务画成有向边:
1 | |
入边最多、同时又不推进的Owner、Producer、队列、设备或远端服务,是优先验证的Common Dependency。
5.33 Decision Tree
1 | |
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 | |
事件名称和字段受内核配置影响,先检查/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 | |
只启用能验证当前依赖边的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 | |
5.40 学完检查题
- Root Cause分类真正要解决什么问题?
- 为什么第一层分类不是最终结论?
- Lock类问题的首要运行时对象是什么?
- 如何区分Owner正在运行、Runnable和Sleeping?
- mutex遗漏unlock与Owner下游阻塞需要哪些不同证据?
- rwsem为什么可能有多个Holders?
- 静态锁顺序能否单独证明现场AB-BA?
- Completion类为什么通常寻找Producer而非Owner?
- Missing Complete最容易遗漏哪些控制流?
- 证明Lost Wakeup需要观察哪些顺序关系?
- 为什么记录到
wake_up()仍不能证明condition成立? - Timeout Missing属于根因还是鲁棒性问题?
- 加超时为什么可能引入迟到Completion竞态?
- 块I/O请求生命周期有哪些关键阶段?
- 为什么文件系统栈可能只是中间层?
- NFS类需要补充哪些远端证据?
- Device I/O为何横跨I/O类和Driver类?
- 调查IRQ Missing时为什么要同时看硬件状态?
- DMA Never Complete至少需要哪些证据?
- 如何证明firmware已经收到命令?
- “Hardware No Response”为什么仍可能不是最终根因?
- Direct Reclaim如何依赖存储I/O?
- Writeback停滞会怎样向上游传播?
- Folio Wait的真正Wait Object上下文是什么?
- Allocation Dependency为何会使Memory分类转向Driver分类?
- 线性长期等待与Dependency Cycle的区别是什么?
- 证明AB-BA需要哪些同时态关系?
- 多级Cycle为什么必须逐边验证?
- 多个hung任务应按哪些字段聚类?
- 相同栈是否说明同一对象?
- 如何识别Common Dependency?
- Decision Tree的输出应该是什么?
- Lock、Completion、I/O和Memory分别需要补什么核心证据?
- 为什么应先检查
available_events? - 哪些trace可验证块请求issue/complete?
- 为什么不要一开始全开trace?
- Fault Containment与Root Cause Fix有什么区别?
- 请画出“Memory reclaim -> writeback -> block I/O -> driver -> device”的依赖链。
- 请把“Task A等mutex,Owner B等Completion,firmware未响应”分成表面、中间和最终类别。
- 你能否不看本文画出完整Root Cause Decision Tree,并为每个分支写出下一条必须收集的证据?
下一章将把分类方法收敛为一套可直接执行的实战流程:从Waiter定位Owner/Producer,递归追踪下一层依赖,为每条边补证据,最终形成可以写入Bug单的Root Cause结论与回归方案。