Linux hung_task故障分析:TASK_UNINTERRUPTIBLE与D状态

第1章:hung_task到底是什么——TASK_UNINTERRUPTIBLE与D状态

看到下面的日志,很多人第一反应是“这个线程在CPU上卡死了”:

1
INFO: task xxx:1234 blocked for more than 120 seconds.

但hung task通常不是CPU忙循环,而是一个Task长时间睡在不可中断等待路径中。真正要追踪的是:它在等什么、谁应该让条件成立、那个Owner或Producer为什么没有继续推进。

1
Waiter → Wait Object → Owner / Producer → Dependency → Root Cause

0. 源码基线与本章边界

本文严格使用Linux v6.1正式版本:

1
2
3
repository: https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git
tag: v6.1
commit: 830b3c68c1fb1e9176028d02ef86f3cf76aa2476

主要源码:

1
2
3
4
5
6
7
8
include/linux/sched.h
include/linux/wait.h
include/linux/completion.h
kernel/sched/core.c
kernel/sched/completion.c
kernel/locking/mutex.c
fs/proc/array.c
kernel/hung_task.c

本章只建立D状态和依赖模型。khungtaskd如何扫描、怎样用nvcsw判断进展,将在下一章展开。

1.1 hung_task到底是什么

hung task是Linux发现“Task长时间处于特定阻塞状态并且没有可观察调度进展”的诊断机制。它打印Task、阻塞时长和栈,提醒系统可能存在长期I/O、锁等待、固件无响应或死锁。

它不是一种新Task State,也不直接证明死锁:

1
2
hung_task warning = 长时间无进展的症状
deadlock = 可能原因之一

1.2 为什么第一章先学Task State

日志常伴随state:D。如果误把D理解为Dead,就会把等待问题错误归因于崩溃;如果误认为D状态仍在运行,又会错误追查CPU占用率。

正确入口是:

1
D → TASK_UNINTERRUPTIBLE → 睡眠等待 → 寻找等待对象和唤醒者

1.3 Linux Task有哪些常见状态

日常工具常显示:

字母 常见含义
R Running或Runnable
S Interruptible Sleep
D Uninterruptible Sleep
T Stopped或Traced
Z Zombie

v6.1在include/linux/sched.h中把TASK_RUNNING定义为0,把TASK_INTERRUPTIBLE和TASK_UNINTERRUPTIBLE定义为不同状态位。exit_state与可运行状态字段也应区分。

1.4 TASK_RUNNING是什么

TASK_RUNNING同时覆盖:

1
2
正在某个CPU上执行
已经可运行、位于或即将位于runqueue等待CPU

所以R不等于“此刻一定占有CPU”。同理,唤醒把Task恢复成Runnable,也不保证立即获得CPU。

1.5 TASK_INTERRUPTIBLE是什么

Task等待事件,但某些Signal可以让等待提前返回:

1
2
3
条件未满足 → TASK_INTERRUPTIBLE → schedule()
↙ ↘
事件唤醒 Signal唤醒

调用者必须处理被Signal中断的返回值和清理路径。

1.6 TASK_UNINTERRUPTIBLE是什么

TASK_UNINTERRUPTIBLE表示Task等待一个不能被普通Signal随意打断的内核条件。用户态工具通常把它报告为D。

1
TASK_UNINTERRUPTIBLE ≈ 用户可见D状态

这是状态映射的实用近似;TASK_KILLABLE、TASK_IDLE等组合状态还带有额外语义,不能全部简单叫D。

1.7 用户看到的S状态和D状态分别代表什么

二者都通常意味着Task已经睡眠并让出CPU。核心差别是允许哪些唤醒原因:

1
2
S:等待事件,Signal也可能打断
D:等待事件,普通Signal不打断

它们不是“轻度卡顿”和“重度卡顿”的等级关系。

1.8 为什么D状态不是“CPU卡死”

典型路径已经调用schedule():

1
2
3
4
Task A设置D状态
→ schedule()
→ 从CPU切走
→ CPU运行Task B

Task A在等待期间通常不消耗CPU。系统可能CPU idle很高,却因为大量关键Task都在D状态而几乎无法提供服务。

1.9 Task为什么要sleep

磁盘、网络、DMA、Firmware、锁和页面状态不会总是立即就绪。忙轮询会浪费CPU:

1
2
while (!ready)
cpu_relax();

可睡眠上下文更适合登记等待者并让出CPU,条件成立后再唤醒。

1.10 为什么不能所有等待都用TASK_INTERRUPTIBLE

若一个底层事务已经把Buffer交给设备,Signal让调用者直接退出并不意味着DMA也停止。中断等待会引入取消、回滚、所有权和迟到完成等复杂状态。

只有API和底层状态机都能正确处理取消时,可中断等待才安全。

1.11 为什么Linux需要TASK_UNINTERRUPTIBLE

不可中断等待为必须完成的状态过渡提供简单约束:

1
提交事务 → 所有权转移 → 等待最终完成 → 回收资源

它不是为了“禁止用户控制进程”,而是避免普通Signal把不能安全取消的内核事务撕开。

1.12 可中断与不可中断等待的本质区别

维度 Interruptible Uninterruptible
等待事件 是 是
普通Signal可打断 可以 不可以
调用者需处理中断返回 是 通常无此分支
用户常见状态 S D
是否占CPU忙等 否 否

是否允许Signal与是否具有超时也是不同维度;不可中断等待仍然可以由带timeout的API限制时间。

1.13 一个Task如何进入睡眠状态

最小模型:

1
2
3
4
5
6
7
for (;;) {
set_current_state(TASK_UNINTERRUPTIBLE);
if (condition)
break;
schedule();
}
__set_current_state(TASK_RUNNING);

真实等待原语还需要等待队列、锁和内存顺序,防止条件检查与唤醒并发时丢失事件。

1.14 set_current_state()与__set_current_state()

v6.1的关键区别:

1
2
3
4
5
#define __set_current_state(state_value) \
WRITE_ONCE(current->__state, state_value)

#define set_current_state(state_value) \
smp_store_mb(current->__state, state_value)

set_current_state()包含内存屏障,使状态写入与随后条件检查正确排序;__set_current_state()不提供这层序列化,适用于调用者已经通过同一把锁等方式保证顺序的场景。

若“登记睡眠状态→检查条件”与Producer的“更新条件→唤醒”排序错误,就可能发生条件已成立但Waiter错过唤醒的竞态。

1.15 schedule()在等待流程里做什么

设置状态只表达“不满足条件时我可以睡”,真正让出CPU的是schedule():

1
2
3
4
current非TASK_RUNNING
→ schedule()
→ 调度器不把它作为普通Runnable任务继续运行
→ 选择next并context_switch

schedule()不是造成卡住的根因,它只是实现睡眠的通用机制。

1.16 Wait Queue是什么

等待队列把“事件”和“等待该事件的Task”关联起来:

1
2
3
4
wait_queue_head
├─ waiter A
├─ waiter B
└─ waiter C

等待宏通常完成:加入队列、设置状态、反复检查条件、调度、被唤醒后清理队列项。直接手写这些步骤容易产生丢失唤醒。

1.17 Waiter、Wait Object、Owner、Producer四个角色

1
2
3
4
Waiter:当前被阻塞的Task
Wait Object:mutex、completion、I/O request、page、设备事件
Owner:持有排他资源的人,例如mutex owner
Producer:使条件成立的人,例如IRQ、worker、hardware、remote server

Completion未必有“Owner”,但一定有应该产生完成事件的Producer。

1.18 一个Task如何从D状态被唤醒

1
2
3
4
5
6
Producer更新condition
→ wake_up/complete/unlock/I/O completion
→ try_to_wake_up一类路径匹配Task状态
→ Task变为TASK_RUNNING
→ 入队或保持可运行
→ 以后被调度

唤醒与实际运行是两个事件,中间可能存在runqueue等待。

1.19 wake_up()大体做什么

wake_up()遍历相应等待队列,调用等待项的唤醒函数。默认唤醒逻辑最终尝试把符合状态掩码的Task恢复为Runnable并入队。

它不会替Waiter修复条件;正确顺序通常是先发布条件变化,再执行唤醒。

1.20 Completion模型怎么理解

1
2
3
4
5
Waiter                         Producer
wait_for_completion(&done) IRQ/worker
→ D状态 → complete(&done)
→ schedule() → done计数变化
→ wake waiter

wait_for_completion()默认是不可中断等待。若IRQ丢失、错误路径漏掉complete()或Firmware不回包,Waiter就可能长期停在D状态。

1.21 mutex等待为什么也可能进入D状态

普通mutex_lock()竞争失败后会睡眠等待,并使用不可中断语义。分析链:

1
Task A → mutex M → owner Task B → B当前依赖

Task A的mutex_lock()栈只说明等待点,根因更可能位于持锁者B或B依赖的下一层。

1.22 I/O等待为什么常见D状态

1
2
3
4
5
read/write/fsync
→ filesystem
→ block layer
→ device/remote storage
→ completion/IRQ

底层操作常不能靠普通Signal安全撤销,因此存储、NFS、Writeback和块设备异常经常表现为D状态。

1.23 Driver等待为什么常见D状态

驱动常采用:

1
2
3
4
5
写命令/Ring
→ 等待completion
→ Device/Firmware执行
→ IRQ或Mailbox response
→ complete

设备无响应、IRQ被mask、DMA未结束、Firmware hang或异常分支漏完成都会断开这条链。

1.24 D状态是不是一定有问题

不是。短暂D状态可能只是正常I/O等待。判断要加入时间和进展:

1
2
D状态 + 很快被唤醒 = 正常现象
D状态 + 长期无进展 = 需要调查

1.25 正常D状态与异常D状态的区别

正常路径具有活跃Producer和有限完成时间;异常路径的条件长期不成立,或唤醒已经发生却因竞态未被Waiter观察。

应检查请求是否推进、Owner是否运行、IRQ计数是否变化、设备是否响应,而不是只统计D状态数量。

1.26 D状态什么时候会变成hung_task

D状态不会“转换成”一种叫hung task的新状态。检测线程周期性观察Task;当符合检测条件且超过配置阈值时打印警告。Task依然处于原来的等待状态。

1.27 hung_task检测的是死锁还是长时间无进展

检测的是Long No Progress。可能原因包括:

1
2
3
4
5
6
锁循环依赖
设备或Firmware无响应
极慢I/O
NFS Server卡顿
内存回收/Writeback停滞
漏唤醒

仅凭warning不能选择其中哪一种。

1.28 为什么hung_task日志只是Symptom

日志打印被观察到的Waiter。比如栈停在wait_for_completion(),只证明它等待的Completion没有及时满足,不证明Completion实现有Bug。

日志的价值是提供第一条依赖边:

1
Task A waits on object X

1.29 为什么Waiter通常不是Root Cause

Waiter按设计睡眠,真正失去进展的往往是Owner/Producer:持锁者没释放、IRQ没来、Worker没执行、远端没回复或Firmware卡住。

调试必须从Waiter栈向外追,而不是在schedule()附近反复阅读。

1.30 本章最重要的Dependency模型

1
2
3
4
5
6
7
8
9
Hung Task(Waiter)
↓ waits for
Wait Object
↓ owned/produced by
Owner / Producer
↓ may wait for
Next Object / Next Producer
↓
Root Cause

锁形成Owner链;Completion/I/O形成Producer链;复杂故障常是两者混合。

1.31 最小源码阅读入口

建议按顺序:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
include/linux/sched.h
TASK_*与set_current_state

include/linux/wait.h
wait_event*宏和wait queue对象

include/linux/completion.h
kernel/sched/completion.c
wait_for_completion/complete

kernel/sched/core.c
schedule与try_to_wake_up坐标

kernel/hung_task.c
下一章再深入

每次只回答:谁设置状态、谁调用schedule、等待对象是谁、谁执行wake。

1.32 本章实验

1.32.1 观察状态与等待点

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

wchan只表示当前睡眠附近的符号,不是根因结论;符号可见性还受内核配置和权限限制。

1.32.2 构造S状态

运行sleep 300后观察S状态,再发送Signal,验证可中断等待能够提前结束。

1.32.3 阅读一对Completion调用

1
2
rg "wait_for_completion" drivers/ -g '*.[ch]'
rg "complete\(" <same-driver-directory>

画出Waiter、Completion对象和Producer,不要只列函数名。

1.32.4 观察真实D状态

不要为了实验故意破坏存储设备。若系统自然出现D状态,可立即保存:

1
2
3
ps -eo pid,tid,stat,wchan:32,comm
cat /proc/<pid>/stack
dmesg -T | tail -200

然后判断是锁、Completion、I/O还是设备等待。

1.33 工程与性能意义

D状态Task不消耗大量CPU,但可能占有锁、Buffer、挂起请求或阻塞上层服务。大量D状态常对应I/O backlog、NFS stall、驱动/Firmware hang或Writeback停滞。

因此:

1
2
CPU idle高 ≠ 系统健康
Load Average高 ≠ CPU一定繁忙

Linux负载统计会考虑某些不可中断任务,I/O卡顿可能造成高Load但低CPU利用率。

1.34 常见误区

误区1:D表示Dead

错误,通常表示不可中断睡眠。

误区2:D状态一直占CPU

错误,Task一般已经通过schedule()让出CPU。

误区3:D状态就是死锁

错误,正常短暂I/O也会出现D状态。

误区4:hung_task就是死锁检测器

错误,它报告长期无进展,不能直接证明依赖环。

误区5:栈顶等待函数就是根因

错误,它往往只是Wait Primitive。

误区6:wait_for_completion()卡住说明Completion有Bug

应先找谁负责complete()以及为什么没执行。

误区7:mutex_lock()卡住说明mutex实现有Bug

应追踪Owner及Owner的下一层依赖。

误区8:wake_up()后Task立即执行

错误,它先变为Runnable,何时执行由调度器决定。

1.35 博客总结

D通常对应TASK_UNINTERRUPTIBLE:Task正在等待不能被普通Signal随意打断的内核条件,并且已经让出CPU。D状态本身可以完全正常,异常的是长期无进展。

进入等待的核心顺序是:

1
登记Waiter/设置状态 → 检查条件 → schedule → Producer更新条件 → wake

set_current_state()的屏障、条件检查和Producer唤醒共同防止丢失唤醒。分析hung task时,Call Trace只给出Waiter所在位置;真正的主线是:

1
Waiter → Wait Object → Owner/Producer → Next Dependency → Root Cause

官方源码入口:

1.36 学完检查题

  1. D状态通常对应哪个内核状态?
  2. TASK_RUNNING为何同时包含正在运行和Runnable?
  3. S与D的核心区别是什么?
  4. D状态为什么通常不占用CPU?
  5. 为什么某些内核事务不能被普通Signal随意打断?
  6. 设置TASK_UNINTERRUPTIBLE后为什么还要调用schedule()?
  7. set_current_state()与__set_current_state()的关键差别是什么?
  8. 条件检查和唤醒排序错误可能造成什么问题?
  9. Wait Queue保存的核心关系是什么?
  10. wake_up()与Task真正开始运行有何区别?
  11. Completion路径中的Waiter与Producer分别是谁?
  12. mutex等待时为什么应继续寻找Owner?
  13. I/O、NFS和Firmware问题为什么常表现为D状态?
  14. D状态什么时候正常,什么时候可疑?
  15. hung task是否是一种新的Task State?
  16. hung task warning为什么不能证明死锁?
  17. wchan为什么只是线索?
  18. 为什么schedule()通常不是Call Trace中的根因?
  19. CPU idle高时系统为什么仍可能严重卡顿?
  20. 请画出Waiter到Root Cause的完整依赖链。

下一章进入kernel/hung_task.c,分析khungtaskd → watchdog → check_hung_uninterruptible_tasks() → check_hung_task()如何判断一个Task“太久没有进展”。


Linux hung_task故障分析:TASK_UNINTERRUPTIBLE与D状态
https://cj0510.github.io/2026/10/09/Linux Kernel/Debug/Linux-hung-task故障分析-第1章-TASK_UNINTERRUPTIBLE与D状态/
作者
CJ1018
发布于
2026年10月9日
许可协议
CC BY-NC-SA 4.0