【Linux从入门到放弃】探究进程如何退出以进程等待的前因后果

🧑‍💻作者: @情话0.0
📝专栏:《Linux从入门到放弃》
👦个人简介:一名双非编程菜鸟,在这里分享自己的编程学习笔记,欢迎大家的指正与点赞,谢谢!

在这里插入图片描述

进程退出和等待

  • 前言
  • 一、进程创建
    • 1.1 fork函数
    • 1.2 写时拷贝
    • 1.3 fork常规用法
    • 1.4 fork调用失败的原因
  • 二、进程退出
    • 2.1 进程退出场景
      • 2.1.1 查看退出码
      • 2.1.2 退出码的含义
    • 2.2 如何理解进程退出?
    • 2.3 进程退出的方式
  • 三、进程等待
    • 3.1 进程等待的原因
    • 3.2 什么是进程等待?
    • 3.3 进程等待的方式
      • 3.3.1 wait方法
      • 3.3.2 waitpid方法
    • 3.4 子进程退出状态
    • 3.5 非阻塞式等待
  • 总结


前言

之前的几篇博客已经是对进程的相关概念做了详细了解,现阶段对进程的定义为内核数据结构加上该进程对应的代码和数据,操作系统对进程通过先描述再组织的方式做管理。有了这些预备知识,接下来就是要学习如何控制进程,也就是在操作上该怎么做。


一、进程创建

1.1 fork函数

  关于fork函数的知识,此篇博客有详细介绍:进程创建

进程调用fork,当控制转移到内核中的fork代码后,内核做:

  1. 分配新的内存块和内核数据结构给子进程
  2. 将父进程部分数据结构内容拷贝至子进程
  3. 添加子进程到系统进程列表当中
  4. fork返回,开始调度器调度

在这里插入图片描述

在调用fork函数之后,系统会将父进程的代码拷贝一份给子进程,同时会有两个执行流分别执行父进程和子进程,要注意的是子进程不会去执行fork之前的代码。

1.2 写时拷贝

  父子进程代码共享,父子再不写入时,数据也是共享的,当任意一方试图写入,便以写时拷贝的方式各自一份副本。具体见下图:
在这里插入图片描述

在修改内容之前,父子进程的在物理内存页的数据、代码指向同一块位置,如子进程对数据进行修改,,那么此时就会发生写时拷贝,在物理内存页重新开辟一块空间将修改后的数据存入其中。
因为在操作系统是不允许空间的浪费,所以不会将父进程的所有代码数据都在物理内存中重新拷贝一份,而是通过写时拷贝的方式在子进程需要使用(修改)数据的时候才会重新开辟空间,它是一种按需申请资源的策略。

1.3 fork常规用法

  1. 一个父进程希望复制自己,使父子进程同时执行不同的代码段。例如,父进程等待客户端请求,生成子进程来处理请求。
  2. 一个进程要执行一个不同的程序。例如子进程从fork返回后,调用exec函数。

1.4 fork调用失败的原因

  1. 系统中有太多的进程
  2. 实际用户的进程数超过了限制

二、进程退出

2.1 进程退出场景

a. 正常运行完毕(1. 结果正确  2. 结果不正确)
b. 崩溃了(进程异常)  崩溃的本质:进程因为某些原因,导致进程收到了来自操作系统的信号(kill -9)

2.1.1 查看退出码

我们一般在写C语言程序都会在main函数结束时返回 0,这个0代表着该进程的退出码,在linux中,可通过这样的指令查看进程的退出码:echo $?。看下面代码:

int add_to_top(int num)
{int sum=0;for(int i=1;i<=num;i++){sum+=i;}return sum;
}int main()
{int ret=add_to_top(100);if(ret==5050)return 1;else return 0;
}

上面的代码要实现的功能:从1加到100,若和为5050,则返回1,否则返回0。通过下图可以看到该进程的退出码为1,表示结果正确。但是奇怪的是,后两次的查看退出码都为了0,这是因为该指令只会保留最近一次执行的进程的退出码!后两次代表着该条指令执行后的退出码。
在这里插入图片描述

2.1.2 退出码的含义

我们看到的退出码都是数字,对于程序员来说,我们可能知道一些退出码所代表的含义,但是对于一般人来说看到这些数字并不了解所蕴含的意义。所以对于一般人来说,如果你只给他退出码是没有价值,因为他并不知道这些退出码代表的含义。关于退出码的含义我们可以自定义,下面看一下C语言所提供的退出码的含义。

int main()
{for(int i=0;i<200;i++){printf("%d:%s\n",i,strerror(i));}return 0;
}

这只是前二十个,后面还有更多。当然这是在linux操作系统下,在windows下所提供的退出码含义是不同的。

在这里插入胜多负少描述

2.2 如何理解进程退出?

关于进程的退出,可以理解的是操作系统内少了一个进程,操作系统要释放进程对应的内核数据结构+代码和数据。

2.3 进程退出的方式

  1. main函数return。而其他函数的return仅仅代表该函数的返回。对于这种方式来说,进程执行本质是main执行流执行,当main函数执行完时代表着进程也就结束了。
  2. exit函数退出。exit函数所包含的数字为该进程的退出码,在函数任意位置调用直接使进程退出。
  3. _exit函数退出。直观感觉上和exit的功能是一样的,但是在一些细节是不一样的。exit函数在退出的时候会自动刷新缓冲区,而_exit函数不会刷新缓冲区。它们两个的关系是一种包含和被包含的关系。从下面这个图可以得到一个暗藏的点:缓冲区不在操作系统内。

在这里插入图片描述

三、进程等待

3.1 进程等待的原因

  1. 之前讲过若子进程先退出,而父进程并没有读取子进程状态,就可能造成‘僵尸进程’的问题,进而造成内存泄漏。
  2. 进程一旦变成僵尸状态,那就刀枪不入,“杀人不眨眼”的kill -9 也无能为力,因为谁也没有办法杀死一个已经死去的进程。
  3. 我们为什么要创建子进程,目的就是为了让子进程帮助我们去完成某些事情,关于父进程派给子进程的任务完成的情况,可能我们不会关心完成的对不对,也可能会关心子进程运行完成的结果对还是不对,亦或是否正常退出。
  1. 避免内存泄漏(必)
  2. 获取子进程的执行结果。(可能)

关于子进程的退出结果,有三种可能性:
a. 代码跑完,结果对;
b. 代码跑完,结果不对;
c. 代码运行异常;
关于结果对或不对,可以通过退出码的方式判别,代码运行异常则是收到某种信号。因此衡量一个进程运行的怎样是通过退出码+信号的方式来执行的。

3.2 什么是进程等待?

通过系统调用,获取子进程退出码或者退出信号的方式,同时释放内存问题。

3.3 进程等待的方式

3.3.1 wait方法

pid_t wait(int *status);

返回值:成功返回被等待进程pid,失败返回-1。
参数:输出型参数,获取子进程退出状态,不关心则可以设为NULL

//代码功能:父进程在休眠5秒的过程中子进程先运行2秒,然后子进程退出,2秒之后,父进程对子进程做进程等待操作。
int main()
{pid_t ret=fork();if(ret==0){//子进程int cnt=2;while(cnt--){printf("我是子进程,我现在活着呢,我离死亡还有%d秒,pid:%d,ppid:%d\n",cnt,getpid(),getppid());sleep(1);}_exit(0);}sleep(5);//父进程pid_t ret_id=wait(NULL);printf("我是父进程,等待子进程成功,pid:%d,ppid:%d\n",getpid(),getppid());return 0;
}

在运行代码之后我们应该观察到的现象:父子进程的状态最开始都为运行状态,子进程经2秒输出2条语句,然后退出变为僵尸状态,父进程依然为运行状态,再过3秒之后,父进程对子进程等待回收,然后全部退出。

在这里插入图片描述

3.3.2 waitpid方法

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

返回值: 当正常返回的时候waitpid返回收集到的子进程的进程ID; 如果设置了选项WNOHANG,而调用中waitpid发现没有已退出的子进程可收集,则返回0; 如果调用中出错,则返回-1,这时errno会被设置成相应的值以指示错误所在;

参数 pid

  1. pid=-1,等待任意一个子进程,与wait等效。
  2. pid>0,等待其进程ID与pid相等的子进程。

参数 status:

  1. WIFEXITED(status):若为正常终止子进程返回的状态,则为真。(查看进程是否是正常退出)
  2. WEXITSTATUS(status): 若WIFEXITED非零,提取子进程退出码。(查看进程的退出码)

参数 options:

  1. WNOHANG: 若pid指定的子进程没有结束,则waitpid()函数返回0,不予以等待。若正常结束,则返回该子进程的ID。

3.4 子进程退出状态

  1. 在 wait 和 waitpid 中,都有一个status参数,该参数是一个输出型参数,由操作系统填充。它的功能是为了获取子进程的退出状态。如果传递NULL,表示不关心子进程的退出状态信息。否则,操作系统会根据该参数,将子进程的退出信息反馈给父进程。
  2. status不能简单的当作整形来看待,可以当作位图来看待,具体细节如下图(只研究status低16比特位):
    在这里插入图片描述
  3. 通过对上图理解,我们应该明白关于子进程的退出状态。如果进程是正常退出,那么status位图的低八位为0,次低八位为进程的退出状态,也就是通过这次低八位获取进程的退出码。如果进程是被某种信号所杀而导致的异常退出,则只需要关心低七位,读到的结果为导致该进程退出的终止信号所对应的数字,coredump标志位目前不需要了解。
int main()
{pid_t id=fork();if(id==0){//子进程int cnt=2;while(cnt--){printf("我是子进程,我现在活着呢,我离死亡还有%d秒,pid:%d,ppid:%d\n",cnt,getpid(),getppid());sleep(1);}// int a=10;// a/=0;_exit(123);}sleep(5);int status=0;pid_t ret_id=waitpid(id,&status,0);printf("我是父进程,等待子进程成功,pid:%d,ppid:%d,status signal:%d,status code:%d\n",getpid(),getppid(),(status&0x7F),((status>>8)&0xff));return 0;
}

看上面这段代码,如果按照这样的逻辑,那么最终的运行结果为(只看退出状态):父进程获取到子进程的退出信号肯定为0,因为是正常退出,退出状态则为数字123;若将那两条注释的代码取消,那么子进程就会因为除0操作导致异常退出,那么此时父进程就会读到对应的退出信号,输出结果为该信号对应的数字。
在这里插入图片描述

  1. 父进程是如何获取子进程的退出状态信息的呢?子进程有自己的PCB、地址空间、页表和内存,而在PCB的内部会有两个属性:exit_code、exit_signal。当子进程执行完毕时将main函数的返回值写到 exit_code 中,如果出现异常操作系统则将遇到信号所对应的数字编号写到 exit_signal 中。当子进程退出后,操作系统会将这份PCB维护起来,所以就需要通过wait/waitpid这样的系统调用接口将从这份PCB读到的这两个属性以上面那种位图的方式设置到status参数中。
  2. 父进程在wait的时候,如果子进程没退出,那父进程在干什么?在子进程没有退出的时候,父进程只能一直在调用waitpid进行等待——阻塞等待

3.5 非阻塞式等待

waitpid(id,&status,WNOHANG)

  上一小节的 waitpid 方法为阻塞等待,而非阻塞等待与阻塞等待的区别在于第三个参数的不同阻塞等待是在子进程还没有退出的时候父进程只能一直等待直到子进程退出,非阻塞等待是子进程还没有退出时,父进程可以干一些其他事情而不是什么事情不干就在等待子进程退出。
  下面这段代码将通过非阻塞的形式让父进程在还未等待到子进程的退出信息的时候去执行其他事情。

#define TASK_NUM 10
void sync_disk()
{printf("这是一个刷新数据的任务!\n");
}
void sync_log()
{printf("这是一个同步日志的任务!\n");
}
void net_send()
{printf("这是一个进行网络发送的任务!\n");
}
typedef void (*func_t)();
func_t other_task[TASK_NUM] = {NULL};  //函数指针数组int LoadTask(func_t func)
{int i = 0;for(; i < TASK_NUM; i++){if(other_task[i] == NULL) break;}if(i == TASK_NUM) return -1;else other_task[i] = func;return 0;
}
void InitTask()
{for(int i = 0; i < TASK_NUM; i++) other_task[i] = NULL;LoadTask(sync_disk);LoadTask(sync_log);LoadTask(net_send);
}
void RunTask()
{for(int i = 0; i < TASK_NUM; i++){if(other_task[i] == NULL) continue;other_task[i]();}
}
int main()
{pid_t id=fork();if(id==0){//子进程int cnt=5;while(cnt--){printf("我是子进程,我现在活着呢,我离死亡还有%d秒,pid:%d,ppid:%d\n",cnt,getpid(),getppid());sleep(1);}_exit(123);}InitTask();while(1){int status=0;pid_t ret_id=waitpid(id,&status,WNOHANG);if(ret_id==-1){printf("等待错误!\n");break;}else if(ret_id==0){//子进程还未退出,父进程执行RunTask函数RunTask();sleep(1);}else{if(WIFEXITED(status))//正常退出{printf("我是父进程,等待子进程成功,pid:%d,ppid:%d,status signal:%d,status code:%d\n",getpid(),getppid(),(status&0x7F),WEXITSTATUS(status));}else//非正常退出printf("我是父进程,等待子进程成功,pid:%d,ppid:%d,status signal:%d,status code:%d\n",getpid(),getppid(),(status&0x7F),((status>>8)&0xff));break;}}return 0;
}

在子进程正常退出并且父进程等待成功的时候可以通过宏的方式来获取子进程的退出码,之前的方法优雅度或者可扩展性都不太好,当WIFEXITED(status)为真的时候,通过WEXITSTATUS(status)获取退出码,若不为真也就是异常退出时只能使用以前的方法。


总结

总结:

  本文深入探讨了操作系统中进程管理的三个核心方面:进程的创建、退出和等待。首先,我们了解了进程创建的过程,它涉及到操作系统如何为新进程分配必要的资源,包括内存空间和处理器时间,并初始化进程表以跟踪和管理进程状态。接着,我们讨论了进程退出的不同方式,如正常退出、异常退出以及由于接收到信号导致的退出,每种方式都对系统稳定性和资源管理产生不同的影响。
  最后,我们详细分析了进程等待的概念,即一个进程可能需要暂停执行,直到满足特定条件。这可能包括等待I/O操作完成、等待获取资源或等待其他进程的结束。文章强调了实现有效等待机制的重要性,并指出了同步和通信在确保系统资源合理利用和进程间顺畅协作中的关键作用。
  通过这篇博客,我们不仅学习了关于进程操作的基本知识,还加深了对于操作系统内部机制如何协同工作的理解。这些内容为我们进一步研究计算机科学的其他领域打下了坚实的基础。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/web/38391.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

常见反爬及应对

一&#xff0c;特殊混淆的还原 1.1 还原 AAEncode 与 JJEncode AAEncode是一种JavaScript代码混淆算法&#xff0c;利用它&#xff0c;可以将代码转换成 颜文字 表示的JavaScript代码。 去掉代码最后的 (‘‘)&#xff0c;这是函数的自调用&#xff0c;去除后就是函数的声明…

【揭秘】国内十大顶尖AI大模型,引领智能科技新纪元

大模型大模型通常指的是参数量非常大、数据量也非常大的深度学习模型。这些模型由数百万到数十亿甚至更多的参数组成&#xff0c;需要海量的数据和强大的计算资源进行训练和推理学习的模型。大模型设计的目的在于提高模型的表示能力和性能、应对复杂数据集和任务、提升泛化能力…

嵌入式学习——硬件(Linux系统在2440上的启动)——day57

1. Linux2.6系统在s3c2440上的启动过程分三个阶段 1.1 启动u-boot 1.2 启动Linux内核 1.3 挂载根文件系统 2. bootloader 2.1 定义 bootloader的本质是一个裸机程序&#xff0c;bootlood专门是为了能够正确地启动linux操作系 统&#xff0c;在系统初上电时需要对系统做一些…

BK145FRC10HSK、BK165FRC10HSK电液比例开环控制变量泵放大器

BK15FRC10HAK、BK35FRC10HAK、BK45FRC10HAK、BK55FRC10HAK、BK70FRC10HSK、BK80FRC10HSK、BK90FRC10HSK、BK100FRC10HSK、BK120FRC10HSK、BK145FRC10HSK、BK165FRC10HSK、BK180FRC10HSK电液比例开环控制柱塞泵主要是在传统的液压泵基础上&#xff0c;增加了电液比例控制先导阀。…

从零开始实现大语言模型(二):文本数据处理

1. 前言 神经网络不能直接处理自然语言文本&#xff0c;文本数据处理的核心是做tokenization&#xff0c;将自然语言文本分割成一系列tokens。 本文介绍tokenization的基本原理&#xff0c;OpenAI的GPT系列大语言模型使用的tokenization方法——字节对编码(BPE, byte pair en…

【bug报错已解决】ERROR: Could not find a version that satisfies the requirement

&#x1f3ac; 鸽芷咕&#xff1a;个人主页 &#x1f525; 个人专栏: 《C干货基地》《粉丝福利》 ⛺️生活的理想&#xff0c;就是为了理想的生活! 文章目录 引言一、问题描述1.1 报错示例1.2 报错分析 二、解决方法2.1 方法一2.2 方法二 三、总结 引言 有没有遇到过那种让人…

墨烯的C语言技术栈-C语言基础-003

三.数据类型 1.char // 字符数据型 2.short // 短整型 3.int // 整型 4.long // 长整型 5.long long // 更长的整型 6.float // 单精度浮点数 7.double // 双精度浮点数 为什么写代码? 为了解决生活中的问题 购物,点餐,看电影 为什么有这么多类型呢? 因为说的话都是字符型…

CM-UNet: Hybrid CNN-Mamba UNet for Remote Sensing Image Semantic Segmentation

论文&#xff1a;CM-UNet: Hybrid &#xff1a;CNN-Mamba UNet for Remote Sensing Image Semantic Segmentation 代码&#xff1a;https://github.com/XiaoBuL/CM-UNet Abstrcat: 由于大规模图像尺寸和对象变化&#xff0c;当前基于 CNN 和 Transformer 的遥感图像语义分割方…

openGauss真的比PostgreSQL差了10年?

前不久写了MogDB针对PostgreSQL的兼容性文章&#xff0c;我在文中提到针对PostgreSQL而言&#xff0c;MogDB兼容性还是不错的&#xff0c;其中也给出了其中一个能源客户之前POC的迁移报告数据。 But很快我发现总有人回留言喷我&#xff0c;而且我发现每次喷的这帮人是根本不看文…

2024广州智能音箱展|广州蓝牙耳机展

2024广州智能音箱展|广州蓝牙耳机展 时间&#xff1a;2024年11月29日-12月1日 地点&#xff1a;广州琶洲保利世贸博览馆 【展会简介】 中国是全球最大的音频产品制造基地和消费市场&#xff0c;随着国内外互联网巨头纷纷瞄准音频行业并投入巨资布局AI产品矩阵&#xff0c;音…

pom.xml文件加载后没有变成maven图标

原因&#xff1a; 开启了IDEA的节电模式 现象为&#xff1a; xml会变橙色&#xff0c;yml变粉色&#xff0c;自动提示关闭等 把这个节能模式的勾选给取消掉就可以正常显示了

python提取图片中的文字写入excel文件,并打包为exe可执行文件

python提取图片数据写入excel&#xff0c;并打包为exe可执行文件 1. 以下面的图片为例2. python环境需要的依赖包3. 创建交互式窗口4. 读取文件夹下的所有文件并提取数据5. 提取图片中字段的代码6. 打包代码为exe可执行文件安装打包依赖文件运行打包代码 1. 以下面的图片为例 2…

入门Salesforce:必须掌握的20+基础专业术语!

Salesforce的发展令人印象深刻。在过去的20年中&#xff0c;Salesforce创建了一个由管理员、开发人员、顾问和用户组成的生态系统&#xff0c;不断颠覆创新CRM&#xff0c;促进平等和多样性。 作为初学者&#xff0c;探索Salesforce领域就像学习一门新语言。Salesforce中有着大…

YOLOv8改进 | 卷积模块 | 减少冗余计算和内存访问的PConv【CVPR2023】

秋招面试专栏推荐 &#xff1a;深度学习算法工程师面试问题总结【百面算法工程师】——点击即可跳转 &#x1f4a1;&#x1f4a1;&#x1f4a1;本专栏所有程序均经过测试&#xff0c;可成功执行&#x1f4a1;&#x1f4a1;&#x1f4a1; 专栏目录 &#xff1a;《YOLOv8改进有效…

Vue3详解

vite和webpack区别 vite vite使用原生ES模块进行开发&#xff0c;无需在编译时将所有代码转换为JS打包&#xff0c;从而提供了更快的热更新和自动刷新功能&#xff1b; vite在开发模式下没有打包步骤&#xff0c;而是利用浏览器的ES Module Imports特性实现按需编译&#xff…

Firefox 编译指南2024 Windows10篇- 编译Firefox(三)

1.引言 在成功获取了Firefox源码之后&#xff0c;下一步就是将这些源码编译成一个可执行的浏览器。编译是开发流程中的关键环节&#xff0c;通过编译&#xff0c;我们可以将源代码转换为可执行的程序&#xff0c;测试其功能&#xff0c;并进行必要的优化和调试。 对于像Firef…

git命令含有中文,终端输出中文乱码的问题

目录 1、[当前代码页] 的936 (ANSI/OEM - 简体中文 GBK) 是导致中文乱码的原因 2、这样会导致什么问题呢&#xff1f; (1) 问题一: 【属性】选项的【字体】无法识别自定义文字样式&#xff0c;【默认值】选项可选自定义字体样式&#xff0c;却无法覆盖【属性】选项 (2) 问题…

品牌推广怎么样?掌握正确做法,让品牌大放异彩!

品牌推广对于初创公司来说是一项至关重要的任务。在市场众多品牌中&#xff0c;如何脱颖而出&#xff0c;是每个品牌方都要考虑的问题。 作为一名手工酸奶品牌的创始人&#xff0c;目前全国复制了100多家门店&#xff0c;我来分享下&#xff0c;如何推广&#xff0c;可以让品牌…

通过shell脚本创建MySQl数据库

通过shell脚本创建数据库 #!/bin/bashserverIP10.1.1.196 SERVER_NAMEecho $serverIP | cut -d . -f4cat<<EOF>db.sql drop database if exists ${SERVER_NAME}_scheduler; drop database if exists ${SERVER_NAME}_kms; drop database if exists ${SERVER_NAME}_uim…

Centos7修改yum源

安装好系统后&#xff0c;网络能通信&#xff0c;源也没有配置&#xff0c;但是安装软件失败。 解决办法&#xff1a;配置阿里yum源 # curl -o /etc/yum.repos.d/CentOS-Base.repo https://mirrors.aliyun.com/repo/Centos-7.repo # yum clean all # yum make cache再次安装软…