Linux hung_task故障分析:如何读一条hung_task日志

第4章:如何读一条hung_task日志——从Call Trace找到Wait Object

0. 源码基线与本章边界

本文以 Linux v6.1 为源码基线,对应上游标签提交830b3c68c1fb1e9176028d02ef86f3cf76aa2476。主要参考:

本章不试图仅凭一条栈“猜出Bug”,而是建立可重复执行的证据流程:

1
2
日志字段 -> 栈分层 -> Wait Primitive -> Wait Object
-> Owner / Producer -> 下一层依赖 -> Root Cause证据

4.1 一条hung_task日志应该怎么看

先看一份抽象日志:

1
2
3
4
5
6
7
8
9
INFO: task foo:1234 blocked for more than 120 seconds.
Tainted: P O 6.1.x #1
task:foo state:D stack:0 pid:1234 ppid:1
Call Trace:
__schedule+0x...
schedule+0x...
wait_for_completion+0x...
my_device_xfer+0x...
my_ioctl+0x...

不要直接得出“my_device_xfer()有Bug”。先把日志拆成身份、时间、状态、等待方式和业务入口,再追具体对象及其依赖。

4.2 日志头部有哪些关键信息

第一轮只提取事实:

1
2
3
4
5
6
Task name:谁被报告
PID/PPID:如何关联现场和调用来源
State:当前属于哪类等待
Blocked time:检测器观察到多久没有调度进展
Kernel version/taint:分析使用哪份源码,运行环境是否被修改
Call Trace:Waiter如何走到睡眠点

不要在事实提取阶段混入根因结论。

4.3 Task Name怎么看

comm只有固定长度,日志中的任务名可能被截断,也不一定唯一。它首先帮助判断任务类型:用户进程线程、内核线程、workqueue worker、文件系统线程还是驱动业务线程。

需要结合PID、/proc/<pid>/comm、cmdline、status和业务日志确认身份,不能只靠名字关联唯一实体。

4.4 PID怎么看

PID用于把告警和运行时证据关联起来:

1
2
3
cat /proc/<pid>/status
cat /proc/<pid>/stack
cat /proc/<pid>/wchan

它也可用于过滤ftrace、perf和业务日志。PID只是索引,不是原因;现场消失或PID复用后,必须结合时间戳和任务名谨慎关联。

4.5 State怎么看

state:D表明任务处于不可中断睡眠的用户可见表示。分析问题立即从“为什么占CPU”切换为:

1
2
3
它通过什么原语睡下?
具体等待什么对象?
谁应该改变对象状态并唤醒它?

注意某些内部状态是位组合,诊断时应以对应版本源码和完整日志为准。

4.6 blocked for more than xxx seconds怎么看

v6.1的秒数来自:

1
(jiffies - t->last_switch_time) / HZ

所以它表示自检测器最近观察到该任务context switch计数变化后经过的时间,不是精确记录某个等待函数入口的秒表。扫描间隔和检测线程调度延迟会影响报告时刻。

4.7 Tainted怎么看

taint是一组内核运行历史标志。不同字母占据固定位置,例如P常表示加载过专有模块,O表示加载过树外模块。它提醒你当前现场可能包含厂商或第三方代码,也会影响上游问题受理。

1
2
Tainted = 环境证据
Tainted != 某个模块已经被证明是根因

应按日志对应内核版本的tainted-kernels.rst解码,不要凭印象解释所有字符。

4.8 Call Trace是什么

Call Trace是该任务当前内核栈的符号化表示,展示函数调用关系和偏移。对hung_task而言,它主要告诉我们Waiter沿什么路径进入当前等待。

受架构、编译优化、内联、栈展开能力和符号质量影响,调用栈可能不完整或包含偏差。因此它是关键证据,但不是自动生成的根因报告。

4.9 Call Trace为什么不能只看最上面

栈顶经常是:

1
2
3
__schedule
schedule
schedule_timeout

这些函数只说明任务怎样离开CPU。大量完全不同的等待最终都会汇聚到调度器,如果把公共汇聚点当根因,就无法区分锁、Completion、I/O和网络等待。

4.10 Scheduler Frame是什么

Scheduler Frame是通用睡眠框架,例如:

1
2
3
4
5
__schedule()
schedule()
schedule_timeout()
schedule_preempt_disabled()
io_schedule()中的schedule()

它们回答“CPU是怎么切走的”,一般不回答“业务条件为什么没成立”。除非证据指向调度器自身异常,否则先把它们标成框架层。

4.11 Wait Primitive是什么

Wait Primitive是第一个揭示等待类别的同步接口或慢路径,例如:

1
2
3
4
5
6
wait_for_completion*()
mutex_lock*() / __mutex_lock_common()
down_read() / down_write() / rwsem_*_slowpath()
wait_event*()展开后的子系统等待函数
io_schedule()
wait_on_page*()

它回答“任务用什么机制等待”,是从公共调度框架进入业务分析的桥梁。

4.12 Business Wait Point是什么

Business Wait Point是Wait Primitive下面第一个说明“为何等待”的子系统函数:

1
2
3
wait_for_completion()       <- 机制
spi_sync_locked() <- SPI同步传输等待
my_sensor_read() <- 业务请求

它通常能帮助定位源码调用点和实参,但仍未必是根因。真正对象可能在函数内部,真正故障可能在另一个执行上下文。

4.13 如何识别Completion等待

典型栈形态:

1
2
3
4
__schedule
schedule_timeout
wait_for_completion_timeout / wait_for_completion
driver_submit_sync

固定翻译为:当前任务在等一个struct completion获得完成令牌。下一步确认具体Completion实例,并找所有complete()、complete_all()和重初始化路径。

4.14 如何识别Mutex等待

典型栈可能包含mutex_lock()、mutex_lock_interruptible()或__mutex_lock_common()。固定翻译:任务正在等待某个mutex成为可获得状态。

下一步不是搜索“mutex有什么Bug”,而是定位具体mutex字段、当前Owner以及Owner为何没有走到对应mutex_unlock()。

4.15 如何识别RWSEM等待

常见符号包括:

1
2
3
4
rwsem_down_read_slowpath
rwsem_down_write_slowpath
down_read
down_write

需要判断Waiter是Reader还是Writer,并寻找当前Reader集合或Writer持有者。大量任务收敛到同一rwsem时,通常应寻找共同持有路径。

4.16 如何识别Wait Event等待

wait_event*()是宏,优化后未必直接出现在栈里。调用栈可能只保留子系统的等待helper、prepare_to_wait_event()或schedule()。

因此要回到业务函数源码寻找类似:

1
wait_event(obj->waitq, obj->ready);

同时记录wait queue和condition,二者缺一不可。

4.17 如何识别IO Wait

io_schedule()、wait_on_page*()、块层请求等待和文件映射等待常提示I/O语义。它们表明任务的睡眠被标记为I/O等待,但不会自动告诉你是哪块磁盘或哪条request。

继续追踪具体page、bio、request、gendisk、队列、驱动和完成回调,并结合块层trace与设备日志验证。

4.18 如何识别Filesystem与NFS等待

本地文件系统栈可能出现filemap_*、页锁、writeback、journal和submit_bio;NFS路径常出现nfs_*、rpc_*、socket等待等。

1
2
本地FS -> page/writeback/journal -> block layer -> device
NFS -> RPC request -> network -> remote server

函数前缀只用于选择调查方向,不能替代具体对象和运行时证据。

4.19 怎么从Wait Primitive定位Wait Object

Wait Primitive只能说明“怎么等”。Wait Object必须从调用点实参、结构体字段和对象生命周期中还原:

1
2
3
wait_for_completion(&cmd->done);  // cmd->done
mutex_lock(&dev->lock); // dev->lock
wait_event(req->wait, req->done); // req->wait与req->done

同一函数可能服务大量对象,只有找到本次调用使用的实例,才能继续追Owner或Producer。

4.20 怎么从源码找到Wait Object

固定步骤:

  1. 使用日志对应版本和配置找到Business Wait Point;
  2. 沿控制流定位等待API的直接调用点;
  3. 记录实参表达式及其所属结构体;
  4. 查对象在哪里初始化、如何关联当前请求、何时销毁;
  5. 若静态源码无法确定本次实例,使用vmcore、调试符号、trace或动态探针补充运行时地址。

不要拿其他版本中“名字相似”的实现替换现场版本。

4.21 怎么找到Owner

Owner概念主要适用于mutex、rwsem等资源。静态搜索所有获取与释放路径,列出可能持有者;再用运行时证据确认实际持有者:

1
2
3
4
5
all task stacks
lockdep输出
ftrace锁事件
crash/vmcore中的锁和task状态
业务对象状态

源码只能证明“谁可能持有”,不能单独证明“故障瞬间谁正在持有”。

4.22 没有Owner时怎么找Producer

Completion、条件等待和I/O请求通常没有传统Owner。此时寻找负责让状态推进的Producer:

1
2
3
4
5
6
Completion -> complete()调用者
wait_event -> condition写入者 + wake_up()调用者
I/O request -> end_io/完成回调/IRQ
RPC -> 网络接收路径或远端服务
Timer -> 定时器回调
Work -> workqueue worker

4.23 怎么找complete()调用者

从具体字段出发搜索:

1
rg 'complete(_all)?\s*\(&cmd->done\)' <source-tree>

若对象通过helper传递,应继续追helper、回调注册和函数指针,而不是只做文本匹配。还要检查reinit_completion()、取消、超时、复位和设备移除路径,确认事件没有被错误消费或重置。

4.24 怎么找wake_up()调用者

条件等待必须同时检查两个动作:

1
2
1. 谁把condition更新为true?
2. 谁在正确时机对同一wait queue执行wake_up*()?

只找到wake_up()不够;如果它更新了错误对象、先后顺序不当或condition仍为false,Waiter醒来后还会继续睡眠。

4.25 怎么找unlock()路径

从具体锁字段搜索获取和释放:

1
rg 'mutex_(lock|unlock).*dev->lock' <source-tree>

检查每条成功、错误、重试和早退路径是否成对释放,并关注持锁期间是否调用了可能长时间睡眠的下游操作。必要时结合锁trace确认真实持锁时间。

4.26 怎么找IO Completion路径

从Wait Object沿完成方向反向验证:

1
2
3
4
5
6
7
8
Waiter等待page/request
<- 谁提交bio/request
<- 驱动是否dispatch
<- 设备是否接受命令
<- IRQ是否产生
<- ISR/线程化中断是否运行
<- end_io/complete request是否执行
<- Waiter是否被唤醒

应把块层trace、/proc/interrupts、控制器错误日志和设备状态组合起来,避免只凭一层栈下结论。

4.27 多Task hung怎么分析

先做聚类,而不是逐条孤立阅读。为每个任务提取:

1
Task identity | Wait Primitive | Business Wait Point | Object候选

按相同调用后缀、同一业务函数、同一设备/挂载点或同一对象分组。大量Waiter通常是同一个上游停顿向外传播的结果。

4.28 Common Dependency怎么找

把每条栈转换成依赖边:

1
2
3
A -> Mutex M -> Task X
B -> Mutex M -> Task X
C -> Completion C -> Worker X

共同Owner、Producer、设备队列、workqueue、文件系统事务或远端服务,就是优先验证的Common Dependency。栈函数相同不一定对象相同,必须进一步确认实例。

4.29 什么时候需要看其他CPU或Task stack

出现锁Owner、workqueue Producer、内核线程协作或疑似依赖环时,需要查看其他任务栈。all CPU backtrace适合了解当时各CPU正在运行什么,但Sleeping Owner未必正在CPU上,所以还要查看all task stacks或vmcore。

目标是回答:依赖链下一节点当前在运行、Runnable,还是也在等待?

4.30 什么时候需要看IRQ

当Completion或I/O的Producer理论上位于硬件中断路径时,检查:

1
2
3
4
5
6
设备是否使能中断
中断号和CPU亲和性是否正确
/proc/interrupts计数是否增长
ISR是否进入并确认正确状态位
ISR是否执行到完成/唤醒分支
是否存在中断风暴、屏蔽或共享IRQ误判

4.31 什么时候需要看Workqueue

当Producer由queue_work()或queue_delayed_work()调度时,验证work是否成功入队、目标workqueue是否有可用worker、worker是否被更早的work堵塞,以及当前work是否在等待同一资源形成自依赖。

“已经queue”不等于“已经运行”,更不等于“已经执行到wake/complete”。

4.32 什么时候需要看Block IO

栈出现io_schedule()、page wait、bio/request或文件系统回写路径时,检查:

1
2
3
4
5
request是否生成、合并和dispatch
队列是否freeze/quiesce
设备是否timeout/reset
完成中断是否到达
end_io是否执行

可结合blktrace、tracepoints、控制器日志和/sys/block状态,但必须注意观测工具本身的开销。

4.33 什么时候需要看NFS或RPC

出现nfs_*、rpc_*或网络等待时,应检查RPC任务状态、重传、挂载选项、网络连通性、收包路径和服务端状态。硬挂载在远端不可用时可能长期等待,这是策略和故障共同作用,不应直接归咎于本地调度器。

4.34 什么时候需要看Driver或Firmware

出现mailbox、DMA、设备命令或驱动私有wait helper时,按协议检查:命令是否真正发出,ring/FIFO是否推进,firmware是否接收,硬件状态机是否完成,IRQ/doorbell是否返回,错误恢复是否遗漏通知。

厂商模块taint可以提高调查优先级,但仍需完整证据链。

4.35 如何建立Dependency Chain

每条边都应写成“主体等待对象,对象依赖谁改变”:

1
2
3
4
5
Task A
-> waits for cmd->done
-> completed by device_irq()
-> device_irq depends on IRQ 42
-> IRQ depends on hardware command completion

边旁边记录证据:调用点、对象地址、Owner PID、trace事件、中断计数或寄存器状态。没有证据的边标记为“假设”,不要伪装成结论。

4.36 Symptom、Immediate Cause和Root Cause如何区分

示例:

1
2
3
4
Symptom:Task A被报告blocked 184秒
Immediate Cause:Task A等待Mutex M
Intermediate Cause:Mutex M由Task B持有
Root Cause:Task B等待设备完成,但设备中断未到达

“mutex导致hung”只描述了直接阻塞关系。Root Cause应该解释依赖链为何停止,以及什么证据排除了其他可能。

4.37 本章分析模板

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
[现场]
Kernel release/commit/config:
Timestamp:
Taint:

[Waiter]
Name/PID/PPID:
State:
Reported duration:

[栈分层]
Scheduler frames:
Wait primitive:
Business wait point:
Request origin:

[对象]
Wait object/type/address:
Condition:
Object lifetime:

[依赖]
Owner or Producer:
Next dependency:
Other affected tasks:

[证据]
Source call site:
Owner/task stack:
IRQ/workqueue/I/O/RPC evidence:
Relevant logs/trace:

[结论]
Symptom:
Immediate cause:
Root-cause hypothesis:
Confirmed evidence:
Missing evidence:

4.38 本章实验

4.38.1 手工分层Call Trace

选一条包含wait_for_completion()的历史日志,用四种颜色或标签标记Scheduler、Wait Primitive、Business Wait Point和Request Origin。先只陈述每层事实,不下结论。

4.38.2 静态反查对象

在匹配版本源码中找到等待调用点,记录实参字段、初始化点、所有complete()/wake_up()/unlock()以及错误出口。区分直接调用、helper和函数指针回调。

4.38.3 运行时复核

若现场仍存在:

1
2
3
cat /proc/<pid>/stack
cat /proc/<pid>/wchan
cat /proc/<pid>/status

预测:wchan只能给出附近等待符号,无法直接告诉你具体对象和Producer。

4.38.4 多栈聚类

把多条hung任务整理成表格,按Wait Primitive、Business Point、设备/挂载点和对象候选分组,再画Common Dependency图。

4.39 工程意义

结构化分析能避免“看到哪个函数就改哪个函数”。对驱动而言,把永久等待改成超时可以限制故障影响,但超时只是Fault Containment;硬件为何不完成、IRQ为何丢失仍是Root Cause问题。

高质量故障报告应该包含版本、配置、完整日志、对象与依赖链、验证证据及未确认假设,使问题可以复现和复核。

4.40 常见误区

误区1:Call Trace第一行就是Root Cause

栈顶通常是公共调度框架。

误区2:看到schedule()就怀疑调度器

绝大多数睡眠等待最终都会调用调度器。

误区3:wait_for_completion()卡住说明Completion坏了

应找具体Completion及Producer。

误区4:mutex_lock()卡住说明mutex实现坏了

应找具体锁、实际Owner和Owner的下一层依赖。

误区5:io_schedule()说明CPU调度异常

它主要表达I/O等待语义,应继续追request完成链。

误区6:静态源码能直接确定现场Owner

源码只能列出可能路径,实际Owner属于运行时状态。

误区7:多个相同栈一定等待同一对象

相同函数可以服务多个实例,必须验证对象地址和业务关联。

误区8:增加timeout就是根因修复

timeout可能避免永久挂起,但不解释事件为何没有完成。

误区9:Tainted证明第三方模块有Bug

它只说明环境历史,应作为调查上下文而非定罪证据。

4.41 博客总结

阅读hung_task日志应从事实提取开始:确认任务身份、PID、D状态、报告时长、内核版本和taint,再对Call Trace分层。__schedule()、schedule()等是公共睡眠框架;wait_for_completion()、mutex/rwsem慢路径、条件等待和io_schedule()揭示Wait Primitive;其下方第一个子系统函数通常是Business Wait Point。

Wait Primitive只回答“怎么等”,必须回到匹配版本源码,通过等待API实参确认Wait Object。mutex/rwsem继续寻找运行时Owner;Completion、wait_event和I/O继续寻找Producer及完成路径。多个hung任务先聚类并寻找Common Dependency,最终将结论分成Symptom、Immediate Cause和Root Cause,并为依赖图中的每条边附上证据或明确标为假设。

4.42 学完检查题

  1. 第一轮阅读hung_task日志应提取哪些事实?
  2. 为什么任务名可能不唯一或被截断?
  3. PID如何帮助关联运行时证据?
  4. state:D会把调查方向引向什么问题?
  5. 报告秒数为什么不是等待入口的精确计时?
  6. Tainted字段能证明第三方模块是根因吗?
  7. Call Trace受哪些因素影响?
  8. 什么是Scheduler Frame?
  9. 什么是Wait Primitive?
  10. 什么是Business Wait Point?
  11. 为什么不能停在schedule()?
  12. Completion等待的下一步固定问题是什么?
  13. mutex等待的下一步固定问题是什么?
  14. rwsem Reader和Writer等待要分别关注什么?
  15. 为什么wait_event()可能不直接出现在优化后的栈里?
  16. I/O栈中应如何找到具体request?
  17. 本地文件系统与NFS的依赖方向有何不同?
  18. 为什么Wait Primitive还不是Wait Object?
  19. 如何用源码实参确认Wait Object?
  20. 静态源码为什么不能单独确认实际Owner?
  21. Completion没有Owner时应该找什么?
  22. 搜索complete()时为什么还要检查helper和函数指针?
  23. 条件等待为什么必须同时查condition writer与wake_up()?
  24. 如何审查mutex的异常返回路径?
  25. 一条块I/O完成链包含哪些关键环节?
  26. 多任务hung时为什么应先聚类?
  27. 相同栈是否必然代表同一Wait Object?
  28. 什么时候需要all task stacks而不仅是all CPU backtrace?
  29. 哪些证据可以验证IRQ Producer?
  30. work已入队为什么仍可能不执行?
  31. Block Queue freeze可能怎样影响Waiter?
  32. NFS硬挂载为何可能长期D状态?
  33. 驱动/固件链应验证哪些协议节点?
  34. Dependency Chain中的每条边应记录什么?
  35. Symptom、Immediate Cause与Root Cause如何区别?
  36. wchan能提供什么、不能提供什么?
  37. 为什么timeout常是Fault Containment而非Root Cause Fix?
  38. 请用本章模板拆解一条Completion类hung栈。
  39. 请画出“Waiter -> mutex -> Owner -> Completion -> IRQ -> Hardware”的依赖链。
  40. 哪些缺失证据会阻止你把根因假设升级为结论?

下一章将把Root Cause按Lock、Completion/Wait、I/O、Driver、Memory和Dependency Cycle分类,为每一类建立典型栈、关键补充证据和验证路线。


Linux hung_task故障分析:如何读一条hung_task日志
https://cj0510.github.io/2026/10/09/Linux Kernel/Debug/Linux-hung-task故障分析-第4章-如何读hung_task日志/
作者
CJ1018
发布于
2026年10月9日
许可协议
CC BY-NC-SA 4.0