【Linux我做主】探秘进程状态

发布于:2025-07-30 ⋅ 阅读:(25) ⋅ 点赞:(0)

进程状态

github地址

有梦想的电信狗

0. 前言

​ 进程状态是操作系统课程的核心概念之一,也是理解进程调度的关键基础。本文将带您深入探讨操作系统中进程状态的底层机制,特别是Linux系统中的具体实现。我们将从经典的操作系统理论出发,逐步过渡到Linux内核中的实际状态管理,揭示R(运行)、S(睡眠)、D(磁盘休眠)、T(停止)等状态的本质区别。通过代码演示和命令操作,您将直观看到:

  • 进程如何在运行队列和等待队列中迁移
  • 僵尸进程(Z状态)的产生原理和危害
  • 孤儿进程如何被操作系统领养
  • 挂起状态的底层内存交换机制

无论您是操作系统初学者,还是希望深入理解Linux进程管理的开发者,本文都将为您揭示进程状态背后的技术真相。让我们开始这段探索之旅!


1. 一般操作系统学科的进程状态及相关概念

  • 进程的不同状态,就是把进程的PCB放在不同的队列中
    • 放入运行队列,为运行态
    • 放入等待队列,为阻塞态

1.1 运行状态

1. 运行队列、运行态

一个CPU会维护一个运行队列,需要被运行的进程的task_struct会被链入到运行队列中。

多个CPU会维护多个运行队列

在这里插入图片描述

运行队列
  • 每个CPU都有一个进程的队列,存放每个进程的PCB。进程的PCB在该队列中排队,这个队列就叫运行队列
运行状态(R状态)
  • 处于运行队列中(代表进程已经准备好了,随时可以被调度)的进程正在被CPU运行的进程,所处的状态就叫运行态

2. 时间片

思考?

  1. 一个进程只要把自已放到CPU上开始运行了,是不是一直要执行完毕,才把自己放下来?

不是!!! 当我们的程序内部写了while(1);时,系统内的其他进程依然可以正常运行!

如果一直要执行完毕才把自己放下来,那么就意味着其他的进程再也不会被调度了

为了防止进程上去就不下来的情况,每个进程都有一个叫做 时间片 的概念

时间片就是一个int类型的变量,用来记录每个进程在CPU上运行的时间

//时间片
int time_space;    // 时间片为一个固定的时长
  • 每个进程运行固定的时间片后,会被放置到运行队列的尾部,重新排队等待运行

3. 并发执行

正因为有了时间片的概念,在一个足够长的时间段内,所有的进程才都会被执行!

  • 多个进程,在一个足够长时间段内都会被执行,称为并发执行。每个进程交替运行一个时间片后重新在运行队列中排队

4. 进程切换

  • 多个进程,在一个时间段内,都会被执行,称为并发执行,每个进程交替运行一个时间片
  • 多个进程要交替运行,因此一定会存在,大量的把进程从CPU上放上去,拿下来的动作。这个动作叫做进程切换

1.2 阻塞状态

  • 在等待特定设备(硬件)就绪的进程,该进程所属的状态,叫做阻塞状态
    • 如果一个进程要用scanf读取键盘的输入,但键盘内一直没有输入时,进程就只能在该键盘设备的等待队列中进行等待。在键盘的等待队列中,等待键盘的输入。
    • 内存中有成百上千个等待队列,每个设备都有自己的等待队列
      • 进程需要等待设备时,操作系统就把该进程的PCB链接到设备的等待队列
      • 进程需要等待进程时,操作系统就把该进程的PCB链接到特定进程的等待队列

PCB处于等待队列中的进程,所处的状态,就叫做阻塞状态


等待队列的本质

  • 等待队列是操作系统内核为每个资源(如外设)维护的数据结构,用于管理因等待该资源而阻塞的进程。例如:
    • 当一个进程需要读取键盘输入但键盘数据尚未就绪时,它会被加入键盘设备的等待队列。
    • 当设备数据就绪(如键盘缓冲区有输入),内核会唤醒该队列中的所有或部分进程。
  • 等待队列的实现通常基于双循环链表,通过等待队列头(wait_queue_head_t)管理所有等待项(wait_queue_entry_t),每个项关联一个休眠的进程。

1.3 挂起状态

  • 挂起状态:一个进程,只有PCB在内存中,代码和数据不在内存中,被操作系统换出了,此时该进程称为挂起状态

    • 例如,一个进程在长时间的等待键盘输入,很长时间键盘都没有输入,一直在等待,该进程处于阻塞状态

    • 该进程在等待的过程中,操作系统突然内存资源严重不足了,操作系统为了保证运作正常,需要想办法省出来内存资源

    • 一个进程,无论属于运行态还是阻塞态,只要在没有被CPU调度运行的那一刻,当前进程的代码和数据,在内存中是处于空闲状态的(没有被CPU运行)。此时操作系统想的办法是,把当前进程的PCB保留下来,再把当前进程处于内存中的代码和数据,交换到外设中(例如交换到磁盘中)。相当于一个进程,只有PCB在排队,对应的代码和数据存放在外设中。这样就为其他进程省出来了内存空间

  • 内存数据的换出与换入

    • 换出:把当前进程的PCB保留下来,把当前进程处于内存中的代码和数据,交换到外设中(例如交换到磁盘中),该过程称为换出
    • 换入换出后的某个时刻,进程在等待的设备就绪了,把这个进程放入运行队列,再把处于外设(比如磁盘)中的代码和数据加载到内存中,该过程称为换入

以上所说的的挂起,严格来说,是阻塞挂起状态。挂起就是挂起到外设中了

还有运行挂起状态,就绪挂起状态,但很少遇见

  • 对于挂起状态的进程,用户通常是无法感知到的

操作系统的理论,放在任何一个特定的操作系统上,都是成立的,但是不同的操作系统的实现不一样。下面我们来看Linux系统对操作系统理论的具体实现。


2. Linux的进程状态是如何维护的

  • 进程的不同状态,就是把进程的PCB放在了不同的队列中,就有了不同的状态
  • 不同的状态,决定了该进程当下要做什么事
    • r状态:接下来要被调度运行了
    • **阻塞状态:**等条件就绪。等设备准备好,把状态改成运行状态,将PCB链入到CPU的运行队列中,等待CPU再调度运行
    • **挂起状态:**运行之前,把曾经换出的代码和数据换入。再把进程状态改成运行状态,将PCB链入到CPU的运行队列中,等待CPU再调度运行

我们来看Linux内核源代码中如何定义进程的状态:

/*
* 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 */
};
  • Linux系统中进程状态分为七种
    • R运行状态(running):并不意味着进程一定在运行中,它表明进程要么是在运行中要么在运行队列里。
    • S睡眠状态(sleeping):意味着进程在等待事件完成(这里的睡眠有时候也叫做可中断睡眠(interruptible sleep))
    • D磁盘休眠状态(Disk sleep)有时候也叫不可中断睡眠状态(uninterruptible sleep),在这个状态的
      进程通常会等待IO的结束
    • T停止状态(stopped): 可以通过发送 SIGSTOP 信号给进程来停止(T)进程。这个被暂停的进程可以通过发送 SIGCONT 信号让进程继续运行。
    • t状态:暂不区分Tt状态
    • X死亡状态(dead):这个状态只是一个返回状态,你不会在任务列表里看到这个状态

不要用我们人的感受去衡量CPU的速度


1. 运行状态(R)

1. 辨析R与S

一个程序正在被CPU运行或在运行队列中排队,所处的状态就是运行状态R。来看如下程序:

#include <stdio.h>

int main() {
    
    while1(1) {
        printf("hello Linux World\n");
    }
    return 0;
}

在这里插入图片描述


为什么该进程在运行,状态却是S状态;

  • 再看如下代码:
#include <stdio.h>

int main() {
    
    while1(1); 
    //{
    //    printf("hello Linux World\n");
    //}
    return 0;
}

在这里插入图片描述

while循环中的printf函数去掉后,进程的状态就变成了R,这是为什么呢?

  • while循环中有printf时,进程为S状态,去掉后变为R状态。下面我们来分析原因:
    • 不要用我们人的速度去衡量CPU的速度

原因是,由于CPU的速度极快,printf向显示器中打印时,显示器并不是一直处于可以写入的状态,该进程需要等待显示器设备就绪,需要花费大量的时间等待显示器设备就绪,因此进程长时间处于S状态,S状态对应于操作系统理论中的阻塞状态

去掉printf后,没有访问任何外设,无需等待IO设备就绪,因此处于R状态。也就是运行状态

2. top命令的R与S状态

  • 再看我们常用的top命令

top命令也是检测进程的信息将其打印在显示器上,需要等待显示器设备就绪,因此top进程会时隐时现

  • top运行是R状态,等待显示器就绪时是S状态。

在这里插入图片描述

3. 神秘的+号和&号

  • 细心的宝子,就会发现,我们在查看进程状态时,标识进程状态的字母右上角都有一个+号,这是什么含义?

在这里插入图片描述

这里的R+标识是进程在前台运行

  • 前台运行:进程运行后,bash命令行无法再输入命令。这就属于前台运行的现象。

如果我们想让进程在后台运行,可以进行如下操作:

  • 执行时加上&可以让进程在后台运行。

在这里插入图片描述

加上&myproc在后台运行。再次查看时,进程的状态就变成了Rbash中也就可以输入命令了。


另外需要注意的是

  • 如果一个进程处于R状态(非R+)状态,那么该后台运行的进程无法通过ctrl+c终止。原因是bash命令行终止,我们的命令行失效了,无法读取输入,ctrl+c就也失效了。

  • 需要通过kill命令杀掉

在这里插入图片描述


2. 阻塞状态(S)

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

阻塞状态(S),又被称为浅度睡眠状态,可以随时被唤起

系统中的大部分进程都处于S状态。因为当系统中没有任务时,大部分的进程处于等待资源的状态,因此处于阻塞状态

bash进程大部分时候也处于阻塞状态,等待用户输入命令

  • 我们来看以下处于阻塞状态的进程:

在这里插入图片描述

  • 进程等待键盘的输入时,该进程也就处于了阻塞状态

  •   int main() {
          //while(1){
          //    printf("hello world\n");
          //    sleep(1);
          //}
          int a = 0;
          printf("Please Enter #:");
          scanf("%d", &a);
          printf("%d\n", a);
          return 0;
      }
    

在这里插入图片描述

3. 深度睡眠状态(D)

disk sleep

操作系统在内存资源严重不足时,是会杀掉进程的

disk sleep D状态的一个例子就是:进程在等待磁盘写入完毕期间,该进程不能被任何人kill掉,包括操作系统

状态定义

  • 进程因等待I/O操作(如磁盘读写、网络传输或外设访问)而进入阻塞,且无法被任何信号中断,包括SIGKILLkill -9)。

其他特征

  • 数据完整性保障:操作系统设计此状态是为了确保关键I/O操作(如写磁盘)不被意外中断,防止数据损坏或丢失。
  • 资源占用:进程的task_struct(PCB)仍驻留内存,但不会参与CPU调度。

深度睡眠状态(D)为什么不会被杀死,因为D状态的进程不响应操作系统的任何请求


4. 暂停状态T(stopped)

1. 定义与触发机制

  • 本质:进程被信号(如 SIGSTOP)强制暂停,进入不可自主恢复的停滞状态。
  • 触发方式:
    • 用户级停止:终端按 Ctrl+Z 发送 SIGSTOP 信号。
    • 系统级停止:管理员通过 kill -SIGSTOP <PID> 手动暂停进程。
    • 调试器控制被调试器(如GDB)跟踪时,在断点处暂停(此时状态为 t
  • GDB调试为进程暂停状态T(stopped)被触发的主要场景

2. 核心特性

  1. 暂停执行:进程代码停止运行,不占用CPU资源,但保留内存等上下文。
  2. 资源保留:进程的 task_struct(PCB)和内存数据不被释放,可随时恢复。
  3. 信号响应:
    • 不响应普通信号:如 SIGTERM(终止信号)无法终止T状态进程。
    • 仅响应特定信号:必须通过 SIGCONT(继续信号)恢复运行。

可以通过向进程发送特定信号的方式,使进程暂定和恢复

  • 当前进程每隔一秒输出一个hello world,处于睡眠状态(阻塞状态)
  • 可以发送18和19号信号使其恢复和暂停

在这里插入图片描述

  • 发送19号信号后,进程暂停,终端出现相应提示

在这里插入图片描述

  • 对暂停的进程发送18号信号后,进程继续运行

在这里插入图片描述

3. S状态和T状态的区别

1. 定义与触发机制
  • T 状态(Stopped)
    • 定义:进程被强制暂停执行,通常由调试信号或管理操作触发。
    • 触发方式:
      • 用户或管理员发送 SIGSTOP 信号(如终端按 Ctrl+Z 或执行 kill -19 <PID>)。
      • 调试器(如 GDB)在断点处暂停进程(此时状态为 t,即跟踪停止)。
  • Sleep 状态(S 状态)
    • 定义:进程因等待资源就绪(如 I/O 操作、数据输入)而主动进入休眠。
    • 触发方式:
      • 调用阻塞式系统调用(如 scanf() 等待键盘输入、read() 等待磁盘 I/O)。
      • 资源未就绪时由内核自动挂起(如网络请求未响应)。
2. 核心特性对比
特性 T 状态(Stopped) Sleep 状态(S 状态)
可中断性 ❌ 不响应普通信号(如 SIGTERM ✅ 可被信号唤醒(如 SIGINTSIGKILL
恢复条件 必须接收 SIGCONT 信号(kill -18 资源就绪时自动恢复,或由信号强制唤醒
资源占用 保留内存和文件描述符,不消耗 CPU 保留内存和资源,不消耗 CPU
典型场景 调试断点、系统维护、批处理任务暂停 等待 I/O(磁盘、网络)、用户输入
3. 行为差异详解
  1. 响应信号的能力
    • T 状态:仅响应 SIGCONT(恢复)和 SIGKILL(强制终止),不响应 SIGTERM 等普通终止信号。
    • S 状态:可被任意信号中断(如 kill -9 能立即终止进程)。
  2. 恢复流程
    • T 状态:必须通过 kill -18 <PID> 显式恢复,或由调试器控制(如 GDB 的 continue 命令)。
    • S 状态:当等待的资源就绪(如磁盘 I/O 完成、键盘输入到达),内核自动将其重新加入运行队列。
  3. 对系统的影响
    • T 状态:常用于主动控制进程生命周期(如调试时检查变量),暂停后进程状态稳定,无数据损坏风险。
    • S 状态:若因 I/O 故障长时间阻塞,可能拖慢系统响应,但可通过优化 I/O 或硬件解决。
4. 常见误区与注意事项
  • 混淆点
    • Tt 的区别:t调试专用的暂停(如 GDB 断点),比 T 多一层保护(不响应 SIGCONT)。
    • Sleep 的子状态S可中断睡眠D(Disk Sleep)是不可中断睡眠(如写磁盘关键数据),后者无法被任何信号终止。
  • 风险提示
    • T 状态进程长期暂停:可能导致依赖进程超时(如网络服务),需及时恢复。
    • S 状态滥用:频繁 I/O 阻塞可能引发系统负载升高,需优化代码逻辑或使用异步 I/O。

💡 一句话区别

T 状态是“被人为按了暂停键”S 状态是“自己躺下等快递” ——前者需外部唤醒,后者到货自动起床。


5. 死亡进程(X)与僵尸进程(Z)

1. 死亡状态(X状态)

  1. 定义与核心特性
    • 瞬时性:X状态是进程完全终止后的最终状态,表示进程所有资源(包括内存、文件描述符、内核数据结构)已被操作系统彻底回收。
    • 不可见性:该状态仅存在于进程资源释放的瞬间(通常持续微秒级),无法通过pstop等工具观察到
    • 触发条件:当进程通过exit()系统调用正常结束,且父进程已通过wait()/waitpid()读取其退出状态后,进程立即进入X状态并消失。
  2. 设计意义
    • 资源清理:X状态是内核回收资源的最后一步,确保无内存泄漏或PID浪费。
    • 状态闭环:标志进程生命周期正式结束,是状态机中的终点(如状态转换图:Z → X)。

2. 僵尸状态(Z状态)

  1. 定义与核心特性

    • 残留性:子进程已终止(代码停止执行),但父进程未调用wait()回收其退出状态,导致进程的task_struct(PCB)仍驻留内核进程表。
    • 资源占用:
      • 释放所有内存和CPU资源,仅保留PCB记录退出码(约1.7KB内存)。
      • 持续占用PID号,可能导致系统PID耗尽(上限由/proc/sys/kernel/pid_max设定)。
    • 不可杀性:无法通过kill -9终止,因进程实际已死亡,仅留“尸体”。
  2. 触发场景与危害

    • 典型场景:父进程未正确处理SIGCHLD信号,或存在逻辑缺陷(如未调用wait())。
    • 系统风险:
      • 内存泄漏:大量僵尸进程积累消耗内核内存,极端情况导致无法创建新进程(PID耗尽)。
      • 性能下降:进程表满负荷时,系统调度效率降低。
  3. 检测与清除

    • 检测命令:

      ps aux | grep ' Z '      # 筛选Z状态进程
      ps -eo stat,pid,cmd | grep '^Z'  # 显示僵尸进程详情
      
    • 清除方法:

      • 终止父进程:父进程退出后,僵尸进程被init(PID 1)接管并自动清理。
      • 修复父进程:添加SIGCHLD信号处理函数,调用waitpid()回收子进程。
      • 系统重启:极端情况下强制重启释放所有残留资源。

3. 操作演示及其注意事项

以下代码:

  • 子进程循环5次后结束,父进程为死循环
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

int main() {
    pid_t pid = fork();
    if (pid == 0) {
        // child
        int cnt = 5;
        while (cnt) {
            printf("I am child, pid: %d, ppid: %d, cnt: %d\n", getpid(), getppid(), cnt);
            cnt--;
            sleep(1);
        }
        exit(0);
    } else {
        // father
        while (1) {
            printf("I am father, pid: %d, ppid: %d\n", getpid(), getppid());
            sleep(1);
        }

        // 父进程没有针对子进程干任何事情
    }
    return 0;
}

在这里插入图片描述

在这里插入图片描述

5秒过后,子进程退出,父进程仍在运行。父进程的代码中没有对子进程的资源进行回收,因此变成僵尸进程

  • 子进程一般退出的时候,如果父进程没有主动回收子进程信息,子进程会一直让自己处于Z状态。进程的相关资源尤其是task_struct结构体不能被释放。<defunct>的意思即为死的,意为僵尸进程
    • 未来父进程将子进程回收后,操作系统才能将子进程的资源进行释放。
    • 如果一个进程一直处于僵尸进程,自身的内存资源会被一直占用,从而引发内存泄露。之后可以在父进程中调用waitpid();函数解决该问题

在这里插入图片描述

4. 僵尸进程的危害

**僵尸进程(Zombie Process)**在Linux系统中是已终止但未被父进程完全回收资源的子进程。尽管其代码执行已结束,仍会占用系统资源并引发连锁问题。主要危害如下:

1. 资源占用与进程表耗尽
  • 进程表项占用:每个僵尸进程占据内核进程表中的一个条目,导致系统可用进程槽位减少。进程表大小有限(通常由内核参数 pid_max 定义),大量僵尸进程可能耗尽所有槽位。
  • 新进程创建失败:当进程表满时,系统无法创建新进程(如 fork() 系统调用返回失败),导致服务崩溃或任务调度中断。
2. 系统性能下降
  • 管理开销增加:操作系统需持续维护僵尸进程的元数据(如PID、退出状态),占用CPU时间和内核资源,尤其在频繁创建进程的场景下(如Web服务器),显著拖慢响应速度。
  • 内存与文件描述符泄漏:若僵尸进程未释放打开的文件或网络端口,可能间接导致文件描述符耗尽或端口占用,影响其他进程的正常操作。

3. 孤儿进程

现象

在这里插入图片描述

  • 这次我们使用以下代码测试,让父进程先退出,观察结果。
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

int main() {
    pid_t pid = fork();
    if (pid == 0) {
        // child
        int cnt = 500;
        while (cnt) {
            printf("I am child, pid: %d, ppid: %d, cnt: %d\n", getpid(), getppid(), cnt);
            cnt--;
            sleep(1);
        }
        exit(0);
    } else {
        // father
        int cnt = 5;
        while (cnt--) {
            printf("I am father, pid: %d, ppid: %d\n", getpid(), getppid());
            sleep(1);
        }

        // 父进程没有针对子进程干任何事情
    }
    return 0;
}
  • 以上代码父进程五秒后先退出,子进程500秒后再退出,现象如下

在这里插入图片描述

==那么PID为1的进程是什么进程呢?==我们使用top命令查看

在这里插入图片描述

在这里插入图片描述

孤儿进程概念

  • 孤儿进程(Orphan Process)指父进程已终止或退出,但子进程仍在运行的进程。此时子进程失去父进程的管理,成为“孤儿”。操作系统会将其交由init进程(PID=1)接管,确保资源正常回收。

父子进程中,如果父进程先退出,子进程的父进程会被改成1号进程,也就是操作系统进程。此后子进程会运行在后台

定义:父进程是1号进程的进程,被称为孤儿进程,标识该进程被系统领养!


产生原因

孤儿进程的形成通常源于以下场景:

  1. 父进程意外终止:如程序崩溃、系统错误或强制杀死父进程(kill -9)。
  2. 父进程设计缺陷:父进程未正确处理子进程退出逻辑(如未调用wait()回收资源)。
  3. 异步进程创建:父进程启动子进程后立即退出,未等待子进程结束。
  4. 长时间运行的子进程:父进程先于子进程结束(常见于后台任务或守护进程)。
  5. 孤儿进程因父进程退出而被init进程收养自动转为后台运行,且不受原终端影响
  • ubuntu22.04系统中,1号进程为init进程,和CentOS系统中的systemd进程一样,都表示系统进程

为什么要被系统领养?

因为孤儿进程的相关资源也需要被释放,否则会引起内存泄漏

在这里插入图片描述

此处,父进程终止后,原来的子进程直接被操作系统领养,变成孤儿进程。操作系统直接将该进程领养并回收了。

父进程终止后,僵尸子进程会被 init(或 systemd)接管,并由其回收资源,不再是僵尸进程

  • 因此一个进程创建子进程后,父进程的代码中要有对子进程的回收操作。

4. 结语

通过本文的系统性解析,我们深入理解了操作系统进程状态的核心机制及其在Linux中的具体实现。关键要点总结如下:

  1. 状态本质:进程状态实质是PCB在不同队列中的位置映射
  2. 状态转换
    • R状态在运行队列等待CPU调度
    • S/D状态在设备等待队列阻塞
    • Z状态因父进程未回收资源而滞留
  3. 特殊进程
    • 僵尸进程:必须通过父进程wait()回收资源
    • 孤儿进程:由**init进程(PID=1)**自动接管回收
    • 若父进程因未调用wait()导致子进程僵尸化,终止父进程是有效的清理手段:原僵尸子进程会被init进程回收,彻底释放资源。此机制是Linux系统的重要设计,确保即使父进程异常退出,僵尸进程也不会永久残留。
    • ​实际建议​​:在编写多进程程序时,应通过wait()SIGCHLD信号处理函数主动回收子进程,避免依赖父进程终止作为常规清理方式
  4. 实践启示
    • 避免僵尸进程积累(防止PID耗尽)
    • 长时间阻塞操作使用D状态(保障数据完整性)
    • 后台进程使用&符号管理(R状态无+

通过while(1)配合printf观察S/R状态转换,或使用fork()创建僵尸进程的实验,能极大加深对进程状态的理解。建议读者实际操作文中的代码示例,观察不同状态下的进程行为。


以上就是本文的所有内容了,如果觉得文章对你有帮助,欢迎 点赞⭐收藏 支持!如有疑问或建议,请在评论区留言交流,我们一起进步

分享到此结束啦
一键三连,好运连连!

你的每一次互动,都是对作者最大的鼓励!


征程尚未结束,让我们在广阔的世界里继续前行! 🚀


网站公告

今日签到

点亮在社区的每一天
去签到