Linux 七大进程状态深度解析
内核源码中的状态定义
进程状态存储在
task_struct的state和exit_state字段中,定义于include/linux/sched.h:
struct task_struct { volatile long state; /* 运行中状态:-1 不可运行, 0 可运行, >0 停止 */ int exit_state; /* 退出中状态:EXIT_ZOMBIE / EXIT_DEAD */ // ... };具体状态标志:
/* tsk->state 使用 */ #define TASK_RUNNING 0x0000 #define TASK_INTERRUPTIBLE 0x0001 #define TASK_UNINTERRUPTIBLE 0x0002 #define TASK_STOPPED 0x0004 #define TASK_TRACED 0x0008 /* tsk->exit_state 使用 */ #define EXIT_ZOMBIE 0x0010 #define EXIT_DEAD 0x0020关键区分:
state字段管理进程"活着"时的状态(R/S/D/T/t),
exit_state字段管理进程"退出过程中"的状态(Z/X)。两者互斥使用。
一、R --- TASK_RUNNING(运行态 / 就绪态)
定义
#define TASK_RUNNING 0x0000值为
0,是所有状态中唯一表示"可以被调度执行"的状态。语义拆解
TASK_RUNNING实际上包含了传统操作系统中的两个子状态:
子状态 含义 运行中(Running) 进程正在 CPU 上执行指令 就绪(Ready) 进程已准备好,在运行队列(runqueue)中等待被调度 Linux 不区分这两者,统一用
TASK_RUNNING表示。区别仅在于:该进程的
task_struct是否正在被某个 CPU 的current指针指向。进入 R 态的触发条件
- 进程被
fork创建后,初始即为TASK_RUNNING- 睡眠进程等待的事件发生,被
wake_up()唤醒- 停止态进程收到
SIGCONT信号- 时间片耗尽后被调度器换下 CPU,但仍保持 R 态(在就绪队列中)
离开 R 态的触发条件
- 主动调用
schedule()放弃 CPU(如等待某资源)- 时间片耗尽,被调度器抢占(但仍在 R 态就绪队列中,不算离开)
- 收到停止信号进入 T 态
- 调用
exit()退出运行队列与调度
所有处于
TASK_RUNNING的进程都挂在每 CPU 运行队列(struct rq)上。CFS 调度器从红黑树中挑选
vruntime最小的进程执行。
CPU 0 的运行队列(红黑树,按 vruntime 排序): [P3: vruntime=100] / \ [P1: vruntime=80] [P5: vruntime=150] / [P2: vruntime=120] 调度器选择最左侧节点(vruntime 最小)→ P1 上 CPU 执行实际场景
# 查看正在运行的进程 ps aux | awk '$8 ~ /R/'一个 CPU 密集型程序(如死循环计算)会长期处于 R 态:
while (1) { // 纯计算,不调用任何阻塞函数 result += compute_something(); }
二、S --- TASK_INTERRUPTIBLE(可中断睡眠态)
定义
#define TASK_INTERRUPTIBLE 0x0001语义
进程在等待某个事件发生,主动放弃 CPU 进入睡眠。
但它可以被信号唤醒------当一个信号递达时,进程会被唤醒去处理信号,即使等待的事件尚未发生。
进入 S 态的典型场景
场景 内核操作 sleep(10)定时器未到期前睡眠 read()阻塞终端/网络等待数据到达 wait()/waitpid()等待子进程退出 pause()等待任意信号 等待互斥锁/信号量 锁被其他进程持有 select()/poll()/epoll_wait()等待 I/O 事件 内核中的睡眠机制
进程进入可中断睡眠的标准模式:
// 定义等待队列节点 DEFINE_WAIT(wait); // 将自己加入等待队列,并设置状态 prepare_to_wait(&wq, &wait, TASK_INTERRUPTIBLE); if (!condition) { schedule(); // 主动调度,放弃 CPU } // 被唤醒后,从等待队列移除 finish_wait(&wq, &wait);唤醒路径有两条:
- 事件发生:其他进程/中断调用
wake_up(&wq),遍历等待队列唤醒进程- 信号递达:内核在信号处理路径中检查进程状态,如果是
TASK_INTERRUPTIBLE,则唤醒进程被信号唤醒后的行为
进程被信号唤醒后,通常会检查是否有信号待处理。如果有,系统调用(如
read)会返回-EINTR,表示"被信号中断":
ssize_t ret = read(fd, buf, size); if (ret == -1 && errno == EINTR) { // 被信号中断,可以选择重试 continue; }这就是为什么很多健壮的代码会在循环中重试被
EINTR中断的系统调用。实际场景
绝大多数后台服务进程在空闲时都处于 S 态------它们在等待客户端请求、等待 I/O 完成。
# 统计 S 态进程数量(通常占绝大多数) ps aux | awk '$8 ~ /^S/' | wc -l
三、D --- TASK_UNINTERRUPTIBLE(不可中断睡眠态)
定义
#define TASK_UNINTERRUPTIBLE 0x0002语义
进程在等待不可中断的事件,通常是底层 I/O 操作(磁盘读写、NFS 挂载等)。
与 S 态的核心区别:不能被信号唤醒,只能由等待的事件完成来唤醒。
为什么需要不可中断?
这是最常被问到的面试点。假设进程正在执行磁盘
read:
- 进程通过系统调用进入内核态
- 内核向磁盘控制器发出 I/O 请求
- 磁盘正在机械地寻道、读写(毫秒级延迟)
- 进程需要等待 I/O 完成
如果此时允许信号中断这个进程:
- 进程可能在 I/O 完成前就退出或处理信号
- 内核态的 I/O 操作可能处于不一致状态
- 已发出的磁盘请求无法干净地回滚
- 可能导致数据损坏或内核崩溃
因此,在 I/O 操作的关键路径上,进程必须设为不可中断睡眠,确保 I/O 完成前进程不会被干扰。
进入 D 态的典型场景
场景 说明 磁盘 read/write等待物理 I/O 完成 fsync/fdatasync等待数据刷入磁盘 NFS 网络文件系统操作 等待网络 I/O,网络不通时可能长时间 D 态 直接 I/O( O_DIRECT)绕过页缓存,直接等待磁盘 某些设备驱动操作 如磁带机、原始设备访问 D 态与系统负载
D态进程会被计入系统负载(load average)。这是一个重要的知识点:
Linux 的 load average 不仅统计 R 态进程,还统计 D 态进程。
因此,当系统 load 很高但 CPU 使用率很低时,往往意味着大量进程处于 D 态------磁盘 I/O 是瓶颈。
# 查看 D 态进程 ps aux | awk '$8 ~ /D/' # 用 iostat 确认磁盘瓶颈 iostat -x 1D 态能被杀死吗?
**不能。 因为 D 态进程不响应信号,
kill -9(SIGKILL)也无法终止它。**进程会一直保持 D 态,直到:
- 等待的 I/O 操作完成
- I/O 超时(某些驱动有超时机制)
- 系统重启
这也是 D 态进程令人头疼的原因------如果磁盘或 NFS 出现故障,D 态进程可能永远卡在那里,唯一的解决办法是修复 I/O 子系统或重启。
内核中的 io_schedule()
进入不可中断睡眠通常使用
io_schedule():
// 设置为不可中断睡眠并调度 set_task_state(tsk, TASK_UNINTERRUPTIBLE); io_schedule(); // 内部调用 schedule(),并统计 iowait 时间
io_schedule()与普通schedule()的区别在于:它会将等待时间计入iowait(/proc/stat中的 iowait 字段),这也是top中%wa的来源。
四、T --- TASK_STOPPED(停止态)
定义
#define TASK_STOPPED 0x0004语义
进程被暂停执行,既不在 CPU 上运行,也不在等待任何事件。它完全"冻结",直到收到恢复信号。
进入 T 态的方式
方式 说明 SIGSTOP信号不可被捕获/忽略/阻塞,强制停止进程 SIGTSTP信号终端 Ctrl+Z发送,可被捕获SIGTTIN信号后台进程尝试读取终端时 SIGTTOU信号后台进程尝试写入终端时 ptrace附加调试器附加时,目标进程进入停止态 从 T 态恢复
只有一个方式:收到
SIGCONT信号。
# 停止一个进程 kill -STOP 1234 # 查看状态(应为 T) ps -o pid,stat,cmd -p 1234 # 恢复进程 kill -CONT 1234终端作业控制中的 T 态
Ctrl+Z是最常见的触发场景:
$ sleep 100 # 按下 Ctrl+Z ^Z [1]+ Stopped sleep 100 $ jobs [1]+ Stopped sleep 100 $ bg %1 # 后台继续运行(发送 SIGCONT) [1]+ sleep 100 & $ fg %1 # 前台恢复运行 sleep 100T 态进程的资源
处于 T 态的进程:
- 保留所有内存资源(不会被换出,但可以被回收)
- 保留文件描述符
- 不在运行队列中,不会被调度
- 不响应除 SIGCONT 外的大多数信号(但信号会被记录为挂起)
注意:SIGKILL 可以终止 T 态进程吗?
可以。SIGKILL 对 T 态进程有效,进程会直接进入退出流程。但其他常规信号(如 SIGTERM)在 T 态下不会被立即处理,要等进程恢复到 R/S 态后才会递达。
五、t --- TASK_TRACED(追踪态)
定义
#define TASK_TRACED 0x0008语义
进程被调试器追踪时的暂停状态。当
gdb等调试器通过ptrace附加到进程,并命中断点或单步执行时,目标进程进入TASK_TRACED。与 T 态(STOPPED)的区别
这是一个高频面试考点:
对比项 T(STOPPED) t(TRACED) 触发方式 SIGSTOP / SIGTSTP / Ctrl+Z ptrace 断点 / 单步 恢复方式 SIGCONT 信号 调试器调用 ptrace(PTRACE_CONT) 能否被 SIGCONT 唤醒 能 不能(必须由 ptrace 恢复) 信号处理 挂起信号等待恢复 调试器可拦截信号 ps 显示 Tt(小写)ptrace 与 TRACED 态
ptrace是 Linux 提供的进程追踪机制,gdb、strace、ltrace等工具都基于它。
// 父进程(调试器)追踪子进程 pid_t child = fork(); if (child == 0) { // 子进程:允许被追踪 ptrace(PTRACE_TRACEME, 0, NULL, NULL); execl("/bin/ls", "ls", NULL); } else { // 父进程:等待子进程事件 int status; waitpid(child, &status, 0); // 设置断点、单步执行等 ptrace(PTRACE_SINGLESTEP, child, NULL, NULL); // 子进程执行一条指令后进入 TASK_TRACED }当子进程命中断点时,内核会发送
SIGTRAP信号,子进程进入TASK_TRACED,父进程通过waitpid获知。实际场景
# 用 gdb 调试一个程序 gdb ./myprogram (gdb) break main (gdb) run # 另一个终端查看 ps aux | grep myprogram # wxx 7800 0.0 0.1 ... t 10:30 0:00 ./myprogram # ↑ 小写 t,表示 TRACED 态
六、Z --- EXIT_ZOMBIE(僵尸态)
定义
#define EXIT_ZOMBIE 0x0010注意这是定义在
exit_state字段中,而非state字段。语义
进程已经执行完毕,释放了用户态资源(内存、文件描述符等),但内核中的**
task_struct尚未释放****。它像一具"尸体"一样留在进程表中,等待父进程来"收尸"。**僵尸进程的产生流程
1. 子进程调用 exit() 或从 main 返回 ↓ 2. 内核释放子进程的用户态资源(地址空间、文件描述符、信号处理等) ↓ 3. 子进程的 exit_state 设为 EXIT_ZOMBIE ↓ 4. 内核向父进程发送 SIGCHLD 信号 ↓ 5. 子进程保留 task_struct(包含退出码、资源使用统计等信息) ↓ 6. 父进程调用 wait()/waitpid() 读取子进程退出信息 ↓ 7. 内核释放子进程的 task_struct → 进入 EXIT_DEAD如果步骤 6 从未发生,子进程就永远停留在僵尸态。
为什么需要僵尸态?
这是面试高频问题。僵尸态存在的意义是:
保存子进程的退出状态和资源使用信息,供父进程查询。
父进程可能想知道:
- 子进程是正常退出还是被信号杀死?
- 退出码是多少?
- 子进程累计使用了多少 CPU 时间?
- 子进程的最大内存驻留集是多少?
这些信息存储在
task_struct中,必须等父进程通过wait()读取后才能释放。如果子进程一退出就完全销毁,父进程就无法获取这些信息了。代码演示
#include <stdio.h> #include <stdlib.h> #include <unistd.h> int main() { pid_t pid = fork(); if (pid < 0) { perror("fork"); return 1; } if (pid == 0) { // 子进程:立刻退出 printf("【子进程】PID=%d,即将退出\n", getpid()); exit(42); // 退出码 42 } else { // 父进程:不调用 wait,睡眠 60 秒 printf("【父进程】PID=%d,子进程 PID=%d,不回收,睡眠 60 秒\n", getpid(), pid); sleep(60); } return 0; }运行后另开终端:
ps aux | grep -E 'Z|defunct' # wxx 8101 0.0 0.0 0 0 pts/0 Z 10:35 0:00 [zombie_demo] <defunct>注意僵尸进程的
VSZ和RSS都是0------用户态内存已释放,只剩内核数据结构。僵尸进程的危害
危害 说明 占用 PID 每个僵尸进程占用一个 PID 号,系统 PID 有限(默认 32768) 占用内核内存 每个 task_struct约 1.7KB,数量多时不可忽视无法直接杀死 kill对僵尸进程无效,因为它已经"死了"影响进程创建 PID 耗尽后, fork会失败返回EAGAIN如何处理僵尸进程
方法一:父进程调用 wait() / waitpid()
int status; waitpid(pid, &status, 0); // 阻塞等待指定子进程 // 或 wait(&status); // 等待任意子进程
方法二:SIGCHLD 信号处理(非阻塞回收)
void sigchld_handler(int sig) { int saved_errno = errno; // WNOHANG:不阻塞,没有已退出的子进程则立即返回 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; sigaction(SIGCHLD, &sa, NULL); // ... }为什么用
while循环?因为多个子进程可能同时退出,而信号不排队,一次 SIGCHLD 可能对应多个子进程退出。
方法三:父进程先退出,让 init 收养
如果父进程先于子进程退出,子进程会被
init(PID 1)收养。init有一个专门的 SIGCHLD 处理函数,会自动回收所有孤儿进程,不会产生僵尸。
方法四:两次 fork(高级技巧)
pid_t pid = fork(); if (pid == 0) { // 第一个子进程 pid_t grandchild = fork(); if (grandchild == 0) { // 孙子进程:执行实际任务 execvp(...); } // 第一个子进程立即退出,孙子进程被 init 收养 exit(0); } // 父进程等待第一个子进程(很快退出) waitpid(pid, NULL, 0);孙子进程的父进程(第一个子进程)立即退出,孙子被 init 收养,自动回收,父进程也无需长期等待。
如何"杀死"一个已经存在的僵尸进程
僵尸进程无法被直接杀死,因为它已经退出了。唯一的方法是:
杀死它的父进程:父进程死后,僵尸进程被 init 收养,init 会自动回收
让父进程调用 wait:如果父进程还在运行,可以通过调试器等方式让它执行 wait
重启系统:终极手段
找到僵尸进程的父进程 PID
ps -o ppid= -p <zombie_pid>
杀死父进程(谨慎!)
kill
七、X --- EXIT_DEAD(死亡态)
定义
#define EXIT_DEAD 0x0020同样定义在
exit_state字段中。语义
进程的最终状态。父进程已经通过
wait()回收了子进程的退出信息,内核即将释放task_struct。这个状态极其短暂------通常只持续几个指令周期,ps命令几乎不可能捕捉到。进入 X 态的流程
父进程调用 waitpid() ↓ 内核调用 release_task() ↓ 将子进程 exit_state 设为 EXIT_DEAD ↓ 从进程表中移除(PID 可重用) ↓ 释放 task_struct 内核栈 ↓ 释放其他剩余内核资源 ↓ 进程彻底消失为什么需要 X 态?
既然 Z 态之后进程就要被释放了,为什么还要一个中间的 X 态?
原因是并发安全。在多 CPU 系统中,释放
task_struct的过程不是原子的。设置
EXIT_DEAD标志可以:
告知其他 CPU:这个进程正在被释放,不要访问它的
task_struct作为
release_task()中的状态检查,防止重复释放在释放过程中保持一个稳定的状态标识
// kernel/exit.c 中的 release_task()
void release_task(struct task_struct *p)
{
// ...
p->exit_state = EXIT_DEAD; // 标记为死亡态
// ... 执行释放操作
call_rcu(&p->rcu, delayed_put_task_struct); // 延迟释放
}X 态的可见性
# 几乎不可能看到 X 态进程 ps aux | awk '$8 ~ /X/' # 通常无输出如果你真的看到了 X 态进程,那可能是:
- 系统处于极高负载,释放操作被延迟
- 内核存在 bug
- 你在
ps输出刷新的瞬间恰好捕捉到了
状态转换全景图
fork() 新建 ───────────────────────────→ R (TASK_RUNNING) │ ┌───────────────────┼───────────────────┐ │ │ │ 等待事件/资源 时间片到 收到停止信号 (可中断) (仍在就绪队列) (SIGSTOP/Ctrl+Z) │ │ │ ▼ ▼ ▼ S (可中断睡眠) R T (停止态) D (不可中断睡眠) │ │ │ 事件完成/I/O完成 SIGCONT (D态只能被事件唤醒) │ (S态还可被信号唤醒) │ │ │ └───────────────────┬───────────────────┘ │ ▼ R │ ┌────────────────────┼────────────────────┐ │ │ │ ptrace断点 exit()/信号 正常执行 │ │ │ ▼ ▼ │ t (追踪态) Z (僵尸态) │ │ │ │ ptrace CONT 父进程 wait() │ │ │ │ └────────────────────┼────────────────────┘ │ ▼ X (死亡态) │ ▼ 资源释放,消失
补充知识
1. 进程状态与 load average 的关系
Linux 的
load average统计的是处于不可中断睡眠(D)和运行/就绪(R)状态的进程数的指数衰减平均值。
load average = 正在运行的进程(R) + 等待I/O的进程(D)这解释了一个经典问题:为什么 CPU 使用率很低,但 load average 很高?
→ 因为大量进程在 D 态等待磁盘 I/O,它们被计入 load 但不消耗 CPU。
2. ps 中 STAT 列的附加字符
除了基本状态字符,
ps的 STAT 列还可能包含附加修饰符:
字符 含义 s会话首进程(session leader),通常是 shell l多线程进程 +位于前台进程组 <高优先级(nice 值为负) N低优先级(nice 值为正) L有页面锁定在内存中(mlock) 例如
Ssl+= 可中断睡眠 + 会话首进程 + 多线程 + 前台进程组。
3. 内核线程的状态
内核线程(如
kthreadd、kworker、migration)也使用相同的状态机。你在ps中看到的[kworker/0:0]这类带方括号的进程就是内核线程。它们通常处于 S 态(等待工作)或 R 态(处理工作)。
4. 空闲进程的状态
每个 CPU 都有一个空闲进程(idle task,PID 0),当没有其他 R 态进程时运行。空闲进程不占用进程表项,也不在
ps中显示。
高频面试题
Q1:Linux 有哪几种进程状态?分别是什么含义?
参考答案:
- R(Running):运行或就绪,在 CPU 上执行或在运行队列等待调度
- S(Sleeping):可中断睡眠,等待事件,可被信号唤醒
- D(Disk sleep):不可中断睡眠,通常等待 I/O,不能被信号唤醒
- T(Stopped):停止态,被 SIGSTOP 等信号暂停,SIGCONT 恢复
- t(Traced):追踪态,被调试器暂停,由 ptrace 恢复
- Z(Zombie):僵尸态,进程已退出但父进程未回收,保留 task_struct
- X(Dead):死亡态,父进程已回收,task_struct 即将释放
Q2:D 态和 S 态有什么区别?为什么需要 D 态?
参考答案:
- S 态可被信号唤醒,D 态不能被信号唤醒
- D 态用于保护内核态关键操作(主要是 I/O)的完整性。如果在磁盘 I/O 过程中允许信号中断,可能导致 I/O 操作处于不一致状态,引发数据损坏
- D 态进程会被计入 load average,S 态不会
kill -9无法杀死 D 态进程
Q3:僵尸进程是怎么产生的?如何避免和处理?
参考答案:
- 产生:子进程退出后,父进程未调用
wait()/waitpid()回收,子进程的task_struct残留在进程表中- 避免:
- 父进程显式调用
wait()或waitpid()- 注册
SIGCHLD信号处理函数,在其中用waitpid(-1, NULL, WNOHANG)循环回收- 父进程先退出,子进程被 init 收养自动回收
- 两次 fork,让孙子进程被 init 收养
- 处理已存在的僵尸:杀死其父进程,让 init 收养并回收
Q4:T 态和 t 态有什么区别?
参考答案:
- T 态由 SIGSTOP/SIGTSTP/Ctrl+Z 触发,可被 SIGCONT 恢复
- t 态由 ptrace 调试触发(断点、单步),只能由调试器通过
ptrace(PTRACE_CONT)恢复,SIGCONT 无效- T 态进程的信号会被挂起,t 态进程的信号可被调试器拦截和修改
Q5:为什么 load average 高但 CPU 使用率低?
参考答案:
- load average 统计的是 R 态 + D 态进程数
- D 态进程在等待 I/O(通常是磁盘),不消耗 CPU 但计入 load
- 这种情况说明系统的瓶颈在磁盘 I/O,而非 CPU
- 可用
iostat -x、iotop排查磁盘瓶颈,用ps aux | awk '$8 ~ /D/'查看 D 态进程
Q6:
kill -9能杀死哪些状态的进程?不能杀死哪些?参考答案:
- 能杀死:R、S、T、t、Z(僵尸进程本就已死,kill 无意义但不报错)
- 不能杀死:D 态(不可中断睡眠,不响应任何信号,包括 SIGKILL)
- X 态瞬间即逝,不存在杀死的问题
Q7:fork 之后子进程的初始状态是什么?
参考答案:
TASK_RUNNING。子进程被创建后直接加入运行队列,等待被调度执行。- 但子进程是否立即运行取决于调度器,父子进程的执行顺序不确定。
Q8:进程退出的完整流程是什么?
参考答案:
- 进程调用
exit()(或从 main 返回,编译器自动插入 exit)- 内核执行
do_exit():关闭文件描述符、释放地址空间、释放信号处理等用户态资源- 设置
exit_state = EXIT_ZOMBIE,向父进程发送SIGCHLD- 保留
task_struct(含退出码、资源统计)- 父进程调用
wait()/waitpid(),内核执行release_task()- 设置
exit_state = EXIT_DEAD,释放task_struct和内核栈- 进程彻底消失,PID 可被重用
总结
Linux 的七大进程状态构成了一个完整的生命周期状态机:
- R 是进程"活着且能动"的状态
- S/D 是进程"睡着等事件"的状态,区别在于能否被信号打断
- T/t 是进程"被暂停"的状态,区别于是被信号还是调试器暂停
- Z/X 是进程"已死"的状态,区别在于是否已被父进程回收
理解这些状态不仅能帮助你写出更健壮的代码(正确处理
EINTR、避免僵尸进程),更是排查线上问题(load 高、进程卡死、系统响应慢)的必备知识。在面试中,进程状态也是操作系统和 Linux 系统编程方向的高频考点。

