进程创建
在linux环境下,我们使用系统调用接口 fork()函数,来创建进程!
参数:无参;
返回值:
创建失败,返回负值;
创建成功,对于子进程返回0,对于父进程返回子进程的pid;
fork返回值的认识
可以移步至我的另一篇文章:linux下的进程地址空间----页表
fork创建失败的原因
1、物理内存不够了用了;创建子进程也是需要消耗物理内存的!
2、父进程创建子进程的上限到了,os为了限制用户父进程无限制的创建子进程,通常都会给父进程设置一个"进程上限";
fork函数的应用场景
1、一个父进程希望复制自己,使父子进程同时执行不同的代码段。例如:父进程等待客服端请求,生成子进程来处理请求;
2、一个进程需要执行不同的程序。例如:子进程从fork函数返回后,调用add()接口,父进程去执行sub()接口;
进程终止
进程终止的三种方式
1、程序正常运行,最后结果正确;
2、程序正常运行,最后结果错误;
3、程序异常终止!(比如:出现除以0、对空指针解引用、kill -9 命令杀掉进程等等)这本质上:就是os向该进程发送了一种信号!
进程退出码
进程退出码存在的前提就是程序正常运行完毕!在其他情况下,进程退出码没有意义!
那么什么是进程退出码?
一个程序是从main函数开始的吧,那么终止也是终止与main函数对吧,main函数最后是不是有个return 0;这个0就是进程退出码;
return 0,表示该进程正常运行,且运行结果正确;
return 其他数据,表示该进程正常运行,但是运行结果错误,那么到底是为什么错误呢?作为父进程需要知道原因,我们此时return 的数字就表示这个错误原因,这个数字也是进程退出码!
当然不止main函数的返回值是进程退出码,exit()/_exit()函数的参数都是进程退出码!
我们可以理解为exit()/-exit()//函数的参数等价于return 数字;
return 数字只能在main函数内部进行返回,但是如果我们在main函数以外的地方,遇到了不合理的地方想要提前终止进程(比如利用malloc开辟空间失败,我们就可以提前终止进程),我们就可以利用exit()/_exit(),来提前终止掉该进程!
exit()/_exit()//的参数就相当于main函数的返回值!0:表示正常运行结束,结果运行正确;其他数字表示正常运行结束,结果运行错误!
那么exit()/与_exit()看起来长得很像啊,那么他们有区别吗?
当然有,我们可以通过下面一段测试代码来看待结果:
分析:printf在输出“hello world!”的时候,会首先将字符串发送到“缓冲区”,但是由于字符串末尾没有’\n’,printf后面没有输入语句,printf语句后面没有fflush强制刷新缓冲区!我们并不会立即看到字符串,在休眠2s过后进程调用exit提前终止掉了,也就是程序结束了,程序结束是会刷新缓冲区,也就是说我们会看到“hello world!”,那么结果到底是不是?我们运行一下:
结果的确是这样!
那么如果我们再来利用_exit()函数来提前终止程序呢?
我们会发现什么也没有!!!
嗯?为什么会这样?
首先我们的字符串会被先发送到缓冲区,这是没有问题的?问题是我们利用_exit()提前结束掉进程过后,屏幕上没有出现字符串!但是利用exit()提前结束,确有字符串!
这到底是为什么?
首先我们需要明白,exit()是c语言库函数里面的函数接口,而_exit()不属于c语言,_exit()是系统调用;exit()内部也是通过调用_exit()系统调用来终止进程的,只不过exit()内部更加丰富,在结束进程之前,会做一些工作,比如:执行用户定义的清理
函数、刷新缓冲区、关闭流等;但是_exit()没有这么多前戏,就是纯粹的终止进程!它不会刷新缓存区,因此我们在调用_exit()结束进程的时候,没有刷新缓冲区,直接就结束掉进程了,我们的字符串也就一直停留在缓冲区,没有机会输出到屏幕上!
同时我们的return 数字;准确上来说等价于exit();因此在平常的使用中我们更推荐使用exit();
为什么需要进程退出码?
子进程是被父进程创建的,父进程创建子进程是为了让子进程帮助自己完成某样任务,那么子进程运行结束了,子进程到底把这个任务完成的怎么样?是好?还是坏?作为父进程是需要知道的!进程退出码就是用于告诉父进程它交给子进程完成的任
务完成的怎么样的信息!
父进程再把这个信息报告给我们用户!
但是对于我们用户来说,进程退出码就是一个单纯的数字,我们作为人类只看的懂字符串!因此我们需要一样表来映射进程退出码的对应信息!在c语言库中,给我们提供了一张这样的映射表,我们可以通过strerror()函数来访问这种表!
参数: 错误号;
返回值: 错误号对应的字符串首地址!
通过下面一段程序,我们来输出一下“错误表”中对应的错误信息:
运行结果:
我们可以看到“错误表”里面总共记录了134个错误信息;当然这只是linux环境下的,window环境下可能会不一样!这张“错误表”虽然是c语言库给我们提供的,但是我们没必要一定要遵守这张表,我们可以映射自己的“错误表”;当然这些错误信息最多也就255个;
如何查看进程退出码?
好,现在我知道了什么是进程退出码和为什么要有进程退出码,那么能不能让我们见一见进程退出码?
当然可以:查看进程退出码用命令echo $?
//这个命令是查看最近一次进程运行结束的退出码!
比如现在我们故意将我们的进程退出码设置"特殊"一点,方便我们观察:
当然我们也可以用于产看系统指令的进程退出码,比如:
我们可以用这个进程退出码去查一下c库的“错误表”:
发现ls的进程退出码(2)的确实对应的“no such file or directory”的错误信息,表示ls命令采用了c库的“错误表”;
当然也有一些是命令是不遵循的,比如:
查看c库“错误表”:
我们发现是匹配不上的,说明kill命令并没有采用c库里面的错误信息,而是采用了自己设计的!
进程等待
为什么要进行进程等待?
进程等待是谁等?等谁?
答案是:父进程等,等子进程!
明白了这一点,我们再来谈为什么要进行进程等待?
1、子进程不可能直接就推出了,父进程需要获取子进程的退出码,来获知交给子进程的任务完成的怎么样;
2、因此,子进程在终止过后会进入僵尸状态,以此来让父进程获取子进程的退出码;但是处于僵尸状态有一个弊端:僵尸不死不灭,就算是用kill -9
命令也无法将其杀死!因为它本身就已经终止了,没办法杀死一个已经死去的进程!但是,该进程占据的空间还没有释放,如果没有人来释放掉该空间的话,就会造成内存泄漏!为此父进程在获取到子进程的退出码过后,会顺便将子进程的空间也一并释放了;
什么是进程等待?
知道了为什么需要进行进程等待,我们也就明白了进程等待的本质:
进程等待的本质:获取子进程退出码以此来知晓父进程交代给子进程的任务子进程完成的怎么样,并顺便释放掉子进程的空间!
怎么进行进程等待?
我们可以使用系统调用wait()/waitpid();来实现!
在具体使用之前,我们需要先了解一下这两个系统调用:
pid_t wait(int*status);
linux下包含头文件:sys/types.h和sys/wait.h
参数: 输出型参数用于获取进程退出码和信号(如果程序正常运行结束的话,信号是0);当然如果我们不关心进程的退出码和信号的话,我们可以将其设置为null;
返回值: 等待成功:返回等待进程的pid;等待失败(比如父进程没有子进程),返回-1;
下面我们就先来实际用一下wait()系统调用:
运行结果:
实际上我们不需要使用sleep()函数,当父进程调用wait函数时,父进程也会自动的等待子进程运行完毕!相当于在子进程运行的这段期间,父进程相当于卡在了wait函数内部!这叫做阻塞等待!父进程会被os放入一个等待队列中进行等待子进程的运行结束!
静态图片不好演示父进程等待的这个过程,读者可以自行验证!
注意:wait是处理当前进程中最先进入僵尸状态的子进程;
上面介绍了wait()系统调用,接着我们来介绍一下waitpid()系统调用,waitpid与wait功能相似,wait是处理最先处于僵尸状态的子进程,waitpid是处理指定pid的子进程;
pid_t waitpid(pid_t pid,int*status,int options);
参数: pid:指定等待的子进程;pid=-1时则处理最先处于僵尸状态的子进程;
- status:输出型参数;与wait的status功能一样;
- option:决定父进程在等待的过程中是否可与去做其他事情;
- 0:不可以;wnohang:可以!
返回值::
option=0:
- 等待成功,返回子进程pid;
- 等待失败,返回-1;
option=wnohang:
- 等待成功,返回子进程pid;
- 子进程还没运行结束,返回0;
- 等待失败(子进程不存在),返回-1,
具体用法如下:
现在如果我们关心子进程的退出信息呢?
那么我们的status参数就不能在传空指针了,我们需要传递一个int类型的指针过去,这个int类型的指针必须指向一块有效的空间!相当于:
也就是说status必须指向一个int类型的空间,这个int类型的空间用来存储子进程的退出码+信号信息!
是的!你没听错!这个int空间会被用来存储两种信息!
- int 类型有4个字节对吧!而我们的退出码也就0~255种情况一个字节就能存储起来,我们的信号信息也就几十个情况,也能用一个字节存储起来!
- status不能简单的当作整形来看待,可以当作位图来看待,具体细节如下图(只研究status低16比特位):
那么我们如何拿出到进程退出码和终止信号?
我们可以利用(status指向的整形>>8)&0xff的方式来拿出进程退出码!(status&0x7f)的方式来拿出终止信号;
下面我们来具体演示一下:
我们设置的子进程的退出码是107,如果子进程是正常运行结束的话,那么终止信号也就是0;
那么结果是不是呢?
答案确实是这样?
当然如果程序是异常终止的话,那么信号也是其它数字,这时候在看进程退出码已经没有任何意义了!
(我们这里故意制造一个,子进程除以0的语句,来使子进程异常终止:)
那么父进程是如何获取到子进程的进程退出码的和终止信号的?
在子进程的pcb结构体中,有两个变量:
int exit_code;//用于记录当前进程的退出码; int exit_signal//用于记录当前进程的终止信号;
当子进程进入僵尸状态,也就是子进程被终止掉了!os会根据子进程终止时的退出码和终止信号来填充exit_code和exit_signal;当父进程使用wait()/waitpid()系统调用的时候,os就会将子进程pcb里面的
exit_code和exit_signal存储于status指针指向的int空间中,而status所指向的空间是属于父进程的,父进程也就自然而然的拿到了子进程的退出码和终止信号!
父进程在使用wait()/waitpid()的期间在做什么?
通过上面使用wait()的时候我们发现。即使不用sleep()让父进程,而是直接调用wait()接口,父进程依旧会等待子进程的运行结束,父进程并没有直接越过wait()接口而去执行后面的指令!在子进程运行期间,父进程就像是卡在了wait()函数内部,我们
把这种情况叫做阻塞等待!在子进程运行期间,os会把父进程放入一个阻塞队列中进行等待!在此期间父进程什么也做不了!在进程的pcb结构体中有一个指针指向该pcb的父进程pcb:
当我们的子进程运行结束过后,os会根据子进程parent指向找到该子进程的父进程,然后将其父进程从阻塞队列中唤醒!然后在让父进程来获取子进程的进程退出码和终止信号!最后顺便完成子进程的空间释放!
看到这里,或许我们会疑惑,父进程在使用wait()接口等待的时候就真什么也不做,一直等着子进程,是不是效率有点低了?能不能让父进程在等待的这段时间去做一点别的事情?或者说让父进程每隔一个时间段去看一看子进程运行结束没有,总之就是不要让父进程一直等待着子进程?
当然可以!当然使用wait()接口肯定不行,因为参数太少了!只要父进程已使用wait()接口等待,父进程就会卡在wait()函数内部,直到子进程运行结束!但是我们可以使用waitpid()接口哇!
还记得waitpid()有个参数option吗?当option为0的时候,父进程在等待子进程期间就会进入阻塞队列中进行等待,也就是说父进程会卡在waitpid()内部,知道子进程运行结束!当option=wnohang的时候父进程不会进入阻塞队列中,而是检测一下子进程的状态,当子进程运行结束时waitpid()会返回子进程的pid;当子进程正在运行时waitpid(),返回0;当等待失败时,waitpid()返回-1;至此当父进程使用waitpid()并且option参数设计成wnohang时父进程就不会卡在waitpid()接口内部,不管是那种情况这个“模式”下的waitpid()都是会返回一个值,我们就可以根据这个返回值来设计父进程下一步该做什么?如果返回值是0,说明子进程还在运行,我们可以让父进程去执行一些其他代码,然后循环回来继续接收一下waitpid()的返回值!像这种情况叫做阻塞轮询!
下面我们通过具体的代码来表现一下阻塞轮询:
运行结果:
从运行结果我们可以看出,当我们把option参数设成wnohang过后,父进程没有卡在waitpid()内部,而是和子进程一起在运行!
现在我们再来对比一下,阻塞等待(也就是option参数设计成0即可):
我们可以看到在使用阻塞等待的时候,父进程没有去做其他事情(比如打印乘法口诀表)而是一直等待者子进程的运行结束,当子进程运行结束后,父进程通过waitpid()接收到子进程pid,pid>0不满足while进入的条件!于是直接去执行了printf子进程运行结束的语句!
这就是阻塞等待与阻塞轮询的区别!
总结
以上为个人经验,希望能给大家一个参考,也希望大家多多支持代码网。
发表评论