Linux hung_task故障分析:TASK_UNINTERRUPTIBLE与D状态
第1章:hung_task到底是什么——TASK_UNINTERRUPTIBLE与D状态
看到下面的日志,很多人第一反应是“这个线程在CPU上卡死了”:
1 | |
但hung task通常不是CPU忙循环,而是一个Task长时间睡在不可中断等待路径中。真正要追踪的是:它在等什么、谁应该让条件成立、那个Owner或Producer为什么没有继续推进。
1 | |
0. 源码基线与本章边界
本文严格使用Linux v6.1正式版本:
1 | |
主要源码:
1 | |
本章只建立D状态和依赖模型。khungtaskd如何扫描、怎样用nvcsw判断进展,将在下一章展开。
1.1 hung_task到底是什么
hung task是Linux发现“Task长时间处于特定阻塞状态并且没有可观察调度进展”的诊断机制。它打印Task、阻塞时长和栈,提醒系统可能存在长期I/O、锁等待、固件无响应或死锁。
它不是一种新Task State,也不直接证明死锁:
1 | |
1.2 为什么第一章先学Task State
日志常伴随state:D。如果误把D理解为Dead,就会把等待问题错误归因于崩溃;如果误认为D状态仍在运行,又会错误追查CPU占用率。
正确入口是:
1 | |
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 | |
所以R不等于“此刻一定占有CPU”。同理,唤醒把Task恢复成Runnable,也不保证立即获得CPU。
1.5 TASK_INTERRUPTIBLE是什么
Task等待事件,但某些Signal可以让等待提前返回:
1 | |
调用者必须处理被Signal中断的返回值和清理路径。
1.6 TASK_UNINTERRUPTIBLE是什么
TASK_UNINTERRUPTIBLE表示Task等待一个不能被普通Signal随意打断的内核条件。用户态工具通常把它报告为D。
1 | |
这是状态映射的实用近似;TASK_KILLABLE、TASK_IDLE等组合状态还带有额外语义,不能全部简单叫D。
1.7 用户看到的S状态和D状态分别代表什么
二者都通常意味着Task已经睡眠并让出CPU。核心差别是允许哪些唤醒原因:
1 | |
它们不是“轻度卡顿”和“重度卡顿”的等级关系。
1.8 为什么D状态不是“CPU卡死”
典型路径已经调用schedule():
1 | |
Task A在等待期间通常不消耗CPU。系统可能CPU idle很高,却因为大量关键Task都在D状态而几乎无法提供服务。
1.9 Task为什么要sleep
磁盘、网络、DMA、Firmware、锁和页面状态不会总是立即就绪。忙轮询会浪费CPU:
1 | |
可睡眠上下文更适合登记等待者并让出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 | |
真实等待原语还需要等待队列、锁和内存顺序,防止条件检查与唤醒并发时丢失事件。
1.14 set_current_state()与__set_current_state()
v6.1的关键区别:
1 | |
set_current_state()包含内存屏障,使状态写入与随后条件检查正确排序;__set_current_state()不提供这层序列化,适用于调用者已经通过同一把锁等方式保证顺序的场景。
若“登记睡眠状态→检查条件”与Producer的“更新条件→唤醒”排序错误,就可能发生条件已成立但Waiter错过唤醒的竞态。
1.15 schedule()在等待流程里做什么
设置状态只表达“不满足条件时我可以睡”,真正让出CPU的是schedule():
1 | |
schedule()不是造成卡住的根因,它只是实现睡眠的通用机制。
1.16 Wait Queue是什么
等待队列把“事件”和“等待该事件的Task”关联起来:
1 | |
等待宏通常完成:加入队列、设置状态、反复检查条件、调度、被唤醒后清理队列项。直接手写这些步骤容易产生丢失唤醒。
1.17 Waiter、Wait Object、Owner、Producer四个角色
1 | |
Completion未必有“Owner”,但一定有应该产生完成事件的Producer。
1.18 一个Task如何从D状态被唤醒
1 | |
唤醒与实际运行是两个事件,中间可能存在runqueue等待。
1.19 wake_up()大体做什么
wake_up()遍历相应等待队列,调用等待项的唤醒函数。默认唤醒逻辑最终尝试把符合状态掩码的Task恢复为Runnable并入队。
它不会替Waiter修复条件;正确顺序通常是先发布条件变化,再执行唤醒。
1.20 Completion模型怎么理解
1 | |
wait_for_completion()默认是不可中断等待。若IRQ丢失、错误路径漏掉complete()或Firmware不回包,Waiter就可能长期停在D状态。
1.21 mutex等待为什么也可能进入D状态
普通mutex_lock()竞争失败后会睡眠等待,并使用不可中断语义。分析链:
1 | |
Task A的mutex_lock()栈只说明等待点,根因更可能位于持锁者B或B依赖的下一层。
1.22 I/O等待为什么常见D状态
1 | |
底层操作常不能靠普通Signal安全撤销,因此存储、NFS、Writeback和块设备异常经常表现为D状态。
1.23 Driver等待为什么常见D状态
驱动常采用:
1 | |
设备无响应、IRQ被mask、DMA未结束、Firmware hang或异常分支漏完成都会断开这条链。
1.24 D状态是不是一定有问题
不是。短暂D状态可能只是正常I/O等待。判断要加入时间和进展:
1 | |
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 | |
仅凭warning不能选择其中哪一种。
1.28 为什么hung_task日志只是Symptom
日志打印被观察到的Waiter。比如栈停在wait_for_completion(),只证明它等待的Completion没有及时满足,不证明Completion实现有Bug。
日志的价值是提供第一条依赖边:
1 | |
1.29 为什么Waiter通常不是Root Cause
Waiter按设计睡眠,真正失去进展的往往是Owner/Producer:持锁者没释放、IRQ没来、Worker没执行、远端没回复或Firmware卡住。
调试必须从Waiter栈向外追,而不是在schedule()附近反复阅读。
1.30 本章最重要的Dependency模型
1 | |
锁形成Owner链;Completion/I/O形成Producer链;复杂故障常是两者混合。
1.31 最小源码阅读入口
建议按顺序:
1 | |
每次只回答:谁设置状态、谁调用schedule、等待对象是谁、谁执行wake。

1.32 本章实验
1.32.1 观察状态与等待点
1 | |
wchan只表示当前睡眠附近的符号,不是根因结论;符号可见性还受内核配置和权限限制。
1.32.2 构造S状态
运行sleep 300后观察S状态,再发送Signal,验证可中断等待能够提前结束。
1.32.3 阅读一对Completion调用
1 | |
画出Waiter、Completion对象和Producer,不要只列函数名。
1.32.4 观察真实D状态
不要为了实验故意破坏存储设备。若系统自然出现D状态,可立即保存:
1 | |
然后判断是锁、Completion、I/O还是设备等待。
1.33 工程与性能意义
D状态Task不消耗大量CPU,但可能占有锁、Buffer、挂起请求或阻塞上层服务。大量D状态常对应I/O backlog、NFS stall、驱动/Firmware hang或Writeback停滞。
因此:
1 | |
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 | |
set_current_state()的屏障、条件检查和Producer唤醒共同防止丢失唤醒。分析hung task时,Call Trace只给出Waiter所在位置;真正的主线是:
1 | |
官方源码入口:
- Linux v6.1
include/linux/sched.h - Linux v6.1
include/linux/wait.h - Linux v6.1
kernel/sched/completion.c - Linux v6.1
kernel/hung_task.c
1.36 学完检查题
- D状态通常对应哪个内核状态?
TASK_RUNNING为何同时包含正在运行和Runnable?- S与D的核心区别是什么?
- D状态为什么通常不占用CPU?
- 为什么某些内核事务不能被普通Signal随意打断?
- 设置
TASK_UNINTERRUPTIBLE后为什么还要调用schedule()? set_current_state()与__set_current_state()的关键差别是什么?- 条件检查和唤醒排序错误可能造成什么问题?
- Wait Queue保存的核心关系是什么?
wake_up()与Task真正开始运行有何区别?- Completion路径中的Waiter与Producer分别是谁?
- mutex等待时为什么应继续寻找Owner?
- I/O、NFS和Firmware问题为什么常表现为D状态?
- D状态什么时候正常,什么时候可疑?
- hung task是否是一种新的Task State?
- hung task warning为什么不能证明死锁?
wchan为什么只是线索?- 为什么
schedule()通常不是Call Trace中的根因? - CPU idle高时系统为什么仍可能严重卡顿?
- 请画出Waiter到Root Cause的完整依赖链。
下一章进入kernel/hung_task.c,分析khungtaskd → watchdog → check_hung_uninterruptible_tasks() → check_hung_task()如何判断一个Task“太久没有进展”。