进程状态:R、S、D、Z 各自在等什么

进程状态:R、S、D、Z 各自在等什么
隔壁老康难度 ★★★☆☆ · 前置:fork 到底复制了什么 | 一句话结论:调度器只关心「能不能跑」,
R以外的状态都在等。S和D的差别只有一件事:信号能不能把它揪起来,所以D状态下kill -9无效,要先看wchan在等什么。
线上服务卡住的那个下午,我连上去干的第一件事是找 D,因为 D 是唯一一种既不吃 CPU 又不吃内存、却能让整条请求链卡死的状态,命令不长,把所有进程的 pid、状态、等待点和命令名列出来,再用 awk 只留第一个字母是 D 的那几行,wchan:22 是限制列宽,免得等待点太长把整个对齐撑歪,这个写法我是从一篇排查 NFS 的旧帖里抄来的,抄的时候没多想,自己照着敲了一遍才明白每个参数在挡什么,ps -eo 是自选列,stat 就是那一列字母,wchan 是等待点,comm 是命令名,四个字段拼在一起刚好够用。
1 | ps -eo pid,stat,wchan:22,comm | awk '$2 ~ /^D/' |
1 | 1421 D >fdatasync mysqld |
两条 D,一个是数据库,一个是备份脚本。
我当时做的第一件事是 kill -9 1421,敲完再 ps 一次,还在,还是那个 D,那一刻我的第一反应是 kill 坏了,第二反应是这内核有 bug,两个都不是,-9 没失效,它只是根本送不进去,信号确实生成了,也确实挂在了进程身上,可那个进程正躺在一个不看信号的等待里,谁也拽不动它。这一篇把 ps 那一列字母拆开,拆完你会发现它比想象中简单得多,状态只有一个含义,就是这个进程现在能不能被调度器跑,别的一概不管。
1 | PID=$(pgrep -n mysqld) # 1421 是我那台机器上的,你这里要填自己的 |
送不进去。
(上面那行是反面教材,别照着敲,pgrep 找不到就说明你机器上没这个进程名,换成你要观察的那个。我至今不知道它当时为什么偏偏在那个点卡住,日志里干干净净,什么都没留下。)
一个字段,几个宏
进程状态是 task_struct 里的一个字段,内核里叫 __state,取值是几个 TASK_* 宏,ps 负责把一个宏翻译成一个字母,中间没有别的东西,也没有隐藏含义,这个字段在 5.14 之前是 volatile long state,之后改成 unsigned int __state 并挪进了 task_struct 的第一个缓存行,为的是让调度器判断能不能跑的时候少一次访存。
1 | struct task_struct { |
这张表就是 ps 用的翻译表。
ps 字母 |
内核宏 | 一句话 | 信号能叫醒? |
|---|---|---|---|
R |
TASK_RUNNING |
正在 CPU 上跑,或者在运行队列里排着 | — |
S |
TASK_INTERRUPTIBLE |
等事件,可以被中断 | 能 |
D |
TASK_UNINTERRUPTIBLE |
等事件,不许被中断 | 不能 |
T |
__TASK_STOPPED |
被 SIGSTOP/SIGTSTP 停住 |
只有 SIGCONT 能 |
t |
__TASK_TRACED |
被调试器或 strace 停住 |
只有 SIGCONT 能 |
Z |
EXIT_ZOMBIE |
已经退出,等父进程收尸 | 已经死了 |
X |
EXIT_DEAD |
连收尸都完成,一闪而过 | — |
I |
TASK_IDLE |
空闲的内核线程,不参与负载计算 | — |
P |
TASK_PARKED |
被「停放」起来的内核线程,只在 kthread_park() 时出现 |
不能 |
剩下的三行反过来,S 是这一整列里最常见的一个,原因很朴素,绝大多数进程的绝大多数时间都在等,等网络、等磁盘、等锁、等一个定时器,一个真正在烧 CPU 的进程反而是少数派,所以一台健康的机器看起来几乎全是 S,看到满屏 S 不必紧张,看到满屏 R 才该紧张,因为那说明有东西在抢 CPU 又抢不到,load average 会跟着往上拱。
R 那一行有个容易误解的地方,它不区分正在跑和在排队,调度器眼里这两件事是一回事,都可运行,只是还没轮到,所以 ps 里一堆 R 不代表 CPU 很忙,只代表有很多东西想跑,反过来,8 核机器上冒出 20 个 R 才是真的不够用了,load average 那时候一定是顶着的,而且这个数字跟进程数没有直接关系,跟你排队的时长有关系。
STAT 后面还能再挂一两个后缀。
| 后缀 | 意思 |
|---|---|
s |
这是会话首进程(session leader) |
l |
多线程(有多个 task_struct 共享地址空间) |
+ |
在前台进程组里(会收到终端的 Ctrl-C) |
< |
高优先级(nice 值小于 0) |
N |
低优先级(nice 值大于 0) |
L |
有内存页被锁在物理内存里(mlock) |
所以 Ss 是可中断睡眠加会话首进程,Sl+ 是睡眠加多线程加前台,R< 是可运行加高优先级,ps aux 里那一列长得像乱码,因为它真的就是一个位标志组合,不是随便拼的字母,每一位都是一个独立的开关,S 说它在睡,s 说它是一群进程的头,+ 说它在前台,三个字母同时为真,它就写成 Ss+。
调度器只问一个问题
要挑一个进程跑,调度器的判断只有一条,这个进程是不是 TASK_RUNNING,是就进运行队列,不是就躺着等别人把它改成 TASK_RUNNING,所有其他状态的含义归根到底都是不在运行队列里,S 在等数据、D 在等 IO、T 被人按住了、Z 已经散了只是还没埋,九种状态九种说法,落到调度器眼里只有两句话,能跑,或者不能跑,干净得有点意外。
stateDiagram-v2 [*] --> R: fork/clone R --> S: 等事件,可中断 R --> D: 等事件,不可中断 S --> R: 事件到达,或信号 D --> R: 只有事件到达 R --> T: SIGSTOP T --> R: SIGCONT R --> t: 被 ptrace 停下 t --> R: SIGCONT R --> Z: exit Z --> [*]: 父进程 wait
图上最值得盯的是 D --> R 那条边,它只有一条进路,事件到达,没有第二条,S 那边多出来的那条信号箭头,就是几篇之后讲信号时所有麻烦的全部来源。
S 和 D 只差一件事:信号能不能揪它
同样是等事件,内核为什么要分两种,因为等待的时候进程可能正拿着某个锁,或者正处在一个不能被打断的中间态,这时候如果允许信号把它揪起来,它会在一个自己都还没理顺的地方被塞回用户代码,接着就是内存损坏,这种 bug 比死锁难查一百倍,因为它在崩溃的那一刻看起来完全无辜,真正出事的地方在几百毫秒之前,日志上只会留下一句看不懂的段错误,你对着 core 文件翻半天,发现问题出在一个跟崩溃毫无关系的驱动里。
所以内核给了两种等待原语,差别只有一格。
TASK_INTERRUPTIBLE |
TASK_UNINTERRUPTIBLE |
|
|---|---|---|
| 谁能让它醒 | 事件 + 信号 | 只有事件 |
| 醒了之后 | 检查 signal_pending(),有就返回 -ERESTARTSYS |
继续睡 |
| 典型场景 | read 等数据、accept 等连接 |
等块设备 IO 完成、等某个内核锁 |
S 状态的进程收到 SIGTERM,内核把 __state 改回 TASK_RUNNING,进程被唤醒,走到最近的检查点发现 signal_pending() 为真,于是中断当前操作、退回用户态、执行信号处理函数,返回值是 -ERESTARTSYS,libc 看到这个值就把这次系统调用重做一遍或者返回 EINTR,进程于是活了,这一整条路上每一步都在检查信号,每一步都留了退路,而 D 状态收 SIGTERM 就完全是另一回事,内核把 TIF_SIGPENDING 置上,但不动 __state,进程继续睡,schedule() 压根不返回,信号就那么挂着,直到事件终于来了它才醒过来,然后去处理一个早就过期的信号,kill -9 失效的全部机制就在这两句话里,信号没丢,只是没人看。
这个状态为什么非有不可
回到不能被打断这个理由,举个具体的,write 一个文件数据要落盘,内核把页挂进块设备的请求队列,然后等这个请求完成的中断,这中间如果进程被信号叫起来、退回去、又调了 exit,那个挂起的 IO 请求还指着它的内存页,页已经被释放、被别的进程复用,DMA 写到哪去,所以这段时间必须不可中断,宁可让进程卡着,也不能让数据结构坏掉,这个取舍在单机上看着很粗暴,放到一个跑着几千个进程的宿主机上就是唯一讲得通的选择,因为一次 DMA 写错地址污染的是别人的内存,不是自己的。
代价也很直接。
那个 IO 永远不完成,进程就永远醒不来。
磁盘坏了、NFS 服务端没了、FUSE 后端挂了,全长这样,这就是生产上说的 D 状态堆积,除了重启机器基本没别的办法,而且重启之后如果根因还在,它会在同一个地方再卡一次,第二次你就有经验了,知道该先去看存储而不是先去看代码。
2.6.25 之后加了个折中,TASK_KILLABLE,本质是不可中断睡眠、但只对致命信号例外。
1 | /* 内核里的写法,用户态看不到这个宏 */ |
普通信号叫不醒,SIGKILL 可以,既保住了不能随便打断的语义,又给运维留了个逃生口,最早用上它的是 NFS,毕竟 NFS 是最容易卡死的文件系统,不过 ps 里没有 K 这种状态可以看,跑在 TASK_KILLABLE 上的进程显示出来还是 D,所以你看到一个 D 敲不死,别急着下结论说它是纯 D,它也可能是没人给它发 SIGKILL。
(表里那个 P 也很少见,只有内核线程被 kthread_park() 停放时才有,普通进程碰不到,我写了三年也只在一个调试内核线程的场景里见过一次。)
见到 D 先看 wchan
D 不好稳定复现,但很好认,因为内核会告诉你它卡在哪。
wchan 里能看到 |
大概率是什么 |
|---|---|
fdatasync / blkdev_issue_flush |
等磁盘刷数据 |
nfs_* / rpc_* |
NFS 服务端不通,在超时重试 |
fuse_* |
FUSE 或 sshfs 的后端进程挂了 |
io_schedule |
排队等 IO,设备被打满 |
rwsem_down_* / mutex_lock |
等内核锁,一般不是好事 |
throttle_* |
cgroup 的 io.max 在限流(cgroup 是内核给一组进程设资源上限的机制,IO 这块内存那一阶会顺带讲) |
wchan 是 wait channel 的缩写,意思是正在等哪个函数返回,这是 D 状态唯一的现场证据,比任何监控图表都直接,监控只能告诉你这个进程还在,wchan 能告诉你它在等谁,这两条信息差着一整个排查方向,一个让你继续加机器,一个让你去修 NFS,而且这个文件是内核现算出来的,读一次算一次,不是缓存,所以它反映的是你读的那一刻,不是五分钟前的快照。
(我最初以为 D 就是 disk 的缩写,看到 D 就去找磁盘,这个误解保持了大概两个月,直到一次 NFS 挂死,wchan 指向 rpc_wait_bit_killable,跟本地磁盘一点关系都没有,那之后我再看 D 都先读一遍等待点,因为名字会骗人,函数名不会。)
把每个字母各造一次
全机的状态分布,一行命令就能数出来。
1 | ps -eo stat | cut -c1 | sort | uniq -c | sort -rn |
1 | 148 S |
S 压倒性多,这是正常机器的样子,绝大多数进程都在等点什么。
造 R 最简单的办法是让 dd 一个无穷流进 /dev/null。
1 | dd if=/dev/zero of=/dev/null & |
大概能看到 R,也可能一闪就变 S,因为 dd 大部分时间在等缓冲,要稳定的 R 得用死循环。
1 | timeout 5 bash -c 'while :; do :; done' & |
S 最好造。
1 | sleep 100 & |
然后把它停住,同一个进程三个字母就都见过了。
1 | kill -STOP $! |
1 | pid stat comm |
T 和 t 只差大小写,来源完全不同,T 是作业控制(SIGSTOP/SIGTSTP),t 是 ptrace,strace -p 挂上去就是这个状态,两者都能用 SIGCONT 拉回来,但 t 有时候还得把调试器终止掉,因为调试器不放手,进程就一直停在断点上等你,这就是为什么 kill -CONT 对 t 时灵时不灵。
1 | sleep 300 & |
僵尸需要一点配合,得让父进程既不退出也不收尸,下面这段 Python 先 fork 一个 /bin/true,然后自己睡三十秒,子进程退出之后就成了没人收的尸体。
1 | python3 -c " |
1 | 9012 9011 Z true <defunct> |
<defunct> 就是僵尸,注意它的父进程 9011 还活着,父进程不退,僵尸就不会消失,这也是为什么容器里 PID 1 写错的时候能攒出一屏 <defunct>。
最后是 D 的观察手法。
1 | ps -eo pid,stat,wchan:24,comm | awk '$2 ~ /D/' |
/proc/PID/stack 打的是内核栈,能直接看出它卡在哪一行,要 root,而且内核得开了 CONFIG_STACKTRACE,没开的话会报 Permission denied 或者干脆没这个文件,前者是权限问题,后者是编译选项问题,两种报错长得挺像,我一开始以为都是权限。
/proc/PID/status 的 State 行则比 ps 更啰嗦。
1 | State:\tD (disk sleep) |
括号里那个词是内核给的注解,D 对应 disk sleep,这个名字起得有误导性,D 不一定在等磁盘,只是历史上大部分时候是这样,名字就留下了,跟 nice 值可以大于零一样属于陈年债。
(这段是我翻内核源码翻出来的,文档里只有一句 it is not interruptible,翻到 fs/nfs 那边才知道原来最早是 NFS 在用。)
那几个看着矛盾的状态
有人说 R 就是正在用 CPU,其实 R 包含可运行但在排队,8 核机器上 20 个 R 说明 CPU 不够用,load average 也会顶上去,但也可能是 20 个进程同时被唤醒又同时睡回去,短时间的 R 高峰看着吓人,其实什么事都没有。有人说 kill -9 一定管用,D 状态下信号只是挂起,而且如果那个 IO 永远不回来,进程就永远不死,只能重启或者等存储恢复,这条我吃过大亏,第一次遇到 D 的时候满机房找坏盘,其实是 NFS 服务端重启了。
有人说 D 是磁盘 IO 专用,任何不可中断的等待都会变 D,包括等内核锁、NFS、FUSE,看 wchan 才知道具体是什么。有人说僵尸进程占了很多资源,僵尸占的是一个 PID 和一个 task_struct 的空壳,内核栈已经释放了,不占 CPU、不占 fd、不占物理页,它真正的危害是占 PID,PID 被打满之后新进程创不出来,报 fork: Cannot allocate memory,一个跟内存毫无关系的错误。
有人说 S 状态的进程没影响,它不占 CPU,但可能占着连接、缓冲区、锁,一台机器上百万个 S 一样能把内存吃光。有人说 T 和 t 是一回事,一个大写一个小写,背后是两套机制,kill -CONT 能救 T,救 t 有时候得把调试器终止掉。
一个字母,一个问题
状态就一个含义,能不能被调度。
R 能,剩下都不能。
S 和 D 的差别只在信号能不能唤醒,kill -9 对 D 无效不是 bug,是设计。
D 大量堆积的时候看 wchan 和 /proc/PID/stack,那是唯一的现场证据。
僵尸只占一个 PID,危害是耗尽 PID 而不是内存。
这些状态能被看见,是因为内核把它们存在一处固定的地方,进程能查、ps 能读、kill 也能改。改的是状态,不是进程本身。











