Re: Linux系统篇(十八)进程篇·三:深度硬核!全面起底 Linux 进程状态变化与内核链表动态解绑


◆ 博主名称: 晓此方-CSDN博客 大家好,欢迎来到晓此方的博客。
⭐️Linux系列个人专栏: 【主题曲】Linux
⭐️此方的GitHub: github_此方
⭐️ Re系列专栏:我们思考 (Rethink) · 我们重建 (Rebuild) · 我们记录 (Record)


文章目录


概要&序論

  Hello大家好,我是此方 ,上文我们初步探讨了**进程** 这个话题,了解了进程的概念和进程的父子关系,今天将继续深入了解进程的各种状态,好的,现在我们开始吧。

一、操作系统的"进程状态"

  如下图,精辟的总结了课本上提到的进程状态,确实有些复杂。不过我想要说的是,这张图讲解的是操作系统的进程状态的总理论,而我们今天要讲的Linux进程状态,和它的关系是特殊性与普遍性的关系。

二、Linux中的进程状态

2.1进程的状态有哪些

  为了弄明白正在运行的进程是什么意思,我们需要知道进程的不同状态 。一个进程可以有几个状态(在Linux内核里,进程有时候也叫做任务)。

  下面的状态在kernel源代码里定义:

cpp 复制代码
/*
 * The task state array is a strange "bitmap" of reasons to sleep. 
 * Thus "running" is zero, and you can test for combinations of 
 * others with simple bit tests.
 */
static const char *const task_state_array[] = {
  "R (running)", /*0 */
  "S (sleeping)", /*1 */
  "D (disk sleep)", /*2 */
  "T (stopped)", /*4 */
  "t (tracing stop)", /*8 */
  "X (dead)", /*16 */
  "Z (zombie)", /*32 */
};
  • R运行状态(running): 并不意味着进程一定在运行中,它表明进程要么是在运行中要么在运行队列里。
  • S睡眠状态(sleeping): 意味着进程在等待事件完成(这里的睡眠有时候也叫做可中断睡眠(interruptible sleep))。
  • D磁盘休眠状态(Disk sleep): 有时候也叫不可中断睡眠状态(uninterruptible sleep),在这个状态的进程通常会等待 IO 的结束。
  • T停止状态(stopped): 可以通过发送 SIGSTOP 信号给进程来停止(T)进程。这个被暂停的进程可以通过发送 SIGCONT 信号让进程继续运行。
  • X死亡状态(dead): 这个状态只是一个返回状态,你不会在任务列表里看到这个状态。

  一句话都听不懂?没有关系,拆开来我们一个一个讲:

2.2调度队列FIFO算法&&进程运行状态

  在谈论进程的运行状态之前,我需要先讲一讲进程的调度队列:我们上一篇中讲到了,进程是由PCB+代码与数据构成的,进程的PCB里面有一个指针指向下一个PCB,于是就形成了"全局进程链表"。

  我们这里讲的队列和全局链表非常相似,为什么这个它即是链表又是队列?我们后面会讲。

先抛出一个结论:"进程在调度队列里面==进程处于运行状态"。

  通过进程的调度队列,我们也得到一个名词:"进程的FIFO算法": 进程按照队列的规则先进先出,依次被CPU调用执行。

2.3设备树&&进程的阻塞状态

2.3.1什么是设备树

  我们的计算机,它的摄像头,麦克风,键盘鼠标,显示器以及磁盘网卡等等,一系列的硬件,他们都是操作系统通过硬件驱动调用的,

  然而硬件驱动调用硬件不是直接访问硬件本身,实际上其内部也经厉了"先描述再组织"的过程,操作系统将他们的数据封装成为一个结构体 ,然后将他们组织在一起于是就形成了设备树。

  我后来发现这一段的讲解极为不准确,设备树和等待队列没有任何关系。 我在后文(3.2.2)有纠错。

2.3.2什么是阻塞状态

  设备树上的每一个结点都有一个等待队列,输出一个结论:进程在等待队列中的时候,就是阻塞状态。

  那么,什么是阻塞?当一个进程正在运行,但它发起的某个请求(通常是 I/O 请求)暂时无法得到满足 ,或者必须等待某个事件(如磁盘数据读取、网卡数据到达)发生时,操作系统会剥夺该进程的 CPU 使用权,将其从运行队列(Run Queue)中移出。

  然后该进程就会被链入对应设备的等待队列当中。 等待设备就绪。

  什么是就绪?又一个问题。这很简单,我们键盘键入,硬盘读取,这些都是设备就绪。

2.4交换分区与进程挂起

  挂起首先你要知道,这是一种非常极端的情况,一般不会发生。什么时候会发生呢?你的操作系统认识到内存资源严重不足的时候 (我们所讲的内存资源是"物理内存"而不是"虚拟内存",放在虚拟地址空间中讲

2.4.1交换分区的定义

  交换分区,我们首先需要了解的概念。它是磁盘中的一个概念,在磁盘中有这么一块临时区域,用来临时存放从内存中交换过来的代码。

2.4.2挂起与优先挂起

  那么当程序挂起,就是操作系统将程序的代码和数据从内存中提取出来,挂在这个交换分区当中。以此来腾出空间给其他进程使用。缓减内存压力。这个过程中PCB不会动。

  那么谁会被优先带走呢?答案是正在阻塞的进程。他们不着急使用CPU资源,所以先挂起。 在极端中的极端情况,有可能运行队列中靠后的一些进程的代码和数据也会被挂起来。当然这很少见。

2.5进程的状态变化

  从运行到阻塞,从阻塞到挂起,从运行到挂起,以及他们的所有的反方向。在之前我们全部讲完了。现在给一张总结图:

三、理解内核链表的相关话题

3.1内核链表不是传统链表

  我们Linux中的一个个PCB,他们被链接起来形成一个链表,这没有错,但是现在我要讲的可能会颠覆你的认知。

  Linux内核使用的链表不是传统的链表,传统的链表我们可以画一个潦草的示意图:

  一个带头循环双向链表,每一个结点里面都会有两个struct Node*指针,指向上一个/下一个结点。

  那么Linux的内核链表呢?试想一下:把前后指针独立封装一下,让它和你的数据解耦,会发生什么?这就是Linux内核链表的想法。

cpp 复制代码
struct list_head {
    struct list_head *next, *prev;
};
struct task_struct{
    int x, y, z;
    struct list_head links;
    ...
};

  也即是说,现在这个链表本身依赖的不是指针,而是这样一个连接器结构体,它来帮助前后PCB建立联系。

3.2为什么内核链表要这么设计

3.2.1更方便的偏移量计算------offset宏

  现在,如果你手里只有一个指向 links 成员的指针,想要拿到整个 task_struct 的首地址,你就必须知道 links 在这个结构体里"往后挪了多少距离"。

结构体首地址 = 成员地址 − 偏移量 结构体首地址 = 成员地址 - 偏移量 结构体首地址=成员地址−偏移量

  有一种非常通用的计算方式:
o f f s e t = & ( ( ( s t r u c t t a s k _ s t r u c t ∗ ) 0 ) → l i n k s ) offset = \&(((struct\ task\_struct\ *)0)\to links) offset=&(((struct task_struct ∗)0)→links)

  这种计算方式实在是太妙了。

  • 首先人为设定起点:将 0 强制转换为结构体指针,相当于在内存地址 0 的位置"虚拟"出一个结构体。
  • 算位置:访问成员 links 并取地址,编译器会根据内存对齐规则,算出它相对于起点(即地址 0)的距离。
  • 空位即偏移:因为起点是 0,所以得到的成员地址在数值上就精准等于它在结构体里的偏移量。

  C自带一个宏可以直接获取这个偏移量,上面的讲解就是它的底层。

  • 首先包含头文件:#include <stddef.h>
  • 第一个参数填结构体类型,第二个填成员名。
cpp 复制代码
size_t offset = offsetof(struct task_struct, links);

3.2.2彻底理清"流动"的本质:多链表动态解绑与重组

  这一段内容会再一次颠覆你的认知。

  在探讨进程如何在各种队列中"流动"之前,我们需要先纠正一个极易混淆的误区:

⚠️ 重要纠错:设备树 ≠ \neq = 等待队列

我上文把"设备树"和"等待队列"混为一谈,认为进程阻塞是挂在设备树上。这是极其不准确的!设备树和等待队列没有半毛钱关系。

  • 设备树 (Device Tree):是系统启动时,内核用来清点、初始化硬件的静态"点名册",不参与运行时进程的调度。
  • 等待队列 (Wait Queue):是驱动程序内部维护的运行时动态链表,专门用来组织正在等待该设备的进程。
3.2.2.1物理上:它们"住在一起"

  在物理内存中,一个进程有且仅有一个 task_struct(进程控制块 PCB)实例。这个结构体非常庞大,包含了一个进程的所有核心信息(PID、状态、优先级等)。它就像一个巨大的"多功能插线板",身上长满了各种不同的接口 ------ 这些接口就是内嵌在其中的 struct list_head 成员变量。

3.2.2.2逻辑上:它们属于"完全不同的绳子"

  虽然所有的插口都长在同一个 PCB 上,但每个插口连接的都是不同的、相互独立的、没有交集的独立链表。这就是著名的"用绳子穿过实体"的设计:

  • 全局链表(Tasks List) :像一根红色的绳子,横向穿过每个 PCB 的 tasks 插口。只要进程存活,这条线就永远不解绑,内核通过它来遍历系统中的所有进程。
  • 运行队列(Runqueue) :像一根橙色的绳子,只穿过那些状态为"就绪(Ready)"或"运行(Running)"的 PCB 的 run_list 插口。调度器(Scheduler)只在这条线上挑选进程去 CPU 上执行。
  • 某一条等待队列(Wait Queue) :设备千千万,等待队列的蓝线也有千千万。每个硬件设备(网卡、键盘、磁盘等)都有自己专属的一条等待队列线。
3.2.3.3"流动"是如何发生的?

当一个进程从"运行态"变成"阻塞态"(例如等待读取磁盘文件)时,内核绝对不会去搬动或复制这个巨大的 PCB,它在幕后只干了两件事:

  1. 解绑(断开旧线) :调用 list_del,把该 PCB 负责"运行队列"的 run_list 钩子从橙线上取下来。
  2. 重连(挂上新线) :调用 list_add,把该 PCB 负责"等待队列"的 wait_list 钩子,挂到该磁盘设备特有的那条蓝色等待队列线上。
3.2.2.4核心总结
  • 实体唯一 :内存中只有 task_struct 一个实体静静地呆着。
  • 状态改变 = 关系解绑与重组 :进程所谓的"状态流动",物理本质上只是不同颜色的绳子(链表指针)在 PCB 的不同插口上进行解绑和重新勾连的过程
  • 高效率的秘密 :这种设计让进程状态切换变成了极其高效的指针操作( O ( 1 ) O(1) O(1) 复杂度),不需要任何内存拷贝,完美体现了 Linux 内核"扁平化且高效"的管理哲学。

四、进程状态的查看

4.1回顾Linux的进程状态类型

  把上面你看不懂的那张表搬过来,现在你至少能读懂一部分了,接下来我们来查看一下进程的状态

cpp 复制代码
/*
 * The task state array is a strange "bitmap" of reasons to sleep. 
 * Thus "running" is zero, and you can test for combinations of 
 * others with simple bit tests.
 */
static const char *const task_state_array[] = {
  "R (running)", /*0 */
  "S (sleeping)", /*1 */
  "D (disk sleep)", /*2 */
  "T (stopped)", /*4 */
  "t (tracing stop)", /*8 */
  "X (dead)", /*16 */
  "Z (zombie)", /*32 */
};

  进程状态的本质就是一个在PCB中的整型,是一个状态数组的下标。

4.2运行R与睡眠S状态

  Linux的Sleep就先当于操作系统的阻塞状态

cpp 复制代码
#include <iostream>
#include <unistd.h> 
int main() {
    std::cout << std::unitbuf;
    int count = 0;
    pid_t pid = getpid();
    while (true) {
        std::cout << "我是后台进程, PID: " << pid << ", 循环次数: " << ++count << std::endl;
        sleep(1); 
    }
    return 0;
}

  为什么我们查到的进程状态都是S?因为我们的进程是一个循环打印,假设执行一次是1毫秒,在这毫秒中99%的时间是在等待显示屏就绪,1%的时间是拿来打印,于是运气不好我们查不出来

cpp 复制代码
#include <iostream>
#include <unistd.h>
int main() {
    // 打印当前的 PID,方便你后续去查状态或杀进程
    std::cout << "进程已启动, PID: " << getpid() << ",请在另一个终端观察其状态..." << std::endl;
    // 纯 CPU 计算的死循环,没有任何 I/O 打印,也没有 sleep
    while (true) {}
    return 0;
}

  不让他IO,查出来全部都是R

  如果进程在前台,那么状态前面就有一个+号,否则没有。

  加上&表示当前进程在后台启动

bash 复制代码
[whb@bite-alicloud lesson13]$ ./myprocess &

4.3停止T与追踪停止状态t

  t:表示进程处于"正在被追踪而停止"的状态。 虽然它也属于停止状态,但它与大写 T 有细微的语义区别:

  • 大写 T (Stopped):通常是用户主动发送信号(如 Ctrl+Z)让进程停下的。
  • 小写 t (Tracing stop):专门用于 Debugger(调试器)环境。当进程被系统追踪(比如你在用 gdb 调试时),进程在等待调试器发送下一个指令(c/s)期间,状态会显示为小写的 t。

  关于19号信号稍微补充一下,信号还没有学:在 Linux 信号机制中,19号信号SIGSTOP

  SIGSTOP 的唯一作用就是强制停止(挂起)一个进程 。当进程接收到这个信号时,它的状态会立即转变为 T (Stopped)。你按下ctrl +Z也是传递这个信号给进程。

  • 一种很有趣的场景 :含 scanf 等阻塞式输入函数的进程被放到后台运行。
  • 行为分析
    • 后台进程无法直接从终端读取用户输入。
    • 当进程尝试执行 scanf 等终端输入系统调用时,内核会向该进程发送 SIGTTIN 信号 ,强制将其暂停(Stopped, T)
  • 解决办法 :用户需要通过 fg 命令 将该进程调回前台,才能正常输入并继续执行。

4.4深度睡眠D

4.4.1对比一般睡眠状态

  浅睡眠S,比如有一个scanf在等待阻塞 。然后我们直接按ctrl+c可以直接kill它。或者执行指令kill -9 PID杀死(这个是信号的知识,后面会讲)但是深度睡眠D状态,怎么都不会杀死。如何杀掉D进程?1.等他自己醒来。2.物理方法(重启计算机/断电)

4.4.2深度睡眠的意义

  场景:一个进程正在向磁盘写入数据

但是磁盘写入数据的速度超级慢(相对于进程而言),于是进程就要去等待磁盘(进入S状态)。等待过程中可能发生一种非常极端的内存不足情况,这个时候OS就要开始杀没有用的进程。

  这个时候可能会误杀这个正在"摆烂"的进程。于是这个时候磁盘还没有读完数据但是进程已经被干掉了,磁盘拿着数据不知道该怎么办,于是就只能把数据丢弃了。这个时候用户还不知道,于是就丢失了数据。 (如果这个数据是医院里几千个病人的病历,或者是银行的转账信息,这是非常危险的。)

  为了防止操作系统发生上述情况。于是一种新的状态被设计出来。D状态,不可中断。

4.4.3测试方法

  构建一个块级 IO,通过以下命令模拟高强度的磁盘写入操作:

bash 复制代码
dd if=/dev/zero of=~/test.txt bs=4096 count=100000
  • dd: 磁盘拷贝/转换命令。
  • if=/dev/zero: 输入文件(数据来源,源源不断地产生零字节)。
  • of=~/test.txt: 输出文件(拷贝到的目标文件路径)。
  • bs=4096: 每次拷贝的数据块大小(Block Size),此处为 4096 字节(4KB)。
  • count=100000: 一共拷贝的数据块数量(这里一共拷贝 100000 次)。

好的本期内容就到这里,如果对你有帮助,还不要忘记点赞三联支持。我是此方,我们下期再见。bye!

相关推荐
HLC++10 小时前
Linux的进程间通信
android·linux·服务器
华清远见IT开放实验室12 小时前
实验室建设案例 | 石家庄科技信息职业学院嵌入式实验室——从底层硬件到系统应用,一所应用型高校的嵌入式人才培养这样落地
linux·arm开发·stm32·嵌入式硬件·高校·实验室建设
groundhappy14 小时前
idalib安装和codex ida-mcp配置
linux·开发语言·python
通信小小昕15 小时前
Ubuntu 26.04 中文输入法安装
linux·运维·ubuntu
张小姐的猫16 小时前
【Linux】网络编程 —— HTTP协议(上)
linux·运维·服务器·网络·http·单例模式·策略模式
栩栩云生16 小时前
AI 写代码犯的错,早被写进了错题集
linux·安全·ai编程
imc.1118 小时前
linux基础IO
linux·运维·服务器
BelongPanda19 小时前
Linux Nginx 纯手动 Let‘s Encrypt 泛域名证书配置教程
linux·nginx
酷可达拉斯20 小时前
Linux操作系统-shell编程(0)
linux·运维·服务器·python·云计算
2301_7779983420 小时前
Linux中断机制:操作系统如何高效运行
linux·运维·服务器