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

发布时间:2026/5/21 22:28:43

Re: Linux系统篇(十八)进程篇·三:深度硬核!全面起底 Linux 进程状态变化与内核链表动态解绑 ◆ 博主名称 晓此方-CSDN博客大家好欢迎来到晓此方的博客。⭐️Linux系列个人专栏 【主题曲】Linux⭐️此方的GitHub github_此方⭐️Re系列专栏我们思考 (Rethink) · 我们重建 (Rebuild) · 我们记录 (Record)文章目录概要序論一、操作系统的“进程状态”二、Linux中的进程状态2.1进程的状态有哪些2.2调度队列FIFO算法进程运行状态2.3设备树进程的阻塞状态2.3.1什么是设备树2.3.2什么是阻塞状态2.4交换分区与进程挂起2.4.1交换分区的定义2.4.2挂起与优先挂起2.5进程的状态变化三、理解内核链表的相关话题3.1内核链表不是传统链表3.2为什么内核链表要这么设计3.2.1更方便的偏移量计算——offset宏3.2.2彻底理清“流动”的本质多链表动态解绑与重组3.2.2.1物理上它们“住在一起”3.2.2.2逻辑上它们属于“完全不同的绳子”3.2.3.3“流动”是如何发生的3.2.2.4核心总结四、进程状态的查看4.1回顾Linux的进程状态类型4.2运行R与睡眠S状态4.3停止T与追踪停止状态t4.4深度睡眠D4.4.1对比一般睡眠状态4.4.2深度睡眠的意义4.4.3测试方法概要序論Hello大家好我是此方上文我们初步探讨了[进程]这个话题了解了进程的概念和进程的父子关系今天将继续深入了解进程的各种状态好的现在我们开始吧。一、操作系统的“进程状态”如下图精辟的总结了课本上提到的进程状态确实有些复杂。不过我想要说的是这张图讲解的是操作系统的进程状态的总理论而我们今天要讲的Linux进程状态和它的关系是特殊性与普遍性的关系。二、Linux中的进程状态2.1进程的状态有哪些为了弄明白正在运行的进程是什么意思我们需要知道进程的不同状态。一个进程可以有几个状态在Linux内核里进程有时候也叫做任务。下面的状态在kernel源代码里定义/* * 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. */staticconstchar*consttask_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内核链表的想法。structlist_head{structlist_head*next,*prev;};structtask_struct{intx,y,z;structlist_headlinks;...};也即是说现在这个链表本身依赖的不是指针而是这样一个连接器结构体它来帮助前后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(((structtask_struct∗)0)→links)这种计算方式实在是太妙了。首先人为设定起点将 0 强制转换为结构体指针相当于在内存地址 0 的位置“虚拟”出一个结构体。算位置访问成员 links 并取地址编译器会根据内存对齐规则算出它相对于起点即地址 0的距离。空位即偏移因为起点是 0所以得到的成员地址在数值上就精准等于它在结构体里的偏移量。C自带一个宏可以直接获取这个偏移量上面的讲解就是它的底层。首先包含头文件#include stddef.h第一个参数填结构体类型第二个填成员名。size_t offsetoffsetof(structtask_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它在幕后只干了两件事解绑断开旧线调用list_del把该 PCB 负责“运行队列”的run_list钩子从橙线上取下来。重连挂上新线调用list_add把该 PCB 负责“等待队列”的wait_list钩子挂到该磁盘设备特有的那条蓝色等待队列线上。3.2.2.4核心总结实体唯一内存中只有task_struct一个实体静静地呆着。状态改变 关系解绑与重组进程所谓的“状态流动”物理本质上只是不同颜色的绳子链表指针在 PCB 的不同插口上进行解绑和重新勾连的过程。高效率的秘密这种设计让进程状态切换变成了极其高效的指针操作O ( 1 ) O(1)O(1)复杂度不需要任何内存拷贝完美体现了 Linux 内核“扁平化且高效”的管理哲学。四、进程状态的查看4.1回顾Linux的进程状态类型把上面你看不懂的那张表搬过来现在你至少能读懂一部分了接下来我们来查看一下进程的状态/* * 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. */staticconstchar*consttask_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就先当于操作系统的阻塞状态#includeiostream#includeunistd.hintmain(){std::coutstd::unitbuf;intcount0;pid_t pidgetpid();while(true){std::cout我是后台进程, PID: pid, 循环次数: countstd::endl;sleep(1);}return0;}为什么我们查到的进程状态都是S?因为我们的进程是一个循环打印假设执行一次是1毫秒在这毫秒中99%的时间是在等待显示屏就绪1%的时间是拿来打印于是运气不好我们查不出来。#includeiostream#includeunistd.hintmain(){// 打印当前的 PID方便你后续去查状态或杀进程std::cout进程已启动, PID: getpid()请在另一个终端观察其状态...std::endl;// 纯 CPU 计算的死循环没有任何 I/O 打印也没有 sleepwhile(true){}return0;}不让他IO查出来全部都是R如果进程在前台那么状态前面就有一个号否则没有。加上表示当前进程在后台启动[whbbite-alicloud lesson13]$ ./myprocess4.3停止T与追踪停止状态tt表示进程处于“正在被追踪而停止”的状态。虽然它也属于停止状态但它与大写 T 有细微的语义区别大写 T (Stopped)通常是用户主动发送信号如 CtrlZ让进程停下的。小写 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深度睡眠D4.4.1对比一般睡眠状态浅睡眠S比如有一个scanf在等待阻塞。然后我们直接按ctrlc可以直接kill它。或者执行指令kill -9 PID杀死这个是信号的知识后面会讲但是深度睡眠D状态怎么都不会杀死。如何杀掉D进程1.等他自己醒来。2.物理方法重启计算机/断电4.4.2深度睡眠的意义场景一个进程正在向磁盘写入数据但是磁盘写入数据的速度超级慢相对于进程而言于是进程就要去等待磁盘进入S状态。等待过程中可能发生一种非常极端的内存不足情况这个时候OS就要开始杀没有用的进程。这个时候可能会误杀这个正在“摆烂”的进程。于是这个时候磁盘还没有读完数据但是进程已经被干掉了磁盘拿着数据不知道该怎么办于是就只能把数据丢弃了。这个时候用户还不知道于是就丢失了数据。如果这个数据是医院里几千个病人的病历或者是银行的转账信息这是非常危险的。为了防止操作系统发生上述情况。于是一种新的状态被设计出来。D状态不可中断。4.4.3测试方法构建一个块级 IO通过以下命令模拟高强度的磁盘写入操作ddif/dev/zeroof~/test.txtbs4096count100000dd: 磁盘拷贝/转换命令。if/dev/zero: 输入文件数据来源源源不断地产生零字节。of~/test.txt: 输出文件拷贝到的目标文件路径。bs4096: 每次拷贝的数据块大小Block Size此处为 4096 字节4KB。count100000: 一共拷贝的数据块数量这里一共拷贝 100000 次。好的本期内容就到这里如果对你有帮助还不要忘记点赞三联支持。我是此方我们下期再见。bye!

相关新闻