Linux hung_task故障分析:如何读一条hung_task日志
第4章:如何读一条hung_task日志——从Call Trace找到Wait Object
0. 源码基线与本章边界
本文以 Linux v6.1 为源码基线,对应上游标签提交830b3c68c1fb1e9176028d02ef86f3cf76aa2476。主要参考:
kernel/hung_task.c:告警生成与sched_show_task()调用;kernel/sched/core.c:任务信息及栈打印;include/linux/wait.h:wait_event*();kernel/sched/completion.c:Completion等待;kernel/locking/mutex.c:mutex慢路径;kernel/locking/rwsem.c:rwsem慢路径;Documentation/admin-guide/tainted-kernels.rst:taint标志解释。
本章不试图仅凭一条栈“猜出Bug”,而是建立可重复执行的证据流程:
1 | |
4.1 一条hung_task日志应该怎么看
先看一份抽象日志:
1 | |
不要直接得出“my_device_xfer()有Bug”。先把日志拆成身份、时间、状态、等待方式和业务入口,再追具体对象及其依赖。
4.2 日志头部有哪些关键信息
第一轮只提取事实:
1 | |
不要在事实提取阶段混入根因结论。
4.3 Task Name怎么看
comm只有固定长度,日志中的任务名可能被截断,也不一定唯一。它首先帮助判断任务类型:用户进程线程、内核线程、workqueue worker、文件系统线程还是驱动业务线程。
需要结合PID、/proc/<pid>/comm、cmdline、status和业务日志确认身份,不能只靠名字关联唯一实体。
4.4 PID怎么看
PID用于把告警和运行时证据关联起来:
1 | |
它也可用于过滤ftrace、perf和业务日志。PID只是索引,不是原因;现场消失或PID复用后,必须结合时间戳和任务名谨慎关联。
4.5 State怎么看
state:D表明任务处于不可中断睡眠的用户可见表示。分析问题立即从“为什么占CPU”切换为:
1 | |
注意某些内部状态是位组合,诊断时应以对应版本源码和完整日志为准。
4.6 blocked for more than xxx seconds怎么看
v6.1的秒数来自:
1 | |
所以它表示自检测器最近观察到该任务context switch计数变化后经过的时间,不是精确记录某个等待函数入口的秒表。扫描间隔和检测线程调度延迟会影响报告时刻。
4.7 Tainted怎么看
taint是一组内核运行历史标志。不同字母占据固定位置,例如P常表示加载过专有模块,O表示加载过树外模块。它提醒你当前现场可能包含厂商或第三方代码,也会影响上游问题受理。
1 | |
应按日志对应内核版本的tainted-kernels.rst解码,不要凭印象解释所有字符。
4.8 Call Trace是什么
Call Trace是该任务当前内核栈的符号化表示,展示函数调用关系和偏移。对hung_task而言,它主要告诉我们Waiter沿什么路径进入当前等待。
受架构、编译优化、内联、栈展开能力和符号质量影响,调用栈可能不完整或包含偏差。因此它是关键证据,但不是自动生成的根因报告。
4.9 Call Trace为什么不能只看最上面
栈顶经常是:
1 | |
这些函数只说明任务怎样离开CPU。大量完全不同的等待最终都会汇聚到调度器,如果把公共汇聚点当根因,就无法区分锁、Completion、I/O和网络等待。
4.10 Scheduler Frame是什么
Scheduler Frame是通用睡眠框架,例如:
1 | |
它们回答“CPU是怎么切走的”,一般不回答“业务条件为什么没成立”。除非证据指向调度器自身异常,否则先把它们标成框架层。
4.11 Wait Primitive是什么
Wait Primitive是第一个揭示等待类别的同步接口或慢路径,例如:
1 | |
它回答“任务用什么机制等待”,是从公共调度框架进入业务分析的桥梁。
4.12 Business Wait Point是什么
Business Wait Point是Wait Primitive下面第一个说明“为何等待”的子系统函数:
1 | |
它通常能帮助定位源码调用点和实参,但仍未必是根因。真正对象可能在函数内部,真正故障可能在另一个执行上下文。
4.13 如何识别Completion等待
典型栈形态:
1 | |
固定翻译为:当前任务在等一个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 | |
需要判断Waiter是Reader还是Writer,并寻找当前Reader集合或Writer持有者。大量任务收敛到同一rwsem时,通常应寻找共同持有路径。
4.16 如何识别Wait Event等待
wait_event*()是宏,优化后未必直接出现在栈里。调用栈可能只保留子系统的等待helper、prepare_to_wait_event()或schedule()。
因此要回到业务函数源码寻找类似:
1 | |
同时记录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 | |
函数前缀只用于选择调查方向,不能替代具体对象和运行时证据。
4.19 怎么从Wait Primitive定位Wait Object
Wait Primitive只能说明“怎么等”。Wait Object必须从调用点实参、结构体字段和对象生命周期中还原:
1 | |
同一函数可能服务大量对象,只有找到本次调用使用的实例,才能继续追Owner或Producer。
4.20 怎么从源码找到Wait Object
固定步骤:
- 使用日志对应版本和配置找到Business Wait Point;
- 沿控制流定位等待API的直接调用点;
- 记录实参表达式及其所属结构体;
- 查对象在哪里初始化、如何关联当前请求、何时销毁;
- 若静态源码无法确定本次实例,使用vmcore、调试符号、trace或动态探针补充运行时地址。
不要拿其他版本中“名字相似”的实现替换现场版本。
4.21 怎么找到Owner
Owner概念主要适用于mutex、rwsem等资源。静态搜索所有获取与释放路径,列出可能持有者;再用运行时证据确认实际持有者:
1 | |
源码只能证明“谁可能持有”,不能单独证明“故障瞬间谁正在持有”。
4.22 没有Owner时怎么找Producer
Completion、条件等待和I/O请求通常没有传统Owner。此时寻找负责让状态推进的Producer:
1 | |
4.23 怎么找complete()调用者
从具体字段出发搜索:
1 | |
若对象通过helper传递,应继续追helper、回调注册和函数指针,而不是只做文本匹配。还要检查reinit_completion()、取消、超时、复位和设备移除路径,确认事件没有被错误消费或重置。
4.24 怎么找wake_up()调用者
条件等待必须同时检查两个动作:
1 | |
只找到wake_up()不够;如果它更新了错误对象、先后顺序不当或condition仍为false,Waiter醒来后还会继续睡眠。
4.25 怎么找unlock()路径
从具体锁字段搜索获取和释放:
1 | |
检查每条成功、错误、重试和早退路径是否成对释放,并关注持锁期间是否调用了可能长时间睡眠的下游操作。必要时结合锁trace确认真实持锁时间。
4.26 怎么找IO Completion路径
从Wait Object沿完成方向反向验证:
1 | |
应把块层trace、/proc/interrupts、控制器错误日志和设备状态组合起来,避免只凭一层栈下结论。
4.27 多Task hung怎么分析
先做聚类,而不是逐条孤立阅读。为每个任务提取:
1 | |
按相同调用后缀、同一业务函数、同一设备/挂载点或同一对象分组。大量Waiter通常是同一个上游停顿向外传播的结果。
4.28 Common Dependency怎么找
把每条栈转换成依赖边:
1 | |
共同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 | |
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 | |
可结合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 | |
边旁边记录证据:调用点、对象地址、Owner PID、trace事件、中断计数或寄存器状态。没有证据的边标记为“假设”,不要伪装成结论。
4.36 Symptom、Immediate Cause和Root Cause如何区分
示例:
1 | |
“mutex导致hung”只描述了直接阻塞关系。Root Cause应该解释依赖链为何停止,以及什么证据排除了其他可能。
4.37 本章分析模板
1 | |
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 | |
预测: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 学完检查题
- 第一轮阅读hung_task日志应提取哪些事实?
- 为什么任务名可能不唯一或被截断?
- PID如何帮助关联运行时证据?
state:D会把调查方向引向什么问题?- 报告秒数为什么不是等待入口的精确计时?
- Tainted字段能证明第三方模块是根因吗?
- Call Trace受哪些因素影响?
- 什么是Scheduler Frame?
- 什么是Wait Primitive?
- 什么是Business Wait Point?
- 为什么不能停在
schedule()? - Completion等待的下一步固定问题是什么?
- mutex等待的下一步固定问题是什么?
- rwsem Reader和Writer等待要分别关注什么?
- 为什么
wait_event()可能不直接出现在优化后的栈里? - I/O栈中应如何找到具体request?
- 本地文件系统与NFS的依赖方向有何不同?
- 为什么Wait Primitive还不是Wait Object?
- 如何用源码实参确认Wait Object?
- 静态源码为什么不能单独确认实际Owner?
- Completion没有Owner时应该找什么?
- 搜索
complete()时为什么还要检查helper和函数指针? - 条件等待为什么必须同时查condition writer与
wake_up()? - 如何审查mutex的异常返回路径?
- 一条块I/O完成链包含哪些关键环节?
- 多任务hung时为什么应先聚类?
- 相同栈是否必然代表同一Wait Object?
- 什么时候需要all task stacks而不仅是all CPU backtrace?
- 哪些证据可以验证IRQ Producer?
- work已入队为什么仍可能不执行?
- Block Queue freeze可能怎样影响Waiter?
- NFS硬挂载为何可能长期D状态?
- 驱动/固件链应验证哪些协议节点?
- Dependency Chain中的每条边应记录什么?
- Symptom、Immediate Cause与Root Cause如何区别?
wchan能提供什么、不能提供什么?- 为什么timeout常是Fault Containment而非Root Cause Fix?
- 请用本章模板拆解一条Completion类hung栈。
- 请画出“Waiter -> mutex -> Owner -> Completion -> IRQ -> Hardware”的依赖链。
- 哪些缺失证据会阻止你把根因假设升级为结论?
下一章将把Root Cause按Lock、Completion/Wait、I/O、Driver、Memory和Dependency Cycle分类,为每一类建立典型栈、关键补充证据和验证路线。