:进程状态——从运行、阻塞到内核链表的动态变化)
前言操作系统会通过 PCB进程控制块管理进程也知道了进程创建出来之后需要由 CPU 调度执行。但是这里又有一个问题一个进程创建出来之后是不是就一直在运行呢当然不是。比如我们打开一个程序程序正在执行这时候突然需要从键盘读取数据但是我们还没有输入。程序总不能一直占着 CPU 等我们吧再比如程序需要读取硬盘中的数据而硬盘的速度又比较慢那么在等待数据的这段时间里进程又应该处于什么状态呢所以操作系统需要对进程进行状态管理根据进程当前的情况决定它什么时候运行、什么时候等待、什么时候暂停。今天我们就来学习 Linux 中的进程状态并进一步理解进程状态发生变化时操作系统到底是怎么管理这些进程的一、操作系统的“进程状态”在正式学习 Linux 之前我们先看一下操作系统理论中的进程状态。一个进程从创建到结束并不是一直处于同一种状态而是会经历不同的阶段。通常可以将进程的基本状态分为新建状态New进程正在被创建。就绪状态Ready进程已经准备好运行只需要等待 CPU 调度。运行状态Running进程正在 CPU 上执行。阻塞状态Blocked进程因为等待某个事件暂时不能继续执行。终止状态Terminated进程已经执行结束。我们可以把它想象成一个排队办事的过程。一个人刚来到办事大厅相当于进程被创建准备好材料排队等待工作人员办理相当于就绪轮到自己办理相当于运行办理过程中发现还缺少某份材料需要等待相当于阻塞最后办理结束相当于终止。这里最容易混淆的是就绪状态和阻塞状态。就绪状态我已经准备好了只是还没有轮到我使用 CPU。阻塞状态我现在还不能继续执行因为我正在等待某个条件满足。比如进程 A已经准备好了等待 CPU - 就绪状态 进程 B正在等待磁盘读取数据 - 阻塞状态它们虽然都没有占用 CPU但是原因完全不同。一个是没有得到 CPU 的使用机会另一个是当前任务还不能继续执行。那么进程在这些状态之间是怎么切换的呢这就是操作系统理论中比较基础的进程状态模型。但是我们今天学习的是 Linux所以接下来还需要看看 Linux 是怎么表示这些状态的。二、Linux中的进程状态2.1 进程的状态有哪些在 Linux 中一个进程也有不同的状态。我们可以查看 Linux 内核中对进程状态的定义不同内核版本的具体定义可能有所调整。常见的状态包括状态英文含义RRunning / Runnable运行或可运行SInterruptible Sleep可中断睡眠DUninterruptible Sleep不可中断睡眠TStopped停止tTracing Stop追踪停止ZZombie僵尸状态XDead死亡状态先不要急着背这些英文我们一个一个理解。这里有一个特别容易误解的地方Linux 中的 R 状态不代表进程一定正在 CPU 上运行。它还包括已经准备好、正在等待 CPU 调度的进程。为什么呢因为 CPU 的数量是有限的而系统中的进程可能有很多。假设现在有三个进程进程 A正在 CPU 上执行 进程 B已经准备好等待 CPU 进程 C已经准备好等待 CPU虽然只有 A 真正占用了 CPU但是 B 和 C 也已经具备运行条件。所以在 Linux 中它们都可以属于 R 状态。也就是说正在 CPU 上执行R。已经准备好正在运行队列中等待 CPUR。这也是为什么 Linux 的 R 状态不能简单地理解为操作系统理论中的运行状态它实际上包含了运行和就绪两种情况。2.2 调度队列FIFO算法 进程运行状态既然有些进程已经准备好运行只是在等待 CPU那么操作系统怎么知道应该让谁先运行呢这就涉及进程调度。我们先回忆一下之前学过的 PCB。在 Linux 中进程控制块主要通过task_struct结构体来描述。每个进程都有自己的task_struct里面保存着进程的 PID、状态、优先级、内存管理信息等。操作系统需要管理大量的进程因此会使用各种数据结构组织这些进程。其中我们可以先从最简单的队列开始理解。2.2.1 什么是调度队列假设现在有三个进程它们都已经准备好运行进程 A 进程 B 进程 C但是 CPU 当前只能执行其中一个进程。那么操作系统就需要把这些等待 CPU 的进程组织起来方便调度器选择。我们可以把它理解成一个排队的过程CPU ^ | --------- | 进程 A | --------- ^ | --------- | 进程 B | --------- ^ | --------- | 进程 C | ---------这就是运行队列Run Queue的基本思想。运行队列中保存的是已经具备运行条件、等待调度的任务。当 CPU 空闲或者需要进行进程切换时调度器就会从合适的任务中选择一个让它获得 CPU 的使用权。这里要注意运行队列不等于系统中的全部进程链表。全局进程链表主要用于组织和遍历系统中的进程而运行队列主要服务于 CPU 调度。两者的用途是不一样的。2.2.2 FIFO 算法是什么FIFO 是 First In First Out 的缩写意思就是先进先出。例如先进入队列A 第二个进入B 第三个进入C那么按照最简单的 FIFO 规则处理顺序就是A - B - C这和我们平时排队买东西是一个道理先来的人先处理。不过这里只是为了帮助我们理解队列的组织方式。Linux 的实际进程调度并不是简单地使用一个 FIFO 队列让所有进程严格按照进入顺序执行。Linux 调度器还会考虑任务的调度策略、优先级、运行时间等因素。因此不要把 FIFO 理解成 Linux 调度器的全部工作原理。我们先记住运行队列的作用是组织等待 CPU 的可运行任务而调度器负责从中选择任务执行。2.3 设备树 进程的阻塞状态前面我们说过进程有时候会因为等待某个事件而进入阻塞状态。那么进程为什么会阻塞我们先从计算机中的硬件设备说起。2.3.1 什么是设备树我们平时使用电脑会接触到很多硬件设备键盘鼠标显示器硬盘网卡摄像头这些设备都需要操作系统进行管理。操作系统并不是直接通过普通应用程序去操作每一个硬件而是通常通过设备驱动程序与硬件进行交互。那么操作系统怎么知道计算机里面有哪些设备这些设备又有什么属性这里就会接触到设备树Device Tree。设备树可以理解成一份描述硬件信息的结构化数据。比如计算机硬件信息 | v 设备树 | ------ CPU | ------ 内存 | ------ 磁盘 | ------ 网卡 | ------ 其他设备它主要用于描述硬件设备的组成、地址、中断等信息让操作系统能够了解硬件平台的情况。尤其是在很多嵌入式 Linux 系统中设备树是非常重要的硬件描述机制。但是这里必须区分一个概念设备树不等于等待队列。设备树主要描述硬件信息而等待队列是操作系统在运行过程中用来管理等待某个事件的进程或线程的数据结构。它们不是同一个东西也不是设备树上的每个节点都直接对应一个等待队列。我们接下来真正要学习的是等待队列。2.3.2 什么是阻塞状态假设我们现在正在使用键盘输入内容。程序需要从键盘读取数据scanf(%d,num);但是如果我们还没有输入数据程序怎么办它不能凭空得到我们想输入的内容。于是程序就需要等待键盘输入。这时候如果程序一直占用 CPU 等待用户输入就会造成资源浪费。所以操作系统会让当前进程进入等待状态暂时不参与 CPU 调度。这就是阻塞状态。我们可以把这个过程理解成进程正在运行 | v 执行 scanf() | v 发现还没有输入数据 | v 需要等待键盘输入 | v 进入等待队列 | v 进程进入睡眠状态 | v 键盘输入数据 | v 等待条件满足 | v 进程被唤醒 | v 重新进入可运行状态那么等待队列又是什么等待队列就是操作系统用来组织等待某个事件的任务的一种数据结构。例如进程 A 等待键盘输入。进程 B 等待磁盘 I/O 完成。进程 C 等待网卡接收到数据。它们等待的事情不同因此可以被组织在不同的等待队列中。当对应的事件发生时内核就可以唤醒等待的进程让它重新具备运行条件。这里要特别注意进程被唤醒并不意味着它马上就能使用 CPU。它只是从等待状态变成可运行状态之后还需要等待调度器分配 CPU。唤醒 ≠ 立即运行。2.4 交换分区与进程挂起前面我们讲了进程在等待某个事件时会进入阻塞状态。那么如果系统中的进程越来越多物理内存不够用了怎么办这就涉及内存管理中的交换机制。2.4.1 交换分区的定义我们之前学习虚拟内存时知道了每个进程都有自己的虚拟地址空间。但是虚拟内存最终还是需要通过页表映射到物理内存。而物理内存的容量是有限的。假设现在系统中有很多进程其中一些进程正在运行另一些进程可能已经阻塞了很长时间。这些阻塞进程暂时不需要 CPU如果它们占用了大量物理内存就可能增加系统的内存压力。那么操作系统能不能把暂时不常用的一部分内存数据放到磁盘上呢可以。这就涉及交换空间Swap。交换空间是磁盘上用于辅助内存管理的一块空间可以用于保存被换出的内存页面。它可以是专门的交换分区也可以是交换文件。例如物理内存 | ------------ | | 常用页面 暂时不用的页面 | | v v 保留内存 换出 | v 交换空间 磁盘这样操作系统就可以在一定程度上释放物理内存给其他任务使用。注意交换空间存放的不是简单意义上的整个程序代码而是内存管理过程中被换出的页面。而且交换机制和进程挂起并不是完全相同的概念。2.4.2 挂起与优先挂起那么什么是挂起我们可以先从操作系统理论中的挂起状态理解。进程原本可能处于阻塞状态因为等待某个事件而无法继续执行。如果系统内存压力比较大操作系统就可能把这个进程的一部分内存页面换出到磁盘。在传统操作系统模型中如果进程被暂时移出内存并且暂时不参与调度就可以进一步理解为挂起状态。例如进程 A | v 阻塞等待 | v 内存资源紧张 | v 暂时挂起 | v 部分内存页面换出 | v 交换空间为什么阻塞进程可能会优先成为换出对象因为它正在等待某个事件短时间内可能不会使用 CPU。相比正在积极运行的进程它可能有更多暂时不用的内存页面。不过这并不意味着操作系统一定会优先换出所有阻塞进程。现代 Linux 的内存回收机制会综合考虑页面的使用情况、访问频率、内存压力等因素。另外进程被换出部分内存页面不代表 PCB 也被一起搬到磁盘上。PCB 仍然需要由内核维护操作系统必须知道这个进程的存在以及它当前的状态。这里我们要分清三个概念阻塞进程正在等待某个事件。换出某些内存页面暂时从物理内存转移到交换空间。挂起操作系统理论中的一种进程状态表示进程被暂时停止执行。它们之间有关联但不能直接画等号。2.5 进程的状态变化前面我们已经把各种状态分别介绍了一遍。现在把它们串起来就能理解一个进程在整个运行过程中会发生什么。例如一个程序刚刚创建出来先进入就绪状态等待 CPU 调度。获得 CPU 后进入运行状态。如果程序需要读取磁盘数据就会进入阻塞状态等待 I/O 完成。等到数据读取完成进程被唤醒重新进入就绪状态。之后再次获得 CPU继续执行。最后程序执行完毕进入终止状态。可以概括为创建 | v 就绪 | CPU调度 | v 运行 / | \ / | \ v v v 阻塞 就绪 终止 | v 等待事件 | v 被唤醒 | ------ 就绪这里再补充一个容易忽略的地方进程从运行状态变成就绪状态不一定是因为它主动等待了什么。也可能是因为它的 CPU 时间片用完了或者调度器决定让其他任务运行。所以进程状态的变化既可能由进程自身执行的操作引起也可能由操作系统的调度和事件处理机制引起。到这里我们已经知道了进程为什么会在不同状态之间切换。但是还有一个问题操作系统到底是怎么把进程从一个队列转移到另一个队列的这就需要继续理解 Linux 内核链表。三、理解内核链表的相关话题3.1 内核链表不是传统链表之前我们学习 C 数据结构时接触过链表。例如最普通的双向链表structNode{intdata;Node*prev;Node*next;};每个节点中都保存着数据以及指向前后节点的指针。通过这些指针我们就可以把多个节点连接起来。但是Linux 内核中的链表设计和我们平时写的传统链表有所不同。Linux 内核使用了一种非常经典的设计把链表节点从实际数据中独立出来。先看一下代码structlist_head{structlist_head*next;structlist_head*prev;};structtask_struct{intpid;intstate;structlist_headtasks;// 其他进程信息};这里的list_head就是 Linux 内核链表的核心结构。它里面只有两个指针next指向下一个链表节点。prev指向上一个链表节点。而真正的进程信息还是保存在task_struct中。也就是说list_head本身不保存 PID、状态等进程信息它只负责建立链表之间的连接。我们可以这样理解task_struct ---------------- | pid | | state | | | | list_head | | next | | prev | | | | 其他信息 | ----------------这就像给一个完整的 PCB 单独安装了一个连接接口。链表只需要操作这个接口就可以把不同的 PCB 组织起来。这样设计的好处是什么我们接着看。3.2 为什么内核链表要这么设计3.2.1 更方便的偏移量计算——offset 宏既然list_head只是一个独立的连接接口那么问题来了当我们遍历链表时拿到的是list_head的地址。可是我们真正想操作的是整个task_struct。那么怎么从一个成员变量的地址找到它所在结构体的起始地址呢这就涉及结构体成员的偏移量。例如structtask_struct{intpid;intstate;structlist_headtasks;};假设结构体起始地址1000 pid 1000 state 1004 tasks 1008那么tasks相对于结构体起始位置的偏移量就是1008 - 1000 8如果我们已经知道tasks的地址就可以通过减去偏移量得到整个结构体的起始地址。公式就是结构体首地址 成员地址 - 成员偏移量Linux 内核中有一个非常重要的宏offsetof(type,member)它可以帮助我们获取某个成员在结构体中的偏移量。例如#includestddef.hsize_toffsetoffsetof(structtask_struct,tasks);这样就能得到tasks成员相对于结构体起始地址的偏移量。那么Linux 内核如何通过链表节点找到整个结构体呢它使用了一个非常经典的宏container_of(ptr,type,member)它的作用就是已知结构体中某个成员的地址反过来找到整个结构体的地址。例如structtask_struct*taskcontainer_of(pos,structtask_struct,tasks);其中pos指向tasks成员的指针。struct task_struct成员所在的结构体类型。tasks结构体中的成员名称。这样就可以通过链表节点找到整个 PCB。这就是 Linux 内核链表非常巧妙的地方。3.2.2 彻底理清“流动”的本质多链表动态解绑与重组现在我们已经知道了Linux 内核链表并不是直接把 PCB 当作链表节点而是在 PCB 内部嵌入list_head通过它来建立连接。那么为什么要这么设计我们回到进程状态变化这个问题。假设现在有一个进程 A。它正在运行后来因为需要读取磁盘数据而进入阻塞状态。那么操作系统要怎么管理它我们先想一个问题一个进程是不是只需要被组织在一条链表中显然不是。因为操作系统需要从不同角度管理进程。例如想知道系统中有哪些进程需要全局进程链表。想知道哪些进程可以被 CPU 调度需要调度相关的数据结构。想知道哪些进程正在等待某个事件需要等待队列。所以同一个进程可能需要参与不同的数据组织关系。3.2.2.1 物理上它们住在一起我们先从物理内存角度理解。一个进程对应着内核中的进程描述结构task_struct。我们可以把它想象成一个很大的盒子里面保存着进程的各种信息。而 Linux 内核链表的节点可以嵌入在这个盒子里面。例如structtask_struct{intpid;intstate;structlist_headtasks;structlist_headexample_wait_list;// 其他信息};这里为了方便理解假设我们给结构体中放入了两个不同的链表节点。那么task_struct -------------------- | pid | | state | | | | tasks | | next / prev | | | | example_wait_list | | next / prev | | | | 其他进程信息 | --------------------注意这些list_head都属于同一个task_struct。它们只是不同的连接接口。所以从物理上看它们都住在同一个 PCB 里面。3.2.2.2 逻辑上它们属于完全不同的链表虽然这些连接接口都在同一个 PCB 里面但它们可以分别加入不同的链表。例如全局进程链表 A - B - C - D 某个等待队列 B - D 其他调度相关结构 A - C这里要注意上面的 A、B、C、D 代表同一个进程实体只是为了说明它们可以通过不同的链表成员参与不同的组织关系。它们并没有因为加入不同链表就变成多个进程。我们可以把它想象成一个人同时拥有多个身份在学校他属于班级。在社团他属于社团成员。在图书馆他又属于借阅者。人还是同一个人只是不同的组织关系不同。PCB 也是一样。它还是原来的那个 PCB只不过可以通过不同的链表节点参与不同的管理结构。这里还需要特别纠正一个容易出现的误区不是说 Linux 中所有进程状态切换都一定是从一条普通链表删除再插入另一条普通链表。Linux 的全局进程链表、调度器运行队列、等待队列具体实现并不完全相同。尤其是调度器的运行队列还涉及调度类、优先级和其他调度数据结构。我们这里讲的是内核链表的设计思想而不是说 Linux 所有调度操作都只有list_del()和list_add()两步。3.2.2.3 “流动”是如何发生的现在我们回到进程状态变化。假设进程 A 正在运行。它执行到某个位置需要等待磁盘数据。那么内核就需要让它暂时停止参与 CPU 调度并等待对应的 I/O 事件。从逻辑上看进程 A | v 正在运行 | v 等待磁盘 | v 进入睡眠 | v 等待事件完成从数据结构管理的角度看内核需要将任务设置为相应的睡眠状态。将它从当前不再适合参与调度的运行状态中移开。根据等待机制将它加入对应的等待队列。等待事件发生。条件满足后将它唤醒使其重新具备运行条件。可以把这个过程理解为运行队列 | | 进程 A 等待 I/O v 等待队列 | | I/O 完成 v 可运行状态 | v 等待 CPU 调度这里的重点不是 PCB 被搬来搬去而是进程实体仍然是原来的那个变化的是它的状态以及它参与的数据结构关系。也就是说操作系统不需要重新创建一个 PCB也不需要复制整个 PCB。它只需要根据当前的运行情况更新相应的状态和管理关系。这就是我们所说的进程在不同队列之间的动态组织。3.2.2.4 核心总结这一部分我们主要理解了三个问题第一PCB 是一个实体。进程的核心信息由task_struct保存不会因为进程状态变化就重新创建一个 PCB。第二内核链表节点和 PCB 是分开的。Linux 使用list_head作为连接接口将链表结构与实际数据解耦。第三不同链表可以组织同一个 PCB。通过不同的链表成员内核可以从不同角度组织和管理进程。所以当进程状态发生变化时操作系统需要更新进程状态并根据具体机制调整相关的数据结构关系。这就是 Linux 内核链表设计的巧妙之处。四、进程状态的查看前面我们一直在通过图示理解进程状态。那么在实际使用 Linux 时我们怎么知道一个进程当前处于什么状态呢这就需要使用ps命令。4.1 回顾 Linux 的进程状态类型我们先看一下 Linux 中常见的状态标识状态说明RRunning正在运行或等待 CPUSSleeping可中断睡眠DDisk Sleep不可中断睡眠TStopped停止tTracing Stop追踪停止ZZombie僵尸状态XDead死亡状态这些状态会显示在ps命令的STAT列中。例如psaux也可以使用ps-ef不过想直接查看状态标识ps aux的输出中有一个STAT列非常方便。例如USER PID %CPU %MEM STAT COMMAND root 1234 0.0 0.1 S ./myprocess root 1235 90.0 0.1 R ./myprocess这里PID进程编号。%CPUCPU 使用率。%MEM内存使用率。STAT进程状态。COMMAND进程对应的命令。注意STAT后面有时还会出现其他附加标识例如表示该进程属于前台进程组。4.2 运行 R 与睡眠 S 状态我们先通过代码来观察 R 和 S 状态。4.2.1 观察 S 状态先写一个简单的程序#includeiostream#includeunistd.hintmain(){while(true){std::couthello Linuxstd::endl;sleep(1);}return0;}这里程序每打印一次内容就会调用sleep(1);让当前进程休眠一秒。在这段时间里程序并不需要一直占用 CPU。因此我们可以在另一个终端中查看psaux|grepmyprocess就可能看到STAT: S为什么会出现 S因为进程执行到sleep(1)时需要等待一秒钟。它暂时不具备继续执行的条件所以进入可中断睡眠状态。这里再补充一下S 状态不仅仅是sleep()引起的。像等待键盘输入、等待某些事件等也可能让进程进入 S 状态。4.2.2 观察 R 状态那么如果我们不让进程进行 I/O也不让它休眠只让它一直计算会怎么样可以写成#includeiostream#includeunistd.hintmain(){std::coutPID: getpid()std::endl;while(true){// 一直进行计算}return0;}这个程序进入死循环后会一直尝试执行 CPU 指令。因此它通常会显示为 R 状态。我们再使用psaux|grepmyprocess查看。可以发现进程状态通常是R这说明它处于运行或可运行状态。不过这里有一个小细节如果进程正在 CPU 上执行它是 R如果它已经准备好但是暂时在等待 CPU 调度它同样也是 R。所以R 不等于一定正在占用 CPU。4.2.3 前台进程与后台进程我们在终端中运行程序时还会接触到前台进程和后台进程。例如./myprocess这样启动的程序默认在前台运行。如果我们想让它在后台运行可以在命令后面加上./myprocess这样 Shell 就可以继续接受其他命令。我们还可以通过jobs查看当前 Shell 管理的后台任务。另外使用ps aux时如果进程属于前台进程组STAT列后面可能会出现。这里要区分前台、后台描述的是进程与终端作业控制之间的关系而 R、S、D 等描述的是进程状态。它们不是同一个概念。4.3 停止 T 与追踪停止状态 t除了运行和睡眠进程还可能被暂停。Linux 中常见的停止状态有T停止状态。t追踪停止状态。4.3.1 T 停止状态我们先启动一个程序./myprocess然后按下Ctrl Z程序就会暂停。这时再查看进程状态就可能看到T为什么会这样因为终端在处理Ctrl Z时通常会向前台进程组发送SIGTSTP信号。进程接收到这个信号后就会进入停止状态。那么如何让它继续执行呢可以使用fg把任务重新调回前台。也可以通过bg让它在后台继续运行。这里再补充一个信号kill-STOPPID可以向指定进程发送SIGSTOP强制让它停止。与SIGTSTP不同SIGSTOP不能被程序捕获或忽略。而恢复运行可以使用kill-CONTPID发送继续执行的信号。4.3.2 t 追踪停止状态那么小写的 t 又是什么它通常出现在进程被调试器追踪的场景中。例如我们使用 GDB 调试程序时gdb ./myprocess调试器可以控制程序的执行过程。例如设置断点。单步执行。查看变量。控制程序继续运行。当被调试的进程暂停下来等待调试器进一步控制时就可能处于追踪停止状态。所以T一般的停止状态。t与调试追踪有关的停止状态。两者都表示进程暂时停止执行但产生原因不同。4.4 深度睡眠 D最后我们再看一个比较特殊的状态D 状态。D 状态也叫不可中断睡眠状态。4.4.1 对比一般睡眠状态我们之前讲过 S 状态。例如sleep(10);程序执行后会进入睡眠状态等待十秒。这时候如果我们向进程发送终止信号进程通常可以响应信号并结束。但是 D 状态就不一样了。D 状态通常表示进程正在等待某些不能被普通信号打断的内核操作完成例如某些 I/O 操作。例如进程正在等待磁盘 I/O | v 进入 D | v 等待 I/O 完成 | v 操作完成后继续这时候即使我们向进程发送SIGKILL它也可能暂时无法立即退出。为什么因为进程当前正在等待内核中某个不可中断的操作完成。信号可以处于待处理状态但进程需要等到相应的等待过程结束才有机会处理终止请求。所以不要简单地认为 D 状态的进程永远杀不掉。更准确地说D 状态下进程通常不能立即响应普通信号必须等待不可中断的等待过程结束。4.4.2 深度睡眠的意义为什么 Linux 需要 D 状态我们可以从 I/O 操作理解。假设一个进程正在进行磁盘 I/O。磁盘的处理速度相对于 CPU 来说比较慢所以进程可能需要等待一段时间。如果进程在等待期间可以随意被中断那么某些正在进行的内核操作可能无法按照预期完成。因此对于一些关键的内核等待过程Linux 会使用不可中断睡眠机制。这样可以保证相关操作按照内核预期的流程完成。这里要注意D 状态并不是专门为了防止进程被内存管理机制杀掉而设计的。它的核心目的是让某些关键的等待过程不被普通信号打断。4.4.3 如何观察 D 状态在实际系统中D 状态通常出现在某些 I/O 等待场景中。例如磁盘设备响应缓慢。网络文件系统出现异常。某些驱动程序中的 I/O 操作迟迟没有完成。我们可以通过psaux或者top观察进程状态。如果STAT一栏出现 D就说明进程处于不可中断睡眠状态。不过普通电脑不一定容易稳定复现 D 状态。因为正常情况下磁盘 I/O 往往很快就能完成进程可能还没有来得及被我们观察就已经恢复运行了。所以我们先理解它的产生原因和特点即可。五、本篇总结到这里我们就把 Linux 中的进程状态基本串起来了。最开始我们学习了操作系统理论中的五种基本状态知道了进程并不是一直在运行而是会根据 CPU 调度、I/O 等待和执行情况发生变化。接着我们学习了 Linux 中常见的 R、S、D、T、t、Z、X 等状态理解了它们分别代表什么。然后我们进一步学习了运行队列、等待队列以及内核链表的组织方式。最后通过ps aux和top我们可以在 Linux 中实际观察进程状态。这里最重要的几个知识点R 状态包含正在运行和等待 CPU 调度的任务。S 状态表示可中断睡眠D 状态表示不可中断睡眠。T 和 t 都属于停止状态但产生原因不同。等待队列用于组织等待某个事件的任务设备树不是等待队列。Linux 内核链表通过list_head将链表节点与实际数据结构解耦。进程状态变化不意味着 PCB 被复制或搬动变化的是进程状态及相关管理关系。ps aux可以帮助我们查看进程状态重点关注STAT列。我们可以用一句话总结今天的内容进程状态描述的是进程当前处于什么执行阶段而队列和链表则帮助操作系统组织、管理这些进程。进程从运行到阻塞再从阻塞到就绪最终重新获得 CPU 执行这背后离不开操作系统的调度机制和数据结构设计。下一篇我们继续学习进程等待与进程退出看看父进程是如何等待子进程的以及僵尸进程、孤儿进程又是怎么产生的。我是 Yi我们下篇博客见