进程初识之进程状态

经典理论模型

什么叫做进程状态?状态,决定了进程接下来要做的工作

下面这张图就是操作系统的状态转换图

所有操作系统在实现进程状态变化的时候都要符合上面的理论

下面给出这七种状态的概念

核心进程状态(经典五状态模型)

  1. 新建 (New)
    • 进程正在被创建(加载程序代码、分配内存等)。
    • 尚未准备好执行。
  2. 就绪 (Ready)
    • 进程已获得除 CPU 之外的所有必要资源。
    • 它已加载到内存中,等待操作系统调度器分配 CPU 时间片。
    • 系统中通常有多个进程处于就绪状态,它们排在一个或多个就绪队列中等待调度。
  3. 运行 (Running)
    • 进程已获得 CPU 资源,其指令正在被 CPU 执行。
    • 在单核 CPU 系统中,同一时刻只有一个进程处于运行状态。
    • 在多核系统中,可以有多个进程同时处于运行状态(每个核心一个)。
  4. 阻塞 (Blocked / Waiting)
    • 进程因等待某个事件(如 I/O 操作完成、信号量释放、消息到达、用户输入等)而无法继续执行
    • 进程主动或被动地放弃了 CPU。
    • 处于阻塞状态的进程会被移出就绪队列,放入与所等待事件相关的阻塞队列中。
    • 当等待的事件发生后(如 I/O 完成),操作系统会将该进程移回就绪状态。
  5. 终止 (Exit / Terminated)
    • 进程已完成执行或被操作系统强制终止。
    • 操作系统回收分配给该进程的资源(内存、文件描述符等),但其进程控制块 (PCB) 中的退出状态信息可能会保留一段时间,供父进程查询。

重要补充状态(七状态模型)

  1. 挂起就绪 (Ready Suspended)
    • 进程原本在就绪 状态,但被操作系统从内存交换到磁盘(交换空间/Swap)以释放内存资源。
    • 进程仍然满足执行的条件(除了不在内存中),一旦被换回内存,它就回到就绪状态。
    • 它处于一种"准备好但被暂停"的状态。
  2. 挂起阻塞 (Blocked Suspended)
    • 进程原本在阻塞 状态,也被操作系统从内存交换到磁盘
    • 进程既在等待事件(阻塞),又不在内存中(挂起)。
    • 当等待的事件发生时,进程状态变为挂起就绪 (因为它在磁盘上,不能直接运行)。需要先被换回内存(状态变为就绪),才能等待 CPU。

运行状态、阻塞状态、挂起状态补充说明

接下来我们对最为重要的运行状态、阻塞状态、挂起状态补充说明以便于我们更好的理解

进程竞争资源,本质是竞争两类资源

1 CPU资源

2 外设资源

当进程需要竞争CPU资源时,它就会处于CPU的调度队列中,只要进程处于CPU的调度队列中,它就是运行状态,随时等待CPU调度执行,每一个CPU都会有一个调度队列

当进程需要竞争外设资源时,它就会处于外设的调度队列中,阻塞状态在我们日常敲代码很容易见到

比如你要输入一个数据,当你的程序运行到这里时就会一直等待你输入数据,只要你一直不输入数据,这个程序就会一直等待下去,此时进程所处的状态就是阻塞状态

我们的内存是有限的,当内存资源不足时该怎么办?这时就需要挂起状态了

挂起状态(Suspended State)是进程状态模型中的重要扩展,主要用于解决物理内存不足 的问题。当系统内存紧张时,操作系统会将暂时不活跃的进程从内存移出到磁盘交换区(Swap Space),此时进程进入挂起状态。这是一种用磁盘空间换取内存空间的策略。

阻塞和运行的本质是更改进程的task_struct(PCB)

1 更改task_struct的状态属性

2 将task_struct链入不同的队列

Linux 进程状态

Linux内核源代码怎么说

为了弄明白正在运行的进程是什么意思,我们需要知道进程的不同状态。一个进程可以有几个状态(在Linux内核里,进程有时候也叫做任务)。

下面的状态在kernel(内核)源代码里定义:

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

状态其实就是数字,不同的状态在PCB中表现为不同的数字

在 Linux 中,进程状态通过内核定义,比经典理论模型更细粒度

• R 运行状态(running): 并不意味着进程一定在运行中,它表明进程要么是在运行中要么在运行队列里。

• S 睡眠状态(sleeping): 意味着进程在等待事件完成(这里的睡眠有时候也叫做可中断睡眠(interruptible sleep))。

• D 磁盘休眠状态(Disk sleep)有时候也叫不可中断睡眠状态(uninterruptible sleep),在这个

状态的进程通常会等待IO的结束。

• T 停止状态(stopped): 可以通过发送 SIGSTOP 信号给进程来停止(T)进程。这个被暂停的

进程可以通过发送 SIGCONT 信号让进程继续运行。

• X 死亡状态(dead):这个状态只是一个返回状态,你不会在任务列表里看到这个状态。

进程状态查看

cpp 复制代码
ps aux / ps axj 命令

• a:显示一个终端所有的进程,包括其他用户的进程。

• x:显示没有控制终端的进程,例如后台运行的守护进程。

• j:显示进程归属的进程组ID、会话ID、父进程ID,以及与作业控制相关的信息

• u:以用户为中心的格式显示进程信息,提供进程的详细信息,如用户、CPU和内存使用情况等

当我们在死循环中不断地打印语句时,进程会高频的访问外设,此时进程的PCB在CPU的调度队列和外设的等待队列中来回倒置,只不过由于与CPU相比外设的速度太慢了,所以我们查看时大部分都是处于S状态

我们可以看到,在test进程运行时,它的状态为S+,当停止运行时,就看不见了

在此补充一个特殊的状态,附加符号:+

核心状态字母 + 附加符号的完整含义

  1. S+
    组成:S (可中断睡眠) + + (前台进程组)
    含义:
    进程处于 可中断睡眠状态(等待事件,如 I/O)
    属于 前台进程组(在终端中直接运行,可接收用户输入)
  2. R+
    组成:R (运行/可运行) + + (前台进程组)
    含义:
    进程正在 运行或等待 CPU
    属于 前台进程组(占用终端控制权)
  3. S (无附加符号)
    含义:
    进程处于 可中断睡眠状态
    属于 后台进程组(不占用终端)
  4. R (无附加符号)
    含义:
    进程 正在运行或等待 CPU
    属于 后台进程组

前台进程可以被Ctrl+C终止,后台进程不可以

此时没有访问外设,只访问CPU资源,我们可以看到进程处于R状态

当进程等待输入时,同样处于S状态

D状态

我们都知道S是浅度睡眠状态,可以对外部信号做出响应,可以被Ctrl+C终止,也可以被kill杀掉

与之对应的就是D深度睡眠状态,当进程处于D状态时,不会被终止,杀掉,也不能被唤醒,只能等待进程自己苏醒

D状态的目的是为了保护重要数据,如向磁盘写入的数据

有时候操作系统的内存资源会被大量占用,内存已经快要满了,操作系统濒临崩溃,即使已经将进程挂起,可内存资源还是不足,此时,操作系统为了保全自己,就会杀掉进程以释放内存,被杀掉的进程可能正在向磁盘写入数据,而被杀掉后这些进程的数据就会丢失

而当进程处于D状态时,就算是操作系统也杀不死,这样就能有效保护重要数据

T状态

T状态是暂停状态,可以通过发送 SIGSTOP 信号给进程来停止(T)进程。这个被暂停的

进程可以通过发送 SIGCONT 信号让进程继续运行,通常出现在进程进行了某些违法操作,如关闭键盘文件,禁止进程向键盘文件写入,而进程仍然向键盘写入,此时,进程进行了违法操作,但罪不至死,所以操作系统没有杀掉它,而是令它处于T状态,从而让程序员注意到它,进而处理这个进程

t状态

t " (tracing stop)",追踪暂停状态,常见于调试程序时程序遇断点暂停

Z(zombie)-僵尸进程

进程结束是进程创建的反过程

一个进程被创建出来是为了完成任务的,当这个进程结束的时候,任务完成了吗,完成的怎么样,这些信息都需要反馈给操作系统。因此,进程结束的时候不能立即释放该进程的所有资源,而是要先处于一种僵尸状态,代码数据释放,但是task_struct会保留,task_struct中会记录进程退出时候的退出信息,方便父进程读取退出码

就比如我们写main函数时会return 0;0就表示成功的意思,将该退出信息返回给main的父进程

• 僵死状态(Zombies)是一个比较特殊的状态。当进程退出并且父进程(使用wait()系统调用,后面讲)没有读取到子进程退出的返回代码时就会产生僵死(尸)进程

• 僵死进程会以终止状态保持在进程表中,并且会一直在等待父进程读取退出状态代码。

• 所以,只要子进程退出,父进程还在运行,但父进程没有读取子进程状态,子进程进入Z状态

📌 关键点:僵尸进程会持续存在直到其父进程调用 wait() 系列函数或父进程退出(此时僵尸被 init 进程回收)。

来一个创建维持30秒的僵死进程例子:

cpp 复制代码
#include <stdio.h>
#include <stdlib.h>
int main()
{
    pid_t id = fork();
    if(id < 0)
    {
        perror("fork");
        return 1;
    }
    else if(id > 0)
    { 
        //parent
        printf("parent[%d] is sleeping...\n", getpid());
        sleep(30);
    }
    else
    {
        //child
        printf("child[%d] is begin Z...\n", getpid());
        sleep(5);
        exit(EXIT_SUCCESS);
    }
    return 0;
}
僵尸进程危害

• 进程的退出状态必须被维持下去,因为他要告诉关心它的进程(父进程),你交给我的任务,我办的怎么样了。可父进程如果一直不读取,那子进程就一直处于Z状态?是的!

• 维护退出状态本身就是要用数据维护,也属于进程基本信息,所以保存在task_struct(PCB)中,换句话说,Z状态一直不退出,PCB一直都要维护?是的!

• 那一个父进程创建了很多子进程,就是不回收,是不是就会造成内存资源的浪费?是的!因为数据结构对象本身就要占用内存,想想C中定义一个结构体变量(对象),是要在内存的某个位置进行开辟空间!

孤儿进程

• 父进程如果提前退出,那么子进程后退出,进入Z之后,那该如何处理呢?

• 父进程先退出,子进程就称之为"孤儿进程"

定义:

  • 孤儿进程 :指一个仍在运行的进程(子进程),其父进程已经终止(退出)
关键特征和机制
  1. 父进程先终止: 这是形成孤儿进程的核心原因。父进程可能正常退出(通过 exit() 系统调用),也可能异常终止(如崩溃)。
  2. 子进程仍在运行: 在父进程终止时,这个子进程还在执行自己的代码,没有退出。
  3. 操作系统接管(再父化): 这是处理孤儿进程的关键机制。操作系统(内核)检测到父进程退出后,会立即将init 进程(现代 Linux 系统中通常是 systemd,进程 ID 为 1) 指定为该孤儿进程的新父进程。这个过程称为"再父化"。
  4. 由 init/systemd 管理:
    ○ 资源回收: 当孤儿进程最终终止(运行结束或收到信号终止)时,它的新父进程(init/systemd)会负责调用 wait() 或类似系统调用来回收其进程描述符、释放其占用的系统资源(如内存、文件描述符等)。这确保了系统资源不会因进程终止而泄漏。
    ○ 防止僵尸状态: 如果没有再父化机制,已终止的子进程将变成"僵尸进程"(Zombie Process),其进程描述符会一直保留在系统进程表中等待父进程回收。由 init/systemd 接管后,孤儿进程终止时会被其及时回收,避免了变成僵尸进程。
为什么需要这个机制?
  1. 防止资源泄漏: 这是最主要的原因。进程终止后,操作系统需要回收其资源(内存、文件表、信号量等)。孤儿进程机制确保即使父进程不在了,也会有 init/systemd 这个"超级保姆"来最终负责清理工作。
  2. 维护进程树结构: 在 Unix/Linux 系统中,所有进程(除 init 本身)都必须有一个父进程。孤儿进程机制维护了这种树状结构的完整性。
  3. 避免僵尸进程堆积: 如果没有再父化,所有父进程提前退出的子进程都会变成僵尸进程,最终耗尽有限的进程 ID 资源,导致无法创建新进程。
孤儿进程 vs. 僵尸进程

这两个概念容易混淆,但本质不同:

孤儿进程: 正在运行的子进程,其父进程已死。它已被 init/systemd 收养,最终终止时会被 init/systemd 回收资源。

僵尸进程: 已经终止的子进程,但其父进程尚未调用 wait() 系列函数来读取其退出状态并回收资源。僵尸进程不再运行,不消耗 CPU 和内存(除了其进程表项占用的少量内存),但会占用一个进程 ID。僵尸进程是父进程没有尽到回收责任的结果。

关键区别: 孤儿进程是活着的(运行中),僵尸进程是死了的(已终止但未被回收)。

孤儿进程最终不会变成僵尸进程(因为 init/systemd 会回收它)。

僵尸进程曾经可能是孤儿进程,但更常见的原因是父进程编程错误(忘记调用 wait())。

如何产生孤儿进程(示例)

一个简单的 C 程序片段可以演示:

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

int main() {
    pid_t child_pid = fork(); // 创建子进程

    if (child_pid == 0) {
        // 子进程代码
        printf("我是子进程 (PID: %d),我的父进程是 (PPID: %d)\n", getpid(), getppid());
        sleep(10); // 子进程睡眠 10 秒,让父进程先退出
        printf("子进程醒来,现在我的父进程是 (PPID: %d - 应该是 1)\n", getppid());
        exit(0);
    } else if (child_pid > 0) {
        // 父进程代码
        printf("我是父进程 (PID: %d),创建了子进程 (PID: %d)\n", getpid(), child_pid);
        sleep(2); // 父进程只睡眠 2 秒
        printf("父进程退出\n");
        exit(0); // 父进程退出,子进程还在 sleep
    } else {
        // fork 失败
        perror("fork failed");
        exit(1);
    }
    return 0;
}

运行结果解释:

  1. 父进程创建子进程。
  2. 父进程打印信息后睡眠 2 秒退出。
  3. 此时,仍在睡眠的子进程成为孤儿进程
  4. 操作系统将孤儿进程的父进程 ID 设置为 1 (init/systemd)。
  5. 子进程在睡眠 10 秒后醒来,打印信息时会显示其父进程 ID 为 1
  6. 子进程退出,由 init/systemd 回收其资源。

总结

孤儿进程是父进程意外或计划内提前终止后留下的仍在运行的子进程。操作系统通过将它们"过继"给 init/systemd 进程(PID 1)来妥善管理,确保这些进程最终终止时,其占用的系统资源会被可靠地回收,避免了资源泄漏和僵尸进程堆积问题。这是一个体现操作系统健壮性和资源管理能力的重要机制。

到此,进程初识之进程状态就讲完了,怎么样,是不是感觉大脑里面多了很多新知识。

如果觉得博主讲的还可以的话,就请大家多多支持博主,收藏加关注,追更不迷路

如果觉得博主哪里讲的不到位或是有疏漏,还请大家多多指出,博主一定会加以改正

博语小屋将持续为您推出文章

相关推荐
Wy_编程1 小时前
VS中创建Linux项目
linux
luck_lin2 小时前
linux添加新硬盘挂载分区和数据迁移
linux·运维·分区扩容
iFulling2 小时前
【云原生】CentOS安装Kubernetes+Jenkins
linux·云原生·kubernetes·centos·jenkins
喜欢你,还有大家4 小时前
Linux笔记10——shell编程基础-4
linux·运维·服务器·笔记
不懂机器人4 小时前
linux编程----网络通信(TCP)
linux·服务器·tcp/ip
tanyongxi665 小时前
简易shell
linux·运维·服务器
vortex55 小时前
Python包管理与安装机制详解
linux·python·pip
zcz16071278215 小时前
CentOS 7 服务器初始化完整流程
linux·服务器·centos
Twinkle1755 小时前
linux下的网络编程:TCP(传输控制协议)编程
linux·网络·tcp/ip
Clain5 小时前
如何快速建站 | 云服务器配置+公网ip部署网站全流程详解
linux·运维·服务器