Linux hung_task故障分析:Linux阻塞与等待模型
第3章:Linux阻塞与等待模型——Task为什么会进入D状态
0. 源码基线与本章边界
本文以 Linux v6.1 为源码基线,对应上游标签提交830b3c68c1fb1e9176028d02ef86f3cf76aa2476。主要阅读入口是:
include/linux/wait.h:等待队列接口与wait_event*()宏;kernel/sched/wait.c:等待队列登记、清理与唤醒;kernel/sched/completion.c:Completion等待与完成;kernel/locking/mutex.c:mutex慢路径;kernel/locking/rwsem.c:rwsem等待路径;kernel/sched/core.c:schedule()与io_schedule()。
本章不逐行展开每一种锁算法,而是建立hung_task诊断所需的统一依赖模型:
1 | |
3.1 为什么Linux需要阻塞等待
任务经常需要等待暂时不存在的结果,例如磁盘数据、DMA完成、锁释放或网络响应。如果任务一直循环检查:
1 | |
它会持续占用CPU。若事件可能需要毫秒甚至秒,CPU时间和功耗都会被浪费。阻塞等待让任务在条件不满足时离开CPU,把处理器交给其他可运行任务;事件发生后,再让它重新参与调度。
3.2 Busy Wait和Sleep Wait
1 | |
睡眠等待不是无条件更好:它有入队、调度和唤醒成本,而且不能用于中断上下文或持有禁止睡眠的自旋锁时。
3.3 一个Task睡眠的通用模型
通用流程不是单纯调用schedule():
1 | |
抽象伪代码:
1 | |
“登记等待、检查条件、进入睡眠”的顺序和内存可见性非常关键,否则可能丢失唤醒。
3.4 Wait Queue是什么
普通等待队列由wait_queue_head和若干wait_queue_entry组成:
1 | |
它记录“哪些执行实体正在等待这个事件”。每个entry带有唤醒回调,普通任务通常最终通过try_to_wake_up()变为可运行。
Wait Queue不是CPU的runqueue:前者组织等待事件的任务,后者组织已经可运行、等待CPU的任务。
3.5 Waiter、Wait Object、Owner和Producer
- Waiter:当前被阻塞的任务;
- Wait Primitive:
wait_event()、wait_for_completion()、mutex_lock()等睡眠接口; - Wait Object:具体等待队列、Completion、mutex、rwsem、I/O请求或页面;
- Owner:当前占有互斥资源的任务,例如mutex持有者;
- Producer:负责产生完成事件或让条件成立的一方,例如IRQ、worker、设备或远端服务器。
Owner和Producer不能混为一谈。mutex有明确Owner;Completion通常没有“持有者”,只有负责调用complete()的Producer。
3.6 Wait Condition是什么
等待的本质通常是:
1 | |
条件可以是request->done、缓冲区非空、页锁解除、设备状态变化等。等待队列只解决“睡在哪里、如何被通知”;condition才决定任务是否可以真正返回。
因此即使wake_up()执行过,只要任务醒来后重新检查发现condition仍为false,它仍会再次睡眠。
3.7 prepare_to_wait()做什么
普通等待队列常见入口包括prepare_to_wait()和prepare_to_wait_event()。以v6.1的后者为例,其核心职责是:
1 | |
它只是做好睡眠准备,并不自行切换CPU;真正离开CPU需要后续调用schedule()或相关调度接口。
3.8 schedule()在等待流程中的位置
当当前任务状态已经设为TASK_UNINTERRUPTIBLE后调用schedule(),调度器不会把它当作普通可运行任务继续留在runqueue中,而会选择其他可运行任务。
1 | |
因此调用栈中的schedule()只是“任务实际让出CPU”的底层机制,不等于根因。
3.9 finish_wait()为什么存在
任务退出等待循环时必须完成清理。v6.1的finish_wait()主要做两件事:
1 | |
无论是条件成立、信号打断还是其他返回路径,都必须保证任务状态和等待队列一致,否则可能留下失效entry或错误状态。
3.10 wake_up()到底做什么
wake_up(wq)在v6.1展开为:
1 | |
__wake_up()在队列锁保护下遍历等待者,调用对应唤醒函数。对普通任务,路径最终尝试把符合状态掩码的任务置为可运行并加入相应CPU的runqueue。
1 | |
何时真正执行仍由调度器决定。
3.11 wait_event()整体模型
v6.1的wait_event()最终使用___wait_event(),其主干可以压缩为:
1 | |
宏会在进入等待前先检查一次condition,循环内部也会重新检查。关键心智模型不是宏展开细节,而是:登记、设状态、检查、睡眠、唤醒、复查、清理。
3.12 wait_event_interruptible()和wait_event()区别
wait_event()使用TASK_UNINTERRUPTIBLE,普通信号不能结束等待;wait_event_interruptible()使用TASK_INTERRUPTIBLE,存在待处理信号时可以返回错误,例如-ERESTARTSYS。
选择哪一种不是风格问题,而是接口语义:调用者能否安全处理中途取消?若底层操作不可撤销或中断会破坏状态一致性,可能需要不可中断等待;若用户应能取消,则应考虑可中断或带超时接口。
3.13 Completion整体模型
Completion表达“一次异步工作已经完成”。v6.1的数据结构核心是:
1 | |
注意它使用的是simple wait queue(swait),不是普通wait_queue_head,但诊断模型相同:done是完成令牌/状态,wait组织等待者。
1 | |
3.14 wait_for_completion()在等什么
wait_for_completion()最终进入do_wait_for_common()。如果done==0,当前任务以指定状态加入swait队列并调用schedule_timeout();醒来后继续检查。成功消费一次普通Completion后会执行x->done--。
它等待的不是“CPU重新调度我”,而是Completion对象拥有可消费的完成令牌。分析栈时必须找到具体的struct completion实例。
3.15 complete()与complete_all()做什么
v6.1核心行为是:
1 | |
complete_all()让对象保持“已完成”状态,后续等待者也会通过;若想重用,必须按接口语义正确调用reinit_completion(),并确保不存在仍依赖旧事件的竞争。
3.16 为什么Completion特别适合Driver
驱动经常具有天然的异步边界:
1 | |
它把“提交者”和“完成者”解耦。因此hung_task栈中出现wait_for_completion()时,下一步通常不是研究调度器,而是寻找对应对象的complete()调用路径、IRQ状态和所有错误分支。
3.17 Mutex等待模型
mutex无竞争时走fast path;锁已被占用时进入__mutex_lock_common()等slow path。等待者以mutex_waiter排入锁的等待链表,并在循环中设置相应任务状态、调用schedule_preempt_disabled()睡眠,直到能获得锁或可中断条件成立。
1 | |
默认mutex_lock()使用TASK_UNINTERRUPTIBLE,所以Owner长期不释放锁时,Waiter可能成为hung_task候选。
3.18 Mutex Owner为什么重要
mutex_lock()处的Waiter只是依赖链下游。真正应该继续追的是:
1 | |
锁依赖可能扩展为:
1 | |
最终问题可能是设备不响应,而不是mutex实现错误。
3.19 RWSEM等待模型
rwsem允许多个Reader或一个Writer。竞争时,Reader和Writer进入rwsem_down_read_slowpath()或rwsem_down_write_slowpath(),通过rwsem自己的waiter链表等待。
某个Writer或一批Reader长期不释放信号量,会让许多后续任务堆积。看到大量栈停在相同down_read()/down_write()路径时,应优先寻找共同的struct rw_semaphore和持有路径,而不是逐个分析Waiter。
3.20 IO Wait是什么
I/O等待不是一种单独的任务状态。它描述任务因为I/O进展而睡眠,例如等待块请求、页回写或网络文件系统RPC。
1 | |
任务仍可能使用等待队列、Completion或特定子系统的等待结构,最终通过调度器睡眠和唤醒。
3.21 io_schedule()是什么
v6.1的io_schedule()大体执行:
1 | |
io_schedule_prepare()设置当前任务的in_iowait语义并通知块层调度相关逻辑;随后仍由schedule()完成切换。它表示“本次睡眠被记为I/O等待”,不表示调度器本身造成了I/O故障。
3.22 Block IO等待链
典型链路:
1 | |
任何一层没有推进都可能让上游任务停在D状态。调用栈只显示Waiter所在层,块层trace、设备超时日志和中断计数才能帮助判断下游进度。
3.23 Filesystem等待链
文件系统可能等待页锁、日志事务、回写、冻结状态或块I/O:
1 | |
因此栈中出现ext4、xfs或VFS函数并不自动证明文件系统实现有Bug;必须识别具体Wait Object,并继续追它依赖的下游完成路径。
3.24 NFS等待链
NFS把存储等待延伸到网络和远端:
1 | |
网络不可达、服务器无响应、重传策略或挂载参数都可能造成长时间等待。这里没有本机mutex Owner,Producer可能是网络接收路径和远端服务端。
3.25 Driver等待链
驱动常见链路:
1 | |
分析重点是对象cmd->done、命令生命周期、IRQ是否到达、错误路径是否保证通知Waiter,以及对象是否被过早重置或释放。
3.26 IRQ与Completion链
这条链可以直接转化为验证点:
1 | |
某一层断裂,就解释了为什么Waiter无法变为Runnable。
3.27 Lost Wakeup是什么
经典竞态是:
1 | |
内核等待API通过“先登记和设置状态,再检查条件”、锁及内存屏障组合缩小并关闭这个竞态窗口。手写睡眠协议时若破坏顺序,就可能产生永久等待。
3.28 Missing Complete是什么
驱动中常见的控制流错误:成功路径调用complete(),但超时、复位或错误返回路径遗漏通知。Waiter不知道操作已经失败,只会无限等待。
审查时应逐条检查Producer的所有出口:
1 | |
并确认每条路径都以一致方式结束Waiter生命周期。
3.29 Condition永远不成立是什么
wait_event(wq, ready)被唤醒不代表一定返回。如果Producer调用了wake_up()却没有先正确更新ready,或者更新了错误对象,Waiter醒来后会再次发现false并继续睡眠。
正确分析必须同时搜索:
1 | |
3.30 Dependency Cycle是什么
环形等待才是严格意义上的死锁典型:
1 | |
但hung_task也可能来自非环形故障,例如硬件永不完成、远端服务器无响应或单个Owner陷入计算。因此:
1 | |
3.31 不同等待原语如何统一
底层数据结构不同,但诊断关系相同:
1 | |
这里的“Wait Queue”是抽象概念,不意味着所有原语内部都使用普通wait_queue_head。Completion使用swait,mutex和rwsem拥有专门的waiter队列。
3.32 hung_task时如何识别Wait Primitive
从调用栈底部的调度函数向上找第一个有同步语义的函数:
1 | |
不要停在schedule(),也不要机械地把栈中第一个子系统函数当根因。
3.33 怎么从Wait Primitive找到Wait Object
回到调用点查看实参和容器关系:
1 | |
如果只有汇编地址或优化后的栈,需要结合对应版本的源码、调试符号、结构体生命周期和寄存器/内存现场恢复对象,不能仅凭函数名猜测。
3.34 怎么从Wait Object找到Owner或Producer
搜索策略随对象类型变化:
- mutex/rwsem:找获取与释放路径,结合lockdep、Owner栈和业务对象;
- Completion:搜索同一字段的
complete()/complete_all(); - wait_event:搜索condition写入点和对应
wake_up*(); - I/O:沿request完成回调、驱动IRQ、设备状态向下追;
- NFS/RPC:检查请求状态、网络收包和远端响应。
最终要画出带方向的边:谁依赖谁,谁负责改变状态。
3.35 本章源码阅读主线
第一条主线阅读普通条件等待:
1 | |
第二条主线阅读Completion:
1 | |
第三条只建立锁慢路径坐标:mutex_lock()到__mutex_lock_common(),以及rwsem的read/write slowpath。深入锁算法留到真正遇到相应故障时。
3.36 本章实验
3.36.1 只读观察等待点
1 | |
预测:睡眠任务的wchan可给出阻塞附近的符号,但不能直接显示Wait Object和Producer。
3.36.2 跟踪睡眠与唤醒
在测试机使用调度tracepoint:
1 | |
观察“Waiter切出、Producer执行、Waiter被唤醒、Waiter重新切入”的时间顺序。
3.36.3 Completion最小实验
在可恢复测试环境写一个模块:内核线程调用wait_for_completion(),delayed work在5秒后调用complete()。先预测线程状态和trace顺序,再测量。第二轮故意跳过complete()并缩短hung timeout,观察D状态与告警。
该实验可能永久阻塞模块卸载,必须设计取消与清理路径;不要在生产设备执行。
3.36.4 画依赖图
对一条已有调用栈至少标出:Waiter、Wait Primitive、Wait Object候选、Owner/Producer候选,以及每条边要用什么证据验证。
3.37 工程与性能意义
系统卡顿不一定表现为CPU满载。关键线程可能长时间睡眠,CPU利用率看起来反而不高。性能分析需要同时考虑:
1 | |
正确的超时、可取消路径和统一错误完成机制不仅改善可用性,也决定故障能否被恢复和观测。
3.38 常见误区
误区1:Wait Queue就是runqueue
Wait Queue组织等待事件者,runqueue组织可运行任务。
误区2:所有等待原语都使用普通wait queue
Completion使用swait;mutex和rwsem有专门waiter队列。统一的是依赖模型,不是具体结构。
误区3:schedule()是Root Cause
它只是让任务离开CPU。根因通常是等待条件为何不成立。
误区4:wake_up()后任务立即执行
唤醒只使任务具备运行资格,实际运行由调度器决定。
误区5:被唤醒就表示condition成立
Waiter必须重新检查条件;条件仍为false时会继续睡眠。
误区6:wait_for_completion()卡住说明Completion实现坏了
更常见的是Producer、IRQ、错误路径或对象生命周期问题。
误区7:mutex_lock()卡住说明mutex有Bug
应先查Owner及Owner的下游依赖。
误区8:io_schedule()说明调度器故障
它表达I/O等待语义,应该继续定位具体I/O对象和完成路径。
误区9:所有hung_task都是Lost Wakeup
还可能是Missing Complete、条件永不成立、Owner不释放、依赖环、超慢I/O或硬件不响应。
3.39 博客总结
Linux阻塞等待的共同过程是:任务把自己登记到与事件或资源相关的等待结构,设置可中断或不可中断状态,在条件仍不满足时调用调度接口离开CPU;Owner或Producer更新资源/条件并执行唤醒后,任务成为Runnable,获得CPU后重新检查条件并清理等待项。
wait_event()等待condition,Completion等待完成令牌,mutex/rwsem等待持有者释放资源,I/O等待请求沿驱动、硬件或远端路径完成。实现结构并不完全相同,但hung_task诊断可以统一为:
1 | |
看到调用栈时,不应停在schedule()、wait_for_completion()或mutex_lock()的函数名上,而应继续回答:具体对象是谁,谁负责让它可用,那条路径为什么没有推进。
3.40 学完检查题
- Busy Wait和Sleep Wait各适合什么场景?
- 为什么睡眠接口不能用于所有上下文?
- 普通Wait Queue由哪些核心对象组成?
- Wait Queue和runqueue的职责分别是什么?
- Waiter、Wait Primitive和Wait Object分别是什么?
- Owner与Producer为什么不能完全等同?
- 一个任务睡眠的通用七步流程是什么?
prepare_to_wait_event()做了什么,没做什么?schedule()在等待路径中的角色是什么?finish_wait()为什么必须恢复状态并清理entry?wake_up()为何不等于任务立即执行?wait_event()为何需要循环检查condition?wait_event_interruptible()与wait_event()的核心差异是什么?- v6.1的Completion为什么不能简单说成普通wait queue?
completion.done表达什么?- 普通
complete()与complete_all()的语义有何不同? - 为什么驱动的命令/IRQ流程适合Completion?
- mutex竞争时应该从Waiter继续追谁?
- 多个rwsem Waiter为何提示共同上游阻塞点?
io_schedule()增加了什么语义?- 块I/O等待链可能跨越哪些层?
- NFS等待中的Producer为什么可能在远端?
- 如何按顺序验证IRQ/Completion链?
- Lost Wakeup的竞态窗口是什么?
- Missing Complete应重点审查哪些控制流出口?
- 为什么调用过
wake_up()仍可能hung? - Dependency Cycle与一般长期等待有什么区别?
- 如何从Call Trace识别第一个有业务意义的Wait Primitive?
- 如何从调用点确认具体Wait Object?
- Completion、mutex、wait_event和I/O各应怎样找Owner/Producer?
- 为什么
wchan只能作为入口证据? - 请根据
wait_for_completion(&cmd->done)画出完整依赖图并列出至少四个验证点。
下一章将进入真实hung_task日志阅读:从任务名、PID、状态和Call Trace中排除调度器公共栈,识别第一个业务等待点,恢复Wait Object,并继续寻找Owner或Producer。