
大家好,欢迎来到 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—— 显示进程组、会话、父进程等信息)
- [1. `ps aux` ------ 以用户为中心的详细格式](#1.
- [四、运行队列与 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 状态下的进程无法被信号打断,即使发送 SIGKILL(kill -9)也无法立即杀死它,必须等到 I/O 操作完成、进程自己醒来。这是内核为了保护数据一致性而设计的重要机制,后面我们会详细解释为什么需要 D 状态。
4. T(stopped)------ 停止状态
T 状态表示进程被暂停/停止执行 。可以通过向进程发送 SIGSTOP 信号(19 号信号)使其进入 T 状态。被停止的进程不会占用 CPU,也不会响应普通信号(除了 SIGCONT 和 SIGKILL)。当收到 SIGCONT 信号后,进程会被唤醒并继续运行。
5. t(tracing stop)------ 追踪停止状态
小写 t 状态专门用于调试器(Debugger)环境 。当进程被调试器(如 gdb)追踪时,调试器会通过 ptrace 系统调用控制进程。进程在等待调试器发送下一条指令期间,状态会显示为小写 t。
t 与 T 虽然都属于"停止"类别,但语义不同: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 列就是进程状态。注意它可能包含多个字符,例如 Ss、R+、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 列同样重要,同时还能看到 PPID、PGID、SID 等。
注意 :
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 请求)暂时无法得到满足,或者必须等待某个事件(如磁盘数据读取完成、网卡数据到达、用户键盘输入)发生时,操作系统会:
- 剥夺该进程的 CPU 使用权;
- 将其从运行队列中移出;
- 将其挂到对应设备的等待队列(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。我们可以通过 ps 的 RSS 列或 /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);
意思是:
- 将数字
0强制转换为struct task_struct *类型的指针,相当于在内存地址 0 的位置"虚拟"出一个task_struct。- 访问这个虚拟结构体的
links成员,并取其地址。- 因为结构体基地址是 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 内核提供的一个通用链表节点结构,里面只有两个指针:next 和 prev。一个 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 结构体本身,不会把它的内存地址搬来搬去,也不会复制任何数据。内核只做了两件事:
- 从运行队列中摘除 :调用
list_del(&p->run_list),把该进程的run_list节点从运行队列链表中删掉。- 插入等待队列 :调用
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 缓存可以直接复用这些已经缓存的结构体内存,避免了频繁向内存管理器申请/释放内存的开销,提升了内核的运行效率。
同样的机制也适用于其他内核对象,如 inode、dentry、file 等。
结语:
今天的内容到这里就结束了,希望你能有所收获~
干货整理到手抖,觉得有用的话,赏个三连回回血?__(:ᗤ」ㄥ)_ _