《Linux系统编程之进程基础》【进程状态】

【进程状态】目录

往期《Linux系统编程》回顾:

/------------ 入门基础 ------------/
【Linux的前世今生】
【Linux的环境搭建】
【Linux基础 理论+命令】(上)
【Linux基础 理论+命令】(下)
【权限管理】

/------------ 开发工具 ------------/
【软件包管理器 + 代码编辑器】
【编译器 + 自动化构建器】
【版本控制器 + 调试器】
【实战:倒计时 + 进度条】

/------------ 系统导论 ------------/
【冯诺依曼体系结构 + 操作系统基本概述】

/------------ 进程基础 ------------/
【进程入门】

前言:

hi~,小伙伴们大家好啊, (´∀`)♡

今天是2025年11月17日,星期一,时间过得可真快,一转眼鼠鼠已经走到了大三上半学期的第十二周了。再过5、6周,这半学期就要画上句号了⏳⊙﹏⊙∥

手机上也接连收到了寒潮和大风蓝色预警❄️,天气是越来越冷了,时间也是越来越紧了,不知道大家那边都怎么样了呢?(◕ᴗ◕)

---------- 2025 年 11 月17日(九月二十八)周一

大家注意保暖的同时也让我们继续踏上Linux系统的学习之旅吧!
今天我们来学的是 【进程状态】:🎉٩(◕‿◕)۶🎉

  • 进程状态:将会脱离教材中对进程状态的抽象解释,通俗生动的介绍Linux中具体的进程状态 ₍₍ ◝(・ω・)◟ ⁾⁾

---------------进程状态---------------

1. 什么是进程状态?

进程状态:是操作系统内核对进程当前活动情况的描述。

  • 它用于反映进程正在做什么、是否可被调度执行等状态信息
  • 内核通过管理进程状态,实现对 CPU 等资源的高效分配和调度

2. 进程状态有哪些?

从上面的图示中我们可以看到,进程的状态种类较多 ,而且这些状态之间是可以相互切换的。

在众多进程状态中,我们主要学习 运行阻塞挂起 这三种状态,因为这三种状态是 Linux 系统中最主要的进程状态。

一、运行

进程的运行状态:它反映了进程在 CPU 调度层面的活跃程度。

进程处于运行状态时,有两种可能的情况:

  • 进程正在 CPU 上执行指令:真正占用 CPU 资源,进行计算、逻辑处理等操作
  • 进程处于 "就绪队列" 中:已经准备好执行,只要操作系统的调度器为它分配 CPU 时间片,就能立即被调度到 CPU 上执行

简单来说运行状态是进程 "有机会、有能力" 使用 CPU 执行任务的状态,是进程活跃性的核心体现之一。

二、阻塞

进程的阻塞状态:它反映了进程因等待某个特定事件完成,暂时失去了 "使用 CPU 执行任务的能力",即使操作系统的调度器分配 CPU 时间片,该进程也无法立即执行。

  • 进程处于阻塞状态时,核心表现为 "等待事件、无法就绪"

我们可以用 C 语言中scanf的场景来直观理解进程的阻塞状态:当程序运行到scanf语句时 ,会暂停执行并等待用户的键盘输入,这时程序对应的进程就处于阻塞状态


这一过程的底层逻辑是这样的:每个硬件设备(比如:键盘、鼠标、磁盘等)在操作系统中都对应一个 "等待队列"

当 CPU 执行到scanf语句时,操作系统会检测键盘的状态 ------ 如果此时没有任何按键被按下,意味着进程需要的资源(输入数据)尚未就绪,继续占用 CPU 只会浪费资源。

这时,操作系统会做出两个关键操作:

  1. 将该进程从 CPU 的运行队列中移除,暂时剥夺其 CPU 使用权
  2. 把进程 "挂" 到键盘设备的等待队列中,让它在那里等待所需的输入事件

此后,这个进程就进入了阻塞状态,不再参与 CPU 调度,程序也就表现为 "卡住" 的状态 ------ 这就是我们直观感受到的 "等待输入时程序没反应"。


那么请问,当用户按下键盘时,这个阻塞中的进程会主动 "知道" 吗?

其实不会。即便它正在等待输入,进程本身也无法感知硬件状态的变化。真正的流程是:

  • 当键盘被按下时,硬件会产生一个中断信号,操作系统作为硬件的 "总管家" 会第一时间捕获这个信号,知道 "键盘输入已就绪"
  • 随后操作系统会从键盘的等待队列中找到正在等待输入的进程,将它从阻塞状态唤醒,重新标记为 "就绪" 状态,并把它移回 CPU 的就绪队列中

这样,当 CPU 下次调度时,这个进程就有机会重新获得 CPU 时间片,继续执行scanf后续的逻辑(读取输入数据并处理)

三、挂起

进程的挂起状态 :它表示进程被临时从正常的执行流程中暂停,并且通常会被转移到外存(如:硬盘)中,以释放内存资源,便于操作系统调度其他更需要资源的进程。

  • 处于挂起状态的进程,其程序、数据以及相关的进程控制块(PCB)等信息,部分或全部被移至外存中保存,暂时无法参与 CPU 的调度执行
  • 只有当满足特定条件后,进程才会被重新调回内存,并恢复执行

产生原因:

  • 内存资源紧张:当系统内存中运行的进程过多,内存资源不足时,操作系统为了保证系统的正常运行,会选择一些暂时不活跃的进程,将它们挂起,转移到外存,从而释放出内存空间供其他更急需的进程使用。
  • 用户或系统干预
    • 用户可能根据自身需求,手动挂起某个进程,比如:在任务管理器中暂停某个后台程序
    • 操作系统也可能基于系统管理策略,对某些进程进行挂起操作,例如:当系统进入节能模式时,挂起一些非关键进程

① 阻塞挂起状态

在操作系统中,磁盘上有一个专门的分区叫做 swap 分区(交换分区),它本质上是一块被划出来用作 "临时内存" 的磁盘空间。

swap 分区的大小通常建议设置为物理内存的 1.5 倍到 2 倍(具体可根据系统需求调整),其核心作用是:在物理内存资源紧张时,临时存放从内存中 "挪出" 的进程数据。


当系统物理内存严重不足时,操作系统会主动排查当前进程的状态,优先盯上那些处于阻塞状态的进程 ------ 因为这些进程本就因等待外部事件(如:键盘输入、I/O 完成)而无法执行,暂时用不到 CPU 和内存资源。

操作系统会对这些阻塞进程执行 "换出" 操作:

  1. 将进程的代码段、数据段、完整转移到 swap 分区中存储
  2. 仅在物理内存中保留该进程的PCB(进程控制块) ------ 因为 PCB 体积小,且记录着进程的 ID、状态、挂起前的上下文等关键信息,便于后续恢复

此时,这些被 "换出" 到磁盘的阻塞进程就进入了 阻塞挂起状态

它们既不占用物理内存,也不会参与 CPU 调度,相当于 "暂时休眠" 在 swap 分区中,为其他急需内存的活跃进程腾出了空间。


当阻塞进程等待的事件终于发生(比如:用户按下了键盘),操作系统会立即执行 "换入" 操作:

  • 从 swap 分区中将该进程的代码和数据重新加载回物理内存
  • 结合保留的 PCB 恢复进程的上下文信息
  • 最后将进程从 "阻塞挂起" 转为 "就绪" 状态,放入 CPU 的就绪队列中,等待调度执行

这里需要明确一个关键概念:"挂起" 的核心是 "位置转移"------ 即把进程的核心数据从内存转移到磁盘(swap 分区),而非简单的状态暂停

② 就绪挂起状态

如果系统内存紧张到极致,即便将所有阻塞进程都 "换出" 到 swap 分区后,内存空间仍不足以支撑活跃进程的运行,操作系统就会采取更激进的策略:盯上 CPU就绪队列中的进程。

  • 就绪队列中的进程本是 "准备好了执行,只等 CPU 时间片" 的活跃进程
  • 但为了保证系统不崩溃,操作系统会选择就绪队列中优先级较低、或排在队列末端(暂时轮不到调度)的进程,同样将它们的代码和数据 "换出" 到 swap 分区,仅保留 PCB

这些被临时 "挪出" 内存的就绪进程,就处于 就绪挂起状态


它们虽然仍有执行资格,但因核心数据在磁盘中,无法直接参与 CPU 调度。

  • 当系统内存资源得到释放(比如:某个大进程执行完毕并释放内存),操作系统会根据调度策略,将就绪挂起队列中优先级较高的进程 "换入" 回物理内存
  • 恢复其 "就绪" 状态,重新加入 CPU 就绪队列,等待获取时间片后执行

简单来说:swap 分区是内存的 "备胎",而 "阻塞挂起" 和 "就绪挂起" 都是操作系统在内存不足时的 "应急策略"------ 通过将暂时不用或优先级低的进程 "存" 到磁盘,换取系统的稳定运行,待资源充足后再恢复这些进程的执行。

3. 怎么理解内核链表是如何设计的?

核心是理解 "侵入式链表" 的思想 ------ 把链表节点嵌入到数据结构体中,而非让数据结构体依附于链表


一、普通链表 vs 内核链表(侵入式链表)

先看普通链表 的典型结构(类似图中上方的 struct Node):

c 复制代码
struct Node 
{
      int data;           // 数据域:存储节点自身的数据
      struct Node *prev;  // 指针域:指向前一个节点
      struct Node *next;  // 指针域:指向下一个节点
};

普通链表的特点是 "数据 + 链表指针" 紧耦合 ------ 每个节点既存数据,又存链表的前后指针。

这种设计的问题是:如果有多种不同的结构体(如:struct task_struct 表示进程、struct file 表示文件)都要组织成链表,每种都得单独实现一套链表逻辑,代码冗余且不通用。


二、内核链表的核心:struct list_head

Linux 内核为了解决这个问题,设计了侵入式链表 ,核心是 struct list_head 结构体:

c 复制代码
// 内核链表的"节点"结构:仅包含前后指针,不包含数据
struct list_head
{
       struct list_head *prev;
       struct list_head *next;
};

然后,把 struct list_head 嵌入到任意需要链表组织的结构体中 (比如:表示进程的 struct task_struct):

c 复制代码
struct task_struct
{
       // 进程的核心属性(简化示意)
       pid_t pid;
       int priority;
       // ... 其他属性 ...
    
       // 嵌入链表节点:用于接入"就绪进程链表"
       struct list_head run_list;
    
       // 嵌入链表节点:用于接入"父子进程链表"
       struct list_head child_list;
    
       // 嵌入链表节点:用于接入"等待I/O的进程链表"
       struct list_head io_wait_list;
};

这种设计的核心是 "链表逻辑与数据逻辑解耦"

  • list_head 负责链表的 "连接"
  • task_struct 负责存储进程的业务数据

且一个 task_struct 能同时通过不同的 list_head 接入多个链表。


三、偏移量:从 list_head 反向找到 task_struct

由于 list_head 是嵌入在 task_struct 内部的,内核需要一种方法:通过 list_head 的指针,反向计算出它所属的 task_struct 的起始地址

这依赖于 "编译期确定的偏移量"

  • 编译时,编译器会计算出 list_head 成员在 task_struct 中的 "偏移量"(即该成员相对于 task_struct 起始地址的字节差)

  • 运行时,只要拿到 list_head 的指针,就能通过指针运算得到整个 task_struct 的地址

    c 复制代码
    // 伪代码:通过 list_head 指针找到所属的 task_struct
    struct task_struct *task = (struct task_struct *)
        ((char *)list_head_ptr - offsetof(struct task_struct, list_head_member));

四、多链表共存:一个进程,多链管理

图中多个 struct task_struct 实例,每个内部都嵌入了多个 list_head,这意味着:

  • 每个进程可以同时属于多个不同的链表(比如:既在 "就绪进程链表" 中等待 CPU 调度,又在 "父子进程链表" 中记录家族关系)
  • 每个 list_head 都独立维护自己的双向链表(next/prev 指针只连接同用途的 list_head

Linux 内核通过 "侵入式链表" 设计,实现了 "一套链表逻辑,管理所有结构体"

  • 无需为每种结构体(如:进程、文件、设备等)单独实现链表
  • 一个结构体可同时接入多套链表,满足复杂的管理需求
  • 通过偏移量计算,轻松实现 "从链表节点到完整结构体" 的反向查找,让链表操作与数据访问无缝衔接

这种设计极大提升了内核代码的复用性与灵活性,是内核数据结构设计的经典范例。

4. 真实的操作系统Linux的进程状态是什么样的?

下面这些状态是在 Linux 内核源代码中定义的:

cpp 复制代码
/*
*  任务状态数组是一种特殊的"位图",用于表示进程睡眠的原因
*  因此,"运行中"的状态值为 0,而其他状态可以通过简单的位测试来组合判断
*/
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(运行状态,running):并不意味着进程一定正在 CPU 上运行,它表明进程要么正在 CPU 上执行,要么处于运行队列里(即处于就绪状态,等待被 CPU 调度执行)

  • S(睡眠状态,sleeping):意味着进程在等待某个事件完成(这里的睡眠有时也被称为可中断睡眠(interruptible sleep)
    • 比如:进程在等待 I/O 操作完成等待信号量等资源,在等待期间会暂时让出 CPU,当等待的事件发生时,进程会被唤醒并进入运行状态
  • D(磁盘睡眠状态,Disk sleep):有时候也被称为不可中断睡眠状态(uninterruptible sleep)
    • 处于这个状态的进程通常在等待 I/O 操作的结束,比如等待磁盘读写完成
    • 在这种状态下,进程不会响应外部的中断信号,只能等待 I/O 操作本身完成后才能被唤醒,这是为了保证 I/O 操作的完整性和数据一致性

  • T(停止状态,stopped):可以通过向进程发送 SIGSTOP 信号来让进程进入停止状态。
    • 被暂停的进程可以通过发送 SIGCONT 信号来让进程继续运行
    • 例如:在终端中按下 Ctrl + Z 可以将前台进程暂停,使其进入停止状态,之后若想恢复进程运行,可使用相关命令发送 SIGCONT 信号
  • t(跟踪停止状态,tracing stop):主要用于调试场景。
    • 当进程被调试器(如:gdb)跟踪时,可能会进入这种状态,以便调试器对进程进行单步调试等操作

  • X(死亡状态,dead):表示进程已经结束,相关的资源已经被完全回收,在系统中几乎不会停留,所以一般通过 ps 等命令很难观察到处于该状态的进程。
  • Z(僵尸状态,zombie):进程已经终止(比如:代码执行完毕或者被信号杀死),但它的进程控制块(PCB)还没有被父进程通过 wait() 系列函数回收。
    • 僵尸进程会保留少量信息(如:进程 ID、退出状态等),直到父进程处理后,其 PCB 才会被释放
    • 如果系统中存在大量僵尸进程,会占用进程 ID 等系统资源,需要避免这种情况

一、运行状态 <---> 运行状态 + 就绪状态

在 Linux 系统中,使用 ps ajx 命令查看进程状态时,输出结果中的STAT 列代表进程的状态:

  • S :表示进程处于可中断睡眠状态 。在这个状态下,进程正在等待某些事件的发生。
    • 在本程序中,虽然程序看起来在不断输出内容,但实际上程序执行时并非全程占用 CPU,当需要等待外部资源或事件时,进程会主动释放 CPU 资源,进入 "睡眠" 以避免浪费资源,等待下一次 CPU 调度来继续执行循环体,即便看起来在 "持续输出",进程也会在每次输出间隙短暂睡眠
  • + :表示该进程是前台进程组 的一部分。
    • 在 Linux 中,前台进程组是指当前在终端中占用输入输出的进程组,用户可以通过终端对其进行直接交互(比如:使用 Ctrl+C 发送中断信号来终止进程)

综上所述 :进程状态为 S+ 则反映了该进程处于可中断睡眠状态且属于前台进程组的特性。


疑问:为什么注释掉printf语句后,运行该程序时,其对应的进程的状态就变成了R+?

  • 由于printf 语句被注释,程序进入死循环后,没有涉及到等待 I/O 操作需要进入睡眠状态的情况(比如:向终端输出内容时需要等待终端设备准备好接收数据)
  • 此时进程会不断地占用 CPU 资源,一直在 CPU 上运行或者在就绪队列中等待调度(因为 CPU 可能同时被其他进程占用), 所以进程处于运行状态,显示为R

二、睡眠状态 <---> 阻塞状态

进程的阻塞状态 :在 Linux 系统中对应可中断睡眠状态S,Interruptible Sleep)和不可中断睡眠状态D,Uninterruptible Sleep),是进程在等待特定事件发生时所处的一种状态

① 可中断睡眠状态

可中断睡眠状态(S)

  • 含义
    • 进程因为等待某个事件(如:I/O 操作完成、特定信号到达、获取某种资源等)而暂停执行,进入睡眠状态
    • 在这种状态下,进程会让出 CPU 资源,以便其他进程可以获得 CPU 时间片来执行
  • 特点
    • 进程对信号是响应的,当等待的事件发生或者接收到特定的信号(如:SIGKILLSIGCONT等)时,进程会被唤醒,并从阻塞队列转移到就绪队列,等待被 CPU 调度执行
    • 例如:一个进程调用read函数读取磁盘文件内容,由于磁盘 I/O 操作相对较慢,在数据读取完成之前,进程会进入可中断睡眠状态,此时如果收到SIGCONT信号,进程可能会被唤醒 ,但如果是读取数据的事件完成了,也会唤醒进程
  • 场景
    • 常见于网络请求、磁盘读写、等待用户输入等场景
    • 比如:一个网络爬虫程序在发起网络请求获取网页数据时,在数据返回之前,该请求对应的进程会进入可中断睡眠状态,直到服务器返回数据后才会被唤醒继续执行

② 不可中断睡眠状态

(1)睡眠与阻塞核心区别在于?

不可中断睡眠状态(D)

  • 含义
    • 同样是进程在等待某事件发生而暂停执行,但与可中断睡眠状态不同的是,处于不可中断睡眠状态的进程 不会响应信号只有当它等待的事件发生后才会被唤醒
    • 这种状态通常用于进程在等待一些关键的、不能被打断的系统资源或操作完成,以保证数据的一致性和操作的完整性
  • 特点
    • 主要用于内核态中处理一些对硬件设备的访问,如:磁盘 I/O 操作、等待硬件设备完成特定任务等
    • 例如:当进程正在进行磁盘写入操作时,为了防止数据丢失或写入不完整,在写入操作完成之前,进程会处于不可中断睡眠状态,此时即使收到信号也不会被唤醒,直到磁盘写入操作彻底完成
  • 场景
    • 主要出现在与硬件设备交互密切的场景中,比如:在文件系统中对磁盘进行格式化、读写磁盘元数据等操作时,相关进程会进入不可中断睡眠状态,以确保操作顺利进行

阻塞状态是操作系统实现并发处理的重要机制,它使得进程在等待资源或事件时不会占用 CPU 资源,从而提高了系统整体的资源利用率和运行效率。

同时,可中断和不可中断睡眠状态的区分,也保障了系统在不同场景下的稳定性和数据安全性。

(2)小故事:行长问罪------不可中断睡眠的由来!

进程朝着磁盘喊话:"这里有 100M 的数据,麻烦你帮我存起来。"

磁盘探出 "脑袋",像是刚被唤醒,揉了揉 "眼睛" 回应道:"行是行,但你得等我一下,别着急走。毕竟写入过程中可能会失败,比如:磁盘空间满了之类的情况,我也没法提前预料。不过不管成功还是失败,我都会告诉你结果,再由你去告知用户操作是成功还是失败。"

进程听后,觉得在此期间确实没什么别的事可做,只能干等,于是便进入了睡眠状态。

没过多久,操作系统从进程身边路过,问道:"进程,你在这儿干什么呢?" 进程回答:"我正等磁盘把数据写完,好把结果告诉用户。" 操作系统面露难色:"你没看到我忙得团团转吗?现在内存空间严重不足,能腾的地方我都腾了,可还是不够用。" 说完,操作系统就把这个进程 "杀死" 了。由于进程受操作系统管控,只能 "自杀" 退出。

结果,磁盘在写到 90M 数据时,发现磁盘空间满了,写入失败,赶忙朝着进程大喊:"100M 数据写入失败了,你快告诉用户!进程,你还在吗?" 可进程已经 "死" 了,磁盘拿着这 100M 数据,不知道该如何是好。重写是写不进去了,还有其他进程等着让它写数据,它忙得不可开交,最后只能把这 100M 数据丢弃。

就这样,从系统层面来看,100M 的数据被丢掉了,而且用户还毫不知情。


要是这 100M 数据是银行一天的转账记录,那对银行来说,损失可就大了。

银行行长把进程、磁盘和操作系统都叫到了办公室。行长开口问道:"这次事故,你们三个谁来承担责任?"

行长先看向磁盘,磁盘赶忙说:"行长,您别看着我呀,您又不是不知道,我就是个'打杂'的,人家让我干啥我就干啥。我都跟进程说了,让他一定要等我,可最后我写入失败时他不在了,我有什么办法呢。"

行长接着看向进程,进程理直气壮地说:"您看我干什么?我才是受害者呢。我在等待队列里好好等着,突然来了个'不长眼'的把我杀了,我跟它理论不过,也打不过它,只能乖乖退出了。"

行长最后看向操作系统,操作系统解释道:"行长,您知道我对您是最忠心的。您当初赋予了我权力,当内存空间严重不足时,我有权'杀'进程。而且如果今天我不'杀'这个进程,要是因为资源不够导致我操作系统崩溃,那会有几百个进程结束,丢失将近一个 G 的数据呢。"


行长听完他们三个的话,发现每个人说的都有道理,难道错的是自己吗?

最后,银行行长决定:"数据丢了就丢了吧。从今天开始,进程你新增一个状态,叫**'不可中断睡眠'**。处于这个状态时,你有权对任何想要'杀'你的操作不做任何响应。要是进程处于这个状态,操作系统,你就没权'杀'它了。" 操作系统答应道:"好的。" 进程也说道:"这还差不多。"


所以 :从此之后,在对像磁盘这类关键数据存储设备进行高 IO 操作时,进程的状态不再设为S(可中断睡眠),而是设为D(不可中断睡眠)

(3)不可中断睡眠神秘之谜*

当进程处于不可中断睡眠状态时,你只能等待该进程自己醒来,或者对操作系统进行重启操作。但有时候,即便重启系统也无法 "杀掉" 处于这种状态的进程,只有通过断电的方式才能将其终止。

  • 不可中断睡眠状态在系统中是很少出现的,要是你的进程中出现了不可中断睡眠状态,而且持续时间达到秒级,那基本上就意味着你的计算机快要出问题了
  • 因为这很可能是你的磁盘已经老化了,比如:使用了 5 年以上的磁盘,在进行 I/O 操作时容易出现异常,进而导致进程长时间处于不可中断睡眠状态

三、停止状态<---> Linux特色状态

进入停止状态的原因

  • 收到特定信号:最常见的是进程接收到 SIGSTOP 信号(编号为 19)和 SIGTSTP 信号(编号为 20)
    • SIGSTOP 是无条件停止进程,且该信号不能被捕获阻塞忽略
    • SIGTSTP 通常由终端产生,比如用户在终端中按下 Ctrl+Z 组合键 ,就会向当前前台进程组中的所有进程发送 SIGTSTP 信号,使它们进入停止状态
  • 被调试器控制:当使用调试器(如:gdb)对进程进行调试时,调试器可以向进程发送控制信号,使进程在特定的断点处满足特定条件时进入停止状态,方便调试人员检查进程的内存状态、变量值、调用栈等信息
    • 例如:在 gdb 中设置断点后,当程序执行到断点处,进程就会停止

停止状态进程的特点

  • 不参与 CPU 调度:处于停止状态的进程不会被操作系统的 CPU 调度器选中并分配 CPU 时间片,因此不会在 CPU 上执行指令,不会占用 CPU 资源,直到进程被恢复执行
  • 资源占用相对稳定:进程在进入停止状态时,会暂停当前的执行操作,但它所占用的系统资源(如:内存、打开的文件描述符等)并不会被立即释放,操作系统会保留这些资源,以便进程恢复执行时能继续正常运行
  • 可恢复性
    • 停止状态并非进程的最终状态,进程可以被恢复到其他状态(如:运行状态或就绪状态)
    • 向处于停止状态的进程发送 SIGCONT 信号(编号为 18),可以将其唤醒,使其重新进入就绪队列,等待 CPU 调度执行

① 停止状态

停止状态

假设在终端中运行一个长时间执行的程序 code.exe

  • 当按下 Ctrl+Z 组合键时,myprogram 对应的进程就会收到 SIGTSTP 信号,进入停止状态
  • 此时可以查看进程状态,会看到其状态显示为 T

如果想要恢复该进程的执行。可以使用:

  • fg 命令(将停止的前台进程恢复到前台运行)
  • bg 命令(将停止的前台进程恢复到后台运行)

它们本质上是向进程发送了 SIGCONT 信号。

② 跟踪停止状态

跟踪停止状态

四、僵尸状态<---> Linux特色状态

(1)小故事:警察办案------僵尸状态的由来

今天早上你正在户外跑步,途中从你身旁忽然飞快地跑过一个人。就在你继续往前跑时,突然看到前方约 50 米处,那个人 "扑通" 一声直直倒在了地上。你心里一紧,立刻加快脚步跑过去查看,发现他已经没有了呼吸。

你来不及多想,马上掏出手机拨打了 110 报警。没过多久,警察就赶到了现场。他们会不会一来就说 "赶紧把人抬走"?当然不会。警察做的第一件事,是迅速拉起警戒线封锁现场,防止无关人员破坏可能存在的证据;紧接着联系他的家属告知情况,同时通知法医到场 ------ 因为必须先明确他的死因:是自杀、他杀,还是突发疾病导致的自然死亡?这些关键信息都需要通过现场勘查和法医鉴定来确认。

在这个人倒下死亡后,到法医完成现场采样、家属赶来将遗体接走之前,他一直躺在原地等待 "处理" 的这段时间,就可以类比为进程的 "僵尸状态"------ 虽然 "生命活动" 已经停止,但还需要等待 "负责方"(警察、法医、家属)完成必要的信息确认和后续处理,不能直接 "清理"。

而当法医采集完有效证据、家属将遗体正式接走后,意味着所有 "后续流程" 已完成,这个状态就相当于进程进入了 "死亡状态",彻底从现场(系统)中消失。


(2)关于僵尸状态的双重追问

很多人会有疑问:直接用 "死亡状态" 不就够了吗?Linux 系统为什么还要单独设计一个 "僵尸状态"?

答案的核心,其实藏在父子进程的协作逻辑

  • 我们创建子进程的根本目的,是让它完成特定任务(比如:执行一个命令、处理一段计算、完成一次 I/O 操作)
  • 而任务完成得怎么样(是成功执行、异常报错,还是被信号终止),父进程必须知道

这就决定了子进程 "退出" 不能是 "一键清除",而需要一个过渡状态来留存关键信息 ------ 这就是僵尸状态存在的意义。


具体来说,当子进程执行完任务并退出时,Linux 系统会做两件关键操作:

  • 释放 "非必要资源":子进程的代码段、数据段、堆栈等内存资源会被立即释放。

    • 因为它已经不会再被 CPU 调度执行,这些资源留着只会浪费内存,不如还给系统供其他进程使用
  • 保留 "核心关键信息":子进程的PCB(进程控制块)会被完整保留

    • PCB 里记录着子进程的退出状态码(比如:"0" 表示成功,非零表示失败原因)
    • 退出信号(比如:是否被SIGKILL强制终止)
    • CPU 使用时间等核心信息 ------ 这些正是父进程需要的 "任务完成报告"

而从子进程退出系统释放其 代码/数据,到父进程调用wait()/waitpid()函数从 PCB 中读取退出信息的这段时间里,子进程就处于 "僵尸状态"

它的本质是进程实体已消亡,但 "任务结果凭证"(PCB)仍在,等待父进程确认接收


如果没有僵尸状态,直接让子进程退出后进入 "死亡状态"(即:立即释放包括 PCB 在内的所有资源),会出现什么问题?

父进程将彻底无法获取子进程的执行结果

  • 比如无法判断子进程是正常完成任务,还是因为权限不足、资源不够而失败
  • 这会导致父子进程的协作逻辑断裂:父进程发起了任务,却永远不知道任务的执行情况,后续也无法根据结果调整自身逻辑(比如:子进程失败时父进程重新发起任务、子进程成功时父进程继续下一步操作)

简单说 :僵尸状态就是 Linux 为父子进程设计的 "结果交接缓冲区":

  • 它既避免了子进程退出后资源的无效占用
  • 又通过保留 PCB 确保父进程能拿到 "任务完成报告"

最终实现了 "资源高效回收" 与 "进程间信息同步" 的平衡 ------ 这正是它无法被 "死亡状态" 替代的核心原因。

(3)僵尸进程造成的内存泄露问题

僵尸进程:处于僵尸状态的子进程是僵尸进程。

  • 如果父进程一直对僵尸进程不管不顾,既不回收,也不获取子进程的退出信息,那么僵尸进程就会一直存在,进而引发内存泄漏问题

这里有个知识点需要思考:进程都已经退出了,内存泄漏问题还存在吗?

僵尸进程本身不会像常规程序中由于 动态内存分配未释放等原因导致典型的 "内存泄露" (持续占用堆内存等用户可操作的内存空间且无法回收)

但从系统资源管理的角度,它会造成内存相关的不良影响(进程控制块(PCB)占用)

  • 当子进程进入僵尸状态时,其大部分资源(如:代码段、数据段、堆栈等占用的内存)会被释放,但是 PCB 仍然会保留在系统中
  • 虽然单个 PCB 占用的内存空间相对较小,通常是几十到几百字节不等 ,但如果系统中存在大量的僵尸进程,这些 PCB 占用的内存总量就会不断累积
  • 当内存资源紧张时,这些被僵尸进程 PCB 占用的内存无法被其他进程利用,从而造成了内存资源的浪费,从宏观上看类似于一种内存资源被 "泄露" 的情况

  • 其实,常驻内存的进程(比如:后台服务、守护进程这类长时间运行的程序)如果出现内存泄漏,才是最麻烦的------ 因为它们会持续占用资源,逐渐拖垮系统
  • 反而,普通短生命周期的进程,退出时系统会自动回收大部分资源,问题相对没那么突出

那操作系统为什么不主动回收僵尸进程的 task_struct(进程控制块,PCB 的核心载体)呢?

  • 这是因为 task_struct 里保存着子进程的退出状态(比如:是正常结束,还是因信号终止)等关键信息,操作系统需要把这些信息完整地交给父进程
  • 要实现 "信息交付",就必须一直维护 task_struct 不释放,直到父进程主动来获取。也正因为如此,"回收僵尸进程、避免内存泄漏" 的责任,就落到了开发者身上

(4)僵尸进程的对立面------孤儿进程

孤儿进程 :当一个子进程还在运行时,它的父进程却提前终止了,此时这个子进程就会成为孤儿进程

  • 由于父进程已经不存在,没有父进程来对其进行管理和回收资源,系统会自动将孤儿进程的父进程 ID(PPID)更改为 1,也就是让 init 进程(在较新的 Linux 系统中,通常是 systemd 进程,其进程 ID 也为 1 )成为孤儿进程的新父进程
  • init 进程会负责对孤儿进程进行后续的资源回收等管理操作

产生原因:

  • 父进程异常终止:父进程由于程序崩溃收到致命信号(如:SIGKILL)等原因突然结束运行,而此时它创建的子进程可能还在执行任务,没有来得及完成,这种情况下子进程就会变成孤儿进程
  • 父进程正常退出:在一些程序设计中,父进程完成了自身的任务后正常退出,而子进程可能还需要继续运行一段时间来处理其他工作,这时子进程也会成为孤儿进程
    • 比如:父进程负责初始化一些资源并启动子进程来执行特定的长期任务,之后父进程完成初始化工作后退出,子进程继续执行后续任务,从而成为孤儿进程

我们之前常说:"只要登录 Linux 系统,就会有一个 bash 进程为我们创建",那到底是谁创建了这个 bash 呢?

答案是 "系统",而这里的 "系统",本质上就是 Linux 中的1 号进程(在传统系统中是 init 进程,现代系统中多为 systemd 进程)------ 它是系统启动后创建的第一个用户态进程,负责初始化系统服务、管理后续进程,堪称系统进程的 "总管家"。


有人可能会问:"既然有 1 号进程,那有没有 0 号进程呢?"

其实是没有持续存在的 0 号进程的

  • 0 号进程是系统启动初期的 "临时进程"(通常是 idle 进程或 swapper 进程),主要负责初始化内核环境
  • 当系统完成基础启动、1 号进程成功创建后,0 号进程的使命就结束了,相当于被 1 号进程 "接替" 了系统初始化的核心角色

疑问:1 号进程为什么要 "领养" 孤儿进程?如果不领养会怎样?

所谓 "领养",就是当一个子进程的父进程意外终止时,1 号进程会主动接管这个失去父进程的子进程,成为它的新父进程。

这么做的核心目的,就是避免子进程退出后变成僵尸进程

  • 如果不领养,这个子进程就成了 "无主进程",既没有原父进程、也没有新父进程来调用wait()系列函数回收它的退出状态
  • 它的 PCB(进程控制块)就会一直残留系统中,变成僵尸进程,浪费系统资源

简单说:在 Linux 系统中,能 "管" 子进程退出回收的只有两方:

  • 一是子进程的原父进程(天然的管理者)
  • 二是1 号进程(系统层面的 "兜底管理者")

这就像现实世界里,孩子的成长首先由家人负责;如果家人无法照料,政府就会出面接管,确保孩子得到妥善安置 ------1 号进程的 "领养",就是系统层面的 "兜底保障"。


1 号进程领养孤儿进程后,这个孤儿进程通常会转变为后台进程,不再与原终端进行交互绑定。

"后台运行进程" 的概念 :平时我们在终端中用 ./自己的命令 & 这样的格式启动程序时,就是主动将进程放到后台运行。

这种主动启动的后台进程,与孤儿进程转变而来的后台进程,在运行特性上是一致的:

  • 不受 Ctrl+C 影响:因为后台进程不处于当前终端的 "前台进程组",而 Ctrl+C 发送的中断信号(SIGINT)只会作用于前台进程组的进程,所以用 Ctrl+C 无法终止后台进程
  • 仍可输出信息到前台:后台进程虽然在后台运行,但默认情况下仍会将标准输出(如:printf 打印的内容)和标准错误信息输出到当前终端界面,不会因为在后台就停止消息打印
  • 需用信号命令终止:如果要结束后台进程,需要先通过 psjobs 命令找到它的进程 ID(PID),再用 kill -9 [进程ID] 发送强制终止信号(SIGKILL),才能将其彻底杀死 ------ 这是终止后台进程最常用且有效的方式
相关推荐
Bruce_Liuxiaowei2 小时前
HTTPHTTPS探测出网技术详解:跨平台命令与实战方法
运维·windows·安全·网络安全
BS_Li2 小时前
【Linux系统编程】进程控制
java·linux·数据库
因为奋斗超太帅啦2 小时前
Git分布式版本控制工具学习笔记(一)——git本地仓库的基本使用
笔记·git·学习
小龙报2 小时前
《嵌入式成长系列之51单片机 --- 固件烧录》
c语言·开发语言·单片机·嵌入式硬件·51单片机·创业创新·学习方法
言慢行善2 小时前
Docker
运维·docker·容器
利刃大大2 小时前
【c++中间件】etcd存储系统 && 服务注册 && 服务发现 && 二次封装
c++·中间件·服务发现·etcd·服务中心
Yue丶越2 小时前
【C语言】深入理解指针(四)
java·c语言·算法
可可苏饼干3 小时前
LVS服务器
linux·运维·笔记·学习·lvs
L.EscaRC3 小时前
Docker原理浅析(上)
运维·docker·容器