Linux系统篇,进程概念(三):进程状态详解、僵尸进程与孤儿进程的深度解析、Slab机制的了解

系列文章目录


文章目录

  • 系列文章目录
  • 前言
  • 一、从课本理论到Linux实现
  • [二、Linux 进程状态详解](#二、Linux 进程状态详解)
    • [2.1 内核源码中的状态定义](#2.1 内核源码中的状态定义)
    • [2.2 CPU的运行与调度算法FIFO](#2.2 CPU的运行与调度算法FIFO)
    • [2.3 运行、阻塞与挂起](#2.3 运行、阻塞与挂起)
    • [2.3.1 运行态](#2.3.1 运行态)
    • [2.3.2 阻塞态](#2.3.2 阻塞态)
    • [2.3.3 挂起态(阻塞挂起)](#2.3.3 挂起态(阻塞挂起))
    • 图片理解
  • 三、进程状态的深入解析
    • [3.1 状态的本质与查看](#3.1 状态的本质与查看)
    • [3.2 对立的运行R与睡眠S](#3.2 对立的运行R与睡眠S)
    • [3.3 停止的区别:停止T与追踪停止t](#3.3 停止的区别:停止T与追踪停止t)
    • [3.4 不可中断睡眠:D状态](#3.4 不可中断睡眠:D状态)
      • [3.4.1 示例理解](#3.4.1 示例理解)
      • [3.4.2 操作实验](#3.4.2 操作实验)
    • [3.5 将死与已死:Z状态与X状态](#3.5 将死与已死:Z状态与X状态)
      • [3.5.1 Z与X的联系](#3.5.1 Z与X的联系)
      • [3.5.2 Z状态](#3.5.2 Z状态)
        • [3.5.2.1 OS创建子进程](#3.5.2.1 OS创建子进程)
        • [3.5.2.2 僵尸进程定义与信息储存](#3.5.2.2 僵尸进程定义与信息储存)
        • [3.5.2.3 僵尸进程的隐蔽危害](#3.5.2.3 僵尸进程的隐蔽危害)
      • [3.6 孤儿进程](#3.6 孤儿进程)
        • [3.6.1 孤儿进程的定义](#3.6.1 孤儿进程的定义)
        • [3.6.2 孤儿的领养的机制](#3.6.2 孤儿的领养的机制)
        • [3.6.3 代码实验](#3.6.3 代码实验)
      • [3.7 内核结构申请Slab](#3.7 内核结构申请Slab)
  • 总结

前言

  上文我们聊了进程的基本概念和父子关系,今天继续深入,看看进程的状态到底是怎么回事。这部分内容在课本上有理论模型,在Linux里又有具体实现,我们一点点拆开来看。


一、从课本理论到Linux实现

  课本上讲进程状态,通常会给一个三态模型:运行、就绪、阻塞。这是一个普遍性 的理论框架。但Linux作为具体的操作系统,有自己的特殊性------它根据实际需要定义了好几种状态。所以我们学的时候,要明白理论是理论,实现是实现,不能混为一谈。


二、Linux 进程状态详解

2.1 内核源码中的状态定义

  Linux 内核里,进程状态用一个数组来定义:

bash 复制代码
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 状态:运行/就绪

      R 状态表示进程正在运行或者在运行队列里。注意,它不一定正在占着 CPU,只要在调度队列中排队,状态也是 R。

  • S 状态:可中断睡眠

      S 状态也叫可中断睡眠,进程在等待某个事件(比如 I/O 完成),可以被信号唤醒。大多数等待 I/O 的进程都处于这个状态。

  • D 状态:不可中断睡眠

      D 状态也叫不可中断睡眠,通常出现在等待磁盘 I/O 的时候。这个状态的进程不响应任何信号,连 kill -9 都杀不掉,只能等 I/O 完成或者重启系统。这样设计是为了保护重要数据不被误杀。

  • T 状态:停止

      T 状态表示进程被暂停了。可以通过发送 SIGSTOP 信号让它停下来,发送 SIGCONT 信号让它继续。终端里按 Ctrl+Z 就是把前台进程暂停,进入 T 状态。

  • t 状态:追踪停止

      t 状态和 T 很像,但它是专门给调试器用的。比如你用 gdb 调试程序,程序在断点处停下来,状态就是 t。

  • X 状态:死亡

      X 状态是一个瞬时状态,表示进程已经完全终止,资源正在被回收。这个状态在任务列表里一般看不到。

  • Z 状态:僵尸

      Z 状态表示进程已经退出,但父进程还没读取它的退出信息。这个我们下一篇文章会详细讲。

  我们现在不了解其实际意义是正常的,需要先了解一个进程的调度过程和运行过程中状态的变化。

2.2 CPU的运行与调度算法FIFO

  在了解进程调度过程前,我们应先理解到CPU运行的本质,即CPU选择一个PCB去执行,并且之前我们讲过,进程由 PCB + 代码与数据 组成,PCB 里有一个指针指向下一个 PCB,形成全局进程链表

  由上面两点,我们可以很容易的理解这样的结构:

  即CPU的调度队列,这就是FIFO调度算法,实现"先进先出"的功能,与我们以后要学习的优先级等密切相关。

2.3 运行、阻塞与挂起

  该三态,是一个进程在调度过程中会经历的基本状态,我们先来了解这基础的三态,才能对LInux中划分更加详细的状态理解。

2.3.1 运行态

  进程在调度队列中就是运行态,这是LInux中独有的,设计者认为运行与就绪的区别不大,将两者视为运行。

2.3.2 阻塞态

  阻塞:当一个进程在运行时,为实现目的发出的请求,未能等待某种设备或资源就绪(如:网卡、磁盘、键盘等)或未收到被调用的回应时,就会脱离被os从运行队列中剥离

C语言中 scanf 等待输入的时候,就是等待键盘就绪,阻塞的过程

  上面所说阻塞有关的设备和资源,又该怎么由os管理呢?还是Linux的本质:

"先描述,在组织",为管理这些设备,os给出了 struct device 这个结构体,为了实现对硬件的管理,结构体就必然存在指向设备的变量,而对同一个设备调用的进程可能同时存在,因此就还会有个变量存储需要调用自身的进程,如图:

  现在我们了解了阻塞是什么,并且与阻塞有关的结构有了一定的了解。那让我们一起更加深刻地认识阻塞是如何产生的吧:

现在我们有一个要执行scanf的进程,让我们从这个案例出发

  进程执行 scanf(),最终进入底层的 read() 操作 。当键盘暂时没有输入数据时,当前进程无法立即完成读操作,因此进入阻塞等待,将其从运行队列中移除,并放在管理键盘结构体的等待队列当中。

  当用户输入后,键盘硬件产生中断,CPU进入中断处理流程,并将数据放入相应的输入缓冲区,同时唤醒等待该数据的进程即在设备等待队列上连起的PCB,等待调度器再次分配 CPU,随后继续执行之前阻塞的 read(),从缓冲区获取数据并返回给用户程序

2.3.3 挂起态(阻塞挂起)

  挂起的情况是计算机缓解情况、优化效率的一种做法,但同时发生时也是十分危急的:即当计算机的物理内存资源不足时,os会将阻塞的进程的一部分或全部内存映像可以被换出到外存当中 即交换分区中。

2.3.3.1交换分区

  磁盘中存在一块临时空间,用来存储从内存中交换过来的代码

  当硬件可用且内存充足时,os就会将交换分区中的代码和资源重新加载到内存,进行PCB的映射

tips: 当内存严重不足时,甚至会将运行队列末尾的进程挂起(运行挂起)

三、进程状态的深入解析

  当我们有了上面进程运行过程的知识基础,现在让我们详细学习Linux中进程的不同状态

3.1 状态的本质与查看

  进程状态的本质就是 PCB 中的一个整型变量,是状态数组的下标。 我们可以用 ps 命令查看进程状态,STAT 列中的字母就是状态标识

3.2 对立的运行R与睡眠S

  有着这样一个代码,我们执行并且在后台查看进程的状态

bash 复制代码
#include <iostream>
#include <unistd.h>
#include <sys/types.h>

int main() {
    pid_t pid = fork();
    if (pid == 0) {
        // 子进程:死循环,保持R状态
        while (1) {
            // 空循环,不做IO
        }
    } else if (pid > 0) {
        // 父进程:睡眠,保持S状态
        sleep(1000);
    }
    return 0;
}

  结果为:父进程S+ ,子进程R+。S状态相当于阻塞

"+"号区分前后台进程,在 ./test & 就可以后台执行

3.3 停止的区别:停止T与追踪停止t

  • 停止T: 进程由于收到停止信号或受到作业控制等原因,被操作系统主动暂停执行,进入停止状态,暂时不能被调度运行。(SIGTSTP信号,由用户ctrl + z发起)
  • 追踪停止t: 进程由于受到 ptrace 调试/跟踪机制控制,在调试事件发生时被内核暂停,进入追踪停止状态。(常见的为 cgdb调试)

3.4 不可中断睡眠:D状态

  D状态,又称"深睡眠",与S状态"浅睡眠"相对。两者本质的区别:就是能否响应信号 ,浅睡眠能通过ctrl z 和 ctrl c 停止和终止(这两者本质就是发送信号),而深睡眠则不能,只能有两种方法:1.等待睡眠时间结束 2.硬件断点停止运行

3.4.1 示例理解

  场景理解:

  系统在后台执行下载任务时,突然意识到内存严重不足,甚至阻塞挂起进程都没法节省出空间,就会杀掉运行队列中的进程,而该下载任务已经下载到99%,此时被杀死,就会造成内存泄漏

  因此就有了"D状态",常用于与磁盘操作强相关的进程,使得任务不被阻断。

3.4.2 操作实验

bash 复制代码
dd if=/dev/zero of=~/test.txt bs=4096 count=100000
部分 含义
dd 磁盘拷贝/转换命令,用于按块读写数据
if=/dev/zero 输入文件(input file) /dev/zero 是一个特殊的设备文件,会源源不断地产生零字节(\0)
of=~/test.txt输出文件(output file) ~ 表示当前用户的主目录,文件名为 test.txt
bs=4096 每次读写的数据块大小(block size),这里是 4096 字节,即 4KB
count=100000 一共读写 100000 个数据块

3.5 将死与已死:Z状态与X状态

3.5.1 Z与X的联系

  正如它们的命名,Z状态僵尸,X状态死亡,两者是递进的关系,Z状态的下一步就是X。用一个例子来直观的区分一下:

  某天,一个路人在经过你的时候,突然猝死倒地,由于死亡信息未知,必须等警察等执行机关查验,此时现场信息都得保留。这个过程就是僵尸状态,现场信息就是该进程的PCB(task_struct)

  警察随后到来对现场拍照取证,法医对尸体的死亡信息解析处理,处理完后家属决定如何处理尸体。这其中处理完尸体后才算死亡,即X状态

那么让我们先来了解一下Z状态吧

3.5.2 Z状态

3.5.2.1 OS创建子进程

  我们要理解僵尸状态,就必须先了解到一件事:用户启动的进程都是由操作系统fork创建子进程来完成的。而创建进程的目的就是为了实现某个功能/呈现结果,于是就需要返回进程的信息,而这个信息就是PCB

3.5.2.2 僵尸进程定义与信息储存

  僵尸进程就是如此,是一个子进程执行完任务,处于返回自身执行信息的状态,只储存PCB即可,代码与数据都可以释放。

  退出信息部分查看:

bash 复制代码
struct task_struct {
    // ...
    long exit_state;         // 退出状态
    int exit_code;           // 退出码,如 exit(0) 
    int exit_signal;         // 退出信号,如kill -9
    // ...
};

  当进程变为僵尸进程时,退出状态就会设置为EXIT_ZOMBIE,父进程进而通过系统调用wait()waitpid()获取信息,然后子进程完全退出设置X (X状态设置是瞬间完成的,一般查看不到)

3.5.2.3 僵尸进程的隐蔽危害

  PCB作为进程运行信息的载体,创建这样一个消息体需要资源,因此如果父进程不对子进程PCB进行查看回收,就会造成内存泄漏 。同时如果过多进程执行,就会越来越卡顿,并且实际上进程总数有限,类似"占着厕所不那个"。

3.6 孤儿进程

  既然存在僵尸进程"子进程先退出,父进程后退出",那么有没有这样的进程"父进程先退出,子进程还在运行",这样的进程就是孤儿进程。

3.6.1 孤儿进程的定义

  当一个子进程的父进程,因内存不足、信号杀死、异常崩溃等因素退出,该子进程就被称为孤儿进程

3.6.2 孤儿的领养的机制

  操作系统绝不允许进程没有"监护人",因为如果孤儿进程以后退出时没人回收,就会变成僵尸进程,造成内存泄漏。所以 Linux 规定:

父进程先退出后,1 号进程(init 或 systemd)会领养所有孤儿进程,ssh远程连接成功后,为我们调用创建的bash就是它帮我们fork产生的。

  1 号进程是系统启动后创建的第一个用户空间进程,会一直驻留内存。它领养孤儿进程后,会在孤儿进程退出时自动调用 wait() 回收资源,保证不泄漏。

3.6.3 代码实验

  通过ai生成的代码,预测结果:父进程运行五秒退出,子进程一直刷屏

bash 复制代码
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>

int main() {
    pid_t pid = fork();

    if (pid < 0) {
        perror("fork error");
        return 1;
    }
    else if (pid == 0) {
        // 子进程:一直运行,观察 ppid 变化
        while (1) {
            printf("我是子进程,pid = %d, ppid = %d\n", getpid(), getppid());
            sleep(1);
        }
    }
    else {
        // 父进程:只活 5 秒就退出
        printf("我是父进程,pid = %d,5 秒后退出...\n", getpid());
        sleep(5);
        printf("父进程退出!\n");
    }

    return 0;
}

查看后台:

3.7 内核结构申请Slab

  进程的PCB = 内存申请 + 数据初始化 ,从学习C语言的经历可知:内存的开辟与释放时十分消耗资源的。

  而高效的Linux为提高效率,则使用了"内核结构申请 SLAB"就是:内核为常用结构体建一个对象缓存池 ,创建时从池里拿,销毁时还回池里,避免频繁申请物理内存,提高效率。 僵尸进程不回收,就是占着池子里的对象不放,导致池子可用对象变少。

以 task_struct 为例,我们来了解一下Slab的运行过程:

  • 创建缓存: 内核启动时,会为 task_struct 创建一个专门的 SLAB 缓存(task_struct_cachep)

  • 申请对象: 创建新进程时,调用kmem_cache_alloc()从缓存里拿一块空闲内存,用来放 task_struct

  • 初始化: 把这块内存初始化成新的 PCB。

  • 释放对象: 进程被回收后,调用 kmem_cache_free() 把这块内存还回缓存,标记为空闲,而不是还给物理内存。

  • 复用: 下次再创建进程,直接从缓存里拿现成的空闲块,改改字段就能用。

整个过程,物理内存没有频繁申请和释放,只是在内核的缓存池里进出,效率极高。


总结

  Linux进程状态本质是PCB里的一个整型变量,分为R、S、D、T、t、X、Z七种,通过运行、阻塞、挂起进行转换,其中僵尸进程是子先走父不回收,孤儿进程是父先走被1号进程领养,内核用SLAB分配器高效复用PCB。

如果对你有帮助,别忘了点赞支持,我们下期再见。

相关推荐
liulilittle2 小时前
为什么采用全局管理缓存及状态:麻将客户端状态管理
服务器·网络·游戏·客户端·异步·mahjong·麻将
wdfk_prog2 小时前
VMware Ubuntu 虚拟机从 124 GB 压缩到 33 GB
运维·缓存·docker·容器
FlightYe2 小时前
音视频修炼之编码器(一):AVC、HEVC编码器内部
android·linux·c++·音视频
云栖梦泽2 小时前
网络设备驱动(1)
linux·arm开发·嵌入式硬件
2601_966377133 小时前
等保2.0最新标准新增安全数据处理要求后怎么办?等保一体机数据安全模块来助力
运维·网络·安全·等保一体机
2601_963906783 小时前
桌面自动化工具|零代码录制自动化,无需联网本地离线用
运维·自动化
海宇AI3 小时前
零信任架构实战:基于海宇公安二要素认证即时版构建自动化灵活用工准入网关
运维·人工智能·架构·自动化
Jia ming3 小时前
从打补丁到一行配置:PREEMPT_RT 主线化之后,实时内核到底该怎么配?
linux
杨云龙UP3 小时前
MySQL Host is blocked because of many connection errors 导致 JDBC 连接失败排查与解决
linux·运维·网络·数据库·sql·mysql·登录失败