【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操作完成、等待获取资源或等待其他进程的结束。文章强调了实现有效等待机制的重要性,并指出了同步和通信在确保系统资源合理利用和进程间顺畅协作中的关键作用。
  通过这篇博客,我们不仅学习了关于进程操作的基本知识,还加深了对于操作系统内部机制如何协同工作的理解。这些内容为我们进一步研究计算机科学的其他领域打下了坚实的基础。

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

【C++】STL-priority_queue

目录 1、priority_queue的使用 2、实现没有仿函数的优先级队列 3、实现有仿函数的优先级队列 3.1 仿函数 3.2 真正的优先级队列 3.3 优先级队列放自定义类型 1、priority_queue的使用 priority_queue是优先级队列&#xff0c;是一个容器适配器&#xff0c;不满足先进先出…

pdf拆分,pdf拆分在线使用,pdf拆分多个pdf

在数字化的时代&#xff0c;pdf文件已经成为我们日常办公、学习不可或缺的文档格式。然而&#xff0c;有时候我们可能需要对一个大的pdf文件进行拆分&#xff0c;以方便管理和分享。那么&#xff0c;如何将一个pdf文件拆分成多个pdf呢&#xff1f;本文将为你推荐一种好用的拆分…

监控平台zabbix介绍与部署

目录 1.为什么要做监控 2.zabbix是什么&#xff1f; 3.zabbix 监控原理 4.Zabbix 6.0 新特性 5.Zabbix 6.0 功能组件 6.部署zabbix 6.1 部署 Nginx PHP 环境并测试 6.2 部署数据库 6.3 向数据库导入 zabbix 数据 6.4 编译安装 zabbix Server 服务端 6.5 修改 zabbix…

中小企业如何防止被查盗

在当前的商业环境中&#xff0c;小企业面临诸多挑战&#xff0c;其中之一便是如何在有限的预算内满足日常运营的技术需求。由于正版软件的高昂成本&#xff0c;一些小企业可能会选择使用盗版软件来降低成本。 我们联网之后存在很多风险&#xff0c;你可以打开自己的可以联网的电…

【面试系列】机器学习工程师高频面试题及详细解答

欢迎来到我的博客&#xff0c;很高兴能够在这里和您见面&#xff01;欢迎订阅相关专栏&#xff1a; ⭐️ 全网最全IT互联网公司面试宝典&#xff1a;收集整理全网各大IT互联网公司技术、项目、HR面试真题. ⭐️ AIGC时代的创新与未来&#xff1a;详细讲解AIGC的概念、核心技术、…

Java--创建对象内存分析

1.如图所示&#xff0c;左边为一个主程序&#xff0c;拥有main方法&#xff0c;右边定义了一个Pet类&#xff0c;通过debug不难看出&#xff0c;当启动该方法时&#xff0c;有以下该步骤 1.运行左边的实例化Pet类对象 2.跳转至右边定义Pet类的语句 3.跳回至左边获取Pet类基本属…

代码生成器使用指南,JeecgBoot低代码平台

JeecgBoot 提供强大的代码生成器&#xff0c;让前后端代码一键生成&#xff0c;实现低代码开发。支持单表、树列表、一对多、一对一等数据模型&#xff0c;增删改查功能一键生成&#xff0c;菜单配置直接使用。 同时提供强大模板机制&#xff0c;支持自定义模板&#xff0c;目…

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

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

JSON JOLT常用示例整理

JSON JOLT常用示例整理 1、什么是jolt Jolt是用Java编写的JSON到JSON转换库&#xff0c;其中指示如何转换的"specification"本身就是一个JSON文档。以下文档中&#xff0c;我统一以 Spec 代替如何转换的"specification"json文档。以LHS(left hand side)代…

kaggle量化赛金牌方案(第七名解决方案)

获奖文章(第七名解决方案) 致谢 我要感谢 Optiver 和 Kaggle 组织了这次比赛。这个挑战提出了一个在金融市场时间序列预测领域中具有重大和复杂性的问题。 方法论 我的方法结合了 LightGBM 和神经网络模型,对神经网络进行了最少的特征工程。目标是结合这些模型以降低最终…

arco disign vue 日期组件的样式穿透

问题描述: 对日期组件进行样式穿透. 原因分析: 如图,日期组件被展开时它默认将dom元素挂载到body下, 我们的页面在idroot的div 里层, 里层想要穿透外层是万万行不通的. 解决问题: 其实官网提供了参数,但是并没有提供例子, 只能自己摸索着过河. 对于日期组件穿透样式,我们能…

收集了很久的全网好用的磁力搜索站列表分享

之前找资源的时候&#xff0c;收集了一波国内外大部分主流的磁力链接搜索站点。每一个站可能都有对应的优缺点&#xff0c;多试试&#xff0c;就能知道自己要哪个了。 全网好用的磁力链接 大部分的时候&#xff0c;我们用国内的就可以了&#xff0c;速度块&#xff0c;而且不…

Snappy使用

Snappy使用 Snappy是谷歌开源的压缩和解压的开发包&#xff0c;目标在于实现高速的压缩而不是最大的压缩 项目地址&#xff1a;GitHub - google/snappy&#xff1a;快速压缩器/解压缩器 Cmake版本升级 该项目需要比较新的cmake&#xff0c;CMake 3.16.3 or higher is requi…

51单片机第23步_定时器1工作在模式0(13位定时器)

重点学习51单片机定时器1工作在模式0的应用。 在51单片机中&#xff0c;定时器1工作在模式0&#xff0c;它和定时器0一样&#xff0c;TL1占低5位&#xff0c;TH1占高8位&#xff0c;合计13位&#xff0c;也是向上计数。 1、定时器1工作在模式0 1)、定时器1工作在模式0的框图…

8619 公约公倍

这个问题可以通过计算最大公约数 (GCD) 和最小公倍数 (LCM) 来解决。我们需要找到一个整数&#xff0c;它是 a, b, c 的 GCD 的倍数&#xff0c;同时也是 d, e, f 的 LCM 的约数。 以下是解决这个问题的步骤&#xff1a; 1. 计算 a, b, c 的最大公约数。 2. 计算 d, e, f 的最…

流处理系统对比:RisingWave vs ksqlDB

本文将从架构、部署与可扩展性、Source 和 Sink、生态系统与开发者工具几个方面比较 ksqlDB 和 RisingWave 这两款领先的流处理系统。 1. 架构 ksqlDB 是由 Confluent 开发和维护的流处理 SQL 引擎&#xff0c;专为 Apache Kafka 设计。它基于 Kafka Streams 构建&#xff0c;…

鸿蒙:路由Router原理

页面路由&#xff1a;在应用程序中实现不同页面之间的跳转和数据传递 典型应用&#xff1a;商品信息返回、订单等多页面跳转 页面栈最大容量为32个页面&#xff0c;当页面需要销毁可以使用router.clear()方法清空页面栈 router有两种页面跳转模式&#xff1a; router.pushUrl…

Golang 开发实战day15 - Input info

&#x1f3c6;个人专栏 &#x1f93a; leetcode &#x1f9d7; Leetcode Prime &#x1f3c7; Golang20天教程 &#x1f6b4;‍♂️ Java问题收集园地 &#x1f334; 成长感悟 欢迎大家观看&#xff0c;不执着于追求顶峰&#xff0c;只享受探索过程 Golang 开发实战day15 - 用户…

02归并排序——分治递归

02_归并排序_——分治_递归_ #include <stdio.h>void merge(int arr[], int l, int m, int r) {int n1 m -l 1;int n2 r -m;//创建临时数组int L[n1], R[n2];for(int i 0; i < n1; i){L[i] arr[l i];}for(int j 0; j < n2; j){R[j] arr[m 1 j];}int i …

OpenSSH RCE (CVE-2024-6387) | 附poc | 小试

Ⅰ 漏洞描述 OpenSSH 远程代码执行漏洞(CVE-2024-6387)&#xff0c;该漏洞是由于OpenSSH服务器 (sshd) 中的信号处理程序竞争问题&#xff0c;未经身份验证的攻击者可以利用此漏洞在Linux系统上以root身份执行任意代码。 Ⅱ 影响范围 8.5p1 < OpenSSH < 9.8p1 但OpenSS…