2. 进程状态模型:TASK_RUNNING、TASK_INTERRUPTIBLE、TASK_UNINTERRUPTIBLE、TASK_STOPPED、TASK_TRACED、EXIT_ZOMBIE、EXIT_DEAD 状态详解
进程状态是操作系统对进程生命周期中不同阶段的抽象描述。Linux 内核使用一组明确定义的宏来表示进程当前所处的状态,这些状态定义在 include/linux/sched.h 头文件中。理解这些状态是掌握进程调度、同步与资源管理的基石。
Linux 进程状态模型主要分为两大类:运行/等待类(TASK_RUNNING、TASK_INTERRUPTIBLE、TASK_UNINTERRUPTIBLE)和特殊/终止类(TASK_STOPPED、TASK_TRACED、EXIT_ZOMBIE、EXIT_DEAD)。
2.1 核心状态总览
下表列出了 Linux 内核中定义的 7 种主要进程状态及其宏定义与简要说明:
| 状态宏 | 数值 | 含义 | 典型场景 |
|---|---|---|---|
TASK_RUNNING |
0 | 可运行状态(就绪或正在运行) | 进程正在 CPU 上执行,或等待被调度器选中 |
TASK_INTERRUPTIBLE |
1 | 可中断睡眠状态 | 等待 I/O 完成、等待信号量、等待套接字数据 |
TASK_UNINTERRUPTIBLE |
2 | 不可中断睡眠状态 | 等待磁盘 I/O、等待内核锁(如 mutex)、等待内存页 |
TASK_STOPPED |
4 | 暂停状态 | 收到 SIGSTOP、SIGTSTP 等信号 |
TASK_TRACED |
8 | 被跟踪状态 | 被 ptrace 系统调用跟踪(如 gdb 调试) |
EXIT_ZOMBIE |
16 | 僵尸状态 | 进程已终止,但父进程尚未调用 wait() 回收 |
EXIT_DEAD |
32 | 最终死亡状态 | 父进程已调用 wait(),进程即将被彻底销毁 |
task_struct->__state 中(旧版本为 state)。内核通过 set_task_state() 和 __set_current_state() 等内联函数安全地修改该字段。
2.2 运行与睡眠状态详解
2.2.1 TASK_RUNNING
这是进程唯一可能占用 CPU 的状态。处于 TASK_RUNNING 的进程要么正在 CPU 上执行,要么在运行队列(runqueue)中等待调度器分配 CPU 时间片。调度器只从 TASK_RUNNING 状态的进程中选择下一个执行者。
- 就绪(Ready): 进程已具备运行条件,等待调度。
- 运行(Running): 进程正在 CPU 上执行指令。
2.2.2 TASK_INTERRUPTIBLE
进程因等待某个资源(如 I/O 完成、信号量可用)而主动进入睡眠。该状态允许进程接收信号,当信号到达时,内核会将进程唤醒并重新放入运行队列,由进程自行决定是否继续等待或处理信号。
- 典型应用: 网络服务等待客户端连接、终端等待用户输入。
- 优点: 响应性好,信号可以中断长时间等待。
- 缺点: 需要处理信号中断后的“假唤醒”情况,代码逻辑需检查等待条件是否真正满足。
2.2.3 TASK_UNINTERRUPTIBLE
进程同样因等待资源而睡眠,但不响应任何信号。即使发送 SIGKILL,进程也不会被唤醒或终止,必须等到等待的资源就绪后,内核才会将其唤醒。
- 典型应用: 等待磁盘 I/O 完成(如
read()从硬盘读取数据)、等待内存页从交换分区换入。 - 优点: 保证关键 I/O 操作的原子性,避免信号干扰导致数据损坏。
- 缺点: 如果等待的资源永远不返回(如 NFS 服务器宕机),进程将永久阻塞,无法被杀死。这被称为“D 状态进程”。
TASK_UNINTERRUPTIBLE(D 状态)进程且长时间不消失,通常意味着底层 I/O 子系统或存储设备出现问题。此时 kill -9 无效,只能通过修复硬件或重启系统解决。
2.3 特殊与终止状态详解
2.3.1 TASK_STOPPED
进程的执行被暂停。通常由信号触发,如 SIGSTOP(强制暂停)、SIGTSTP(终端暂停)。进程会保留其内存上下文,等待 SIGCONT 信号恢复执行。
2.3.2 TASK_TRACED
进程被另一个进程(通常是调试器,如 gdb)通过 ptrace 系统调用跟踪。当被跟踪的进程收到信号时,会进入 TASK_TRACED 状态并通知跟踪进程,跟踪进程可以检查或修改其状态。
2.3.3 EXIT_ZOMBIE
进程已经终止,释放了大部分资源(内存、文件描述符等),但其 task_struct 仍然保留在进程表中。这是为了让父进程通过 wait() 系统调用获取子进程的退出状态(exit code)和资源使用统计。
- 为什么需要僵尸状态? 父进程需要知道子进程“怎么死的”(正常退出、被信号杀死、还是异常终止)。
- 危害: 如果父进程不调用
wait(),僵尸进程会一直占用进程表项,导致系统能创建的进程数减少。
2.3.4 EXIT_DEAD
这是进程的最终状态。父进程已经调用 wait() 回收了子进程的退出信息,内核将 task_struct 从进程表中移除,并释放所有剩余资源。进程生命周期至此结束。
2.4 进程状态转换图
下图展示了 Linux 进程在 7 种状态之间的主要转换路径:
2.5 状态转换的关键场景
- 调度器选择: 调度器遍历运行队列,从
TASK_RUNNING状态的进程中选择优先级最高的执行。 - 主动睡眠: 进程调用
schedule()或等待锁时,内核将其状态从TASK_RUNNING改为TASK_INTERRUPTIBLE或TASK_UNINTERRUPTIBLE,然后选择另一个进程运行。 - 信号唤醒: 信号处理机制会检查处于
TASK_INTERRUPTIBLE的进程,将其状态改回TASK_RUNNING并放入运行队列。 - 进程终止: 进程调用
exit()系统调用后,内核将其状态设为EXIT_ZOMBIE,发送SIGCHLD信号给父进程。 - 资源回收: 父进程调用
wait()或waitpid()后,内核将僵尸进程状态改为EXIT_DEAD,并释放task_struct。
2.6 内核代码中的状态操作
以下代码片段展示了内核如何安全地修改进程状态:
/* 内核源码:kernel/sched/core.c */
void __sched schedule(void)
{
struct task_struct *prev, *next;
unsigned long *switch_count;
struct rq *rq;
int cpu;
cpu = smp_processor_id();
rq = cpu_rq(cpu);
prev = rq->curr;
/* 如果当前进程不再处于 TASK_RUNNING 状态,则将其从运行队列中移除 */
if (prev->__state != TASK_RUNNING) {
deactivate_task(rq, prev, DEQUEUE_SLEEP);
}
/* 选择下一个要运行的进程 */
next = pick_next_task(rq, prev, &rf);
/* 执行上下文切换 */
context_switch(rq, prev, next, &rf);
}
/* 设置进程为可中断睡眠状态 */
void set_current_state(unsigned int new_state)
{
/* 内存屏障确保状态修改对其他 CPU 可见 */
smp_mb__before_atomic();
current->__state = new_state;
smp_mb__after_atomic();
}
schedule() 之前,必须先将自己的状态设置为非 TASK_RUNNING,否则调度器可能会错误地认为它仍然需要 CPU。
2.7 状态模型与调度器的关系
进程状态模型直接决定了调度器的行为:
- 运行队列(runqueue): 只包含
TASK_RUNNING状态的进程。调度器每次调度时,只从运行队列中选择进程。 - 等待队列(waitqueue): 用于管理
TASK_INTERRUPTIBLE和TASK_UNINTERRUPTIBLE状态的进程。当资源可用时,内核遍历等待队列,唤醒所有等待该资源的进程。 - 负载计算: 系统负载(load average)的计算会考虑
TASK_RUNNING和TASK_UNINTERRUPTIBLE状态的进程数,这也是为什么 D 状态进程会导致负载升高。
理解这 7 种状态及其转换,是深入分析 Linux 调度器、锁机制、I/O 子系统以及进程生命周期管理的前提。在实际问题排查中,通过 ps 或 top 命令观察进程状态(R、S、D、T、Z),可以快速定位系统瓶颈或异常行为。