等待 IO 为什么会进入 D 状态?
低内存场景下大量不可中断睡眠进程的本质原因
在 Linux 线上系统中,很多工程师都遇到过这样的现象:
机器负载突然飙高
top / ps 中大量进程处于 D 状态
进程无法 kill -9
系统并未完全死机,但“几乎不可用”
尤其在 低内存 + IO 压力 的场景下,这种问题更加频繁。
我们从 Linux 内核调度与内存管理的底层语义 出发,系统性回答两个核心问题:
为什么等待 IO 会导致进程进入不可中断睡眠(D 状态)?
为什么低内存会“放大”D 状态进程的数量,甚至造成系统雪崩?
一、什么是 D 状态?不是“卡死”,而是内核的保护机制
在 Linux 中,进程状态并不是一个简单的“运行 / 阻塞”二分模型。
D 状态的官方定义是:
TASK_UNINTERRUPTIBLE —— 不可中断睡眠
其核心特征只有一句话:
进程正在等待某个必须完成的内核事件,期间不能被信号打断
这类事件通常包括:
块设备 IO 完成
页回收(page reclaim)
文件系统元数据操作
swap in / swap out
某些驱动级硬件响应
⚠️ 重点:D 状态不是异常,而是内核刻意设计的一种“不可逃避等待”语义。
二、为什么 IO 等待必须是“不可中断”的?
这是很多人最困惑的地方。
1️⃣ 如果 IO 可被打断,会发生什么?
假设一个进程在执行:
内核流程大致是:
read() └── 文件系统 └── page cache miss └── 提交 block IO └── 进程睡眠,等待 IO 完成
此时,如果允许信号中断这个等待,会带来极其严重的问题:
如果此时进程被强制唤醒甚至退出:
谁来回收这些中间结构?
IO 完成后回调找不到进程怎么办?
页状态如何回滚?
👉 结果只有一个:内核一致性被破坏
2️⃣ 内核的选择:宁可等死,也不能乱来
因此 Linux 内核做了一个非常明确的设计选择:
凡是涉及内核资源一致性的等待,一律使用 TASK_UNINTERRUPTIBLE
也就是说:
这是一种内核级事务保护机制。
三、那为什么低内存会让 D 状态“爆炸式增长”?
这是问题真正危险的地方。
1️⃣ 低内存并不是“没内存”,而是“没干净的内存”
当系统内存吃紧时,内核需要:
回收 page cache
写回脏页(dirty page)
甚至 swap out 匿名页
而这些操作,本质上都依赖 IO。
2️⃣ 关键路径:内存分配 → 回收 → IO → D 状态
当一个进程需要内存(例如 malloc() 或缺页异常):
alloc_pages() └── 内存不足 └── try_to_free_pages() └── shrink_page_list() └── writeback page └── submit IO └── 当前进程进入 D 状态
注意这里一个非常反直觉的事实:
不是“做 IO 的进程”进入 D 状态而是“想要内存的进程”被迫等待 IO
于是你会看到:
统统进入 D 状态。
3️⃣ IO 慢 + 内存紧 = 放大器效应
如果此时再叠加以下任意条件:
磁盘性能差 / IO 饱和
大量脏页尚未写回
swap 在慢盘上
文件系统 journal 阻塞
就会形成经典的系统级死锁表象:
回收需要 IO
IO 被大量请求淹没
新请求无法获得内存
所有进程一起进入 D 状态
⚠️ 这不是死锁,但效果等同于死锁。
四、为什么 D 状态进程 kill 不掉?
这是另一个常见误解。
kill -9 做的事情只有一件:
向进程发送 SIGKILL,上图是32个kill参数含义
而 D 状态的定义本身就意味着:
进程不会响应任何信号,直到等待事件完成
所以:
不是 kill -9 失效
而是 内核明确禁止信号打断该等待
只有当:
进程才有机会处理信号并退出。
五、如何识别“危险的 D 状态风暴”?
1️⃣ 常见症状组合
ps 中大量 D
iowait 不一定高
load average 异常升高
SSH 可连但命令极慢
OOM Killer 迟迟不触发
2️⃣ 常见根因
脏页比例过高(dirty_ratio)
低端内存或 zone 不平衡
swap 在慢盘
块设备队列被打满
文件系统 writeback 堵塞
六、D 状态不是敌人,失控的资源才是
D 状态不是问题本身,它是内核在资源压力下的“最后防线”
真正的问题通常是:
内存与 IO 的耦合设计
回收路径不可控
系统在高压下缺乏背压机制
理解这一点,我们就会明白:
为什么“看起来没做 IO 的进程”会卡死
为什么低内存比高 CPU 更危险
为什么很多线上事故只能重启解决