【Linux】——进程状态&&僵尸进程&&孤儿进程

发布于:2025-03-21 ⋅ 阅读:(26) ⋅ 点赞:(0)

目录

前言

基本进程状态

运行状态

阻塞状态

挂起状态

Linux下的进程状态

僵尸进程

孤儿进程 

结语


前言

  进程的状态反映了它在执行过程中的不同阶段,例如创建、就绪、运行、阻塞和终止等。这些状态之间的转换由操作系统的调度算法和进程的行为共同决定。通过了解进程状态的变化,我们不仅可以更好地理解程序的执行流程,还能在开发过程中更高效地调试和优化代码。

  在本篇博客中,我们将深入探讨进程的几种主要状态,分析它们之间的转换关系,并结合实际应用场景,帮助读者掌握进程状态管理的核心概念。无论你是操作系统初学者,还是希望进一步巩固相关知识的开发者,相信本文都能为你提供有价值的参考。接下来,让我们从进程的基本状态开始,逐步揭开进程管理的奥秘。


1.基本进程状态

我们在《看完这一篇,99%的人都懂进程》一文中,以及认识到,在内存中的进程是动态的,是会变化的

这里指的变化,就是进程状态的变化

操作系统下的进程,有三个基本状态

  1. 运行状态
  2. 阻塞状态
  3. 挂起状态

接下来,我们将一一对这些进程基本状态进行讲解

1.1运行状态

当进程被CPU调度,或者处于CPU的调度队列当中的时候,就代表此时进程处于运行状态

现代计算机或有单核、双核、甚至多核

拿单核来说,我们说过操作系统管理硬件和软件,其中管理的硬件也包括CPU

在内存空间中,操作系统会为CPU维护一个运行队列

当一些进程准备好,可以运行,操作系统就会将这些进程放入CPU的运行队列当中,再根据调度算法让CPU调度其中的进程

操作系统如何得知该进程准备好了呢?

通过进程PCB上的状态属性(status),当 状态属性= 运行,操作系统就会将这个进程放入CPU的运行队列当中

而PCB上的状态属性,又是谁设置的呢?

也是操作系统!操作系统会对进程进行判断,根据进程目前的情况、与其关联的硬件情况,对进程额状态判定,设置进程PCB的状态属性

1.2阻塞状态

当进程不动了,运行不下去,无法执行接下来的代码,我们就任务该进程处于阻塞状态

当进程处于运行状态,会进入运行队列

那么进程处于阻塞状态,也会进行阻塞队列

我们的进程多多少少会与硬件进行交互,获取硬件数据或者发送给硬件数据

但是如果硬件此时没有数据给进程或者没有能力接收进程的数据,即硬件资源没有准备好,此时的进程就会卡住,无法进行下一步,从而阻塞,当相关资源准备好了,进程就会重新进入运行状态,开始执行下一步代码。

如何知道硬件资源准备好,又如何知道硬件资源没有准备好呢?

操作系统是管理软件和硬件的好手,它管理诸多的硬件

操作系统在内存空间中会为每个硬件创建相应的结构体对象,并用数据结构将这些结构体对象联系起来,进行统一管理(先描述,再组织)

操作系统对硬件结构体对象管理,就是对硬件本身进行管理,也能从硬件结构体对象的属性上,获得硬件的信息,如资源是否准备好等等。

1.3挂起状态

当一个阻塞进程被操作系统操作,其在内存空间的代码和数据被操作系统置换到磁盘,而进入不能运行的状态,我们将这种状态称为挂起状态

为什么被挂起?

  1. 用户主动挂起
  2. 操作系统迫于特殊情况进行挂起

我们讨论一下第二种情况

当内存接近圆满,整个计算机的运行会收到影响,甚至宕机

为了让计算机能够正常运行,操作系统就会找到处于阻塞状态的进程

这些处于阻塞状态的进程,不仅不执行任务,还占据内存空间,没用还消耗资源

操作系统就会将这些暂时不工作的进程的代码和数据调换到磁盘之上,待到内存空间富足,再将这些代码和数据重新加载到内存中

内存与外设的交互,会大大增加时间损耗,这属于操作系统为了让计算机存活的无奈之举。

磁盘中保存从内存而来的数据与代码的区域,叫作  swap区

swap区的大小是用户可以主动设置的,建议为内存的一倍,不建议将该区域设置过大,过大会导致操作系统依赖,不断将代码和数据写入该区域,从而导致频繁地内存和外设交互,大大降低效率

2.Linux下的进程状态

我们上面介绍的进程状态,是操作系统中的基本进程状态

每个操作系统中都会有,但不一定和上面的基本状态名字相同,即同内不同外

以下是Linux中的具体进程状态

其中,R运行状态就是我们上面的运行状态,睡眠状态就是阻塞状态

来浅浅看一段有意思的代码

#include<iostream>
#include<unistd.h>

int main(){
    while(1){
        sleep(1);
        printf("hello fun_code!\n");
    }
    return 0;
}

朴素无华,运行一下,并使用 ps ajx 进行查看

 

可以发现,该进程处于睡眠状态,这是为什么呢?

代码中的打印语句,printf是向显示器中打印,而显示器资源也是需要准备的,当显示器资源没有准备好的时候,该进程就会进入阻塞队列,因为频繁的打印,导致该进程不断进入阻塞队列,又不断唤醒运行,但Linux操作系统为了方便,就会将该进程置为睡眠状态,偶尔有运行状态

其中,进程的状态的都是由操作系统决定的,操作系统再根据这个状态对进程安排

2.1深度睡眠状态(disk sleep)

上面介绍的睡眠状态属于浅度睡眠,浅度睡眠的进程是可以被操作系统终止的

只要用户向操作系统发出终止进程的请求,浅度睡眠的进程就会被终止

而深度睡眠的进程,不会被操作系统强行终止

为什么会有深度睡眠呢?

进程访问资源而得不到资源的时候,就会陷入睡眠

如果此时的进程,是在对磁盘写入数据,将数据交给磁盘写入的时候,进程也会进入等待队列,等待写入数据完成 

这时候,如果终止该进程,我们将无法得知数据写入磁盘是否完成,如果严重甚至会导致数据在磁盘的位置丢失。

如果数据十分重要,丢失的话将造成无法挽回的损失,为了照顾这种向磁盘写入数据的进程,操作系统就会将其设置为深度睡眠状态,无论发生什么,都不会强行终止这个进程!

2.2暂停状态(T)

在Linux命令行中,可以发送暂停信号给目标进程,让其进入等待队列,这种进程状态成为暂停状态

2.3追踪暂停状态(T)

进程的暂停状态是可以控制的,如使用debug进程调试另一进程,另一进程所处于的状态就是最终暂停状态

2.4死亡状态(X)

当进程完成任务,代码和数据,以及PCB全部在内存空间中被销毁时,进程状态就是死亡状态

3.僵尸进程

在《轻松搞定进程fork》一文中,我们提过,我们创建进程是为了让新的进程替我们完成其他任务

新进程去执行任务就够了吗?

我们也需要知道任务完成得如何

如何知道呢?

进程完成相应任务就会退出,它的退出信息就需要被我们获取,有了这个退出信息,我们就知道进程任务完成得如何

这个退出信息在哪里?

一切都在进程的PCB当中!

所以,当一个进程退出的时候,我们不会让其立刻退出,我们得知道这个进程的任务完成情况,进程退出有两个阶段

  1. 进程退出,代码和数据在内存中被销毁,退出信息被操作系统写入进程的PCB当中
  2. 进程的PCB被读取,PCB在内存空间中被销毁

我们将进程退出时,代码和数据被销毁,但PCB仍然存在,退出信息没有被获取时的状态称为僵尸状态!处于僵尸状态的进程,即为僵尸进程

回收进程PCB的操作必须是进程的创建者完成的,所以一般回收进程的就是创建该进程的父进程或者操作系统

这也就是为什么所有的子进程必须有父进程的原因

如果子进程的PCB的退出信息,一直没有被父进程或者操作系统获取,该进程就会一直是僵尸状态,其PCB就会一直存在于内存空间,造成内存泄露

如果子进程的PCB一直没有被父进程或者操作系统获取,该进程PCB就需要被操作系统管理维护,一定程度增加操作系统不必要的工作量

因此,获取僵尸进程的PCB刻不容缓,我们可以使用 wait函数 和 waitpid函数,来回收子进程

3.1wait函数

wait() 函数会阻塞调用进程,直到任意一个子进程终止。它返回终止子进程的进程 ID(PID),并将子进程的退出状态存储在 status 参数中,包含于sys/types.h 和 sys/wait.h 头文件中

函数造型

pid_t wait(int *status);

参数说明

  • status:指向一个整数的指针,用于存储子进程的退出状态。可以使用宏(如 WIFEXITEDWEXITSTATUS 等)来解析状态。

返回值

  • 成功时返回终止子进程的 PID。
  • 失败时返回 -1(例如没有子进程)

基本用法

int status;
pid_t child_pid = wait(&status);

注意:wait是等待当前父进程的所有子进程,等待成功一个就直接返回结果 

3.2waitpid函数

waitpid函数 比 wait函数 更灵活,可以指定要等待的子进程 PID,并提供非阻塞选项。

函数造型

#include <sys/types.h>
#include <sys/wait.h>

pid_t waitpid(pid_t pid, int *status, int options);

参数说明

  • pid:指定子进程的pid
  • status:同 wait(),用于存储子进程的退出状态。
  • options:控制函数行为的选项,WNOHANG为非阻塞等待,没有结果立刻返回0,WUNTRACED会等待子进程停止或终止。

返回值说明

  • 成功时返回终止子进程的 PID。
  • 如果指定了 WNOHANG 且没有子进程终止,返回 0。
  • 失败时返回 -1

基本用法

int status;
pid_t child_pid = waitpid(pid, &status, 0);

4.孤儿进程 

子进程一定是父进程创建的,当父进程比子进程先退出的时候,子进程就相当于没有了父进程,我们将这种没有父进程的进程,称为孤儿进程

没有父进程,就没有办法回收孤儿进程的退出信息

为了避免孤儿进程的退出信息没有父进程回收,操作系统会为其分配一个父进程,让这个父进程完成对子进程的退出信息获取,这个新分配的父进程一般是操作系统本身,所以我们才会说,子进程的PCB中的退出信息,一般由父进程或者子进程获取。

验证一下:新分配的父进程是操作系统本身

pid_t pid = fork();

    if (pid == 0) {
        // 子进程
        printf("Child process is running, PID: %d\n", getpid());
        sleep(10); // 等待父进程终止
        printf("Child process is still running, now parent PID: %d\n", getppid());
    } else if (pid > 0) {
        // 父进程
        printf("Parent process is running, PID: %d\n", getpid());
        sleep(2); // 父进程提前终止
        printf("Parent process is done\n");
    } else {
        perror("fork failed");
        return 1;
    }

    return 0;

在这段代码下,父进程会比子进程先退出,子进程会重新被操作系统分配父进程

运行一下

这个pid为1的进程,就是最先启动的进程——操作系统! 


结语

  通过本次关于进程状态的讲解,我们从进程的基本状态出发,深入探讨了进程的生命周期及其状态转换机制。无论是进程的创建、就绪、运行、阻塞还是终止,每一种状态都反映了操作系统对资源管理和任务调度的精妙设计。

  希望本次讲解能为你提供清晰的知识框架,并激发你对操作系统更深层次的探索兴趣。如果你有任何疑问或想进一步讨论相关话题,欢迎随时交流。感谢你的阅读,期待在未来的学习中与你再次相遇!