深入Linux序列:进程的终止与等待

news/2025/2/27 10:23:06

在这里插入图片描述

在之前的学习中,我们知道我们的进程在运行结束的时候,那么它并不会立即进入死亡状态,而是先进入僵尸状态,维持僵尸状态一段时间,那么此时在僵尸状态中的进程,那么它的内核数据已经移出内存被清理了,但是该进程在操作系统内对应的task_struct结构体不会被立即被回收清理,那么之所以不会被立即回收清理,就是因为我们需要知道获取到进程是最后是怎么退出的,而这个信息就被保存在我们该进程所对应的task_struct结构体当中,那我们需要获取到进程退出的信息。
那么接下来我们对于进程的终止无非概括起来就有这三个疑问:我们进程的终止的信息是什么,保存在哪里,以及我们要获取该进程的终止信息有什么用?那么接下来我便围绕这三个问题展开我们的下文,那么废话不多说,就让我们进入正文部分的讲解

进程的终止

进程就和我们人类一样,有着生命的开始与生命的结束,那么对于进程来说,那么我们进程一般是被用户创建来完成某项任务以及工作的,那么一旦我们该进程执行完他所有的内容,那么我们必须要知道它做的怎么样,不可能该进程处理执行完它所有的代码然后结束之后,我们就对它不管不问了,就好比于你在单位上你每天工作完都需要向单位上的领导或者上级汇报你今天干了什么事情,究竟是完成了领导交给我的任务还是没完成还是说我自己就摆烂不想做了,那么这些事情领导都有权知道然后再进行相应的决策。

那么现在我们知道我们为什么要获取进程终止的信息了,那么目的就是知道进程完成它的工作完成的怎么样,那么接下来的问题就是我们如何查询我们进程的终止的信息呢

那么我们知道我们一个进程的上下文就在我们进程代码的main函数当中,那么在大学的时候,我们平常在写c语言或者c++代码的时候,都是公式的引个头文件然后写一个main函数然后return 0,但是我们从来都没有思考过,我们在main函数定义的return 0的作用是什么,为什么要有return以及为return的为什么是0而不是1或者2等其他的数字

那么这个在main函数的return的内容,那么它就是我们程序的退出码,而退出码就反应了我们进程终止时的一个状态,那么我们进程结束时退出一般有三种场景,分别是:

  • 1: 执行完该进程所有的代码内容,并且结果正确
  • 2: 执行完该进程所有的代码内容,但结果不正确
  • 3: 进程在执行的途中出现异常,没有执行完所有的代码然后退出了

那么对于这三种情况,其实我们真正关心的只有第二和第三种情况,就好比于小学时你考试考了90分,考了全班第一名,那么你爸根本不会对你有所过问,更不会问你为什么考了90分,但是如果说你考了13分,并且还是全班倒数第一,那么你爸肯定就要责问你,为什么考了13分,是什么原因导致的,那么你得向你爸解释说到是因为状态不好还是说当天肚子不舒服导致的。

那么在刚才的例子中,你考了13分或者倒数第一,那么就类似于你的进程要么出现了第二种或者第三种情况,那么究竟是为什么出现这个原因,那么就需要退出码来进行解释,那么我们的退出码的每一个值都有具体的含义,那么退出码为0代表着进程执行完所有的代码内容,并且结果正确,而如果退出码为1或者2,那么说明进程出现了相应的问题

那么在我们的Linux下,也有相应的指令能够看到获取到最近一次结束的进程的退出码,那么该指令也就是echo $?

那么输入该指令接着下一行就能够显示反馈最近一次结束进程的退出码
在这里插入图片描述

那么这里我们一定要注意我刚才所说的那第一个以及第二场景中关于结果是否正确的问题,我觉得很多读者其实包括之前我在内,都会误以为这个结果是我们代码层面上的逻辑出现的错误,比如是我们要编写一个代码要实现两个数a和b相加,结果代码写成了a和b相乘,那么这是不是就意味着出现了我们上面所说的第二种场景,也就是代码执行完了,但是结果是错的呢,这里我们注意这里的结果不是指的是我们所谓代码的逻辑层面上的结果,因为操作系统它哪里能知道能检查我们写的代码想要完成什么,想要干什么,那么这里的结果是指的是语言或者说语法层面上的,比如我们指针是否出现了野指针是否有越界访问等,所以一定要注意理解

那么我们除了我们的退出码,其实我们还有错误码的存在,那么所谓的错误码就是我们调用我们的库函数出现失败或者异常,那么此时我们调用库函数失败的错误信息会保存到我们的全局变量中errno当中,那么如果我们还是正常执行完我们所有的代码,那么我们的退出码是是0,但是执行完所有的代码不代表我们的代码没出现任何错误,如果有比如有库函数的调用失败发生,那么我们可以通过错误码在我们的代码中定义一个检查逻辑,看是否出现库函数的调用出错,而我们的errno只记录最新一次库函数调用出错的错误码,
我们可以接着借组perror函数,能够将我们的错误码对应的含义转换为字符串,perror函数会返回一个字符类型的指针,那么这里注意我们的错误码不是我们的退出码,我们检查该进程的退出状态的时候,关心的是退出码而不是所谓的错误码

那么了解了我们的退出码等概念,那么我们就可以在代码中定义相应的逻辑比如if else的逻辑判断我们代码如果出现了这样的错误的结果,那么我们会进行怎么样的处理,然后return一个非0的退出码,那么这个非0的退出码的含义则是由我们用户自己来定义的

#include <stdio.h>

int main() {
    FILE *file = fopen("example.txt", "r");
    
    if (file == NULL) {
        printf("File not found.\n");
        return 1;  // 文件不存在,返回非零值
    }

    // 如果文件存在,进行其他操作
    // ...

    fclose(file);
    printf("File found and processed.\n");
    return 0;  // 成功完成所有操作,返回0
}

那么进程会专门记录进程的退出码以及异常的情况,那么在我们进程所对应的task_struct结构体中会专门有两个字段分别是exit_code以及exit_signal来记录这两个内容,所以这就是为什么我们进程在进入僵尸状态之后我们该进程对应的task_struct结构体不会被立即回收清理掉的原因,因为得有进程来接收这两个字段的内容来获取该进程的退出情况。

进程的等待

那么前面详细的介绍我们进程的终止对应的退出码就为了给我们进程的等待做铺垫。

那么我们知道我们父进程在代码层面上可以调用我们的系统调用接口也就是fork函数来创建一个子进程,那么我们知道我们父进程创建子进程的目的就是为了让子进程来帮组父进程来完成某项特定的任务或者工作,就好比我们自己在写代码的时候,我们代码所有的逻辑以及各种模块我们不会都全部定义在我们的main函数当中,而是会自己定义一些函数来完成这个主函数特定的功能与模块,在将处理后的结果返回给我们的主函数即可,这样的好处就是功能模块化使得逻辑清晰便于管理。

同理我们的父进程创建子进程的思路和我们上面的函数的例子异曲同工,就是父进程让子进程帮自己办点事,那么我们可以通过一个循环来创建一批子进程,然后根据fork函数的返回值让子进程有着自己的执行流,然后我们可以用过一个函数来封装子进程的执行内容,那么这就是我们一个刚才的模块化思想的简单应用
在这里插入图片描述
在这里插入图片描述

而在此过程中父进程要知道子进程完成的怎么样,那么则是需要获取子进程的退出码等内容

那么我们就需要我们的系统调用接口,这里有两个函数wait函数与waitpid函数,来获取我们子进程的退出码等内容。

那么wait函数的功能是waitpid函数的子集,所以我们一般使用waitpid函数要多一些,那么我们的wait函数的参数是一个int类型的指针,而这个参数的作用我在后文介绍waitpid的时候会进行讲解,而wait的返回值则有3种,分别是要么大于0,或者等于0以及小于0.

那么大于0则代表我们等待的子进程结束,那么我们这个返回值就是子进程的PID而小于0则是等待出错,当我们等待的不是该父进程对应的子进程或者该父进程没有创建子进程,那么就会返回一个小于0的返回值,而返回值为0,说明我们该父进程等待的子进程还没有运行结束,而waitpid的返回值也是和wait函数是同样的内容
在这里插入图片描述

在这里插入图片描述


那么我们知道当我们的父进程调用了fork函数创建了子进程,那么此时我们的父进程与子进程会有着不同的执行流,那么当我们父进程执行到我们的wait函数语句的时候,那么此时我们父进程不会执行wait函数之后的语句了,而是一直卡在wait函数这里等待我们子进程的结束,那么此时我们父进程的状态会从原来的R状态修改为S状态也就是阻塞等待状态。

那么在我们之前的学习中,我们知道我们的S状态是因为我们该进程需要与我们的io设备交互,此时要等待我们相应的io设备就绪有响应才能进行之后的代码,比如我们在代码中调用了我们的printf函数需要向显示器做写入或者调用了scanf函数要获取用户键盘上的输入,但是在这里我们知道调用wait函数,那么如果等待的子进程还在运行,那么此时我们的父进程也会陷入等待状态,而进程对应的task_struct结构体中也有对应的一个指针,指向该进程对应的等待队列,那么此时该父进程就会被放到等待队列当中去


而对于waitPID来说,那么它有三个参数:

  1. pid

    :指定要等待的子进程的PID。

    • 可以是特定的子进程PID,也可以是特殊值如-1(等待任何子进程)。
  2. status

    :一个指向int类型的指针,用于存储子进程的退出状态。

    • 这个参数是一个输出型参数,waitpid()会将子进程的退出状态保存在这里。
  3. options

    :等待选项,可以是0或其他标志的组合。

    • 如果设置为0,则表示采用阻塞式等待,直到指定的子进程终止。

输出型参数也就是int类型的指针,那么它则是带出我们子进程的退出码以及异常等信息,而之前我们说过我们进程的结构体中专门有两个字段来记录该进程终止时的退出码以及异常状况,那么此时我们的系统调用函数wait就可以获取到这两个字段然后通过位运算将其保存到传入的int类型的参数中,那么我们知道我们一个int类型的数有32个二进制位,那么此时我们这个数据的最低的8位则是用来表示进程的异常,而接着的后8位则是用来表示进程的退出码,那么同样我们可以通过我们的位运算来获取得到对应的内容,但是我们Linux下专门定义了对应的宏可以用来专门来将其转换得到我们的子进程的退出码,那么这个宏就是:

WEXITSTATUS(status):
如果子进程正常终止,则返回其退出码。


而对于option选项的话,如果option是0的话,那么意味着我们采取的是阻塞式等待我们的子进程的退出信息,那么一旦我们该子进程还在运行,那么我们就陷入等待,

那么这里我的option也可以采取的是非阻塞式等待,那么就需要我们设置为WONOHANG,那么WONOHANG是Linux控制waitpid函数行为的一个宏定义,而这个选项就意味着我们都waitpid是采取非阻塞方式来等待
那么我还是举一个例子来理解我们阻塞式等待与非阻塞式等待的一个区别,就好比你现在打电话查询你的考试信息,那么阻塞式等待就是你在电话的那一头一直不挂着电话,然后自己什么也不干,就一直等待电话的另一头给你考试的信息,而非阻塞式等待的话,则是你先打一通电话,来查询是否有结果,如果没有结果,那么就将电话给挂掉,然后自己在玩会儿手机敢自己的事情,然后过了一段时间,再打电话进行查询,重复这样的过程知道最终获取到信息。

所以我们的刚才例子中的第二种方式就是我们的非阻塞+轮询的方式,那么我们的父进程此时的状态不会被设置为S状态,而是可以继续执行wait之后的代码,然后时不时的在调用waitpid来进行查询,那么这里就是通过我们的循环来实现,那么这就是我们的非阻塞+轮询的方式来等待,相比于之前啥都不干,那么非阻塞等待意味着父进程能够执行自己之后的代码,显然这种方式要更高效一点

#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>

int main() {
    pid_t pid = fork();
    if (pid == -1) {
        perror("fork");
        exit(EXIT_FAILURE);
    } else if (pid == 0) {
        // 子进程
        printf("Child process is running...\n");
        sleep(5);  // 模拟子进程执行一些任务
        printf("Child process is exiting with status 42\n");
        exit(42);
    } else {
        // 父进程
        int status;
        while (1) {
            pid_t child_pid = waitpid(pid, &status, WNOHANG);
            
            if (child_pid == -1) {
                perror("waitpid");
                exit(EXIT_FAILURE);//EXIT_FAILURE是一个用于表示程序异常终止的宏
            }

            if (child_pid == 0) {
                // 子进程尚未终止,父进程可以继续执行其他任务
                printf("Child process is still running. Polling...\n");
                sleep(1);  // 等待一段时间再继续轮询
            } else {
                // 子进程已终止
                if(WIFSIGNALED(status)==0)
                    printf("Child process exited with status %d\n", WEXITSTATUS(status));
                
                break;  // 退出循环
            }
        }
    }
    return 0;
}

结语

那么这就是本篇文章的全部内容,主要讲述了我们的进程的终止与等待,那么我的下一篇文章将介绍进程的替换的内容,那么我会持续更新,希望你能够多多关注与支持,那么如果本篇文章对你有所帮助的话,那么还请多多三连加关注支持一下哦,你的支持就是我最大的动力!
在这里插入图片描述


http://www.niftyadmin.cn/n/5869944.html

相关文章

9.编写负载均衡模块|编写judge功能|postman进行调试(C++)

编写负载均衡模块 代码整体结构 oj_control.hpp // code: #include... // input: "" void Judge(const std::string &number, const std::string in_json, std::string *out_json) {// 0. 根据题目编号&#xff0c;直接拿到对应的题目细节// 1. in_json进行反…

(学习总结26)Linux工具:make/Makefile 自动化构建、Git 版本控制器 和 gdb/cgdb 调试器

Linux工具&#xff1a;make/Makefile 自动化构建、Git 版本控制器 和 gdb/cgdb 调试器 make/Makefile 自动化构建在 Linux 命令行中的命令 makeMakefile 的基本操作1. 编写与删文件基本操作2. 总是执行操作3. 执行操作时的依赖性4. 定义变量或命令操作与其它简单操作5. 多文件操…

DDNS-GO 动态域名解析

什么是 DDNS DNS&#xff08;域名系统&#xff09; 是互联网的电话簿&#xff0c;将易于记忆的域名&#xff08;如 www.example.com&#xff09;转换为计算机可以理解的 IP 地址&#xff08;如 192.0.2.1&#xff09;。这使得用户无需记住复杂的数字地址即可访问网站。 DDNS&…

centos设置 sh脚本开机自启动

1. start.sh脚本 #!/bin/bash# 依赖docker&#xff0c;等待xxx容器完全启动 sleep 60curl -X POST "localhost:8381/models?urlmymodel.mar&model_namemymodel&batch_size1&max_batch_delay10&initial_workers1"sudo /usr/local/nginx/sbin/nginx …

大白话React 虚拟 DOM,好处在哪里?跟vue有什区别

大白话React 虚拟 DOM&#xff0c;好处在哪里&#xff1f;跟vue有什区别 React虚拟DOM 概念&#xff1a;可以把虚拟DOM想象成是对真实DOM的一种“虚拟描述”&#xff0c;就好像是真实DOM在电脑里的一个“替身”。它其实就是用JavaScript对象来表示DOM节点和它们之间的关系。比…

redission的原理

分布式锁的实现 Redisson 最出名的功能之一是分布式锁&#xff08;RLock&#xff09;。它的锁机制基于 Redis 的原子性操作&#xff1a; 使用 SET NX&#xff08;SET if Not eXists&#xff09;命令尝试获取锁&#xff0c;并设置一个过期时间&#xff08;防止死锁&#xff09;…

组件传递props校验

注意&#xff1a;prop是只读的&#xff01;不可以修改父组件的数据。 可以检验传过来的内容是否类型没问题。 App.vue <template><div><!-- <parentDemo/> --><componentA/></div></template> <script> import ComponentA …

港科大提出开放全曲音乐生成基础模型YuE:可将歌词转换成完整歌曲

YuE是港科大提出的一个开源的音乐生成基础模型&#xff0c;专为音乐生成而设计&#xff0c;专门用于将歌词转换成完整的歌曲&#xff08;lyrics2song&#xff09;。它可以生成一首完整的歌曲&#xff0c;时长几分钟&#xff0c;包括朗朗上口的声乐曲目和伴奏曲目。YuE 能够模拟…