这篇博客是对于进程控制的完整篇,有进程创建、进程中止、进程等待、进程程序替换的相关概念理解和图片的形式进行的测试演示便于理解。
一、进程创建
- 进程创建有以下几种情况:
(1) ./ cmd 将程序运行起来,就创建了一个进程。
(2) fork()创建进程。
fork函数
写时拷贝的理解:
在数据第一次写入到某个存储位置时,首先将原有内容拷贝出来,写到另一位置处,
然后再将数据写入到存储设备中,该技术只拷贝在拷贝初始化开始之后修改过的数据。
注意:
创建进程是有成本的所以进程创建也可能失败。
体现在时间和空间上,每个进程都会伴随的创建PCB和mm_struct等。 - 进程=程序 + 进程PCB + 程序地址空间mm_struct + 页表。
- 子进程在父进程fork之后,开始顺序执行之下的代码。fork()之前的代码对于子进程也是可见的,只是不执行。都是按照顺序执行以下的代码的。
缺页中断:在父子进程对于共享空间(只读权限)发生修改时,OS先缺页中断一下,然后进行写时拷贝,等再将数据写入到存储设备中,该技术只拷贝在初始化开始之后修改过的数据上。
然后你再改变值,这样就保证了进程的独立性。
二、进程中止
1. 进程退出场景
-
进程退出的场景有进程正常结束结果对,不对和代码异常中止。
-
为什么main函数总会加上 return 0?
内个return 0
的0 就是进程退出码,根据返回值判断执行情况,衡量对错。
main函数的返回值实际上是进程的退出码
echo $?
是输出最近一次进程退出时的退出码。(注意,我们输入的指令本身也是一个进程,当然他也有进程退出码。) -
结果表示:
0:表示success
!0: failed。
那为什么会有退出码的区别呢?直接告诉对错就行呗。
答:原因是更想知道为什么不正确?有多种可能。所以用数字充当一种可能性。
man 3 strerro
:将错误数字转化为错误信息。
如下图显示,每一个数字都对应着一个失败的原因。我们注意到,这个原因也是有上限的。
2. 进程中止
VS中是程序崩溃 ,返回值是136(unknowed)(未知错误).
而一旦程序崩溃,退出码就变得没有意义了。就像作弊之后成绩就没有意义了。
3. 进程退出方式
我们知道:
- main函数中的return代表进程退出,非main函数叫做函数返回。
- exit(12);//进程中止码设置为12
exit在函数任意地方调用,都代表整个进程中止,参数是进程退出码。 - _exit();
我们知道\n
作用是将缓冲区里面的消息立即刷新出来
而我们用 exit和 return,却没有立即打印出来。 因为数据是被暂时保存在输出缓冲区中,exit(EXIT_SUCCESS)
或者main return
本身就会要求系统进行缓冲区刷新。
而_exit(12)
中止进程时,强制终止进程,不要进行进程的后续收尾工作,比如刷新缓冲区(用户级缓冲区)等操作。 但是不刷新缓冲区不等价于不释放空间,所以不会造成内存泄漏。 - 如下图所示:
4. 进程退出的作用
作用就是让OS层面上少了一个进程,free掉PCB ,free掉mm_struct,页表和各种和映射关系代码+数据。
三、进程等待
1. 是什么?
子进程创建的目的是为了帮助父进程完成某种任务,而这种任务需要让父进程fork之后,等待子进程退出,得到任务结果。进程等待需要通过wait/waitpid
等待子进程退出,获取相应结果。
2. 为什么让父进程等待呢?
(1) 通过获取子进程退出的信息,能够得知子进程执行的结果。
(2) 可以保证:时序问题,子进程先退出父进程后退出,父进程得活过子进程,才有意义。
(3) 进程退出的时候先进入僵尸状态(已经死掉了其实),进而会造成内存泄漏的问题,所以需要通过父进程wait释放该子进程占用的空间。
3. 如何进程等待?
- wait()所要包含的头文件是
#include<sys/types.h>
#include<sys/wait.h>
2 .进程等待的例子:
僵尸进程回收的演示
4. waitpid()介绍和简单使用
(1).第一个参数是进程pid
另外介绍一下另一个接口waitpid();以及一下两条常用命令的演示
先不考虑后两个参数,后面会做详细解释。
pid_t ret = waitpid(id,NULL,0);//等待指定一个进程pid 后两个先不管新
pid_t ret = waitpid(-1,NULL,0);//等待任意一个进程,等价于wait.
(2)第二个参数status(得到退出信息)
我们会想
如何获取子进程的退出信息?
就是status,waitpid的第二个参数
waitpid是系统调用函数,给函数传入一个参数,获得参数相关参数,要用status,只能是传指针。
int status=0;
pid_t ret = waitpid(id,&status,0);
父进程拿到什么status结果,一定和子进程如何退出强相关。
- 子进程退出的话题,就是刚刚讲过的进程退出,就3种情况。
最终一定要让父进程通过status得到子进程的运行结果(可能的三种结果,用退出码分辨)。 - 三种结果是退出码来进行分辨的,外加上是否收到信号,
- 如果一个进程出现异常问题,就会导致自己收到了某种信号,如果没有信号就说运行正常。
- 所以,我们对于status的认识不能简单的认为是int类型,
对于status
的构成,不能简单只认为是int类型(示意图)
32个比特位:只使用低16个比特位,高16比特位暂时不用。
次低8位,代表进程退出时的退出码,低0~7位代表信号,如果是0就说明是正常中止的,也就是没有接收到信号。
如何获取退出码呢?
将status的次低8位右移八位,将0~7位覆盖,然后和 1111 1111 (oxFF)按位与操作获得子进程退出码。
如何获得信号呢?
也就是后7位,直接按位与 0111 1111(0x7F)
waitpid(& status); status的正常中止和异常中止的测试代码和演示如下:
bash是命令行启动的所有进程的父进程。
bash一定是通过wait方式得到子进程的退出结果,所以我们能看到echo $?查到子进程的退出码。
直接用下面的宏就可以获得子进程的退出信息。
为了不用在获取子进程退出码和信号时的位操作运算来检查是否正确结束,提供了 WEXITSTATUS(status)
这个宏。
同样可以实现僵尸进程的回收(不做演示)
总结如何理解waitpid
如图所示:
(3) 第三个参数options(阻塞等待和非阻塞等待)
waitpid(ID,&status,0)
,这个0就是默认行为是阻塞等待,父进程只等着子进程结束再做处理。
WNOHANG:
设置等待凡是是非阻塞等待。
举个例子进行理解一下:
例1.
(1) 等待女朋友下楼,设么都不干,你干等着就是阻塞等待。
(2) 每隔几分钟催一次,达到轮询检测的目的。就叫非阻塞等待,你并不会因为等待卡主自己的行为
这种就是基于非阻塞状态的轮询访问。
非阻塞等待:检测运行状态,完成一次等待的状态可能需要多次检测,基于非阻塞状态的轮询方案。
例2.
操作系统层面,父进程等待子进程。二者都是等待的一种方式
提问理解
**谁等?**父进程
**等什么?**子进程,子进程退出是一种事件。waitpid()来等待
阻塞是不是意味着父进程不被调用执行了呢?
进程是怎么等待的呢?
等待队列当中,将R状态转化为s状态,当子进程完成之后,将父进程从等待队列中拿回运行队列中。 返回的本质就是S->R,从而被CPU调度。
阻塞的本质是,本质是父进程的PCB放入等待队列,并将进程的状态转换为S状态。 所以这就是为什么某一阻塞等待时,上层应用就卡住了。
就叫做应用或者程序hang住了。
WNOHANG
:非阻塞
(1). 子进程根本就没退出。
(2) 子进程退出,waitpid(调出成功或者失败)。
四、进程程序替换
1. 程序替换
创建子进程的目的是啥?
if-else语句实现让子进程执行父进程的一部分代码。
如果想让子进程执行一个全新的程序呢?
采用进程的程序替换。
程序替换的原理:
进程不变,仅仅替换当前进程的代码和数据的技术叫做进程的程序替换。图示如下:
程序替换的理解
程序本质就是文件 = 程序代码 + 程序数据
用一个老进程的壳子执行了新的代码和数据,代码替换的时候,没有创建新的程序进程。
简单的将磁盘中的数据加载到物理内存中间,不改变前边虚拟内存的映射关系。
数据代码如何加载到内存?
- 其中一个接口 execl();演示如下:
程序替换的本质是不是就是把指定的程序的代码和数据加载到特定进程的上下文中!!
C/C++程序要运行,必须加载到内存中。
-
如何加载呢?通过加载器!-> exec*程序替换函数。
-
子进程在进行程序替换时,父进程干自己的事情,为什么没有受影响呢?
进程具有独立性。
-
虽然父子进程的代码是共享的,但是当发生,
程序替换时,就会更改代码区的代码,也会发生写时拷贝。
只要程序替换成功,就不会执行后续代码,意味着exec *
系列函数成功的时候,
不需要返回值检测,如果返回了,就说明调用失败了。
所以程序如何加载到内存呢?用加载器
2. 六种程序替换函数的解析
execl(path ,*arg, ……);
execl("要执行文件的路径","命令参数列表");
// 你的要执行的目标程序的全路径,所在路径/文件名;
// 要执行的目标程序在命令行上怎么执行,这里的参数就怎么一个一个的传递进去;
// …… :可变参数列表(C语言深度解剖),必须以NULL作为参数传递的结束。
只要execl返回了,必定是出错了。
execv(路径,数组指针);//数组中就是各种可变参数列表的参数
3 .
execlp("要执行的文件名字","命令参数列表");
自动在环境变量PATH里面找到文件
execvp(文件名字,数组指针);
execle(路径,数组指针,环境指针);//环境自己传就行
为了便于演示,我们先要理解一下操作:
注:这里插入,我们声明两个文件 myload.c myexe.c
1. 当我们想要make 同时编译执行两个文件应该如何配置Makefile?
2.如图文字
3. 用execle(),在myload.c文件中执行myexe.c,并将环境变量插入到myexe中。
execve(路径,可变参数列表的数组指针,环境变量指针);
有了这个接口这样就可以实现多种语言程序的同时编译执行。
下图演示C语言程序调用Python语言的文件执行
为什么有这么多接口?
是为了满足不同的应用场景。
没有本质区别,只是参数不同而已。其实就一个execve(),其他都是对于这个的封装。