登录服务器准备例行巡检,发现load average已经飙到25。top命令按下去,CPU的us只有3%,wa占到了90%以上,下面密密麻麻的进程全是D状态。我扫了一眼iowait,果然红得发紫。这种"机器没死、但所有IO都在原地打转"的场面,我遇到的次数不算少。D状态就是不可中断睡眠,进程在内核态等着某个IO完成,等不到就永远睡在那一行,kill -9都拿它没办法。最要命的还不是等,而是你不知道它在等什么、等的是哪个文件。所以每次碰到这种场景,我的标准动作都一样:把长时间D状态进程的内核堆栈打印出来,再把它们手上握着的文件绝对路径全部挖出来,真相基本就藏在这两样东西里。这篇文章就是把这条排查链路完整拆开,从定位进程、抓堆栈、挖路径到自动化脚本,一步不落。方法不依赖玄学,全是/proc文件系统、内核栈跟踪和文件描述符解析的硬功夫,你在生产环境里一样能复现。
1. D状态和iowait:先搞懂你面对的是什么问题
1.1 D状态到底是什么,为什么比R状态更难缠
Linux进程状态里有几个常见字母:R是运行态,S是可中断睡眠,D就是不可中断睡眠,Z是僵尸,T是停止。D状态的全称叫TASK_UNINTERRUPTIBLE,意思是这个进程已经进入了内核态,正在等待一个不能被打断的内核操作完成。典型场景是等待磁盘I/O、等待NFS网络回包、等待锁、等待内存回收等。
为什么说D状态比R状态难缠?进程在R状态,说明CPU不够用,你通过top能清楚看到谁在消耗CPU,可以调整进程优先级,可以随时kill掉让它重来。但D状态不一样——进程已经在内核里睡过去,信号机制对它完全不生效,kill -9发过去也没反应,只能干等内核把那个I/O完成。更要命的是,进入D状态的进程会被计入load average。Linux的load值统计的就是R状态和D状态进程的总数,所以当你看到load很高、CPU却是闲的时候,不用怀疑,大概率是有一堆进程卡在D状态。
不过要强调一句:D状态短暂出现是正常的。任何一次磁盘读写都可能让进程在D状态待上几十毫秒甚至几秒,比如内存页换出、文件系统journal提交。真正需要警惕的是"长时间"D状态。我个人的经验阈值是超过30秒,如果整个系统里同时出现几十个长期D状态的进程,那基本不用怀疑,存储链路肯定出了大问题。
1.2 iowait升高不等于存储一定挂掉
iowait的含义是CPU处于空闲状态、同时在等待I/O完成的时间比例。很多人一看到wa高就觉得磁盘坏了,这是个误解。iowait高只能说明CPU在等I/O,并不能直接判断I/O能力有问题。比如系统刚开机,需要从磁盘读大量文件到page cache,这时候wa高是正常现象;再比如你人为执行dd从一个盘往另一个盘拷贝大文件,CPU可能也会大量时间在等I/O完成。
所以遇到wa高,先别急着下结论。正确的做法是看iowait的变化趋势、看具体哪些进程在D状态、再看I/O设备的利用率。这里可以参考vmstat的输出:如果r列(运行队列)也很高,说明既有CPU压力又有I/O压力;如果r列很低、b列(阻塞进程)很高,那就是I/O压力的典型表现。另外最好用iostat -x 1看各个设备的%util和svctm,把"进程D在等哪个设备"和"这个设备的服务能力"对应起来。等半天只能靠猜,有了设备层面的数据才能往下走。
我在实际排查中还发现过一个细节:iowait是全局CPU维度的统计,并不是按进程来算的。所以哪怕只有一个进程在疯狂读盘,它可能只让其中一颗CPU的wa变高,在top里看起来却是整体wa不算离谱。这种情况下,只看wa容易漏掉问题,必须结合进程状态和I/O统计来看。
1.3 判断"必须抓堆栈"的几个场景
不是所有D状态都需要大动干戈抓堆栈。判断标准很简单:D状态持续时间长、数量多、且影响了线上服务,那就必须动手。我总结过几个反复出现的典型场景:
- 存储多路径或磁盘阵列故障:进程大面积进入D状态,iostat显示某个设备util接近100%,但读写量不大,可能是链路问题而不是真实负载。
- 文件系统卡顿:ext4、xfs等文件系统的journal提交线程卡死,用户进程读写文件时全部堵在日志提交环节,堆栈里能看到jbd2相关函数。
- 网络文件系统故障:NFS、CIFS这类网络文件系统最坑,服务端宕机或网络抖动会让客户端进程长时间D在RPC等待上,堆栈里通常有nfs和rpc关键函数。
- 内存回收和page cache回写问题:大量脏页需要回写,但存储设备响应极慢,导致进程卡在writeback等待路径上。
上面这些场景,光看进程状态分不清谁是谁,只有把它们的内核堆栈和访问的文件路径挖出来,才能快速区分是哪个子系统出了问题。这就引出下文的具体操作。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 锁定"霸榜"的D状态进程
2.1 一条命令快速筛出所有D状态进程
第一步永远是先找出来哪些进程处于D状态。最简单的命令是ps配合awk:
bash复制ps -eo state,pid,ppid,wchan:32,comm | awk '$1=="D"'
这个命令会列出状态为D的进程,显示PID、父进程、内核等待点和进程名。不过ps的wchan列默认截断得比较短,32列以内的函数名基本够看,再长就得去/proc里看完整版。
如果想把信息打得更全,可以用下面这个更直接的方式,遍历/proc目录:
bash复制for pid in $(ls /proc | grep -E '^[0-9]+$'); do
if [ -r /proc/$pid/stat ]; then
state=$(awk '{print $3}' /proc/$pid/stat)
if [ "$state" == "D" ]; then
