目录
[二、调度 && 运行 && 阻塞 && 挂起](#二、调度 && 运行 && 阻塞 && 挂起)
[2.1. 画图 + 例子](#2.1. 画图 + 例子)
[2.2. 阻塞与挂起的区别](#2.2. 阻塞与挂起的区别)
[2.3. 上述例子的流程图](#2.3. 上述例子的流程图)
[三、Linux 内核链表 的底层工作原理和内存布局图](#三、Linux 内核链表 的底层工作原理和内存布局图)
[3.1. 结构定义](#3.1. 结构定义)
[3.2. 如何通过链表节点找到宿主结构体](#3.2. 如何通过链表节点找到宿主结构体)
[3.3. 图示可视化](#3.3. 图示可视化)
[四、Linux 的进程状态](#四、Linux 的进程状态)
[4.1. Linux内核源代码怎么说](#4.1. Linux内核源代码怎么说)
[4.2. 进程状态查看](#4.2. 进程状态查看)
[4.3. 用代码展示不同的进程状态](#4.3. 用代码展示不同的进程状态)
[4.3.1. R 运行状态](#4.3.1. R 运行状态)
[4.3.2. S 睡眠状态 (sleeping)](#4.3.2. S 睡眠状态 (sleeping))
[4.3.3. t 状态(跟踪停止)](#4.3.3. t 状态(跟踪停止))
[4.3.4. T 停止状态 (stopped)](#4.3.4. T 停止状态 (stopped))
[4.3.5. D 磁盘休眠状态 (Disk sleep)](#4.3.5. D 磁盘休眠状态 (Disk sleep))
[4.3.6. 僵死状态 (Zombies)](#4.3.6. 僵死状态 (Zombies))
一、教材中经典的进程状态模型

这张图详细描述了进程在生命周期中可能经历的各种状态,以及导致状态发生转换的条件。
图中的主要状态包括:
-
创建 (New):进程正在被创建。
-
执行 (Running):进程正在 CPU 上运行(图上方有 CPU 指向它)。
-
终止 (Terminated):进程执行结束,正在被系统回收。
-
活动就绪 (Active Ready):进程准备好运行,等待 CPU 调度。
-
静止就绪 (Suspended Ready):进程准备好运行,但被挂起(换出到外存),即使有 CPU 也无法运行,需要先"激活"。
-
活动阻塞 (Active Blocked):进程因等待某事件(如 I/O)而暂停,且驻留在内存中。
-
静止阻塞 (Suspended Blocked):进程因等待某事件而暂停,且被挂起(换出到外存)。
关键的状态转换过程:
-
调度:活动就绪 → 执行(系统分配 CPU)。
-
时间片完:执行 → 活动就绪(时间片用完,被切换下来)。
-
事件发生:活动/静止阻塞 → 活动/静止就绪(等待的事件满足了)。
-
挂起 (Suspend):执行/活动就绪/活动阻塞 → 静止就绪/静止阻塞(系统把进程调出到外存,以释放内存)。
-
激活 (Activate):静止就绪/静止阻塞 → 活动就绪/活动阻塞(系统把进程从外存调入内存)。
简单来说,这张图展示了一个进程从创建 到终止的过程中,是如何被操作系统调度、等待资源以及被挂起和激活的。
上述的内容是 教材中经典的**进程状态模型,**其中箭头指向是不同状态之间转换的事件发生,但是这个过于复杂,并且不好理解,我们主要讲解的是从内核、数据结构的视角,对 不同状态进行理解。
二、调度 && 运行 && 阻塞 && 挂起
2.1. 画图 + 例子

我们在第一章 --- 初步了解操作系统中,我们聊过:管理 ,在 内核中是通过 双向链表 进行管理的,其中上面是一个示意图。
进程的调度: 就是CPU 按照一定的顺序,选取调度队列中一个 task_struct 执行。
小问题:之前不是说 PCB 是由 双向链表 来管理的,怎么现在又扯到了 队列?
这个是设计的原因,把PCB设计成了,**既可以成为一个 全局的双向链表,又可以把相关进程的放到一个队列当中,**换而言之,一个 PCB 节点,可以是 A 的数据结构,又可以是 B 的数据结构。
运行:进程在调度队列中,进程的状态都是 running
**阻塞:等待某种设备或者资源就绪,**例如:键盘、显示器、网卡、磁盘等。
运行的时候,是存在一个 运行队列,阻塞的时候,也会存在一个队列,是等待队列。

这些硬件设备的伪代码可以是这样的:
cpp
struct device{
ind id;
in vender;
int staurs
void*data;
struct device*next;
struct device*prev;
int type;
struct task_struct* wait_queue;
}
里面会有一个 等待队列。
我们下面以 按住键盘 这个事件为例,进行展开:
- 当代码运行到 cin 或者 scanf 的时候,会阻塞,等待键盘硬件就绪。当我们没有按住键盘的时候,称为键盘文件没有就绪。
- 阻塞的话,调度队列就会查询为什么阻塞,把相应的 PCB节点放到对应硬件设施的PCB的wait_queue 中。
- 按住键盘,属于硬件就绪,操作系统作为硬件的管理者,就会查询对应硬件设备的节点,将运行状态设置为activate,并且检查等待队列,发现指针不为空,就把对应的节点的状态改为 运行状态,把该进程重新链接回去到 运行队列 里。
- 此时这个进程还没有被调度,键盘中的数据没有被读取。在 CPU 的调度之下,在运行scanf,把数据从设备中去读取到进程的上下文当中,然后被我们拿到。

还会发生这个情况:
当我们的内存资源严重不足了,有些进程不会被访问,但是还站着内存。
例如在阻塞队列中的代码和数据就不会被执行。
此时,磁盘就会把 阻塞队列中的代码和数据,放到 swap 交换分区,只将 PCB 存下来。
此时就是挂起状态:内存资源严重不足,操作系统做一些页面置换的算法,把一些不会被调度的进程或者内存块,交换到对应的磁盘上,此时进程只有PCB的,成为挂起进程。
等到有资源了,输入内容了,操作系统知道了,就会把之前把之前的数据和代码 换入到 原来的PCB上

2.2. 阻塞与挂起的区别
| 状态 | 是否在运行队列 | PCB 是否在内存 | 代码/数据是否在内存 | 原因 |
|---|---|---|---|---|
| 阻塞 | ❌ 不在 | ✅ 在 | ✅ 在 | 等待设备/资源就绪 |
| 挂起 | ❌ 不在(通常) | ✅ 在(部分) | ❌ 被换出到交换分区(swap) | 内存资源严重不足 |
-
阻塞 → 挂起 :当内存不足时,操作系统可能将阻塞队列中的进程 的代码和数据交换到磁盘(swap),只保留 PCB,此时进程进入挂起阻塞状态。
-
唤醒时:需要先换入代码和数据(从 swap 读回内存),再进入就绪队列。
2.3. 上述例子的流程图
运行队列 (runqueue)
│
├─ 进程A (运行) → scanf → 阻塞
│ ↓
│ 从运行队列移除
│ ↓
└─ 进程B (运行) 键盘等待队列 (wait_queue)
│
用户按键 (中断)
↓
状态 → 就绪
↓
重新放入运行队列
↓
调度器选中 → 继续执行
三、Linux 内核链表 的底层工作原理和内存布局图
3.1. 结构定义
传统链表:结构体 Node 包含 data(数据)以及next/prev(指针)。这种方式导致链表只能存放特定类型的数据,通用性差。

内核链表:单独定义一个通用的 struct list_head,只包含 next 和 prev 指针。

实际使用 :你想让哪个结构体变成链表节点,就把 struct list_head 作为一个成员嵌入进去(如图中间的 struct task_struct 嵌入了 list_head list)。

3.2. 如何通过链表节点找到宿主结构体
因为链表里存储的只是 list_head 的地址,当你拿到一个 list 节点的指针时,如何反向找到它所属的那个大的 task_struct 结构体呢?
offsetof 宏和 container_of 宏 的底层原理:
-
求偏移量 :利用 (struct task_struct*)0 -> links 假设结构体在 0 地址,直接访问成员 links 的地址,就得到了 links 在 task_struct 中的偏移量(偏移地址)。
-
反向推导 :利用 (char*)list - 偏移量,用当前 list 的内存地址减去该偏移量,就能算出宿主结构体 task_struct 的起始内存地址。
-
转换类型:最后将算出的地址强转为(struct task_struct*),就拿到了完整的结构体指针。

3.3. 图示可视化
-
红色大框 代表一个完整的 task_struct 结构体。
-
蓝色小框 代表嵌入其中的 list_head 节点。
-
箭头 表示链表前后的指针指向。
-
通过这种方式,你可以把任意类型的结构体(进程控制块、文件描述符、内存页等)用同一套 list_head 逻辑串起来,极大提高了代码复用率。

四、Linux 的进程状态
4.1. Linux内核源代码怎么说
cpp
static const char *task_state_array[] = {
"R (running)", /* 0 */
"S (sleeping)", /* 1 */
"D (disk sleep)", /* 2 */
"T (stopped)", /* 4 */
"T (tracing stop)", /* 8 */
"Z (zombie)", /* 16 */
"X (dead)" /* 32 */
};
-
R 运行状态 (running):并不意味着进程一定在运行中,它表明进程要么是在运行中要么在运行队列里。
-
S 睡眠状态 (sleeping):意味着进程在等待事件完成(这里的睡眠有时候也叫做可中断睡眠 (interruptible sleep)。
-
D 磁盘休眠状态 (Disk sleep):有时候也叫不可中断睡眠状态 (uninterruptible sleep),在这个状态的进程通常会等待IO的结束。
-
T 停止状态 (stopped):可以通过发送 SIGSTOP 信号给进程来停止(T)进程。这个被暂停的进程可以通过发送 SIGCONT 信号让进程继续运行。
-
X 死亡状态 (dead):这个状态只是一个返回状态,你不会在任务列表里看到这个状态。
-
僵死状态 (Zombies):是一个比较特殊的状态。当进程退出并且父进程(使用wait()系统调用,后面讲)没有读取到子进程退出的返回代码时就会产生僵死(尸)进程。
-
t 状态(跟踪停止) :当进程被 ptrace 系统调用 所跟踪时产生。最典型的场景是被调试器(如 gdb )或者系统调用追踪器(如 strace)挂起 。当调试器在断点处命中、或者进行单步调试时,被调试的进程就会进入这个 t 状态
4.2. 进程状态查看
bash
ps aux / ps axj 命令
-
a:显示一个终端所有的进程,包括其他用户的进程。
-
x:显示没有控制终端的进程,例如后台运行的守护进程。
-
j:显示进程归属的进程组ID、会话ID、父进程ID,以及与作业控制相关的信息
-
u:以用户为中心的格式显示进程信息,提供进程的详细信息,如用户、CPU和内存使用情况等
4.3. 用代码展示不同的进程状态
4.3.1. R 运行状态
R 运行状态 (running):并不意味着进程一定在运行中,它表明进程要么是在运行中要么在运行队列里。
cpp
#include <iostream>
#include <cstdio>
#include <unistd.h>
using namespace std;
int main()
{
int i = 0;
while (true)
{
i++;
}
return 0;
}
在另一个终端输入以下命令,用于监视:
bash
while :; do ps ajx | head -1; ps ajx | grep myprocess; sleep 1; done

在这代码中,会发现它的状态永远是 R+ ,因为你时刻都在用户态跑 CPU,内核永远不会主动让它睡眠,不会进入到 内核中;
如果你是在 循环中使用打印操作,如下面的代码,就会出现 R 和 S 交替进行。
cpp
#include <iostream>
#include <cstdio>
#include <unistd.h>
using namespace std;
int main()
{
while (true)
{
printf("hello linux\n");
}
return 0;
}

此时我们发现,他的绝大部分时间都是在 S+,并不是 R+。
首先需要确定的是,这个任务一定是有 R+ 的状态,毋庸置疑,但是为什么绝大多数情况下都是 S+呢?
粗略的解释一下:这个 printf 操作,其实是封装了 系统调用 函数的,相当于 会把数据 放入到 内核中, 如果终端此时正在处理之前的数据、缓冲区已满,或者硬件来不及消费,write系统调用就会把进程挂起(进入 S 状态),直到终端驱动程序把数据取走,腾出空间。因此会出现 S+ 的情况。
4.3.2. S 睡眠状态 (sleeping)
S 睡眠状态 (sleeping) :意味着进程在等待事件完成(这里的睡眠有时候也叫做可中断睡眠 (interruptible sleep)。 可以中断,浅睡眠
cpp
#include <iostream>
#include <cstdio>
#include <unistd.h>
using namespace std;
int main()
{
while (true)
{
sleep(1);
}
return 0;
}

4.3.3. t 状态(跟踪停止)
t 状态(跟踪停止) :当进程被 ptrace 系统调用 所跟踪时产生。最典型的场景是被调试器(如 gdb )或者系统调用追踪器(如 strace)挂起 。当调试器在断点处命中、或者进行单步调试时,被调试的进程就会进入这个 t 状态
操作方式:在 scanf 中打入断点,然后使用 gdb 进行调试:
cpp
#include <iostream>
#include <cstdio>
#include <unistd.h>
using namespace std;
int main()
{
printf("我是一个进程,pid:%d\n", getpid());
int x;
scanf("%d", &x);
printf("%d\n", x);
return 0;
}
在 scanf("%d",&x); 打上断点,此时我们看一下,进程状态变化


4.3.4. T 停止状态 (stopped)
T 停止状态 (stopped):可以通过发送 SIGSTOP 信号给进程来停止(T)进程。这个被暂停的进程可以通过发送 SIGCONT 信号让进程继续运行。
操作方式,运行一个进程后,按住 ctrl + z 即可
cpp
#include <iostream>
#include <cstdio>
#include <unistd.h>
using namespace std;
int main()
{
while (true)
{
sleep(1);
}
return 0;
}


如果想要杀死这个 myprocess 进程,可以使用这个命令:
bash
kill -9 pid

4.3.5. D 磁盘休眠状态 (Disk sleep)
D 磁盘休眠状态 (Disk sleep):有时候也叫不可中断睡眠状态 (uninterruptible sleep),在这个状态的进程通常会等待IO的结束。
现在有个场景,就是 os 中的某个进程,要想磁盘写入 100 MB,但是呢,内存资源太紧张了,
一直在 sleep 中,然后 os 发现了这个进程,为节约资源,就把这个进程给杀了。
万一,写入这个100MB的数据,是非常关键的呢,例如金钱的转入,流水信息等?
如果直接删除了,那么谁都不知道这个问题,就会造成一系列的后果。
所以这类进程,是不能删除的,哪怕资源很紧张,因此这种进程的状态称之为D(disk sleep),不可中断睡眠,深度睡眠

输入这个命令:
bash
dd if=/dev/zero of=~/test.txt bs=4096 count=10000 oflag=dsync

命令的含义:用全零数据生成一个 40MB 的文件,但写入方式极其"较真"------每写 4KB 数据,都必须等硬盘物理确认"收到并存好"后,才继续写下一批
用下面的的命令进行检测:
bash
while :; do ps ajx | head -1; ps ajx | grep dd | grep -v grep; sleep 1; done

4.3.6. 僵死状态 (Zombies)
僵死状态 (Zombies) :是一个比较特殊的状态。当进程退出并且父进程 (使用wait()系统调用,后面讲)没有读取到子进程退出的返回代码时就会产生僵死(尸)进程。
意思就是:子进程已经进行完毕,但是父进程一直不管。就会出现僵尸进程!
代码:
cpp
#include <iostream>
#include <cstdio>
#include <unistd.h>
using namespace std;
int main()
{
pid_t id = fork();
if (id < 0)
{
cerr << "创建子进程失败" << endl;
return 1;
}
else if (id == 0)
{
// child
int count = 5;
while (count--)
{
sleep(1);
cout << "我是子进程," << "当前 pid: " << getpid() << " , 父进程id: " << getppid() << " count: " << count << endl;
}
}
else
{
// father
while (true)
{
sleep(1);
cout << "我是父进程," << "当前 pid: " << getpid() << " , 父进程id: " << getppid() << endl;
}
}
return 0;
}
实验效果:

如果父进程一直不管,不收回,不孩子进程的退出信息,那么Z就会一直存在,会造成什么问题,严重的内存泄漏问题!!!
如果进程退出了,内存泄露还存在不?
不存在,因为在学习c语言,malloc申请空间的时候,就算申请空间大,退出程序,该进程就销毁了,就不会出现内存泄漏的时候。
那么什么样的进程具有内存泄漏的问题,是比较的麻烦的。
常驻内存的进程!!! ---- 长期运行
内存泄漏非常危险,因为泄漏会不断累积,最终导致系统内存耗尽。
五、孤儿进程
孤儿进程不属于 Linux 进程状态(如 R、S、D、Z、T 等),它属于一种"进程分类"或"进程场景"。
孤儿进程的定义:
当父进程先于子进程退出(被杀死或正常结束)了,而子进程还在运行,那么这个子进程就变成了"孤儿进程"。
-
父进程如果提前退出,那么子进程后退出,进入Z之后,那该如何处理呢?
-
父进程先退出,子进程就称之为"孤儿进程"
-
孤儿进程被1号init进程领养,当然要有init进程回收喽。
cpp
#include <iostream>
#include <cstdio>
#include <unistd.h>
using namespace std;
int main()
{
pid_t id = fork();
if (id < 0)
{
cerr << "创建子进程失败" << endl;
return 1;
}
else if (id == 0)
{
// child
while (true)
{
sleep(1);
cout << "我是子进程," << "当前 pid: " << getpid() << " , 父进程id: " << getppid() << endl;
}
}
else
{
// father
int count = 5;
while (count--)
{
sleep(1);
cout << "我是父进程," << "当前 pid: " << getpid() << " , 父进程id: " << getppid() << " count: " << count << endl;
}
}
return 0;
}
使用 下面的命令进行监听:
bash
while :; do ps ajx | head -1; ps ajx | grep myprocess; sleep 1; done
看看结果图:


在父进程走之后,子进程 就会被 1 号进程给 "领养",此时 终端仍然会打印这个语句"我是子进程,当前 pid: 789879 , 父进程id: 1" ,需要输入 这个命令才可以杀死:
bash
kill pid
那么 1 号进程是什么呢?输入以下命令就知道了:
bash
ps ajx | head -1 && ps ajx | grep " 1 " | head -5
ls -l /sbin/init

父子进程关系中,如果父进程先退出,子进程要被 1号进程 (可以理解成操作系统)领养,这个被领养的进程(子进程),叫做孤儿进程。
为什么要领养,这是因为 在父进程结束之后,子进程无人看管,然后为了防止出现内存泄漏问题,就交给系统(pid=1)管着
完