搬运并适配自国外社区真实案例。原文出处见文末。
TL;DR
当 top 显示 CPU 70% 以上空闲,但 load average 却飙升到 20+ 甚至 80+,问题几乎确定出在 D 状态(不可中断睡眠,TASK_UNINTERRUPTIBLE)进程 。Linux 内核将 load average 计算为「可运行进程数(R 状态)+ 不可中断睡眠进程数(D 状态)」的指数移动平均。这意味着每个卡在磁盘 I/O、NFS 挂载、或内核锁等待上的进程都会直接推高 load average。本文以 ServerFault 上一位运维人员的真实案例(8 核服务器、load average 84、ps 命令自己都 D 住了)为入口,结合 Red Hat KB 中 NFS4.0 冻结案例(vmcore 分析显示 4454 个进程卡在 rpc_wait_bit_killable)、Brendan Gregg 对 1993 年 Linux 内核历史补丁的考古挖掘,以及 SUSE KB 中「Analysising Processes in D State」的系统化方法,带你从 /proc/PID/stack → task_struct.state → include/linux/sched.h 一路深挖到 kernel/sched/core.c 的 __schedule() 函数,理解为什么你的 kill -9 对 D 状态进程完全无效。
现象
2023 年 1 月,ServerFault 用户 Brian 发出求助帖:
yaml
$ top -o S
top - 14:44:51 up 298 days, 4:47, 1 user,
load average: 84.54, 85.11, 85.33
Tasks: 622 total, 1 running, 621 sleeping, 0 stopped, 0 zombie
%Cpu(s): 11.5 us, 2.0 sy, 0.0 ni, 85.6 id, 0.5 wa, 0.0 hi, 0.3 si, 0.0 st
# 大量进程状态为 D
2153 root 20 0 22904 1156 944 D 0.0 0.0 0:00.01 ps
2425 root 20 0 4372 712 532 D 0.0 0.0 0:00.01 pidof
6608 user1 20 0 14504 844 724 D 0.0 0.0 0:00.00 ps
7103 user1 20 0 14504 828 724 D 0.0 0.0 0:00.01 ps
# ... 十几条 ps/pidof 全部 D 状态
- CPU:85.6% idle、iowait 仅 0.5%------CPU 资源充裕
- load average:84+,8 核机器,load/CPU 比为 10.5
- 关键线索 :就连
ps命令本身也在 D 状态!说明执行进程查看这个操作本身就被阻塞了 - 系统版本:Ubuntu 14.04.5、内核 3.13.0-123、8 核 CPU
这完全违背直觉:CPU 几乎全空,为什么系统比过载还卡?load average 凭什么这么高?
排查过程
第一回合:常规三板斧------top / iostat / vmstat
bash
$ iostat
avg-cpu: %user %nice %system %iowait %steal %idle
8.80 0.00 1.90 0.64 0.00 88.66
Device: tps kB_read/s kB_wrtn/s
sda 37.57 164.32 1690.01
sdb 14.96 239.74 212.36
分析:
- iowait 仅 0.64%------磁盘 I/O 看起来在正常范围内
- 没有明显的磁盘瓶颈
- 但这是「平均值」的陷阱:iowait 统计的是 CPU 在等待 I/O 的时间百分比。如果大量进程卡在 D 状态但根本没有消耗 CPU 周期(纯在内核态等待),iowait 不会涨
- 结论:iowait 低 ≠ 没有 I/O 阻塞。这是很多运维人员的第一个误判点
第二回合:定位 D 状态进程的阻塞点------/proc/PID/stack
Brian 从输出中注意到连 ps 命令本身都卡住了。他找了一个 D 状态 ps 进程(PID 2153),查看内核调用栈:
bash
$ cat /proc/2153/stack
[<ffffffff8119d3e4>] call_rwsem_down_read_failed+0x14/0x30
[<ffffffff8119c742>] __access_remote_vm+0x42/0x1d0
[<ffffffff8119c920>] access_process_vm+0x50/0x70
[<ffffffff8126dbc0>] proc_pid_cmdline+0x8a/0x120
[<ffffffff8126de7f>] proc_info_read+0x9f/0xf0
[<ffffffff8117a1e5>] vfs_read+0x95/0x160
[<ffffffff8117abd9>] SyS_read+0x49/0xa0
分析:
ps命令要读取/proc/PID/cmdline来显示进程的命令行- 这个过程调用
proc_pid_cmdline()→access_process_vm()→__access_remote_vm() __access_remote_vm()需要获取目标进程的mm->mmap_lock(读信号量)call_rwsem_down_read_failed:获取读锁失败,阻塞在信号量上- 为什么读锁获取会失败?因为某个写者持有锁------而且那个写者本身可能也卡在 D 状态
这是 D 状态的一个隐蔽特性 :它不只是「等磁盘」或「等 NFS」,也经常是「等内核锁」。Brendan Gregg 在 2017 年的博客中指出:Linux 0.99.14 中只有 13 个代码路径设置 TASK_UNINTERRUPTIBLE,但到了 Linux 4.12,有近 400 个路径。这些新增的路径大量涉及内核锁原语------mutex_lock()、down()(信号量)、rwsem。这些锁等待也被计入 load average。
第三回合:更系统化的 D 状态分析------SUSE KB 方法论
SUSE 的知识库文章「Analysing processes in D state」提供了一套完整的排查框架,比零散地看个别 /proc/PID/stack 更加系统化:
bash
# 步骤 1:按 wchan(阻塞函数)统计所有 D 状态进程
$ ps -eo pid,state,wchan,comm | awk '$2=="D"' | awk '{print $3}' | sort | uniq -c | sort -rn
48 rpc_wait_bit_killable
17 call_rwsem_down_read
12 wait_on_page_writeback
8 jbd2_log_wait_commit
3 xfs_log_force
分析:
- 48 个进程等 NFS RPC → 问题在 NFS 服务端或网络
- 17 个进程等读写信号量 → 可能有进程持有 mmap_lock 不释放
- 12 个等页面写回 → 磁盘写入慢
- 这不是个别进程的问题,而是系统级阻塞模式
bash
# 步骤 2:查看具体进程的内核栈(前 5 个 wchan 类型的代表进程)
$ for state_type in rpc_wait call_rwsem wait_on_page; do
echo "==== $state_type ===="
ps -eo pid,wchan | grep "$state_type" | head -3 | awk '{print $1}' \
| xargs -I{} sh -c 'echo "PID {}:"; cat /proc/{}/stack 2>/dev/null; echo'
done
bash
# 步骤 3:检查 I/O 子系统------iostat 深度分析
$ iostat -x 1 5
关注这些指标而非平均 tps:
- avgqu-sz(平均队列长度):超过 1 表示 I/O 堆积
- await(平均等待时间):超过 20ms 表示磁盘可能成为瓶颈
- %util:接近 100% 表示设备饱和
第四回合:hung_task 时间炸弹------600 秒超时与 dmesg 警告
当 D 状态进程卡住超过 120 秒(可配置,默认 kernel.hung_task_timeout_secs=120),内核的 hung_task 检测器会在 dmesg 中打印警告。ServerFault 上另一位用户在 CentOS 5.5 上遇到更严重的情况------进程卡了超过 600 秒,最终触发了 HP ASR 看门狗重启:
ini
$ dmesg | grep -A20 "blocked for"
INFO: task in.telnetd:12210 blocked for more than 600 seconds.
"echo 0 > /proc/sys/kernel/hung_task_timeout_secs" disables this message.
in.telnetd D ffff81000102e4a0 0 12210 8899 16297 12762 (L-TLB)
Call Trace:
[<ffffffff80063c6f>] __mutex_lock_slowpath+0x60/0x9b
[<ffffffff80063cb9>] .text.lock.mutex+0xf/0x14
[<ffffffff8009dc66>] flush_workqueue+0x3f/0x87
[<ffffffff801a96ee>] release_dev+0x503/0x67b
[<ffffffff800537af>] tty_release+0x11/0x1a
[<ffffffff80012ad9>] __fput+0xd3/0x1bd
[<ffffffff80023c39>] filp_close+0x5c/0x64
INFO: task dbc:9054 blocked for more than 600 seconds.
dbc D ffff810001036b20 0 9054 1 3272 1795 (L-TLB)
Call Trace:
[<ffffffff80063c6f>] __mutex_lock_slowpath+0x60/0x9b
[<ffffffff8009dc66>] flush_workqueue+0x3f/0x87
[<ffffffff801a96ee>] release_dev+0x503/0x67b
[<ffffffff8837dd1d>] :xfs:xfs_free_eofblocks+0x9d/0x1fe
[<ffffffff80012ad9>] __fput+0xd3/0x1bd
两个进程都卡在 __mutex_lock_slowpath 。其中 dbc 进程调用链从 xfs_free_eofblocks(XFS 文件系统释放 EOF 块)一路到 flush_workqueue → __mutex_lock_slowpath。这是典型的文件系统层 mutex 死锁:一个进程在释放 tty 设备时尝试 flush workqueue,但同时 XFS 的 EOF 块清理也在等同一个 mutex。
这个案例的关键教训:不是所有 D 状态都来自磁盘或 NFS。文件系统的内部 mutex 也会导致 D 状态,而且这些 mutex 死锁往往自我强化------持有锁的进程也在 D 状态等别的东西。
第五回合:关联分析------谁产生了这些 D 状态进程
Red Hat KB 2142081 记录了一个更极端的案例。某企业 NFS4.0 客户端出现高 load average 但 CPU 空闲的故障,系统最终完全冻结。通过 crash 分析 vmcore:
yaml
crash> sys | grep LOAD
LOAD AVERAGE: 4462.05, 3818.55, 2068.32
crash> ps -S
RU: 9 # 可运行进程:仅 9 个!
IN: 723 # 可中断睡眠:723 个
UN: 4454 # 不可中断睡眠:4454 个!!!
crash> foreach UN bt | awk '/#1 / {print $3,$5}' | sort | uniq -c | sort -nr
4454 rpc_wait_bit_killable ffffffffa01a7422
4454 个进程全部卡在同一个内核函数 :rpc_wait_bit_killable()。内核栈追溯显示:
bash
PID: 6181 TASK: ffff880437037520 COMMAND: "httpd"
#0 schedule
#1 rpc_wait_bit_killable [sunrpc]
#2 __wait_on_bit
#3 __rpc_wait_for_completion_task [sunrpc]
#4 nfs4_run_open_task [nfs]
#5 nfs4_do_open [nfs]
#6 nfs4_atomic_open [nfs]
#7 nfs_open_revalidate [nfs]
#8 do_lookup
#9 __link_path_walk
#10 path_walk
#11 do_filp_open
#12 do_sys_open
调用链解读:
- httpd 尝试
open()一个文件 - 文件在 NFS 挂载点上 →
nfs4_atomic_open() - NFS 客户端发出 OPEN RPC →
rpc_run_task()→__rpc_wait_for_completion_task() - NFS 服务端不响应 → 进程无限期卡在
rpc_wait_bit_killable - 函数名中虽然有 "killable",但这个等待仍被标记为
TASK_UNINTERRUPTIBLE - 每个来访 HTTP 请求会 fork 新的 httpd → 新建进程 → 同样卡住 → 滚雪球
这就是 load average 雪崩的经典模式:慢 I/O 源 → 每次请求 fork 进程 → 新进程同样卡住 → D 状态进程数暴增 → load average 爆炸。这时 CPU 完全空闲,因为根本没人用------全在等。
根因:内核代码级解析
1. include/linux/sched.h:TASK_UNINTERRUPTIBLE 的定义
c
/* include/linux/sched.h */
#define TASK_RUNNING 0x0000
#define TASK_INTERRUPTIBLE 0x0001
#define TASK_UNINTERRUPTIBLE 0x0002
#define __TASK_STOPPED 0x0004
#define __TASK_TRACED 0x0008
/* 组合状态 */
#define TASK_KILLABLE (TASK_WAKEKILL | TASK_UNINTERRUPTIBLE)
#define TASK_IDLE (TASK_UNINTERRUPTIBLE | TASK_NOLOAD)
TASK_INTERRUPTIBLE(0x0001):进程在睡眠,但可以被信号(signal)唤醒。kill发的 SIGTERM/SIGKILL 可以打断这种睡眠TASK_UNINTERRUPTIBLE(0x0002):进程在睡眠,信号无法唤醒 。kill -9也杀不死------信号被内核「持有」直到进程出 D 状态才递送TASK_KILLABLE:2.6.25 引入的折中方案------D 状态但可被致命信号(fatal signal)杀死
2. 1993 年历史补丁:为什么把 D 状态计入 load average?
Brendan Gregg 在 2017 年做了一次令人叹服的内核考古------他翻遍了 1993 年的邮件列表压缩包、98000 封邮件、6000+ 邮件摘要,最终在 oldlinux.org 的一个压缩邮箱文件中找到了原始补丁。那是 1993 年 10 月 29 日,Matthias Urlichs 的邮件:
diff
From: Matthias Urlichs <urlichs@...>
Subject: Load average broken ?
Date: Fri, 29 Oct 1993 11:37:23 +0200
The kernel only counts "runnable" processes when computing the load
average. I don't like that; the problem is that processes which are
swapping or waiting on "fast", i.e. noninterruptible, I/O, also
consume resources. It seems somewhat nonintuitive that the load
average goes down when you replace your fast swap disk with a
slow swap disk...
--- kernel/sched.c.orig Fri Oct 29 10:31:11 1993
+++ kernel/sched.c Fri Oct 29 10:32:51 1993
@@ -414,7 +414,9 @@
unsigned long nr = 0;
for(p = &LAST_TASK; p > &FIRST_TASK; --p)
- if (*p && (*p)->state == TASK_RUNNING)
+ if (*p && ((*p)->state == TASK_RUNNING) ||
+ (*p)->state == TASK_UNINTERRUPTIBLE) ||
+ (*p)->state == TASK_SWAPPING))
nr += FIXED_1;
return nr;
}
三行代码,改变了 Linux 运维 30 年 。Matthias 的逻辑是:如果你把快速交换磁盘换成慢速的,系统明显更慢、用户感知更差,但 load average 反而下降(因为进程卡在交换等待而不是 CPU 就绪队列),这「不直觉」。加入 TASK_UNINTERRUPTIBLE 和 TASK_SWAPPING 后,load average 从「CPU 负载」变成了「系统负载」。
24 年后,Matthias 在被 Brendan Gregg 追问时回应:
"The point of 'load average' is to arrive at a number relating how busy the system is from a human point of view. TASK_UNINTERRUPTIBLE means (meant?) that the process is waiting for something like a disk read which contributes to system load. A heavily disk-bound system might be extremely sluggish but only have a TASK_RUNNING average of 0.1, which doesn't help anybody."
3. 为什么 kill -9 对 D 状态进程无效?
由 __schedule() 中的状态检查逻辑决定:
c
/* kernel/sched/core.c */
static void __sched notrace __schedule(unsigned int sched_mode)
{
// ...
if (prev_state & TASK_INTERRUPTIBLE) {
// 检查是否有挂起信号
if (signal_pending_state(prev_state, prev)) {
// 有信号 → 唤醒,不睡眠
prev_state = TASK_RUNNING;
} else {
// 无信号 → 继续睡眠
deactivate_task(rq, prev, DEQUEUE_SLEEP);
}
}
// TASK_UNINTERRUPTIBLE 的分支:无条件睡眠,完全不管信号!
// ...
context_switch(rq, prev, next);
}
关键逻辑:
TASK_INTERRUPTIBLE:signal_pending_state()检查 → 有信号就唤醒TASK_UNINTERRUPTIBLE:不进入信号检查分支 → 直接deactivate_task→ 进程从运行队列移除 → 上下文切换
这就是为什么 D 状态进程成为「僵尸克星」------在内核调度层就决定了信号对它无效。
3. kernel/sched/loadavg.c:load average 如何计入 D 状态
c
/* kernel/sched/loadavg.c - calc_global_load_tick() 调用的核心逻辑 */
/*
* 采样函数 compute the active task count:
* nr_active = nr_running() + nr_uninterruptible()
*
* 关键:nr_uninterruptible() 遍历所有 CPU 的 runqueue,
* 累计 TASK_UNINTERRUPTIBLE 状态的任务数。
*/
long calc_load_fold_active(struct rq *this_rq, long adjust)
{
long nr_active, delta = 0;
nr_active = this_rq->nr_running - adjust;
nr_active += (long)this_rq->nr_uninterruptible;
if (nr_active != this_rq->calc_load_active) {
delta = nr_active - this_rq->calc_load_active;
this_rq->calc_load_active = nr_active;
}
return delta;
}
每个 D 状态进程都被计入 load average。这就是为什么 8 核机器上 84 的 load average 合理------如果有 80+ 个进程在 D 状态,load average 就是 80+。
4. fs/proc/array.c:ps/top 如何显示 D 状态
c
/* fs/proc/array.c */
static const char * const task_state_array[] = {
"R (running)", /* 0x00 */
"S (sleeping)", /* 0x01 */
"D (disk sleep)", /* 0x02 */ /* ← TASK_UNINTERRUPTIBLE */
"T (stopped)", /* 0x04 */
"t (tracing stop)", /* 0x08 */
"X (dead)", /* 0x10 */
"Z (zombie)", /* 0x20 */
"P (parked)", /* 0x40 */
"I (idle)", /* 0x80 */
};
static inline const char *get_task_state(struct task_struct *tsk)
{
unsigned int state = READ_ONCE(tsk->__state);
// ... 掩码处理 ...
return task_state_array[task_state_index(tsk, state)];
}
ps 和 top 通过读取 /proc/PID/status 获取状态,最终调用 get_task_state()。状态索引 2 返回 "D (disk sleep)"------UNIX 传统称法,但在现代内核中 D 状态早已不限于磁盘 I/O。
5. 进程状态转换全图
Linux 内核有 7 种主要进程状态,它们之间的转换由 __schedule() 和 try_to_wake_up() 驱动:
| 状态 | 宏定义 | ps 显示 | 可被信号唤醒 | 计入 load |
|---|---|---|---|---|
| RUNNING | TASK_RUNNING (0x00) | R | N/A | ✅ |
| INTERRUPTIBLE | TASK_INTERRUPTIBLE (0x01) | S | ✅ | ❌ |
| UNINTERRUPTIBLE | TASK_UNINTERRUPTIBLE (0x02) | D | ❌ | ✅ |
| STOPPED | __TASK_STOPPED (0x04) | T | ❌ | ❌ |
| ZOMBIE | EXIT_ZOMBIE (0x20) | Z | ❌ | ❌ |
| KILLABLE | TASK_KILLABLE | D | 仅致命信号 | ✅ |
| IDLE | TASK_IDLE | I | ❌ | ❌ |
从上述表格可以总结出两条核心铁律:
- 在 load average 中:R 状态 + D 状态 = load average 采样值
- 在信号处理上:只有 S 状态(TASK_INTERRUPTIBLE)会被信号打断;D 状态完全免疫
当你发现 load average 很高但 CPU 很闲时,100% 是 D 状态进程在「撑」load average。
解决方案
方案一:识别阻塞源(治本)
bash
# 1. 找出所有 D 状态进程
ps -eo pid,state,wchan,comm | awk '$2=="D"'
# 2. 按阻塞函数分组统计(定位热点)
for pid in $(ps -eo pid,state | awk '/ D /{print $1}'); do
cat /proc/$pid/wchan 2>/dev/null
done | sort | uniq -c | sort -rn
# 输出示例:
# 200 rpc_wait_bit_killable → NFS 问题
# 150 call_rwsem_down_read → 内核锁争用
# 50 wait_on_page_writeback → 磁盘写回慢
方案二:常见 D 状态风暴的针对性处理
| 阻塞函数(wchan) | 可能原因 | 处理方式 |
|---|---|---|
rpc_wait_bit_killable |
NFS 服务端无响应 | 恢复 NFS 服务端;临时 umount -f -l 强制卸载;加 soft 选项避免无限重试 |
call_rwsem_down_read/write |
内核锁争用(常与 /proc 访问有关) |
减少对 /proc 的并发扫描;升级内核(新版本 rwsem 有乐观自旋优化) |
wait_on_page_writeback |
磁盘写入慢 | iostat -x 1 确认;调整 vm.dirty_ratio 和 vm.dirty_background_ratio |
jbd2_log_wait_commit |
ext4 日志提交阻塞 | 检查磁盘延迟;考虑 data=writeback 挂载选项 |
xfs_log_force |
XFS 日志压力大 | 检查磁盘 IOPS;考虑 SSD 或增大日志大小 |
__mutex_lock_slowpath |
文件系统/设备层 mutex | 检查对应文件系统健康状态;看关联进程的完整 Call Trace |
方案三:Brendan Gregg 的 eBPF off-CPU 火焰图(进阶诊断)
当你需要可视化 D 状态进程的内核调用路径时,bcc 的 offcputime 工具可以生成 off-CPU 火焰图:
bash
# 采样 60 秒,只抓 TASK_UNINTERRUPTIBLE(状态值 2)的调用栈
# 需要 Linux 4.8+ 和 bcc 工具集
$ /usr/share/bcc/tools/offcputime -K --state 2 -f 60 > out.stacks
$ awk '{ print $1, $2 / 1000 }' out.stacks \
| flamegraph.pl --color=io --countname=ms > offcpu_unint.svg
这个火焰图的横轴宽度表示进程在各种不可中断等待中花费的时间。Brendan Gregg 在文章中使用这个技术量化了 D 状态对 load average 的贡献------例如他发现在一个生产服务器上,systemd-journal 在 proc_pid_cmdline_read() 中阻塞了 0.07 个 load 单位,而 rwsem 锁等待贡献了 0.23 个单位。
方案四:紧急止损------重启是最后手段
D 状态进程无法被 kill -9 杀死。以下手段按推荐程度排列:
- 恢复 I/O 源(首选)------恢复 NFS 服务端/更换磁盘/降低 IO 负载
- 强制卸载挂载点 ------
umount -f -l /mnt/nfs可以让 NFS 卡住的进程脱离等待 - 重启相关服务------如果阻塞源是某个可控服务,重启它
echo b > /proc/sysrq-trigger------最后的强制重启(sysrq 强制重启)
⚠️ 警告 :
reboot命令可能也会卡住,因为 shutdown 脚本需要遍历所有进程并发送信号------D 状态进程不响应,导致关机流程阻塞。
如何自检你是否中招
bash
#!/bin/bash
# D 状态进程自检脚本
# 用于 Zabbix/Nagios/crontab 持续监控
THRESHOLD=10 # D 状态进程超过此数告警
D_COUNT=$(ps -eo state | grep -c '^D')
LOAD1=$(awk '{print $1}' /proc/loadavg)
CPUS=$(nproc)
LOAD_RATIO=$(echo "scale=2; $LOAD1 / $CPUS" | bc)
echo "D-state processes: $D_COUNT | Load 1min: $LOAD1 | CPUs: $CPUS | Ratio: $LOAD_RATIO"
# CPU 空闲但 D 状态进程多 → 典型 D 状态风暴
CPU_IDLE=$(top -bn1 | grep 'Cpu' | awk '{print $8}' | cut -d'%' -f1)
if [ "$D_COUNT" -gt "$THRESHOLD" ] && [ "$(echo "$CPU_IDLE > 50" | bc)" -eq 1 ]; then
echo "⚠️ 告警:CPU空闲${CPU_IDLE}% 但 $D_COUNT 个进程在 D 状态!"
echo "Top D-state wchan:"
for pid in $(ps -eo pid,state | awk '/ D /{print $1}' | head -20); do
printf " PID %-6s → %s\n" "$pid" "$(cat /proc/$pid/wchan 2>/dev/null)"
done
fi
部署方式:
bash
# 每 5 分钟检查一次
*/5 * * * * /usr/local/bin/check_dstate.sh >> /var/log/dstate.log 2>&1
启示
-
load average 是系统负载,不是 CPU 负载 Linux 的 load average 自 1993 年 patch(Matthias Urlichs, 0.99.14)起就包含了
TASK_UNINTERRUPTIBLE。它的设计初衷是反映「系统的主观繁忙度」,而不只是 CPU 繁忙度。把 load average 除以 CPU 核数来判断系统是否过载是一种误导------必须同时看 CPU idle 和进程状态分布。 -
iowait 低 ≠ 没有 I/O 阻塞 iowait 统计的是 CPU 在 idle 状态下等待 I/O 的时间百分比。如果大量进程卡在 D 状态但根本没在 CPU 上跑,iowait 不会涨。正确的做法是直接数 D 状态进程数量:
ps -eo state | grep -c '^D'。 -
/proc/PID/stack是 D 状态排查的银弹 不需要 eBPF、不需要 crash dump、不需要 root 的额外权限(/proc/PID/stack可读性由ptrace权限控制)。一行命令就能看到进程卡在内核的哪个函数、哪行代码。这是 Brendan Gregg 和 SUSE KB 都提及的第一步排查动作。 -
D 状态进程会自我繁殖 在 NFS 冻结案例中,每个 HTTP 请求都会 fork 新的 httpd → open NFS 文件 → 也卡住 → D 进程越来越多 → load average 指数上升。这种正反馈循环比 CPU 过载更危险,因为它无法通过 OOM killer 自动止损。
-
TASK_KILLABLE 是内核社区给出的「不完全解决方案」 2.6.25 引入的
TASK_KILLABLE允许 D 状态进程响应致命信号,但需要内核文件系统/NFS 代码显式使用。大量旧的代码路径仍使用纯TASK_UNINTERRUPTIBLE。Peter Zijlstra 曾提议用task_struct->in_iowait代替TASK_UNINTERRUPTIBLE来计算 load average,但至今未落地------因为「等待内核锁」的线程是否应该计入系统负载,社区仍有争议。
原始出处:
- ServerFault #1120058: A linux machine with lots of processes in uninterruptible sleep state --- Brian 的实战案例,ps 命令自身也陷入 D 状态,从
/proc/PID/stack追踪到call_rwsem_down_read_failed- Red Hat KB #2142081: NFS4.0: High Load but CPU idle - leading to a complete outage --- vmcore crash 分析显示 4454 个进程卡在
rpc_wait_bit_killable,NFS OPEN 操作阻塞链路完整追溯- Brendan Gregg's Blog: Linux Load Averages: Solving the Mystery --- 挖掘出 1993 年 Matthias Urlichs 的历史补丁,解释了 load average 纳入 TASK_UNINTERRUPTIBLE 的初衷
- ServerFault #1164437: CPU is idle, but huge load, and NFS client stop responding --- NFS 邮件服务器高负载事故,load average 从 400 飙到 10000
- SUSE KB: Analysing processes in D state (uninterruptable sleep) --- 系统化的 D 状态进程分析方法论
- ServerFault #227405: Linux kernel crash analysis. Blocked processes, IO and uninterruptible wait --- CentOS 5.5 上 hung task 600s 超时,dmesg 中 TASK_UNINTERRUPTIBLE 的完整 Call Trace
- LWN.net: The origin of Linux load averages --- 内核社区对 nr_uninterruptible 在 load average 中角色的持续讨论
- Linux Kernel 源码:
include/linux/sched.h(TASK_UNINTERRUPTIBLE 定义)、kernel/sched/core.c(__schedule)、kernel/sched/loadavg.c(calc_load_fold_active)、fs/proc/array.c(task_state_array)
本文首发于 CSDN 专栏《运维漏洞指南》,系列第 9 篇。该系列旨在用「可观测症状 → 排查回合 → 内核源码」的三步法,将国外高质量社区案例(ServerFault / LWN / Red Hat KB)搬运并适配到中文运维场景。
