Linux hung_task故障分析:Linux阻塞与等待模型

第3章:Linux阻塞与等待模型——Task为什么会进入D状态

0. 源码基线与本章边界

本文以 Linux v6.1 为源码基线,对应上游标签提交830b3c68c1fb1e9176028d02ef86f3cf76aa2476。主要阅读入口是:

本章不逐行展开每一种锁算法,而是建立hung_task诊断所需的统一依赖模型:

1
Waiter -> Wait Primitive -> Wait Object -> Owner / Producer -> Wakeup

3.1 为什么Linux需要阻塞等待

任务经常需要等待暂时不存在的结果,例如磁盘数据、DMA完成、锁释放或网络响应。如果任务一直循环检查:

1
2
while (!condition)
;

它会持续占用CPU。若事件可能需要毫秒甚至秒,CPU时间和功耗都会被浪费。阻塞等待让任务在条件不满足时离开CPU,把处理器交给其他可运行任务;事件发生后,再让它重新参与调度。

3.2 Busy Wait和Sleep Wait

1
2
3
4
5
6
Busy Wait                         Sleep Wait
--------- ----------
反复检查条件 登记等待后让出CPU
等待期间仍占CPU 等待期间不占CPU
适合极短、不可睡眠的临界区 适合可能较长且允许调度的等待
典型:spin_lock 典型:mutex、Completion、I/O

睡眠等待不是无条件更好:它有入队、调度和唤醒成本,而且不能用于中断上下文或持有禁止睡眠的自旋锁时。

3.3 一个Task睡眠的通用模型

通用流程不是单纯调用schedule():

1
2
3
4
5
6
7
检查condition
-> 把当前任务登记为Waiter
-> 设置睡眠状态
-> 再次确认是否需要等待
-> schedule()
-> 被唤醒后重新检查condition
-> 清理等待项并恢复TASK_RUNNING

抽象伪代码:

1
2
3
4
5
6
7
for (;;) {
prepare_to_wait(..., state);
if (condition)
break;
schedule();
}
finish_wait(...);

“登记等待、检查条件、进入睡眠”的顺序和内存可见性非常关键,否则可能丢失唤醒。

3.4 Wait Queue是什么

普通等待队列由wait_queue_head和若干wait_queue_entry组成:

1
2
3
4
5
6
wait_queue_head
├─ lock:保护等待者链表
└─ head
├─ wait_queue_entry(Task A)
├─ wait_queue_entry(Task B)
└─ wait_queue_entry(Task C)

它记录“哪些执行实体正在等待这个事件”。每个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
2
while (!condition)
sleep;

条件可以是request->done、缓冲区非空、页锁解除、设备状态变化等。等待队列只解决“睡在哪里、如何被通知”;condition才决定任务是否可以真正返回。

因此即使wake_up()执行过,只要任务醒来后重新检查发现condition仍为false,它仍会再次睡眠。

3.7 prepare_to_wait()做什么

普通等待队列常见入口包括prepare_to_wait()和prepare_to_wait_event()。以v6.1的后者为例,其核心职责是:

1
2
3
4
5
获取wait queue自旋锁
-> 若需要,把entry加入等待队列
-> 处理可中断等待的信号竞争
-> set_current_state(state)
-> 释放锁

它只是做好睡眠准备,并不自行切换CPU;真正离开CPU需要后续调用schedule()或相关调度接口。

3.8 schedule()在等待流程中的位置

当当前任务状态已经设为TASK_UNINTERRUPTIBLE后调用schedule(),调度器不会把它当作普通可运行任务继续留在runqueue中,而会选择其他可运行任务。

1
2
3
Wait Queue:当前任务登记在某个事件的等待者集合中
Task State:说明当前任务不能运行
schedule():完成从当前任务切到下一个任务

因此调用栈中的schedule()只是“任务实际让出CPU”的底层机制,不等于根因。

3.9 finish_wait()为什么存在

任务退出等待循环时必须完成清理。v6.1的finish_wait()主要做两件事:

1
2
__set_current_state(TASK_RUNNING)
-> 如果entry仍在链表中,在队列锁保护下删除entry

无论是条件成立、信号打断还是其他返回路径,都必须保证任务状态和等待队列一致,否则可能留下失效entry或错误状态。

3.10 wake_up()到底做什么

wake_up(wq)在v6.1展开为:

1
__wake_up(wq, TASK_NORMAL, 1, NULL);

__wake_up()在队列锁保护下遍历等待者,调用对应唤醒函数。对普通任务,路径最终尝试把符合状态掩码的任务置为可运行并加入相应CPU的runqueue。

1
2
wake_up() -> Runnable
Runnable != 立即占用CPU

何时真正执行仍由调度器决定。

3.11 wait_event()整体模型

v6.1的wait_event()最终使用___wait_event(),其主干可以压缩为:

1
2
3
4
5
6
7
8
9
init_wait_entry(&entry, ...);
for (;;) {
long intr = prepare_to_wait_event(&wq, &entry,
TASK_UNINTERRUPTIBLE);
if (condition)
break;
schedule();
}
finish_wait(&wq, &entry);

宏会在进入等待前先检查一次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
2
3
4
struct completion {
unsigned int done;
struct swait_queue_head wait;
};

注意它使用的是simple wait queue(swait),不是普通wait_queue_head,但诊断模型相同:done是完成令牌/状态,wait组织等待者。

1
2
Consumer Task -> completion.done
Producer -> complete()/complete_all()

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
2
3
4
5
6
7
complete(x)
-> done加1(饱和保护)
-> swake_up_locked()唤醒一个等待者

complete_all(x)
-> done设为UINT_MAX
-> swake_up_all_locked()唤醒全部等待者

complete_all()让对象保持“已完成”状态,后续等待者也会通过;若想重用,必须按接口语义正确调用reinit_completion(),并确保不存在仍依赖旧事件的竞争。

3.16 为什么Completion特别适合Driver

驱动经常具有天然的异步边界:

1
2
3
进程上下文:写寄存器/提交DMA -> wait_for_completion()
设备侧:执行操作
中断上下文:确认状态 -> complete()

它把“提交者”和“完成者”解耦。因此hung_task栈中出现wait_for_completion()时,下一步通常不是研究调度器,而是寻找对应对象的complete()调用路径、IRQ状态和所有错误分支。

3.17 Mutex等待模型

mutex无竞争时走fast path;锁已被占用时进入__mutex_lock_common()等slow path。等待者以mutex_waiter排入锁的等待链表,并在循环中设置相应任务状态、调用schedule_preempt_disabled()睡眠,直到能获得锁或可中断条件成立。

1
Task A -> mutex M -> owner Task B

默认mutex_lock()使用TASK_UNINTERRUPTIBLE,所以Owner长期不释放锁时,Waiter可能成为hung_task候选。

3.18 Mutex Owner为什么重要

mutex_lock()处的Waiter只是依赖链下游。真正应该继续追的是:

1
2
3
mutex M当前由谁持有?
Owner为何没有执行到mutex_unlock(M)?
Owner是否又在等待另一个对象?

锁依赖可能扩展为:

1
Task A -> Mutex M -> Task B -> Completion C -> IRQ -> Hardware

最终问题可能是设备不响应,而不是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
Task -> I/O相关Wait Object -> Block/Network/Driver -> Device或远端

任务仍可能使用等待队列、Completion或特定子系统的等待结构,最终通过调度器睡眠和唤醒。

3.21 io_schedule()是什么

v6.1的io_schedule()大体执行:

1
2
3
int token = io_schedule_prepare();
schedule();
io_schedule_finish(token);

io_schedule_prepare()设置当前任务的in_iowait语义并通知块层调度相关逻辑;随后仍由schedule()完成切换。它表示“本次睡眠被记为I/O等待”,不表示调度器本身造成了I/O故障。

3.22 Block IO等待链

典型链路:

1
2
3
4
5
6
7
8
Application Task
-> Filesystem
-> bio / request
-> Block Layer
-> Host Controller Driver
-> Storage Device
-> IRQ / completion path
-> 完成request并唤醒Task

任何一层没有推进都可能让上游任务停在D状态。调用栈只显示Waiter所在层,块层trace、设备超时日志和中断计数才能帮助判断下游进度。

3.23 Filesystem等待链

文件系统可能等待页锁、日志事务、回写、冻结状态或块I/O:

1
Task -> filesystem object -> page/writeback/journal -> block I/O

因此栈中出现ext4、xfs或VFS函数并不自动证明文件系统实现有Bug;必须识别具体Wait Object,并继续追它依赖的下游完成路径。

3.24 NFS等待链

NFS把存储等待延伸到网络和远端:

1
Task -> NFS -> RPC request -> socket/network -> remote server

网络不可达、服务器无响应、重传策略或挂载参数都可能造成长时间等待。这里没有本机mutex Owner,Producer可能是网络接收路径和远端服务端。

3.25 Driver等待链

驱动常见链路:

1
2
3
4
5
6
7
Task
-> ioctl/read/write
-> driver_submit_command()
-> wait_for_completion(&cmd->done)
-> Device/Firmware
-> IRQ handler或worker
-> complete(&cmd->done)

分析重点是对象cmd->done、命令生命周期、IRQ是否到达、错误路径是否保证通知Waiter,以及对象是否被过早重置或释放。

3.26 IRQ与Completion链

这条链可以直接转化为验证点:

1
2
3
4
5
硬件状态是否显示操作完成?
-> /proc/interrupts计数是否增长?
-> ISR是否进入?
-> ISR是否走到complete()分支?
-> Completion对象是否与Waiter等待的是同一个实例?

某一层断裂,就解释了为什么Waiter无法变为Runnable。

3.27 Lost Wakeup是什么

经典竞态是:

1
2
3
4
5
Waiter检查condition=false
Producer设置condition并wake_up()
Waiter尚未登记,因此无人被唤醒
Waiter随后登记并睡眠
再也没有第二次事件

内核等待API通过“先登记和设置状态,再检查条件”、锁及内存屏障组合缩小并关闭这个竞态窗口。手写睡眠协议时若破坏顺序,就可能产生永久等待。

3.28 Missing Complete是什么

驱动中常见的控制流错误:成功路径调用complete(),但超时、复位或错误返回路径遗漏通知。Waiter不知道操作已经失败,只会无限等待。

审查时应逐条检查Producer的所有出口:

1
success / hardware error / timeout / cancel / remove / reset

并确认每条路径都以一致方式结束Waiter生命周期。

3.29 Condition永远不成立是什么

wait_event(wq, ready)被唤醒不代表一定返回。如果Producer调用了wake_up()却没有先正确更新ready,或者更新了错误对象,Waiter醒来后会再次发现false并继续睡眠。

正确分析必须同时搜索:

1
2
3
谁修改condition?
修改与wake_up()的顺序是什么?
双方是否使用同一个对象和正确的同步规则?

3.30 Dependency Cycle是什么

环形等待才是严格意义上的死锁典型:

1
2
Task A -> Mutex M1 -> Task B
Task B -> Mutex M2 -> Task A

但hung_task也可能来自非环形故障,例如硬件永不完成、远端服务器无响应或单个Owner陷入计算。因此:

1
2
Deadlock可能触发hung_task
hung_task不等于已经证明Deadlock

3.31 不同等待原语如何统一

底层数据结构不同,但诊断关系相同:

1
2
3
4
5
wait_event: Waiter -> condition/wait queue -> condition Producer
Completion: Waiter -> completion -> complete() Producer
mutex: Waiter -> mutex -> Owner
rwsem: Waiter -> rwsem -> Reader/Writer持有者
I/O: Waiter -> request/page/RPC -> driver/device/remote

这里的“Wait Queue”是抽象概念,不意味着所有原语内部都使用普通wait_queue_head。Completion使用swait,mutex和rwsem拥有专门的waiter队列。

3.32 hung_task时如何识别Wait Primitive

从调用栈底部的调度函数向上找第一个有同步语义的函数:

1
2
3
4
5
6
schedule()/io_schedule()        <- 底层睡眠机制
schedule_timeout() <- 可能有超时
wait_for_completion*() <- Completion等待
wait_event*()或具体等待函数 <- 条件等待
mutex_lock*/down_read/down_write <- 锁等待
业务/驱动函数 <- 谁发起等待

不要停在schedule(),也不要机械地把栈中第一个子系统函数当根因。

3.33 怎么从Wait Primitive找到Wait Object

回到调用点查看实参和容器关系:

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

如果只有汇编地址或优化后的栈,需要结合对应版本的源码、调试符号、结构体生命周期和寄存器/内存现场恢复对象,不能仅凭函数名猜测。

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
2
3
4
5
6
7
8
wait_event()
-> ___wait_event()
-> prepare_to_wait_event()
-> schedule()
-> Producer更新condition
-> wake_up()/__wake_up()
-> try_to_wake_up()
-> finish_wait()

第二条主线阅读Completion:

1
2
3
4
5
6
7
8
wait_for_completion()
-> wait_for_common()
-> do_wait_for_common()
-> swait + schedule_timeout()

complete()
-> done++
-> swake_up_locked()

第三条只建立锁慢路径坐标:mutex_lock()到__mutex_lock_common(),以及rwsem的read/write slowpath。深入锁算法留到真正遇到相应故障时。

3.36 本章实验

3.36.1 只读观察等待点

1
2
ps -eo pid,tid,stat,wchan:32,comm
cat /proc/<pid>/stack

预测:睡眠任务的wchan可给出阻塞附近的符号,但不能直接显示Wait Object和Producer。

3.36.2 跟踪睡眠与唤醒

在测试机使用调度tracepoint:

1
2
3
4
5
sudo trace-cmd record \
-e sched:sched_switch \
-e sched:sched_waking \
-e sched:sched_wakeup \
sleep 5

观察“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
2
On-CPU时间:任务运行花了多久
Off-CPU时间:任务因为什么等待了多久

正确的超时、可取消路径和统一错误完成机制不仅改善可用性,也决定故障能否被恢复和观测。

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
2
3
4
5
Waiter
-> Wait Primitive
-> Wait Object
-> Owner / Producer
-> 状态改变 + Wakeup

看到调用栈时,不应停在schedule()、wait_for_completion()或mutex_lock()的函数名上,而应继续回答:具体对象是谁,谁负责让它可用,那条路径为什么没有推进。

3.40 学完检查题

  1. Busy Wait和Sleep Wait各适合什么场景?
  2. 为什么睡眠接口不能用于所有上下文?
  3. 普通Wait Queue由哪些核心对象组成?
  4. Wait Queue和runqueue的职责分别是什么?
  5. Waiter、Wait Primitive和Wait Object分别是什么?
  6. Owner与Producer为什么不能完全等同?
  7. 一个任务睡眠的通用七步流程是什么?
  8. prepare_to_wait_event()做了什么,没做什么?
  9. schedule()在等待路径中的角色是什么?
  10. finish_wait()为什么必须恢复状态并清理entry?
  11. wake_up()为何不等于任务立即执行?
  12. wait_event()为何需要循环检查condition?
  13. wait_event_interruptible()与wait_event()的核心差异是什么?
  14. v6.1的Completion为什么不能简单说成普通wait queue?
  15. completion.done表达什么?
  16. 普通complete()与complete_all()的语义有何不同?
  17. 为什么驱动的命令/IRQ流程适合Completion?
  18. mutex竞争时应该从Waiter继续追谁?
  19. 多个rwsem Waiter为何提示共同上游阻塞点?
  20. io_schedule()增加了什么语义?
  21. 块I/O等待链可能跨越哪些层?
  22. NFS等待中的Producer为什么可能在远端?
  23. 如何按顺序验证IRQ/Completion链?
  24. Lost Wakeup的竞态窗口是什么?
  25. Missing Complete应重点审查哪些控制流出口?
  26. 为什么调用过wake_up()仍可能hung?
  27. Dependency Cycle与一般长期等待有什么区别?
  28. 如何从Call Trace识别第一个有业务意义的Wait Primitive?
  29. 如何从调用点确认具体Wait Object?
  30. Completion、mutex、wait_event和I/O各应怎样找Owner/Producer?
  31. 为什么wchan只能作为入口证据?
  32. 请根据wait_for_completion(&cmd->done)画出完整依赖图并列出至少四个验证点。

下一章将进入真实hung_task日志阅读:从任务名、PID、状态和Call Trace中排除调度器公共栈,识别第一个业务等待点,恢复Wait Object,并继续寻找Owner或Producer。


Linux hung_task故障分析:Linux阻塞与等待模型
https://cj0510.github.io/2026/10/09/Linux Kernel/Debug/Linux-hung-task故障分析-第3章-Linux阻塞与等待模型/
作者
CJ1018
发布于
2026年10月9日
许可协议
CC BY-NC-SA 4.0