【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代码后,内核做:
- 分配新的内存块和内核数据结构给子进程
- 将父进程部分数据结构内容拷贝至子进程
- 添加子进程到系统进程列表当中
- fork返回,开始调度器调度
在调用fork函数之后,系统会将父进程的代码拷贝一份给子进程,同时会有两个执行流分别执行父进程和子进程,要注意的是子进程不会去执行fork之前的代码。
1.2 写时拷贝
父子进程代码共享,父子再不写入时,数据也是共享的,当任意一方试图写入,便以写时拷贝的方式各自一份副本。具体见下图:
在修改内容之前,父子进程的在物理内存页的数据、代码指向同一块位置,如子进程对数据进行修改,,那么此时就会发生写时拷贝,在物理内存页重新开辟一块空间将修改后的数据存入其中。
因为在操作系统是不允许空间的浪费,所以不会将父进程的所有代码数据都在物理内存中重新拷贝一份,而是通过写时拷贝的方式在子进程需要使用(修改)数据的时候才会重新开辟空间,它是一种按需申请资源的策略。
1.3 fork常规用法
- 一个父进程希望复制自己,使父子进程同时执行不同的代码段。例如,父进程等待客户端请求,生成子进程来处理请求。
- 一个进程要执行一个不同的程序。例如子进程从fork返回后,调用exec函数。
1.4 fork调用失败的原因
- 系统中有太多的进程
- 实际用户的进程数超过了限制
二、进程退出
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 进程退出的方式
- main函数return。而其他函数的return仅仅代表该函数的返回。对于这种方式来说,进程执行本质是main执行流执行,当main函数执行完时代表着进程也就结束了。
- exit函数退出。exit函数所包含的数字为该进程的退出码,在函数任意位置调用直接使进程退出。
- _exit函数退出。直观感觉上和exit的功能是一样的,但是在一些细节是不一样的。exit函数在退出的时候会自动刷新缓冲区,而_exit函数不会刷新缓冲区。它们两个的关系是一种包含和被包含的关系。从下面这个图可以得到一个暗藏的点:缓冲区不在操作系统内。
三、进程等待
3.1 进程等待的原因
- 之前讲过若子进程先退出,而父进程并没有读取子进程状态,就可能造成‘僵尸进程’的问题,进而造成内存泄漏。
- 进程一旦变成僵尸状态,那就刀枪不入,“杀人不眨眼”的kill -9 也无能为力,因为谁也没有办法杀死一个已经死去的进程。
- 我们为什么要创建子进程,目的就是为了让子进程帮助我们去完成某些事情,关于父进程派给子进程的任务完成的情况,可能我们不会关心完成的对不对,也可能会关心子进程运行完成的结果对还是不对,亦或是否正常退出。
- 避免内存泄漏(必)
- 获取子进程的执行结果。(可能)
关于子进程的退出结果,有三种可能性:
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 :
- pid=-1,等待任意一个子进程,与wait等效。
- pid>0,等待其进程ID与pid相等的子进程。
参数 status:
- WIFEXITED(status):若为正常终止子进程返回的状态,则为真。(查看进程是否是正常退出)
- WEXITSTATUS(status): 若WIFEXITED非零,提取子进程退出码。(查看进程的退出码)
参数 options:
- WNOHANG: 若pid指定的子进程没有结束,则waitpid()函数返回0,不予以等待。若正常结束,则返回该子进程的ID。
3.4 子进程退出状态
- 在 wait 和 waitpid 中,都有一个status参数,该参数是一个输出型参数,由操作系统填充。它的功能是为了获取子进程的退出状态。如果传递NULL,表示不关心子进程的退出状态信息。否则,操作系统会根据该参数,将子进程的退出信息反馈给父进程。
- status不能简单的当作整形来看待,可以当作位图来看待,具体细节如下图(只研究status低16比特位):
- 通过对上图理解,我们应该明白关于子进程的退出状态。如果进程是正常退出,那么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操作导致异常退出,那么此时父进程就会读到对应的退出信号,输出结果为该信号对应的数字。
- 父进程是如何获取子进程的退出状态信息的呢?子进程有自己的PCB、地址空间、页表和内存,而在PCB的内部会有两个属性:exit_code、exit_signal。当子进程执行完毕时将main函数的返回值写到 exit_code 中,如果出现异常操作系统则将遇到信号所对应的数字编号写到 exit_signal 中。当子进程退出后,操作系统会将这份PCB维护起来,所以就需要通过wait/waitpid这样的系统调用接口将从这份PCB读到的这两个属性以上面那种位图的方式设置到status参数中。
- 父进程在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操作完成、等待获取资源或等待其他进程的结束。文章强调了实现有效等待机制的重要性,并指出了同步和通信在确保系统资源合理利用和进程间顺畅协作中的关键作用。
通过这篇博客,我们不仅学习了关于进程操作的基本知识,还加深了对于操作系统内部机制如何协同工作的理解。这些内容为我们进一步研究计算机科学的其他领域打下了坚实的基础。
相关文章:

【Linux从入门到放弃】探究进程如何退出以进程等待的前因后果
🧑💻作者: 情话0.0 📝专栏:《Linux从入门到放弃》 👦个人简介:一名双非编程菜鸟,在这里分享自己的编程学习笔记,欢迎大家的指正与点赞,谢谢! 进…...

QT5 static_cast实现显示类型转换
QT5 static_cast实现显示类型转换,解决信号重载情况...

【ES】--Elasticsearch的翻页详解
目录 一、前言二、from+size浅分页1、from+size导致深度分页问题三、scroll深分页1、scroll原理2、scroll可以返回总计数量四、search_after深分页1、search_after避免深度分页问题一、前言 ES的分页常见的主要有三种方式:from+size浅分页、scroll深分页、search_after分页。…...

3.js - 纹理的重复、偏移、修改中心点、旋转
你瞅啥 上字母 // ts-nocheck // 引入three.js import * as THREE from three // 导入轨道控制器 import { OrbitControls } from three/examples/jsm/controls/OrbitControls // 导入lil.gui import { GUI } from three/examples/jsm/libs/lil-gui.module.min.js // 导入twee…...

RS232隔离器的使用
RS232隔离器在通信系统中扮演着至关重要的角色,其主要作用可以归纳如下: 一、保护通信设备 电气隔离:RS232隔离器通过光电隔离技术,将RS-232接口两端的设备电气完全隔离,从而避免了地线回路电压、浪涌、感应雷击、静电…...

一切为了安全丨2024中国应急(消防)品牌巡展武汉站成功召开!
消防品牌巡展武汉站 6月28日,由中国安全产业协会指导,中国安全产业协会应急创新分会、应急救援产业网联合主办,湖北消防协会协办的“一切为了安全”2024年中国应急(消防)品牌巡展-武汉站成功举办。该巡展旨在展示中国应急(消防&am…...

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

JAVA极简图书管理系统,初识springboot后端项目
前提条件: 具备基础的springboot 知识 Java基础 废话不多说! 创建项目 配置所需环境 将application.properties>application.yml 配置以下环境 数据库连接MySQL 自己创建的数据库名称为book_test server:port: 8080 spring:datasource:url:…...

MySQL 重新初始化实例
1、关闭mysql服务 service mysqld stop 2、清理datadir(本例中指定的是/var/lib/mysql)指定的目录下的文件,将该目录下的所有文件删除或移动至其他位置 cd /var/lib/mysql mv * /opt/mysql_back/ 3、初始化实例 /usr/local/mysql/bin/mysqld --initialize --u…...

VCS编译bug汇总
‘typedef’ is not expected to be used in this contex 注册前少了分号。 Scope resolution error resolution : 声明指针时 不能与类名同名,即 不能声明为adapter. cannot find member "type_id" 忘记注册了 拼接运算符使用 关键要加上1b࿰…...

【2024LLM应用-数据预处理】之如何从PDF,PPT等非结构化数据提取有效信息(结构化数据JSON)?
🥰大家知道吗,之前在给AI大模型"喂数据"的时候,我们往往需要把非结构化数据(比如PDF、PPT、Excel等)自己手动转成结构化的格式,这可真是太累人儿了。🥵 幸好现在有了Unstructured这个神级库,它内置的数据提取函数可以帮我们快速高效地完成这个…...

冯雷老师:618大退货事件分析
近日冯雷老师受邀为某头部电商36名高管进行培训,其中聊到了今年618退货潮的问题。以下内容整理自冯雷老师的部分授课内容。 一、引言 随着电子商务的蓬勃发展,每年的618大促已成为消费者和商家共同关注的焦点。然而,在销售额不断攀升的同时…...

JAVA基础教程DAY0-基础知识
JAVA语言的特点 简单性、面向对象、安全性、跨平台性、支持多线程、分布性 面向对象编程(Object-Oriented Programming,简称OOP)是一种编程范式,它通过将数据和操作这些数据的方法封装在一起,以创建对象的形式来组织代…...

鸿蒙开发Ability Kit(程序访问控制):【安全控件概述】
安全控件概述 安全控件是系统提供的一组系统实现的ArkUI组件,应用集成这类组件就可以实现在用户点击后自动授权,而无需弹窗授权。它们可以作为一种“特殊的按钮”融入应用页面,实现用户点击即许可的设计思路。 相较于动态申请权限的方式&am…...

【信息系统项目管理师】18年~23年案例概念型知识
文章目录 18上18下19上19下20上20下21上21下22年上22年下23年上 18上 请简述 ISO 9000 质量管理的原则 领导作用、 过程方法、 管理的系统方法、 与供方互利的关系、 基于事实的决策方法、 持续改进、 全员参与、 以顾客为关注焦点 概念 国家标准(GB/T 1 9000 2008)对质量的定…...

什么是字符串常量池?如何利用它来节省内存?
字符串常量池是Java中一个非常重要的概念,尤其对于理解内存管理和性能优化至关重要。想象一下,你正在管理一家大型图书馆,每天都有无数读者来借阅书籍。 如果每本书每次借阅都需要重新印刷一本,那么图书馆很快就会陷入混乱&#…...

Selenium自动化测试20条常见异常+处理方案
常见的Selenium异常 以下是所有Selenium WebDriver代码中可能发生的一些常见Selenium异常。 1、ElementClickInterceptedException 由于以某种方式隐藏了接收到click命令的元素,因此无法正确执行Element Click命令。 2、ElementNotInteractableException 即使目…...

verilog将信号和常数拼接起来
正确的拼接 1 s_axis_data_tdata {32b0000_0000_0000_0000_0000_0000_0000_0000,32b0011_1111_1000_0000_0000_0000_0000_0000}; 2 注意,信号的两部分都要用{}花括号括起来 s_axis_data_tdata {{32{1b1}},{32b0100_0000_0000_0000_0000_0000_0000_0000}}; 3…...

OpenSSH远程代码执行漏洞 (CVE-2024-6387)
1. 前言 OpenSSH是一套基于安全外壳(SSH)协议的安全网络实用程序,它提供强大的加密功能以确保隐私和安全的文件传输,使其成为远程服务器管理和安全数据通信的必备工具。 OpenSSH 自 1995 年问世近 20 年来,首次出现了…...

高薪程序员必修课-java并发编程的bug源头
前言 Java并发编程虽然强大,但也容易引发复杂的bug。并发编程的bug主要源自以下几个方面:竞态条件、死锁、内存可见性问题和线程饥饿。了解这些bug的源头及其原理,可以帮助开发者避免和解决这些问题。以下是详细的讲解和相应的示例。 1. 竞态…...

c++:#include 某文件.h底层如何寻找其.cpp实现
在C中,当你编写了一个头文件(如MyLibrary.h)和对应的实现文件(如MyLibrary.cpp)时,其他源文件(如main.cpp)只需要包含头文件(#include "MyLibrary.h"ÿ…...

uniapp中如何进行微信小程序的分包
思路:在uniapp中对微信小程序进行分包,和原生微信小程序进行分包的操作基本上没区别,主要就是在pages.json中进行配置。 如图,我新增了一个包diver-page 此时需要在pages.json中的subPackages数组中新增一项 root代表这个包的根…...

win10下安装PLSQL14连接Oracle数据库
问题背景 在使用Oracle开发过程中,经常会使用工具来连接数据库,方便查询、处理数据。其中有很多工具可以使用,比如dbeaver、plsql等。本文主要介绍在win10环境下,plsql14的安装步骤以及安装过程中遇到的一些问题。 安装步骤及问题…...

高考失利咨询复读,银河补习班客服开挂回复
补习班的客服在高考成绩出来后,需要用专业的知识和足够的耐心来回复各种咨询,聊天宝快捷回复软件,帮助客服开挂回复。 前言 高考成绩出来,几家欢喜几家愁,对于高考失利的学生和家长,找一个靠谱的复读补…...

java 代码块
Java中的代码块主要有三种类型:普通代码块、静态代码块、构造代码块。它们的用途和执行时机各不相同。 普通代码块:在方法内部定义,使用一对大括号{}包围的代码片段。它的作用域限定在大括号内,每当程序执行到该代码块时就会执行其…...

vue中避免多次请求字典接口
vuex缓存所有字典项 背景vuex管理所有字典项调用字典接口处理字典项数据的filter页面中使用字典 背景 每次用到字典都需要通过对应的字典type调用一次字典接口,当一个页面用到字典项很多时,接口请求炒鸡多,会导致接口响应超时。 本篇文章改为…...

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

跨越重洋:在Heroku上配置Pip镜像源的终极指南
🌐 跨越重洋:在Heroku上配置Pip镜像源的终极指南 Heroku是一个支持多种编程语言的云平台即服务(PaaS),它允许开发者部署和管理应用程序。然而,由于Heroku的服务器位于海外,直接使用Python的包管…...

SpringBoot + 虚拟线程,性能炸裂!
一、什么是虚拟线程 虚拟线程是Java19开始增加的一个特性,和Golang的携程类似,一个其它语言早就提供的、且如此实用且好用的功能,作为一个Java开发者,早就已经望眼欲穿了。 二、虚拟线程和普通线程的区别 “虚拟”线程…...

Java Character类
Character是char的包装类 转义序列 Character类的方法...