文章目录
- 一、Linux进程五大基本状态
-
- [1. 运行状态(R,Running / Runnable)](#1. 运行状态(R,Running / Runnable))
- [2. 可中断睡眠状态(S,Interruptible Sleep)](#2. 可中断睡眠状态(S,Interruptible Sleep))
- [3. 不可中断睡眠状态(D,Uninterruptible Sleep)](#3. 不可中断睡眠状态(D,Uninterruptible Sleep))
- [4. 停止状态(T,Stopped / Traced)](#4. 停止状态(T,Stopped / Traced))
- [5. 僵死状态(Z,Zombie / Defunct)](#5. 僵死状态(Z,Zombie / Defunct))
- [二、D 状态和Z状态排查](#二、D 状态和Z状态排查)
-
- [1.D 状态排查方法](#1.D 状态排查方法)
-
- [1. wchan & stack](#1. wchan & stack)
- [2. hung_task](#2. hung_task)
- [2. Z 状态排查方法](#2. Z 状态排查方法)
- 3.防止Z状态产生
一、Linux进程五大基本状态
在 Linux 系统中,常用的五大基本状态(通过 ps、top 等命令可查看)分别是:
1. 运行状态(R,Running / Runnable)
含义 :
进程当前正在运行(占用 CPU)或者处于可运行队列中、只要获得 CPU 时间片就能立刻执行。
特点:
包括正在 CPU 上执行的进程,以及就绪等待调度的进程。
这是进程争取 CPU 时的活跃状态。
示例:
一个不停计算的 while(1) 程序通常处于 R 状态;在多核系统上,多个 R 状态进程可能同时运行。
2. 可中断睡眠状态(S,Interruptible Sleep)
含义:
进程正在等待某个事件完成(如等待 I/O 输入、等待锁、等待信号等),并且该睡眠可以被信号唤醒。
特点:
最常见的睡眠状态(大部分时间系统进程都在此状态)。
当等待的资源可用或收到信号时,进程会回到 R 状态。
可以被 kill 命令或其它信号打断。
示例:
Shell 等待用户输入、网络服务等待客户端连接、进程调用 sleep() 或 nanosleep()。
3. 不可中断睡眠状态(D,Uninterruptible Sleep)
含义:
进程正在等待某些不可被中断的 I/O 操作(如直接读写磁盘、等待硬件响应),期间无法响应任何信号。
特点:
通常用于磁盘 I/O、某些设备驱动中的关键操作。
即使发送 SIGKILL 也无法立即终止该进程(必须等待 I/O 完成或系统重启)。
这种设计是为了防止在关键数据写回磁盘时被中断导致数据不一致。
示例:
sync 命令刷新缓冲区、dd 直接写入块设备、NFS 因网络故障僵住时的等待操作。
注意:短时间的 D 状态是正常的,若长时间存在且数量增多,往往提示 I/O 瓶颈或存储设备问题。
4. 停止状态(T,Stopped / Traced)
含义:
进程的执行被暂停,通常是由于收到了 SIGSTOP、SIGTSTP、SIGTTIN、SIGTTOU 等信号,或者被调试器(如 gdb)暂时接管。
特点:
进程不会获得 CPU 时间,也不响应普通信号(除 SIGKILL、SIGCONT 外)。
可以通过发送 SIGCONT 信号让进程恢复到 R 状态继续执行。
示例:
在终端按下 Ctrl+Z 将前台进程挂起 或 gdb 中设置断点后命中暂停 或 使用 kill -SIGSTOP 显式暂停。
5. 僵死状态(Z,Zombie / Defunct)
含义:
进程已经结束运行(调用了 exit 或收到终止信号),但其进程描述符(task_struct)仍然保留,等待父进程调用 wait() 或 waitpid() 来读取其退出状态。
特点:
该进程不再占用任何内存或 CPU 资源(只保留内核中的一个极小结构体)。
无法被杀死(因为它已经"死"了),只能通过让父进程回收它来消除。
若父进程不回收且不退出,僵尸进程会一直存在;若父进程先退出,僵尸进程会被 init(PID=1)进程收养并自动回收。
示例:
父进程编写不当(未调用 wait),导致子进程结束后一直处于 Z 状态。大量僵尸进程可能耗尽进程号上限,影响系统运行。
本文将具体介绍D状态和Z状态的排查与处理。
二、D 状态和Z状态排查
1.D 状态排查方法
1. wchan & stack
wchan(wait channel)能看到 D 状态进程"卡"在内核的哪个具体函数
例如:
bash
ps -eo pid,state,wchan,comm | awk '$2=="D"'
输出
bash
PID S WCHAN COMMAND
123 D io_schedule dd
456 D wait_on_page_bit mysqld
这个例子中进程dd(PID 123) 卡在了 io_schedule, 即磁盘读写,可能的原因是磁盘慢、坏道、NFS 挂了。
而进程mysqld(PID 456)卡在了wait_on_page_bit,即内存页回写,可能的原因是内存压力大。
也可以直接读取某进程的wchan,例如:
bash
cat /proc/798/wchan
可能输出为:
bash
rpc_wait_bit_killable
即卡在RPC 层。
然后,可以通过stack中内容进一步排查具体卡在了哪个函数,例如在刚才的例子中继续检查stack:
bash
cat /proc/789/stack
可能输出为:
bash
[<0>] rpc_wait_bit_killable+0x??/0x?? [sunrpc]
[<0>] __rpc_execute+0x??/0x?? [sunrpc]
[<0>] rpc_run_task+0x??/0x?? [sunrpc]
[<0>] nfs_write_rpc+0x??/0x?? [nfs]
[<0>] nfs_file_write+0x??/0x?? [nfs]
[<0>] vfs_write+0x??/0x??
[<0>] sys_write+0x??/0x??
由此能看出是 NFS 写操作卡在 RPC 层。
2. hung_task
当 D 状态进程超过 120 秒(默认),内核会主动打印带有hung task 关键字的log,因此使用dmesg 指令可以可以获取
bash
dmesg -T | grep "hung_task"
可能输出为:
bash
[Thu May 25 10:15:32 2026] INFO: task dd:123 blocked for more than 120 seconds.
[Thu May 25 10:15:32 2026] Tainted: G W
[Thu May 25 10:15:32 2026] "echo 0 > /proc/sys/kernel/hung_task_timeout_secs" disables this message.
[Thu May 25 10:15:32 2026] dd D 123 1 0x00000000
[Thu May 25 10:15:32 2026] Call Trace:
[Thu May 25 10:15:32 2026] [<ffffffffa0000000>] io_schedule+0x??/0x??
可以看出 dd(PID 123)卡在 io_schedule 超过 120 秒。
2. Z 状态排查方法
wchan、hung_task 等指令对Z状态无用,我们需要一直找到进程的父进程进行处理。
例如我们已经找到了一个Z状态进程:
bash
ps aux | awk '$8=="Z"'
输出:
bash
USER PID STAT COMMAND
root 123 Z [sh] <defunct>
我们需要找到它的父进程:
bash
ps -o ppid= -p 123
例如输出456,我们通知父进程回收:
bash
kill -CHLD 456
以此来回收Z状态子进程123.
如果不行,可以选择直接杀死父进程:
bash
kill -9 456
3.防止Z状态产生
最好防止Z状态产生的方式实在代码中确保父进程回收子进程的退出状态。
例如一个非阻塞式回收的代码示例如下:
cpp
#include <sys/wait.h>
#include <signal.h>
#include <unistd.h>
#include <stdio.h>
#include <errno.h>
void sigchld_handler(int signo) {
int saved_errno = errno;
while (waitpid(-1, NULL, WNOHANG) > 0); // 回收所有已结束的子进程
errno = saved_errno;
}
int main() {
struct sigaction sa;
sa.sa_handler = sigchld_handler;
sigemptyset(&sa.sa_mask);
sa.sa_flags = SA_RESTART | SA_NOCLDSTOP;
sigaction(SIGCHLD, &sa, NULL);
pid_t pid = fork();
if (pid == 0) {
printf("Child exiting\n");
return 0;
} else if (pid > 0) {
printf("Parent doing other work...\n");
sleep(2); // 模拟父进程的工作
}
return 0;
}
或者使用通过第二次 fork,使实际工作的子进程成为孤儿,被 init(PID=1)接管。init 会自动回收子进程,从而避免僵尸。
cpp
#include <sys/wait.h>
#include <unistd.h>
#include <stdio.h>
int main() {
pid_t pid = fork();
if (pid == 0) {
// 第一次子进程
pid_t pid2 = fork();
if (pid2 == 0) {
// 第二次子进程:实际工作的进程
printf("Working child (will be adopted by init)\n");
sleep(2);
return 0;
} else if (pid2 > 0) {
// 第一次子进程直接退出,使第二次子进程成为孤儿
return 0;
}
} else if (pid > 0) {
// 父进程回收第一次子进程(很快完成)
wait(NULL);
printf("Parent: first child reaped, grandchild adopted by init\n");
sleep(3);
}
return 0;
}