系列文章目录
文章目录
- 系列文章目录
- 前言
- 一、从课本理论到Linux实现
- [二、Linux 进程状态详解](#二、Linux 进程状态详解)
-
- [2.1 内核源码中的状态定义](#2.1 内核源码中的状态定义)
- [2.2 CPU的运行与调度算法FIFO](#2.2 CPU的运行与调度算法FIFO)
- [2.3 运行、阻塞与挂起](#2.3 运行、阻塞与挂起)
- [2.3.1 运行态](#2.3.1 运行态)
- [2.3.2 阻塞态](#2.3.2 阻塞态)
- [2.3.3 挂起态(阻塞挂起)](#2.3.3 挂起态(阻塞挂起))
- 图片理解
- 三、进程状态的深入解析
-
- [3.1 状态的本质与查看](#3.1 状态的本质与查看)
- [3.2 对立的运行R与睡眠S](#3.2 对立的运行R与睡眠S)
- [3.3 停止的区别:停止T与追踪停止t](#3.3 停止的区别:停止T与追踪停止t)
- [3.4 不可中断睡眠:D状态](#3.4 不可中断睡眠:D状态)
-
- [3.4.1 示例理解](#3.4.1 示例理解)
- [3.4.2 操作实验](#3.4.2 操作实验)
- [3.5 将死与已死:Z状态与X状态](#3.5 将死与已死:Z状态与X状态)
-
- [3.5.1 Z与X的联系](#3.5.1 Z与X的联系)
- [3.5.2 Z状态](#3.5.2 Z状态)
-
- [3.5.2.1 OS创建子进程](#3.5.2.1 OS创建子进程)
- [3.5.2.2 僵尸进程定义与信息储存](#3.5.2.2 僵尸进程定义与信息储存)
- [3.5.2.3 僵尸进程的隐蔽危害](#3.5.2.3 僵尸进程的隐蔽危害)
- [3.6 孤儿进程](#3.6 孤儿进程)
-
- [3.6.1 孤儿进程的定义](#3.6.1 孤儿进程的定义)
- [3.6.2 孤儿的领养的机制](#3.6.2 孤儿的领养的机制)
- [3.6.3 代码实验](#3.6.3 代码实验)
- [3.7 内核结构申请Slab](#3.7 内核结构申请Slab)
- 总结
前言
上文我们聊了进程的基本概念和父子关系,今天继续深入,看看进程的状态到底是怎么回事。这部分内容在课本上有理论模型,在Linux里又有具体实现,我们一点点拆开来看。
一、从课本理论到Linux实现
课本上讲进程状态,通常会给一个三态模型:运行、就绪、阻塞。这是一个普遍性 的理论框架。但Linux作为具体的操作系统,有自己的特殊性------它根据实际需要定义了好几种状态。所以我们学的时候,要明白理论是理论,实现是实现,不能混为一谈。

二、Linux 进程状态详解
2.1 内核源码中的状态定义
Linux 内核里,进程状态用一个数组来定义:
bash
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 状态:运行/就绪
R 状态表示进程正在运行或者在运行队列里。注意,它不一定正在占着 CPU,只要在调度队列中排队,状态也是 R。
-
S 状态:可中断睡眠
S 状态也叫可中断睡眠,进程在等待某个事件(比如 I/O 完成),可以被信号唤醒。大多数等待 I/O 的进程都处于这个状态。
-
D 状态:不可中断睡眠
D 状态也叫不可中断睡眠,通常出现在等待磁盘 I/O 的时候。这个状态的进程不响应任何信号,连 kill -9 都杀不掉,只能等 I/O 完成或者重启系统。这样设计是为了保护重要数据不被误杀。
-
T 状态:停止
T 状态表示进程被暂停了。可以通过发送 SIGSTOP 信号让它停下来,发送
SIGCONT 信号让它继续。终端里按 Ctrl+Z 就是把前台进程暂停,进入 T 状态。 -
t 状态:追踪停止
t 状态和 T 很像,但它是专门给调试器用的。比如你用 gdb 调试程序,程序在断点处停下来,状态就是 t。
-
X 状态:死亡
X 状态是一个瞬时状态,表示进程已经完全终止,资源正在被回收。这个状态在任务列表里一般看不到。
-
Z 状态:僵尸
Z 状态表示进程已经退出,但父进程还没读取它的退出信息。这个我们下一篇文章会详细讲。
我们现在不了解其实际意义是正常的,需要先了解一个进程的调度过程和运行过程中状态的变化。
2.2 CPU的运行与调度算法FIFO
在了解进程调度过程前,我们应先理解到CPU运行的本质,即CPU选择一个PCB去执行,并且之前我们讲过,进程由 PCB + 代码与数据 组成,PCB 里有一个指针指向下一个 PCB,形成全局进程链表。
由上面两点,我们可以很容易的理解这样的结构:

即CPU的调度队列,这就是FIFO调度算法,实现"先进先出"的功能,与我们以后要学习的优先级等密切相关。
2.3 运行、阻塞与挂起
该三态,是一个进程在调度过程中会经历的基本状态,我们先来了解这基础的三态,才能对LInux中划分更加详细的状态理解。
2.3.1 运行态
进程在调度队列中就是运行态,这是LInux中独有的,设计者认为运行与就绪的区别不大,将两者视为运行。
2.3.2 阻塞态
阻塞:当一个进程在运行时,为实现目的发出的请求,未能等待某种设备或资源就绪(如:网卡、磁盘、键盘等) ,或未收到被调用的回应时,就会脱离被os从运行队列中剥离
C语言中 scanf 等待输入的时候,就是等待键盘就绪,阻塞的过程
上面所说阻塞有关的设备和资源,又该怎么由os管理呢?还是Linux的本质:
"先描述,在组织",为管理这些设备,os给出了 struct device 这个结构体,为了实现对硬件的管理,结构体就必然存在指向设备的变量,而对同一个设备调用的进程可能同时存在,因此就还会有个变量存储需要调用自身的进程,如图:

现在我们了解了阻塞是什么,并且与阻塞有关的结构有了一定的了解。那让我们一起更加深刻地认识阻塞是如何产生的吧:
现在我们有一个要执行scanf的进程,让我们从这个案例出发
进程执行 scanf(),最终进入底层的 read() 操作 。当键盘暂时没有输入数据时,当前进程无法立即完成读操作,因此进入阻塞等待,将其从运行队列中移除,并放在管理键盘结构体的等待队列当中。
当用户输入后,键盘硬件产生中断,CPU进入中断处理流程,并将数据放入相应的输入缓冲区,同时唤醒等待该数据的进程即在设备等待队列上连起的PCB,等待调度器再次分配 CPU,随后继续执行之前阻塞的 read(),从缓冲区获取数据并返回给用户程序
2.3.3 挂起态(阻塞挂起)
挂起的情况是计算机缓解情况、优化效率的一种做法,但同时发生时也是十分危急的:即当计算机的物理内存资源不足时,os会将阻塞的进程的一部分或全部内存映像可以被换出到外存当中 即交换分区中。
2.3.3.1交换分区
磁盘中存在一块临时空间,用来存储从内存中交换过来的代码
当硬件可用且内存充足时,os就会将交换分区中的代码和资源重新加载到内存,进行PCB的映射
tips: 当内存严重不足时,甚至会将运行队列末尾的进程挂起(运行挂起)
三、进程状态的深入解析
当我们有了上面进程运行过程的知识基础,现在让我们详细学习Linux中进程的不同状态
3.1 状态的本质与查看
进程状态的本质就是 PCB 中的一个整型变量,是状态数组的下标。 我们可以用 ps 命令查看进程状态,STAT 列中的字母就是状态标识

3.2 对立的运行R与睡眠S
有着这样一个代码,我们执行并且在后台查看进程的状态
bash
#include <iostream>
#include <unistd.h>
#include <sys/types.h>
int main() {
pid_t pid = fork();
if (pid == 0) {
// 子进程:死循环,保持R状态
while (1) {
// 空循环,不做IO
}
} else if (pid > 0) {
// 父进程:睡眠,保持S状态
sleep(1000);
}
return 0;
}
结果为:父进程S+ ,子进程R+。S状态相当于阻塞
"+"号区分前后台进程,在 ./test & 就可以后台执行

3.3 停止的区别:停止T与追踪停止t
- 停止T: 进程由于收到停止信号或受到作业控制等原因,被操作系统主动暂停执行,进入停止状态,暂时不能被调度运行。(SIGTSTP信号,由用户ctrl + z发起)
- 追踪停止t: 进程由于受到 ptrace 调试/跟踪机制控制,在调试事件发生时被内核暂停,进入追踪停止状态。(常见的为 cgdb调试)
3.4 不可中断睡眠:D状态
D状态,又称"深睡眠",与S状态"浅睡眠"相对。两者本质的区别:就是能否响应信号 ,浅睡眠能通过ctrl z 和 ctrl c 停止和终止(这两者本质就是发送信号),而深睡眠则不能,只能有两种方法:1.等待睡眠时间结束 2.硬件断点停止运行
3.4.1 示例理解
场景理解:
系统在后台执行下载任务时,突然意识到内存严重不足,甚至阻塞挂起进程都没法节省出空间,就会杀掉运行队列中的进程,而该下载任务已经下载到99%,此时被杀死,就会造成内存泄漏
因此就有了"D状态",常用于与磁盘操作强相关的进程,使得任务不被阻断。
3.4.2 操作实验
bash
dd if=/dev/zero of=~/test.txt bs=4096 count=100000
| 部分 | 含义 |
|---|---|
| dd | 磁盘拷贝/转换命令,用于按块读写数据 |
| if=/dev/zero 输入文件(input file) | /dev/zero 是一个特殊的设备文件,会源源不断地产生零字节(\0) |
| of=~/test.txt输出文件(output file) | ~ 表示当前用户的主目录,文件名为 test.txt |
| bs=4096 | 每次读写的数据块大小(block size),这里是 4096 字节,即 4KB |
| count=100000 | 一共读写 100000 个数据块 |
3.5 将死与已死:Z状态与X状态
3.5.1 Z与X的联系
正如它们的命名,Z状态僵尸,X状态死亡,两者是递进的关系,Z状态的下一步就是X。用一个例子来直观的区分一下:
某天,一个路人在经过你的时候,突然猝死倒地,由于死亡信息未知,必须等警察等执行机关查验,此时现场信息都得保留。这个过程就是僵尸状态,现场信息就是该进程的PCB(task_struct)
警察随后到来对现场拍照取证,法医对尸体的死亡信息解析处理,处理完后家属决定如何处理尸体。这其中处理完尸体后才算死亡,即X状态。
那么让我们先来了解一下Z状态吧
3.5.2 Z状态
3.5.2.1 OS创建子进程
我们要理解僵尸状态,就必须先了解到一件事:用户启动的进程都是由操作系统fork创建子进程来完成的。而创建进程的目的就是为了实现某个功能/呈现结果,于是就需要返回进程的信息,而这个信息就是PCB。
3.5.2.2 僵尸进程定义与信息储存
僵尸进程就是如此,是一个子进程执行完任务,处于返回自身执行信息的状态,只储存PCB即可,代码与数据都可以释放。
退出信息部分查看:
bash
struct task_struct {
// ...
long exit_state; // 退出状态
int exit_code; // 退出码,如 exit(0)
int exit_signal; // 退出信号,如kill -9
// ...
};
当进程变为僵尸进程时,退出状态就会设置为EXIT_ZOMBIE,父进程进而通过系统调用wait() 或 waitpid()获取信息,然后子进程完全退出设置X (X状态设置是瞬间完成的,一般查看不到)

3.5.2.3 僵尸进程的隐蔽危害
PCB作为进程运行信息的载体,创建这样一个消息体需要资源,因此如果父进程不对子进程PCB进行查看回收,就会造成内存泄漏 。同时如果过多进程执行,就会越来越卡顿,并且实际上进程总数有限,类似"占着厕所不那个"。
3.6 孤儿进程
既然存在僵尸进程"子进程先退出,父进程后退出",那么有没有这样的进程"父进程先退出,子进程还在运行",这样的进程就是孤儿进程。
3.6.1 孤儿进程的定义
当一个子进程的父进程,因内存不足、信号杀死、异常崩溃等因素退出,该子进程就被称为孤儿进程
3.6.2 孤儿的领养的机制
操作系统绝不允许进程没有"监护人",因为如果孤儿进程以后退出时没人回收,就会变成僵尸进程,造成内存泄漏。所以 Linux 规定:
父进程先退出后,1 号进程(init 或 systemd)会领养所有孤儿进程,ssh远程连接成功后,为我们调用创建的bash就是它帮我们fork产生的。
1 号进程是系统启动后创建的第一个用户空间进程,会一直驻留内存。它领养孤儿进程后,会在孤儿进程退出时自动调用 wait() 回收资源,保证不泄漏。
3.6.3 代码实验
通过ai生成的代码,预测结果:父进程运行五秒退出,子进程一直刷屏
bash
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
int main() {
pid_t pid = fork();
if (pid < 0) {
perror("fork error");
return 1;
}
else if (pid == 0) {
// 子进程:一直运行,观察 ppid 变化
while (1) {
printf("我是子进程,pid = %d, ppid = %d\n", getpid(), getppid());
sleep(1);
}
}
else {
// 父进程:只活 5 秒就退出
printf("我是父进程,pid = %d,5 秒后退出...\n", getpid());
sleep(5);
printf("父进程退出!\n");
}
return 0;
}

查看后台:
3.7 内核结构申请Slab
进程的PCB = 内存申请 + 数据初始化 ,从学习C语言的经历可知:内存的开辟与释放时十分消耗资源的。
而高效的Linux为提高效率,则使用了"内核结构申请 SLAB"就是:内核为常用结构体建一个对象缓存池 ,创建时从池里拿,销毁时还回池里,避免频繁申请物理内存,提高效率。 僵尸进程不回收,就是占着池子里的对象不放,导致池子可用对象变少。

以 task_struct 为例,我们来了解一下Slab的运行过程:
-
创建缓存: 内核启动时,会为 task_struct 创建一个专门的 SLAB 缓存
(task_struct_cachep)。 -
申请对象: 创建新进程时,调用
kmem_cache_alloc()从缓存里拿一块空闲内存,用来放task_struct。 -
初始化: 把这块内存初始化成新的 PCB。
-
释放对象: 进程被回收后,调用
kmem_cache_free()把这块内存还回缓存,标记为空闲,而不是还给物理内存。 -
复用: 下次再创建进程,直接从缓存里拿现成的空闲块,改改字段就能用。
整个过程,物理内存没有频繁申请和释放,只是在内核的缓存池里进出,效率极高。
总结
Linux进程状态本质是PCB里的一个整型变量,分为R、S、D、T、t、X、Z七种,通过运行、阻塞、挂起进行转换,其中僵尸进程是子先走父不回收,孤儿进程是父先走被1号进程领养,内核用SLAB分配器高效复用PCB。
如果对你有帮助,别忘了点赞支持,我们下期再见。
