【Linux 系统篇(十五)】进程 (三) :进程状态深度详解


大家好,欢迎来到 huangjin007_ 的博客
个人主页:huangjin007_
🔥 文章收录专栏:Linux 内功修炼手册(系统篇)
总会有一些坚持
能从冰封的土地里
培育出十万朵怒放的蔷薇


Linux 系统篇(十五) ------ 进程状态深度详解

文章目录

  • [Linux 系统篇(十五) ------ 进程状态深度详解](#Linux 系统篇(十五) —— 进程状态深度详解)
    • 一、为什么进程需要"状态"?
    • [二、Linux 内核源码中的进程状态定义](#二、Linux 内核源码中的进程状态定义)
      • [1. R(running)------ 运行状态](#1. R(running)—— 运行状态)
      • [2. S(sleeping)------ 可中断睡眠状态](#2. S(sleeping)—— 可中断睡眠状态)
      • [3. D(disk sleep)------ 不可中断睡眠状态](#3. D(disk sleep)—— 不可中断睡眠状态)
      • [4. T(stopped)------ 停止状态](#4. T(stopped)—— 停止状态)
      • [5. t(tracing stop)------ 追踪停止状态](#5. t(tracing stop)—— 追踪停止状态)
      • [6. X(dead)------ 死亡状态](#6. X(dead)—— 死亡状态)
      • [7. Z(zombie)------ 僵尸状态](#7. Z(zombie)—— 僵尸状态)
    • [三、查看进程状态:`ps` 命令详解](#三、查看进程状态:ps 命令详解)
      • [1. `ps aux` ------ 以用户为中心的详细格式](#1. ps aux —— 以用户为中心的详细格式)
      • [2. `ps axj` ------ 显示进程组、会话、父进程等信息](#2. ps axj —— 显示进程组、会话、父进程等信息)
    • [四、运行队列与 FIFO 算法](#四、运行队列与 FIFO 算法)
      • [1. 什么是运行队列?](#1. 什么是运行队列?)
      • [2. FIFO 算法](#2. FIFO 算法)
      • [3. 运行队列与全局链表的区别](#3. 运行队列与全局链表的区别)
    • 五、等待队列与阻塞状态
      • [1. 什么是阻塞?](#1. 什么是阻塞?)
      • [2. 什么是等待队列?](#2. 什么是等待队列?)
      • [3. 设备就绪与唤醒](#3. 设备就绪与唤醒)
    • 六、交换分区与进程挂起
      • [1. 什么是挂起?](#1. 什么是挂起?)
      • [2. 交换分区](#2. 交换分区)
      • [3. 谁会被优先挂起?](#3. 谁会被优先挂起?)
      • [4. 挂起与状态的关系](#4. 挂起与状态的关系)
    • 七、内核链表
      • [1. 传统链表 vs Linux 内核链表](#1. 传统链表 vs Linux 内核链表)
      • [2. 为什么这样设计?](#2. 为什么这样设计?)
      • [3. 一个进程如何同时出现在多个队列中?](#3. 一个进程如何同时出现在多个队列中?)
      • [4. 两个链表的节点迁移](#4. 两个链表的节点迁移)
    • 八、实战观察进程状态
      • [1. 一个循环打印的程序:看到 S 状态](#1. 一个循环打印的程序:看到 S 状态)
      • [2. 纯 CPU 空转:看到 R 状态](#2. 纯 CPU 空转:看到 R 状态)
      • [3. 观察 T 状态和 t 状态](#3. 观察 T 状态和 t 状态)
        • [T 状态:用 Ctrl+Z 暂停进程](#T 状态:用 Ctrl+Z 暂停进程)
        • [t 状态:用 gdb 调试程序](#t 状态:用 gdb 调试程序)
      • [4. 观察 D 状态:深度睡眠实验](#4. 观察 D 状态:深度睡眠实验)
      • [5. 后台进程读取终端输入:自动进入 T 状态](#5. 后台进程读取终端输入:自动进入 T 状态)
    • [九、D 状态存在的意义](#九、D 状态存在的意义)
      • [1. 数据丢失的风险](#1. 数据丢失的风险)
      • [2. D 状态的保护作用](#2. D 状态的保护作用)
      • [3. 如何应对 D 状态?](#3. 如何应对 D 状态?)
    • 十、内核对象缓存
    • 结语:

一、为什么进程需要"状态"?

  在操作系统中,进程(Process)是程序执行的一次实例,是资源分配和调度的基本单位。一个系统中可能同时存在成百上千个进程,但 CPU 核心数量有限(甚至只有 1 个),不可能所有进程同时占用 CPU 运行。因此,操作系统必须对进程进行管理:哪些进程正在运行,哪些在等待 I/O,哪些被暂停,哪些已经结束......这些不同的"处境"就是进程的状态。


二、Linux 内核源码中的进程状态定义

  要弄明白"正在运行的进程"到底指什么,我们必须先看内核源码如何定义进程状态。在 Linux 内核中,进程有时也被称为"任务"(task),其状态定义在 include/linux/sched.h 等头文件中。下面这段代码展示了内核中用于展示进程状态的数组:

c 复制代码
/*
 * 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 */
};

  这个状态数组实际上是一个"位图"(bitmap) ,不同状态对应不同的位。R 状态的值是 0,其他状态分别是 1、2、4、8、16、32,它们都是 2 的幂次方,意味着每个状态占用一个独立的二进制位。内核可以用简单的位测试来判断一个进程是否处于某些状态的组合。

  我们日常使用 ps 命令看到的进程状态字符,就是内核根据这个数组映射出来的。下面逐一对每个状态进行解释:

1. R(running)------ 运行状态

  R 状态并不表示进程一定正在 CPU 上执行,它表示进程"具备运行条件",要么正在运行,要么在运行队列(Run Queue)中排队等待被调度。换句话说,R 状态包含了传统操作系统理论中的"就绪态"和"运行态"两种情形。

  在 Linux 中,只要一个进程没有被阻塞(等待 I/O、信号等),也没有被停止,那么它的状态通常就是 R。例如一个死循环计算程序,只要它一直占用 CPU,状态就是 R;如果它被调度器换下 CPU 但仍在就绪队列中等待下一次调度,状态也是 R。

2. S(sleeping)------ 可中断睡眠状态

  S 状态表示进程正在等待某个事件完成 ,例如等待键盘输入、等待网络数据到达、等待定时器到期等。这种睡眠是可中断的(interruptible sleep),意思是:如果进程收到了一个信号,它可以立即被唤醒并处理信号,而不是必须等到等待的事件发生

  因此,我们称 S 状态为"浅睡眠"或"可中断睡眠"。大多数进程在大多数时间里都处于 S 状态,比如后台运行的服务、等待用户输入的 Shell、执行 sleep 命令的进程等。

3. D(disk sleep)------ 不可中断睡眠状态

  D 状态也叫磁盘休眠状态(Disk Sleep)或不可中断睡眠状态(Uninterruptible Sleep)。进程处于 D 状态时,通常正在等待 I/O 操作完成,尤其是磁盘 I/O(如读写磁盘、等待 DMA 传输等)。

  与 S 状态不同,D 状态下的进程无法被信号打断,即使发送 SIGKILLkill -9)也无法立即杀死它,必须等到 I/O 操作完成、进程自己醒来。这是内核为了保护数据一致性而设计的重要机制,后面我们会详细解释为什么需要 D 状态。

4. T(stopped)------ 停止状态

  T 状态表示进程被暂停/停止执行 。可以通过向进程发送 SIGSTOP 信号(19 号信号)使其进入 T 状态。被停止的进程不会占用 CPU,也不会响应普通信号(除了 SIGCONTSIGKILL)。当收到 SIGCONT 信号后,进程会被唤醒并继续运行。

5. t(tracing stop)------ 追踪停止状态

  小写 t 状态专门用于调试器(Debugger)环境 。当进程被调试器(如 gdb)追踪时,调试器会通过 ptrace 系统调用控制进程。进程在等待调试器发送下一条指令期间,状态会显示为小写 t

  tT 虽然都属于"停止"类别,但语义不同:T 通常由用户主动发送信号触发,而 t 是调试器追踪导致的暂停。

6. X(dead)------ 死亡状态

  X 状态只是一个瞬时返回状态 ,表示进程即将被销毁。你几乎不可能在任务列表(如 ps)中看到这个状态,因为进程从退出到被彻底清理的时间极短。

7. Z(zombie)------ 僵尸状态

  Z 状态表示进程已经退出,但它的父进程还没有调用 wait()waitpid() 来回收它的退出状态。僵尸进程不再占用 CPU 和内存资源(除了 PCB 本身),但会保留一个 PID 和一些退出信息,直到父进程回收。

  僵尸进程和孤儿进程是进程状态中的另一大话题,我们将在下一篇文章中详细讲解。


三、查看进程状态:ps 命令详解

  在 Linux 中,最常用的查看进程状态的命令是 ps

1. ps aux ------ 以用户为中心的详细格式

bash 复制代码
ps aux

各参数含义:

  • a:显示一个终端上的所有进程,包括其他用户的进程。
  • u:以用户为中心的格式显示进程信息,提供用户、CPU 和内存使用情况等详细信息。
  • x:显示没有控制终端的进程,例如后台运行的守护进程(daemon)。

该命令输出类似:

复制代码
USER       PID %CPU %MEM    VSZ   RSS TTY      STAT START   TIME COMMAND
root         1  0.0  0.1 225752  9036 ?        Ss   10:00   0:03 /sbin/init
user      1234  0.0  0.0  11520  3320 pts/0    Ss   10:01   0:00 -bash
user      5678  100  0.0  11324   740 pts/0    R+   10:05   0:05 ./busy_loop

  其中 STAT 列就是进程状态。注意它可能包含多个字符,例如 SsR+Ssl 等。第一个字符是主状态(R/S/D/T/Z 等),后面的附加字符表示特殊标志:

  • s:会话领导者(session leader)
  • +:位于前台进程组
  • l:多线程进程
  • <:高优先级
  • N:低优先级
  • L:有些页被锁在内存中

  例如 Ss 表示该进程是会话领导者且处于可中断睡眠状态;R+ 表示该进程正在运行且位于前台。

2. ps axj ------ 显示进程组、会话、父进程等信息

bash 复制代码
ps axj

参数:

  • a:显示所有终端进程。
  • x:显示无控制终端进程。
  • j:显示进程归属的进程组 ID(PGID)、会话 ID(SID)、父进程 ID(PPID),以及与作业控制相关的信息。

  该命令输出的 STAT 列同样重要,同时还能看到 PPIDPGIDSID 等。

注意ps 输出的状态字符只是一个简化的展示。内核内部真正使用的是 task_struct 中的 state 字段(位掩码),它可能同时包含多个状态位,但 ps 只显示最主要的一个。


四、运行队列与 FIFO 算法

1. 什么是运行队列?

  进程的 PCB(task_struct)中有一个指针指向下一个 PCB,这样就形成了一个"全局进程链表",用于遍历系统中的所有进程。但是,调度器并不会在全局链表上挑选进程执行,而是使用专门的运行队列(Runqueue)

  运行队列中存放的是所有处于"就绪"或"运行"状态的进程,即状态为 R 的进程。在单核 CPU 环境下,系统只有一个运行队列;在多核环境下,每个 CPU 核心通常有自己的运行队列。

2. FIFO 算法

  为了简单理解,我们可以把运行队列看作一个 FIFO(First In First Out,先进先出)队列:进程按照到达就绪队列的顺序排队,调度器从队头取出进程放到 CPU 上执行,新就绪的进程排到队尾。当然,Linux 实际使用的调度器远比 FIFO 复杂。

  关键结论:进程在运行队列中 ⇔ 进程处于运行状态(R)。当一个进程被调度器选中,它就从队列中取出并占用 CPU;当它的时间片用完或被抢占,它会重新排到队列尾部。

3. 运行队列与全局链表的区别

  • 全局进程链表:包含所有进程(无论状态),用于管理、遍历和资源回收。
  • 运行队列:只包含 R 状态的进程,是调度器的工作队列。

  两者不是同一条链表,进程通过不同的 list_head 成员连接不同的链表(后面会详细讲内核链表的设计)。


五、等待队列与阻塞状态

1. 什么是阻塞?

  当一个进程正在运行(R 状态),但它发起的某个请求(通常是 I/O 请求)暂时无法得到满足,或者必须等待某个事件(如磁盘数据读取完成、网卡数据到达、用户键盘输入)发生时,操作系统会:

  1. 剥夺该进程的 CPU 使用权;
  2. 将其从运行队列中移出;
  3. 将其挂到对应设备的等待队列(Wait Queue)上。

  此时进程进入阻塞状态 ,在 Linux 中对应 S 状态(可中断睡眠)或 D 状态(不可中断睡眠,用于磁盘 I/O)。

2. 什么是等待队列?

  等待队列是驱动程序内部维护的运行时动态链表,专门用来组织正在等待该设备的进程。例如:

  • 键盘驱动有一个等待队列,所有等待键盘输入的进程都挂在这条队列上;
  • 磁盘驱动有一个等待队列,所有等待磁盘读写完成的进程都挂在这条队列上;
  • 网卡驱动也有自己的等待队列。

关键结论:进程在等待队列中 ⇔ 进程处于阻塞状态(S 或 D)

3. 设备就绪与唤醒

  当硬件设备完成操作(例如磁盘数据已经读取到内存),它会向 CPU 发出一个中断。内核的中断处理程序会找到对应的等待队列,唤醒队列上的进程,将它们从等待队列中移除,重新放回运行队列。此时进程从阻塞状态变为就绪状态(R),等待调度器再次调度。


六、交换分区与进程挂起

1. 什么是挂起?

  挂起(Suspend) 是操作系统在内存资源严重不足时采取的一种极端措施。注意这里的内存资源是指物理内存 (RAM),而不是虚拟内存。当物理内存耗尽,而又有新的进程需要内存时,操作系统会将一些进程的代码和数据从内存中"换出"(Swap Out)到磁盘的交换分区(Swap Partition),以腾出空间。

  被换出的进程就处于挂起状态 。此时进程的 PCB(task_struct)仍然留在内存中,因为内核需要知道这个进程的存在和状态,但它的代码和数据已经被移到了磁盘上。当内存压力缓解后,操作系统会将这些进程重新"换入"(Swap In)内存,恢复执行。

2. 交换分区

  交换分区是磁盘上的一块临时区域,专门用来存放从内存中交换出来的数据。它类似于 Windows 的虚拟内存页面文件。在 Linux 安装时通常会划分一个 swap 分区,也可以使用 swap 文件。

3. 谁会被优先挂起?

  当内存不足时,操作系统会优先选择正在阻塞的进程进行挂起。因为阻塞进程本来就不需要 CPU,它们正在等待某些事件(如 I/O),所以即使被换出内存,对系统性能的影响也较小。只有在极端情况下,运行队列中靠后的就绪进程也可能被换出,但这很少见。

4. 挂起与状态的关系

  在经典操作系统理论中,挂起分为"就绪挂起"和"阻塞挂起"。但在 Linux 的实际实现中,挂起并不直接对应一个独立的进程状态字符(如 ps 中不会显示"挂起")。被换出的进程通常仍然显示为 S 或 T 状态,只是它的内存驻留集(RSS)会减少,甚至为 0。我们可以通过 psRSS 列或 /proc/PID/status 中的 VmSwap 字段观察。


七、内核链表

  理解了进程状态和队列的关系后,我们深入底层,看看 Linux 内核是如何高效地管理这些队列的。

1. 传统链表 vs Linux 内核链表

  我们通常学习的链表是传统链表:每个节点中包含数据域和指针域,指针域指向下一个节点(或前一个节点)。例如:

c 复制代码
struct Node 
{
    int data;
    struct Node *next;
    struct Node *prev;
};

  这种链表的特点是:链表结构和数据类型强耦合,每种数据类型都需要定义自己的链表结构,代码无法复用。

  而 Linux 内核使用的链表设计思想完全不同:把前后指针独立封装成一个结构体,让链表操作与具体数据解耦 。这个结构体就是 list_head

c 复制代码
struct list_head 
{
    struct list_head *next, *prev;
};

  然后在需要被链表管理的数据结构体中,内嵌 一个 list_head 成员:

c 复制代码
struct task_struct {
    int pid;
    int state;
    // ... 其他大量字段 ...
    struct list_head tasks;      // 用于全局进程链表
    struct list_head run_list;   // 用于运行队列
    struct list_head wait_list;  // 用于等待队列
    // ... 可能还有更多 list_head ...
};

2. 为什么这样设计?

  现在有一个问题:链表指针指向的是 list_head 成员的地址,而不是整个 task_struct 的起始地址。如果我们手里只有一个指向 list_head 的指针,如何拿到它所属的整个 task_struct 的地址呢?

  答案是利用偏移量(Offset) 。假设我们知道 list_head 成员在 task_struct 中的偏移量,那么:

复制代码
结构体首地址 = 成员地址 - 偏移量

  那么偏移量如何计算?C 语言提供了一种非常巧妙的方法:

c 复制代码
offset = &(((struct task_struct *)0)->links);

意思是:

  1. 将数字 0 强制转换为 struct task_struct * 类型的指针,相当于在内存地址 0 的位置"虚拟"出一个 task_struct
  2. 访问这个虚拟结构体的 links 成员,并取其地址。
  3. 因为结构体基地址是 0,所以得到的成员地址在数值上就等于该成员在结构体中的偏移量。

  这种写法是 C 语言中计算结构体成员偏移量的经典技巧。C 标准库也提供了一个宏 offsetof 来完成同样的功能:

c 复制代码
#include <stddef.h>
size_t offset = offsetof(struct task_struct, links);

3. 一个进程如何同时出现在多个队列中?

  从物理存储的角度看,每个进程在内核中只对应一份 task_struct 结构体实例,这个结构体里存放了进程的所有元数据:PID、状态、优先级、内存映射、打开的文件等。它不会被复制多份。

  但有趣的是,同一个 task_struct 却可以同时出现在多个不同的链表里

  原因在于 task_struct 内部定义了很多个 list_head 类型的成员变量。list_head 是 Linux 内核提供的一个通用链表节点结构,里面只有两个指针:nextprev。一个 task_struct 里有多少个 list_head,就相当于有多少个"挂载点",每个挂载点都可以独立地链接到某一条链表上,互不干扰。

  常见的几个挂载点如下:

  • tasks :链接到全局进程链表。从进程创建到消亡,这个节点始终挂在全局链表里。内核遍历所有进程时,就是沿着这条链表走的。
  • run_list :链接到运行队列 。只有处于 R 状态(就绪或正在运行)的进程,才会把 run_list 挂到运行队列上。调度器只关心这条队列。
  • wait_list :链接到某个设备的等待队列 。每个硬件设备(磁盘、网卡、键盘等)都有自己的等待队列。当进程因为等待该设备而阻塞时,就把 wait_list 挂到对应的设备等待队列里。

  所以,一个进程完全可以同时挂在全局链表、运行队列(或等待队列)中。这些链表之间没有任何直接联系,它们只是通过 task_struct 中不同的 list_head 成员间接关联。

4. 两个链表的节点迁移

  现在我们来看,当进程从一个状态切换到另一个状态时,内核到底做了什么。

  假设一个进程正在运行(R 状态),突然发起了一次磁盘读取请求。由于磁盘速度远慢于 CPU,进程无法立即得到数据,于是内核会把它从运行队列中拿出来,放到磁盘设备的等待队列里。此时进程状态从 R 变为 D(不可中断睡眠)或 S(可中断睡眠),具体取决于等待类型。

  这个过程中,内核不会移动 task_struct 结构体本身,不会把它的内存地址搬来搬去,也不会复制任何数据。内核只做了两件事:

  1. 从运行队列中摘除 :调用 list_del(&p->run_list),把该进程的 run_list 节点从运行队列链表中删掉。
  2. 插入等待队列 :调用 list_add(&p->wait_list, &disk_wait_queue),把该进程的 wait_list 节点插入到磁盘设备的等待队列链表中。

  等到磁盘 I/O 完成,内核会反过来操作:把 wait_list 从等待队列中摘除,再把 run_list 插回运行队列,进程重新进入就绪状态(R)。

关键结论

  • 实体唯一 :无论进程如何切换状态,内存中始终只有一份 task_struct
  • 状态切换 = 链表节点的移动 :所谓"运行态""阻塞态"的转换,本质上是将不同的 list_head 成员从一个链表摘下来,再挂到另一个链表上。没有内存拷贝,没有结构体搬移。
  • 效率优势:这种设计将状态切换简化为两次指针操作,时间复杂度为 O(1),非常高效。

八、实战观察进程状态

  前面几节把进程状态的理论和内核数据结构讲得差不多了,但光看理论总归有些抽象。这一节我们动手写几个小程序,再用 ps 命令实际观察它们的状态。

1. 一个循环打印的程序:看到 S 状态

  先写一个很简单的 C++ 程序:它每隔一秒打印一行信息,然后进入睡眠。

cpp 复制代码
#include <iostream>
#include <unistd.h>

int main() 
{
    int times = 0;
    pid_t id = getpid();
    while (true) 
    {
        std::cout << "循环打印中, PID: " << id
                  << ", 第 " << ++times << " 次输出" << std::endl;
        sleep(1);
    }
    return 0;
}

  编译后放到后台运行,记住输出的 PID,然后在另一个终端里用下面的命令查看进程状态:

bash 复制代码
ps -p <PID> -o pid,stat,cmd

  你会看到,绝大多数时候 STAT 列显示的是 S,而不是 R

  这其实很好理解 :这个程序虽然有一个死循环,但每轮循环中只做了一件极短的打印动作,随后就调用 sleep(1) 主动让出 CPU。打印本身也是一次输出操作,需要等待终端设备就绪。因此在一秒的时间尺度里,它真正"活跃"的时间只有极短的一瞬,剩下绝大部分时间都在等待下一次打印或等待睡眠结束,所以状态自然显示为 S

2. 纯 CPU 空转:看到 R 状态

  现在我们改一下代码,去掉所有 I/O 和 sleep,让程序进入一个纯计算空循环:

cpp 复制代码
#include <iostream>
#include <unistd.h>

int main() 
:{
    std::cout << "纯计算进程启动, PID: " << getpid()
              << ",请在另一终端观察状态..." << std::endl;
    while (true) {
        // 这里什么都不做,就是不断占用 CPU
    }
    return 0;
}

  编译运行后,再用 ps 查看:

bash 复制代码
ps -p <PID> -o pid,stat,cmd

  这次你会发现状态一直是 R。因为这个进程既没有等待 I/O,也没有主动睡眠,它唯一做的事情就是不断执行空循环,所以始终具备运行条件。

  如果这个程序是在前台运行(没有加 &),状态栏还可能多出一个 +,显示为 R+,表示它在前台进程组中。如果在后台运行,就没有 +,只显示 R

3. 观察 T 状态和 t 状态

T 状态:用 Ctrl+Z 暂停进程

  先在前台启动一个程序,然后按下 Ctrl+Z,这等于向当前前台进程发送了 SIGSTOP 信号,进程会被立即暂停。

  此时用 ps 查看这个进程,会看到它的状态变成了 T。这说明它已经被"停住"了,不再消耗 CPU,也不会继续执行。

  如果想让它继续运行,可以用 bg 命令把它放到后台继续跑,或者用 fg 把它调回前台。

t 状态:用 gdb 调试程序

  准备一个带断点的简单程序,然后用 gdb 加载并运行。当程序执行到断点停下来时,用另一个终端查看它的状态,你会看到显示为小写的 t

  这是因为调试器通过 ptrace 系统调用追踪进程,当进程被调试器暂停、等待下一条调试指令时,它的状态就是 t。这个状态和普通的 T 不同,专门用于调试场景。

4. 观察 D 状态:深度睡眠实验

  D 状态不太容易直接构造,因为它一般出现在进程等待磁盘 I/O 的时候。我们可以借助 dd 命令来模拟大量磁盘写入,从而提高抓到 D 状态的概率。

bash 复制代码
dd if=/dev/zero of=./bigfile bs=1M count=300

  这行命令会从 /dev/zero 不断读取零字节,然后写入当前目录下的 bigfile 文件。参数含义如下:

  • if=/dev/zero:输入来源,一个特殊设备,读取时会产生无限的零字节;
  • of=./bigfile:输出目标文件;
  • bs=1M:每次读写块大小为 1MB;
  • count=300:一共拷贝 300 块,总大小约 300MB。

  在 dd 执行期间,它会在用户态和内核态之间频繁切换,大量时间消耗在等待磁盘写入完成。此时在另一终端中执行 ps,你很可能会看到它的状态是 D

  D 状态的一个典型特点就是"杀不死" 。你可以试试用 kill -9 <PID> 去杀它,命令会执行成功,但进程并不会消失,状态仍然是 D。因为此时进程正卡在内核态等待磁盘 I/O,根本来不及处理任何信号,连 SIGKILL 也不管用。只有当 I/O 操作完成后,进程才会被唤醒,之后才能正常结束。如果很不幸 I/O 一直无法完成(比如磁盘故障),那可能只能重启系统才能解决问题。

5. 后台进程读取终端输入:自动进入 T 状态

  还有一个很有意思的现象:如果一个程序里包含阻塞式输入(比如等待用户从键盘敲入数据),但你却把它放到后台去运行,它会被内核强制暂停。

  比如下面这个简单的程序:

cpp 复制代码
#include <iostream>

int main() 
gc{
    int num;
    std::cin >> num;
    return 0;
}

  编译后,如果在命令末尾加上 & 让它在后台运行,它会尝试从终端读取输入。但是后台进程无权直接读取终端,内核会向它发送一个 SIGTTIN 信号,强制把它暂停,状态变为 T

  解决办法很简单:用 fg 命令把它调回前台,它就能正常等待输入并继续执行了。


九、D 状态存在的意义

1. 数据丢失的风险

  假设没有 D 状态,所有等待 I/O 的进程都处于 S 状态(可中断睡眠)。考虑一个场景:

  一个进程正在向磁盘写入数据。磁盘写入速度相对 CPU 来说非常慢,所以进程会进入睡眠状态等待磁盘完成。在等待过程中,系统可能发生严重的内存不足,操作系统开始"杀"掉一些进程来释放内存。如果这个正在等待磁盘写入的进程被误杀,那么磁盘还没有完成写入,但进程已经被干掉了。磁盘控制器拿着数据不知道该怎么办,只能丢弃数据。用户并不知道数据已经丢失,这可能导致严重后果------比如医院病人的病历、银行的转账记录等。

2. D 状态的保护作用

  为了防止操作系统在关键时刻误杀正在等待 I/O 的进程,内核设计了 D 状态(不可中断睡眠) 。当进程进入 D 状态后,它不能被任何信号打断 ,包括 SIGKILL。这样,即使系统内存严重不足,进程也不会被杀死,直到 I/O 操作完成、数据安全写入磁盘后,进程才会醒来并可能被正常终止。

3. 如何应对 D 状态?

  如果 D 状态进程长时间不消失,通常说明底层硬件或驱动出了问题(例如磁盘故障、网络存储无响应)。此时:

  • 耐心等待:如果只是短暂的 I/O 高峰,进程会自己醒来。
  • 物理方法:如果进程一直卡在 D 状态无法恢复,可能需要重启计算机或断电。

十、内核对象缓存

  进程退出了,它的 task_struct 会立刻归还给操作系统吗?

  答案是否定的。为了提高系统性能,Linux 内核不会频繁地向系统申请和释放内核对象的内存 ,而是采用了 SLAB 分配器(SLAB Allocator)

  SLAB 是一种针对内核对象的内存缓存机制。当进程退出时,它的 task_struct 会被标记为"未使用(unused)",但内核并不会立刻回收这块内存,而是将其放入 SLAB 缓存池中。如果系统短时间内频繁创建和销毁进程,SLAB 缓存可以直接复用这些已经缓存的结构体内存,避免了频繁向内存管理器申请/释放内存的开销,提升了内核的运行效率。

  同样的机制也适用于其他内核对象,如 inodedentryfile 等。


结语:

  今天的内容到这里就结束了,希望你能有所收获~

干货整理到手抖,觉得有用的话,赏个三连回回血?__(:ᗤ」ㄥ)_ _

相关推荐
RisunJan1 小时前
Linux命令-talk(终端实时对话)
linux·运维·服务器
NeilYuen1 小时前
【vemory】高性能KV存储AOF方案设计
linux·c++·redis·缓存
整点bug1 小时前
AI 运维该不该自动执行命令?
运维·ssh·openai
Kurisu_红莉栖2 小时前
关于docker的使用心得
运维·docker·容器
三言老师2 小时前
Rocky Linux 8.6 整机系统备份与迁移方案文档文档用途
linux·运维·服务器·网络
「PlanA」2 小时前
自动化流水线CSDN自动发布文章
运维·自动化
雾时之林2 小时前
Linux----防火墙
linux·运维·网络
海兰3 小时前
【开源工具】BlueKing Lite —— AI 原生的轻量运维平台(二)
运维·人工智能·开源
我星期八休息4 小时前
网络编程—NAT、代理服务与内网穿透
linux·服务器·网络·网络协议