3. 进程状态

目录

一、教材中经典的进程状态模型

[二、调度 && 运行 && 阻塞 && 挂起](#二、调度 && 运行 && 阻塞 && 挂起)

[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):进程因等待某事件而暂停,且被挂起(换出到外存)。

关键的状态转换过程:

  1. 调度:活动就绪 → 执行(系统分配 CPU)。

  2. 时间片完:执行 → 活动就绪(时间片用完,被切换下来)。

  3. 事件发生:活动/静止阻塞 → 活动/静止就绪(等待的事件满足了)。

  4. 挂起 (Suspend):执行/活动就绪/活动阻塞 → 静止就绪/静止阻塞(系统把进程调出到外存,以释放内存)。

  5. 激活 (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 宏 的底层原理:

  1. 求偏移量 :利用 (struct task_struct*)0 -> links 假设结构体在 0 地址,直接访问成员 links 的地址,就得到了 links 在 task_struct 中的偏移量(偏移地址)

  2. 反向推导 :利用 (char*)list - 偏移量,用当前 list 的内存地址减去该偏移量,就能算出宿主结构体 task_struct 的起始内存地址

  3. 转换类型:最后将算出的地址强转为(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)管着


相关推荐
kdxiaojie1 小时前
Linux 驱动研究 —— V4L2 (2)
linux·运维·笔记·学习
DB哥讲数据库1 小时前
MySQL 8.4 安装教程:超详细图文讲解(附mysql安装包)
linux·数据库·mysql·centos
xlxxy_2 小时前
sap获取批次特性报表
java·linux·开发语言·前端·数据库·abap·mm
木卫二号Coding2 小时前
CentOS 系统 Shell 脚本开机自启动方法详解
linux
l1t2 小时前
DeepSeek总结的RegreSQL 2.0测试通过了。计划却没通过。
linux·数据库·postgresql
蝶恋舞者2 小时前
怎样设置软件开机自启动(用的最多,最简单)
linux·运维·服务器
Dawn-bit2 小时前
Linux打包压缩与用户权限管理详解
linux·服务器·数据库
ls_elect2 小时前
PostgreSQL 18.4 SCRAM-SHA-256 认证故障诊断及修复办法
linux·数据库·postgresql
陈同学xxx3 小时前
Hermes Agent 接入飞书,在手机上随时聊天
linux·python·飞书