欢迎光临
我们一直在努力

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 不会换出!

进程的本质:PCB(task_struct) + 代码段 + 数据段

  • PCB:内核用来管理进程的控制块,保存 PID、状态、优先级、链表指针等全部管理信息
  • 代码 & 数据:程序的指令、变量,放在内存;内存不够时可以换入 / 换到磁盘交换分区 swap

队列模型(重中之重):

  • 运行队列 runqueue:就绪 / 正在运行的 PCB,排队等待 CPU 调度;CPU 调度器只从这个队列挑选进程上 CPU 运行
  • 设备等待队列 wait_queue:阻塞进程所在队列。进程等外设(键盘读入、磁盘 IO)时,PCB 从运行队列摘下,挂载到对应外设的等待队列,等待事件完成。
  • 交换分区队列(挂起队列):内存资源吃紧时,内核把进程的代码、数据写入磁盘 swap 分区;PCB 依旧留在内存,加入挂起队列。
  • 在这里插入图片描述


    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. 进程全状态流转总图(理论模型)

  • 新建:程序加载到内存,创建 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 字段就是状态。

    表格

    标记状态全称含义属于理论模型哪一类是否可被信号杀死
    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 程序
  • ./stest

  • 在当前终端按下 Ctrl + Z
  • 终端输出:进程已经停止,状态变为 T

  • 查看状态
  • ps aux | grep stest

    STAT 字段显示T。 4. 恢复进程执行:

    fg
    # 或者发送信号
    kill -SIGCONT pid

    4.5 t 状态 Tracing stop 追踪停止

    一句话:GDB 调试打断点的时候,进程停在断点处,状态为t。属于调试器追踪暂停。

    ✅模拟步骤:

  • 编译代码加上 – g 调试信息
  • g++ s_test.cpp -o stest -g

  • gdb 启动程序
  • gdb ./stest

  • 设置断点,run 运行,触发断点
  • b main
    run

  • 新开终端,ps 查看进程状态,STAT 为t 当 GDB 执行continue,进程继续运行,t 状态消失。
  • 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) 操作:

  • 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,持续占用 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 在内存,等待内存充足时,换回内存,加入运行队列
  • 挂起阻塞:进程代码换磁盘,PCB 挂在设备等待队列,事件触发后,等待内存换回
  • 图形文字描述

    在这里插入图片描述

    挂起和阻塞的区别:

    • 阻塞:代码数据还在物理内存,PCB 放到设备等待队列,只是不抢占 CPU
    • 挂起:代码数据放到磁盘 swap,释放物理内存,PCB 依旧留在内存

    Linux 的 ps 命令没有单独标记挂起就绪 / 挂起阻塞,内核底层存在这个机制,用户态 ps 不直接展示。


    8. 全文总结

  • 进程管理核心是 PCB task_struct,保存进程所有信息,PCB 常驻内存。
  • Linux 用户态可见状态:R、S、D、T、t。
    • R:运行 / 就绪,在 runqueue 运行队列
    • S:可中断阻塞睡眠,能被信号 kill
    • D:不可中断深度 IO 睡眠,kill 无法杀死,禁止随便模拟
    • T:Ctrl+Z SIGSTOP 普通暂停
    • t:GDB 断点追踪暂停
  • 内核链表设计:嵌入式 list_head,同一个 PCB 可以挂载多条独立链表;进程状态切换本质是摘链、挂链,O (1) 高效操作,不拷贝 PCB。
  • 队列划分:运行队列 runqueue(R)、外设等待队列(S/D 阻塞)、swap 交换挂起队列(内存不足)。
  • 阻塞是等待外设事件,挂起是内存不足,代码被换到磁盘 swap,二者不是一回事。
  • 查看状态:ps aux、top;R/S/T/t 都可以在 Ubuntu 写代码模拟;D 状态风险高,不建议模拟。
  • 9. 下回分解预告

    本篇到此结束,Z 僵尸进程、孤儿进程留作下一章《Re:Linux 系统篇(十)进程退出与进程等待》 预告要点:

  • Z 僵尸进程:进程代码执行完毕退出,PCB 没有被父进程回收,残留在内核
  • 孤儿进程:父进程提前退出,子进程被 init/systemd 领养
  • wait/waitpid,父进程回收僵尸 PCB,释放内核资源

  • 在这里插入图片描述

    赞(0)
    未经允许不得转载:171主机测评 » Re:Linux 系统篇(十三):进程篇(二)进程状态深度剖析 | PCB、内核链表、R/S/D/T/t 状态模拟实操
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址