
观众老爷们大家好 这里是邪修KING的独家频道
本文属于系列Linux系统篇 ——操作指令
一起学Linux的小伙伴可订阅专栏: Linux系统篇
系列前置:Linux 开发工具、Makefile、GDB 调试、进程 fork 创建 本篇聚焦:Linux 进程五大核心状态,内核链表的「绳子穿实体」设计、PCB (task_struct) 与队列底层原理,配套 Ubuntu 实操模拟;僵尸 Z、孤儿进程留作下篇《进程等待与退出》讲解
1. 前言:操作系统理论状态 vs Linux 真实进程状态
很多教材画的操作系统进程状态图是理论模型:新建 → 就绪 → 运行 → 阻塞 → 结束,还有挂起就绪、挂起阻塞。 但 Linux 内核没有完全照搬这套理论,内核把状态做了简化与细分,我们在ps命令看到的STAT字符(R/S/D/T/t)就是内核里 task_struct 里面记录的状态标记。
核心一句话: 理论模型用来理解宏观流程;Linux 内核的 R/S/D/T/t 是操作系统真实落地实现。 进程所有状态信息,全部保存在 PCB 结构体 struct task_struct 里面,PCB 常驻内存,就算进程被挂起交换到磁盘,PCB 不会换出!
进程的本质:PCB(task_struct) + 代码段 + 数据段
- PCB:内核用来管理进程的控制块,保存 PID、状态、优先级、链表指针等全部管理信息
- 代码 & 数据:程序的指令、变量,放在内存;内存不够时可以换入 / 换到磁盘交换分区 swap
队列模型(重中之重):

2. 基础概念铺垫:PCB (task_struct)、硬件与三大队列
2.1 PCB task_struct
Linux 内核中,每一个进程,在内核里都会生成一个struct task_struct结构体,也就是 PCB 进程控制块。 里面包含:PID、PPID、进程状态 state、优先级、内存指针、文件描述符、嵌入式链表成员struct list_head links。
重点:PCB 永远保存在内存,哪怕进程代码被换到磁盘 swap,PCB 不会被换出去。内核必须靠 PCB 找到这个进程。
2.2 三大队列的本质:链表
队列底层全部是内核双向循环链表,链表节点不是整个 task_struct,而是嵌入在 task_struct 里面的list_head。
- 运行队列 runqueue:放所有就绪 R 状态的进程,CPU 调度器在这里挑选进程分配时间片。进程只要在运行队列,就代表具备运行资格。
- 设备等待队列:进程发起 IO(读磁盘、读键盘),需要等待外设,进程进入阻塞。内核会把 PCB 的链表节点,从运行队列摘下来,挂载到外设对应的等待队列。此时进程不再抢占 CPU,静静等待外设事件完成。
- 交换挂起队列:内存不足,内核把进程的代码段、数据段写入磁盘 swap 分区,释放物理内存;PCB 依旧留在内存,加入挂起队列。
区分: 阻塞:进程代码数据还在内存,只是不抢占 CPU,挂在设备等待队列。 挂起:进程代码数据写到磁盘 swap,只有 PCB 留在内存,属于内存不足时的特殊场景。
2.3 外设设备树
Linux 所有硬件(磁盘、网卡、键盘)在内核都抽象成设备,每个设备自带独立的等待队列。 比如进程读硬盘文件,进程就挂载到磁盘设备的等待队列;进程等待键盘输入,挂载到键盘设备等待队列。

3. 进程全状态流转总图(理论模型)
⚠️注意:Linux 内核没有单独区分「就绪」和「运行」,统一标记为 R 状态,只要进程在 runqueue 运行队列里,STAT 就是 R。哪怕此刻没有正在占用 CPU,也是 R。
4. Linux 内核真实进程状态详解(R、S、D、T、t)
内核源码中,task_struct里面 state 成员记录状态,我们用ps aux看到 STAT 字段就是状态。
表格
| R | Running | 运行 / 就绪态,进程在 runqueue 运行队列,具备上 CPU 资格 | 就绪 / 运行 | ✅可以 kill |
| S | Sleeping(Interruptible) | 可中断浅睡眠,阻塞等待事件,能被信号唤醒 | 阻塞 | ✅可以 kill |
| D | Disk Sleep(Uninterruptible) | 不可中断深度睡眠,等待磁盘 IO,不能被信号打断 | 阻塞 | ❌常规 kill 杀不死 |
| T | Stopped | 停止状态,收到 SIGSTOP 信号暂停,进程冻结 | 停止 | 收到 SIGCONT 恢复 |
| t | Tracing stop | 追踪停止,GDB 断点调试时,进程停在断点 | 停止 | GDB 控制继续运行 |
4.1 R 状态 Running 运行 / 就绪态
一句话:只要 PCB 挂在运行队列 runqueue,状态就是 R。不一定此刻正在 CPU 上跑! CPU 时间片很短,CPU 高速切换多个进程,绝大多数 R 状态进程只是排队就绪。
- 当 CPU 调度选中,拿到时间片:真正在 CPU 运行
- 时间片用完,放回 runqueue:依旧是 R 就绪态
✅模拟方法:写死循环无 IO 代码,不 sleep、不等待外设。
// r_test.cpp
#include <iostream>
int main()
{
while(true)
{
// 死循环,无IO,持续占用CPU
}
return 0;
}
编译运行
g++ r_test.cpp -o rtest
./rtest
新开终端查看:
ps aux | grep rtest
STAT 列会看到R+,+代表进程在前台运行。
4.2 S 状态 Sleeping 可中断睡眠(浅阻塞)
一句话:进程正在等待某个事件(键盘输入、sleep 休眠),PCB 从运行队列摘下,挂载设备等待队列。可以被信号唤醒。 我们写sleep(1),进程主动放弃 CPU,进入 S 状态;发送 kill 信号,可以直接终止这个进程。
✅模拟代码:
// s_test.cpp
#include <iostream>
#include <unistd.h>
int main()
{
while(true)
{
std::cout << "进程pid: " << getpid() << std::endl;
sleep(1); // 主动休眠,进入S状态
}
return 0;
}
g++ s_test.cpp -o stest
./stest
新开终端执行ps aux,STAT 显示S+。 此时执行kill -9 进程pid,进程直接退出,S 状态能被信号杀死。
4.3 D 状态 Disk Sleep 不可中断深度睡眠
一句话:进程正在等待磁盘 IO 完成,不可被信号打断,常规 kill 命令无法杀死进程。 目的:保护磁盘 IO 的数据一致性。 场景:进程往磁盘写入数据,磁盘硬件读写过程中,内核不允许进程被中断。如果中途杀掉进程,磁盘数据会损坏丢失。 进程 PCB 此时挂载磁盘设备等待队列,D 状态。
⚠️重要提醒:D 状态不建议手动模拟! D 状态进程无法被 kill -9 杀死,只能等待 IO 结束,或者直接重启服务器。线上服务器出现大量 D 进程,会导致服务器卡死,只能重启。 模拟 D 状态会造成虚拟机卡死风险,仅讲解原理,不推荐实操。
4.4 T 状态 Stopped 停止状态
一句话:进程被暂停冻结,PCB 保留,所有代码暂停执行,收到 SIGCONT 恢复运行。 使用信号SIGSTOP(Ctrl+Z)触发。
✅模拟操作:
./stest
终端输出:进程已经停止,状态变为 T
ps aux | grep stest
STAT 字段显示T。 4. 恢复进程执行:
fg
# 或者发送信号
kill -SIGCONT pid
4.5 t 状态 Tracing stop 追踪停止
一句话:GDB 调试打断点的时候,进程停在断点处,状态为t。属于调试器追踪暂停。
✅模拟步骤:
g++ s_test.cpp -o stest -g
gdb ./stest
b main
run
T 和 t 的区分:
- T:普通暂停,Ctrl+Z/SIGSTOP,普通暂停
- t:被调试器 GDB 追踪暂停,断点专用状态
5. Linux 内核链表:task_struct 的设计精髓「同一块 PCB,多条独立链表」
5.1 传统链表 VS Linux 嵌入式 list_head 链表
传统单向 / 双向链表写法:链表节点里面放数据
struct Node{
int data;
struct Node* next;
struct Node* prev;
};
缺点:每一种数据结构,都要重新写一套链表代码,无法复用。
Linux 内核的思路反过来:数据结构体里面嵌入链表钩子list_head
struct list_head{
struct list_head *next;
struct list_head *prev;
};
struct task_struct{
// 一堆进程属性 pid、state…
struct list_head links; // 嵌入在PCB内部的链表钩子
};
比喻:PCB 是主板,list_head就是主板上的多个插座(links 钩子)。 不同链表(全局 tasks 链表、runqueue 运行队列、设备等待队列),插在 PCB 不同插座上。同一块 PCB,可以同时挂在多条完全独立的链表里面。也就是经典的绳子穿实体设计。
5.2 offsetof 宏原理:通过链表指针找回整个 PCB
我们拿到list_head *指针,如何反向找到完整的task_struct? 核心宏offsetof:计算结构体成员,距离结构体起始地址的偏移量。
#define offsetof(type, member) ((size_t)&((type*)0)->member)
公式:结构体首地址 = 成员地址 – 成员偏移量 内核container_of宏就是基于这个原理,拿到链表指针,反向拿到整个 task_struct。
5.3 进程在队列间流动的本质:只修改链表钩子,不拷贝 PCB!
进程从运行态进入阻塞(等待磁盘 IO),内核只做两件 O (1) 操作:
没有拷贝整个 task_struct!只修改两个指针,性能极高,这就是 Linux 内核链表设计的强大之处。
5.4 三条独立链表(同一 PCB,多根绳子)
当进程状态变化,只是把 PCB 从一根绳子摘下,挂到另一根绳子。PCB 实体不变,只改动链表指针。

6. 进程状态实操:Ubuntu 模拟 + 查看状态命令
6.1 查看进程状态工具
① ps aux(最常用,静态快照)
ps aux
字段STAT就是进程状态,额外后缀含义:
- + 前台进程
- s 会话组长
- l 多线程
② top /htop(动态实时查看)
top
实时刷新 CPU、进程状态。
6.2 R 状态模拟(前面代码)
死循环无 IO,持续占用 CPU,ps 看到 R。
注意:如果机器 CPU 多核,有其他进程抢占,偶尔状态会短暂切换,但绝大多数为 R。
6.3 S 状态模拟
sleep 休眠代码,进程等待时间事件,STAT=S,可以 kill 杀死。
6.4 T 状态模拟
运行程序,Ctrl+Z,进程暂停,STAT=T;fg恢复。
6.5 t 状态模拟
gdb 加断点调试,进程停在断点,ps 看到 t 状态。
6.6 D 状态说明
D 是不可中断深度睡眠,不建议模拟。 D 状态进程等待磁盘 IO,kill -9无法终止。如果手动构造 D 进程,虚拟机大概率卡死,只能重启。
什么时候会出现 D:大量磁盘读写、硬盘故障、NFS 网络磁盘挂载异常。排查 D 进程只能等待 IO 完成或者重启。
7. 交换分区 & 进程挂起状态(内存不足场景)
当物理内存不足,操作系统会触发内存回收,把进程代码、数据写入磁盘 swap 交换分区。
关键点: ✅ PCB (task_struct) 仍然保留在内存,不会换出去 ✅ 进程代码段、数据段放到磁盘 swap 分为两种挂起:
图形文字描述

挂起和阻塞的区别:
- 阻塞:代码数据还在物理内存,PCB 放到设备等待队列,只是不抢占 CPU
- 挂起:代码数据放到磁盘 swap,释放物理内存,PCB 依旧留在内存
Linux 的 ps 命令没有单独标记挂起就绪 / 挂起阻塞,内核底层存在这个机制,用户态 ps 不直接展示。
8. 全文总结
- R:运行 / 就绪,在 runqueue 运行队列
- S:可中断阻塞睡眠,能被信号 kill
- D:不可中断深度 IO 睡眠,kill 无法杀死,禁止随便模拟
- T:Ctrl+Z SIGSTOP 普通暂停
- t:GDB 断点追踪暂停
9. 下回分解预告
本篇到此结束,Z 僵尸进程、孤儿进程留作下一章《Re:Linux 系统篇(十)进程退出与进程等待》 预告要点:





