Re:Linux 系统篇(十三):进程篇(二)进程状态深度剖析 | PCB、内核链表、R/S/D/T/t 状态模拟实操

发布时间:2026/9/2 21:06:24
Re:Linux 系统篇(十三):进程篇(二)进程状态深度剖析 | PCB、内核链表、R/S/D/T/t 状态模拟实操 观众老爷们大家好 这里是邪修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 不会换出进程的本质PCBtask_struct 代码段 数据段PCB内核用来管理进程的控制块保存 PID、状态、优先级、链表指针等全部管理信息代码 数据程序的指令、变量放在内存内存不够时可以换入 / 换到磁盘交换分区 swap队列模型重中之重运行队列 runqueue就绪 / 正在运行的 PCB排队等待 CPU 调度CPU 调度器只从这个队列挑选进程上 CPU 运行设备等待队列 wait_queue阻塞进程所在队列。进程等外设键盘读入、磁盘 IO时PCB 从运行队列摘下挂载到对应外设的等待队列等待事件完成。交换分区队列挂起队列内存资源吃紧时内核把进程的代码、数据写入磁盘 swap 分区PCB 依旧留在内存加入挂起队列。2. 基础概念铺垫PCB (task_struct)、硬件与三大队列2.1 PCB task_structLinux 内核中每一个进程在内核里都会生成一个struct task_struct结构体也就是 PCB 进程控制块。里面包含PID、PPID、进程状态 state、优先级、内存指针、文件描述符、嵌入式链表成员struct list_head links。重点PCB 永远保存在内存哪怕进程代码被换到磁盘 swapPCB 不会被换出去。内核必须靠 PCB 找到这个进程。2.2 三大队列的本质链表队列底层全部是内核双向循环链表链表节点不是整个 task_struct而是嵌入在 task_struct 里面的list_head。运行队列 runqueue放所有就绪 R 状态的进程CPU 调度器在这里挑选进程分配时间片。进程只要在运行队列就代表具备运行资格。设备等待队列进程发起 IO读磁盘、读键盘需要等待外设进程进入阻塞。内核会把 PCB 的链表节点从运行队列摘下来挂载到外设对应的等待队列。此时进程不再抢占 CPU静静等待外设事件完成。交换挂起队列内存不足内核把进程的代码段、数据段写入磁盘 swap 分区释放物理内存PCB 依旧留在内存加入挂起队列。区分阻塞进程代码数据还在内存只是不抢占 CPU挂在设备等待队列。挂起进程代码数据写到磁盘 swap只有 PCB 留在内存属于内存不足时的特殊场景。2.3 外设设备树Linux 所有硬件磁盘、网卡、键盘在内核都抽象成设备每个设备自带独立的等待队列。比如进程读硬盘文件进程就挂载到磁盘设备的等待队列进程等待键盘输入挂载到键盘设备等待队列。3. 进程全状态流转总图理论模型新建程序加载到内存创建 PCB就绪进程准备好代码数据在内存在运行队列排队等待 CPU运行进程拿到 CPU 时间片CPU 正在执行代码阻塞运行中的进程需要等待事件磁盘 IO、键盘输入主动让出 CPU进入设备等待队列挂起就绪 / 挂起阻塞内存紧张进程代码数据放到 swap 磁盘PCB 保留在内存结束进程执行完毕释放资源PCB 进入回收阶段下篇讲 Z 僵尸⚠️注意Linux 内核没有单独区分「就绪」和「运行」统一标记为 R 状态只要进程在 runqueue 运行队列里STAT 就是 R。哪怕此刻没有正在占用 CPU也是 R。4. Linux 内核真实进程状态详解R、S、D、T、t内核源码中task_struct里面 state 成员记录状态我们用ps aux看到 STAT 字段就是状态。表格标记状态全称含义属于理论模型哪一类是否可被信号杀死RRunning运行 / 就绪态进程在 runqueue 运行队列具备上 CPU 资格就绪 / 运行✅可以 killSSleepingInterruptible可中断浅睡眠阻塞等待事件能被信号唤醒阻塞✅可以 killDDisk SleepUninterruptible不可中断深度睡眠等待磁盘 IO不能被信号打断阻塞❌常规 kill 杀不死TStopped停止状态收到 SIGSTOP 信号暂停进程冻结停止收到 SIGCONT 恢复tTracing stop追踪停止GDB 断点调试时进程停在断点停止GDB 控制继续运行4.1 R 状态 Running 运行 / 就绪态一句话只要 PCB 挂在运行队列 runqueue状态就是 R。不一定此刻正在 CPU 上跑CPU 时间片很短CPU 高速切换多个进程绝大多数 R 状态进程只是排队就绪。当 CPU 调度选中拿到时间片真正在 CPU 运行时间片用完放回 runqueue依旧是 R 就绪态✅模拟方法写死循环无 IO 代码不 sleep、不等待外设。// r_test.cpp#includeiostreamintmain(){while(true){// 死循环无IO持续占用CPU}return0;}编译运行g r_test.cpp -o rtest ./rtest新开终端查看ps aux | grep rtestSTAT 列会看到R代表进程在前台运行。4.2 S 状态 Sleeping 可中断睡眠浅阻塞一句话进程正在等待某个事件键盘输入、sleep 休眠PCB 从运行队列摘下挂载设备等待队列。可以被信号唤醒。我们写sleep(1)进程主动放弃 CPU进入 S 状态发送 kill 信号可以直接终止这个进程。✅模拟代码// s_test.cpp#includeiostream#includeunistd.hintmain(){while(true){std::cout进程pid: getpid()std::endl;sleep(1);// 主动休眠进入S状态}return0;}g s_test.cpp -o stest ./stest新开终端执行ps auxSTAT 显示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 恢复运行。使用信号SIGSTOPCtrlZ触发。✅模拟操作运行前面的 stest 程序./stest在当前终端按下Ctrl Z终端输出进程已经停止状态变为 T查看状态ps aux | grep stestSTAT 字段显示T。4. 恢复进程执行fg # 或者发送信号 kill -SIGCONT pid4.5 t 状态 Tracing stop 追踪停止一句话GDB 调试打断点的时候进程停在断点处状态为t。属于调试器追踪暂停。✅模拟步骤编译代码加上 - g 调试信息g s_test.cpp -o stest -ggdb 启动程序gdb ./stest设置断点run 运行触发断点b main run新开终端ps 查看进程状态STAT 为t当 GDB 执行continue进程继续运行t 状态消失。T 和 t 的区分T普通暂停CtrlZ/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_headstruct 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) 操作list_del把 PCB 的 runqueue 链表钩子从运行队列摘下list_add把 PCB 的等待队列钩子挂载到磁盘设备等待队列没有拷贝整个 task_struct只修改两个指针性能极高这就是 Linux 内核链表设计的强大之处。5.4 三条独立链表同一 PCB多根绳子全局 tasks 链表一根红线系统内所有存活进程全部串起来。内核遍历系统全部进程走这个链表。只要进程活着就一直在这个链表。运行队列 runqueue橙色绳子。只串联 R 状态就绪 / 运行的 PCB调度器只看这根链表挑选进程。设备等待队列 wait_queue蓝色绳子。每一个硬件设备独立一根链表阻塞进程挂载在这里。当进程状态变化只是把 PCB 从一根绳子摘下挂到另一根绳子。PCB 实体不变只改动链表指针。6. 进程状态实操Ubuntu 模拟 查看状态命令6.1 查看进程状态工具① ps aux最常用静态快照ps aux字段STAT就是进程状态额外后缀含义前台进程s会话组长l多线程② top /htop动态实时查看top实时刷新 CPU、进程状态。6.2 R 状态模拟前面代码死循环无 IO持续占用 CPUps 看到 R。注意如果机器 CPU 多核有其他进程抢占偶尔状态会短暂切换但绝大多数为 R。6.3 S 状态模拟sleep 休眠代码进程等待时间事件STATS可以 kill 杀死。6.4 T 状态模拟运行程序CtrlZ进程暂停STATTfg恢复。6.5 t 状态模拟gdb 加断点调试进程停在断点ps 看到 t 状态。6.6 D 状态说明D 是不可中断深度睡眠不建议模拟。D 状态进程等待磁盘 IOkill -9无法终止。如果手动构造 D 进程虚拟机大概率卡死只能重启。什么时候会出现 D大量磁盘读写、硬盘故障、NFS 网络磁盘挂载异常。排查 D 进程只能等待 IO 完成或者重启。7. 交换分区 进程挂起状态内存不足场景当物理内存不足操作系统会触发内存回收把进程代码、数据写入磁盘 swap 交换分区。关键点✅ PCB (task_struct) 仍然保留在内存不会换出去✅ 进程代码段、数据段放到磁盘 swap分为两种挂起挂起就绪进程代码换磁盘PCB 在内存等待内存充足时换回内存加入运行队列挂起阻塞进程代码换磁盘PCB 挂在设备等待队列事件触发后等待内存换回图形文字描述挂起和阻塞的区别阻塞代码数据还在物理内存PCB 放到设备等待队列只是不抢占 CPU挂起代码数据放到磁盘 swap释放物理内存PCB 依旧留在内存Linux 的 ps 命令没有单独标记挂起就绪 / 挂起阻塞内核底层存在这个机制用户态 ps 不直接展示。8. 全文总结进程管理核心是 PCBtask_struct保存进程所有信息PCB 常驻内存。Linux 用户态可见状态R、S、D、T、t。R运行 / 就绪在 runqueue 运行队列S可中断阻塞睡眠能被信号 killD不可中断深度 IO 睡眠kill 无法杀死禁止随便模拟TCtrlZ SIGSTOP 普通暂停tGDB 断点追踪暂停内核链表设计嵌入式 list_head同一个 PCB 可以挂载多条独立链表进程状态切换本质是摘链、挂链O (1) 高效操作不拷贝 PCB。队列划分运行队列 runqueueR、外设等待队列S/D 阻塞、swap 交换挂起队列内存不足。阻塞是等待外设事件挂起是内存不足代码被换到磁盘 swap二者不是一回事。查看状态ps aux、topR/S/T/t 都可以在 Ubuntu 写代码模拟D 状态风险高不建议模拟。9. 下回分解预告本篇到此结束Z 僵尸进程、孤儿进程留作下一章《Re:Linux 系统篇十进程退出与进程等待》预告要点Z 僵尸进程进程代码执行完毕退出PCB 没有被父进程回收残留在内核孤儿进程父进程提前退出子进程被 init/systemd 领养wait/waitpid父进程回收僵尸 PCB释放内核资源