【linux】linux进程控制(二)(进程等待wait/waitpid)
小编个人主页详情<—请点击
小编个人gitee代码仓库<—请点击
linux系列专栏<—请点击
倘若命中无此运,孤身亦可登昆仑,送给屏幕面前的读者朋友们和小编自己!
目录
前言
【linux】linux进程控制(一)(fork进程创建,exit进程终止)——书接上文 详情请点击<——
本文由小编为大家介绍——【linux】linux进程控制(二)(进程等待)
本文与小编编写的【linux】linux进程概念(三)(进程状态,僵尸进程,孤儿进程,进程优先级)关联性较大,建议读者友友阅读后方蓝字链接的文章之后,基于后方蓝字链接文章的基础上再来学习本文文章详情请点击<——
一、进程等待的重要性
- 我们知道,子进程退出,如果父进程对此漠不关心,那么就会造成僵尸进程,进而造成内存泄露的问题
- 一旦一个进程变成僵尸进程,那么这个进程虽然死亡,但是它的内心有了执念,它非要让它的父进程等待回收它,甚至连kill -9都没有办法杀死这个僵尸进程,因为谁也没办法杀死一个已经死亡的进程
- 父进程派给子进程的任务完成的如何,我们需要知道,是否发生异常,代码是否跑完,如果代码跑完,那么结果对不对,如果不对,那么是因为什么原因造成的不对
- 如果想要处理僵尸进程,需要让父进程通过进程等待的方式,回收子进程的资源,获得子进程的退出信息
二、进程等待三个问题
进程等待是什么
- 通过系统调用wait/waitpid,来对子进程进行状态检测(子进程是否退出,如果退出了进程状态就为Z,为Z状态之后此时进行回收)与回收(通过进程等待,回收子进程的资源,获得子进程的退出信息,并且将子进程的状态从Z状态设置为X死亡状态)的功能
为什么要有进程等待
- 僵尸进程无法被杀死,需要通过进程等待的方式杀死它,进而解决内存泄漏的问题(必须解决的)
- 我们要通过进程等待,获取子进程的退出情况,进而得知我布置给子进程的任务,它完成的怎么样了,我们可以关心子进程的退出情况,也可以不关心子进程的退出情况,这个是可选的(虽然我们有的情况下我们不需要关心子进程的退出情况,但是有的情况下我们需要关心子进程的退出情况,所以为了满足我们的使用需求,操作系统必须给我们提供这个获取子进程的退出情况的功能,我们可以不使用这个功能)
进程等待是怎么做的
- 父进程通过调用wait/waitpid进行僵尸进程的回收问题
三、先看僵尸进程现象
- 那么小编设计一个fork创建子进程的程序,其中子进程会循环间隔1秒打印信息,共3秒,即共打印3次,打印完成后子进程会进行退出,与此同时父进程会一直进行死循环间隔1秒打印
- 运行程序,3秒前,父进程和子进程会悠哉游哉的打印信息,但是当3秒过后,父进程会一直执行打印而无暇等待并回收子进程,那么子进程此时就变成了僵尸进程,并且此时子进程的状态就变成了Z僵尸状态
- 那么在这个过程中,我们将使用脚本去监控进程的状态,脚本代码如下
//脚本代码while :; do ps ajx | head -1 && ps ajx | grep testWait | grep -v grep; sleep 1; echo \"-----------------------------\"; done
#include #include #include #include #include #include void RunChild(){ int cnt = 3; while(cnt) { printf(\"i am child process, pid: %d, ppid: %d, cnt: %d\\n\", getpid(), getppid(), cnt); cnt--; sleep(1); }}int main(){ pid_t id = fork(); if(id < 0)//创建子进程失败情况判断 { perror(\"fork\");//将错误码errno中对应的错误码转化为对应的字符串描述信息 return 1; //并且附带打印\"fork\"这个字符串,再返回退出码1表示错误 } else if(id == 0) { RunChild(); exit(0);//在其他程序中,由于子进程和父进程代码共享所以子进程和父进程分流之后,我们不期望子进程执行父进程 //的那一部分代码,为了安全起见,我们不期望子进程执行后续代码 }//所以这里当子进程执行到这里的时候使用exit终止子进程 else { while(1) { printf(\"i am father process, pid: %d, ppid: %d\\n\", getpid(), getppid()); sleep(1); } } return 0;}
运行结果如下
- 当子进程退出,父进程不去等待子进程,那么子进程就会变成僵尸进程,造成内存泄漏,那么我们应该如何解决呢?那么就是父进程使用wait/waitpid去等待子进程,下面小编先进行介绍wait
- 在进行讲解上述wait的前,我们还应该认识到一个事情,当前子进程的pid是8632,其ppid是8631,这个8631就是父进程,父进程的pid就是8631,同时父进程的ppid是31952,其中31952就是父进程的父进程,那么我们grep过滤一下,查看发现这个货是bash,即bash是当前父进程的父进程,即在命令行中启动的进程是bash的子进程,同时当前父进程的子进程和bash就构成了爷孙关系
四、wait
概念讲解
- 在man的2号手册我们可以查找到关于wait和waitpid的系统调用的说明,在使用这个wait和waitpid的系统调用的时候,我们应该包头文件#include 以及#include
- 由于wait的参数status和waitpid放在一块进行讲解比较好,所以关于waitpid以及wait的参数status小编会在后面waitpid的讲解中一并进行讲解,wait的参数是一个int*指针类型的,小编在使用的时候wait会传NULL进行调用wait等待进程
- wait的作用是等待当前进程的任意一个子进程的状态改变,即检测并等待进程状态变成Z僵尸状态,释放进程的资源,获取进程退出信息
- wait的返回值是一个整数,如果wait等待子进程退出成功,那么会返回子进程的pid,由于子进程的pid是大于0的,所以我们可以判断wait的返回值是否大于0,进而判断是否等待子进程成功,同时我们可以使用wait的返回值是否等于fork的返回值,因为fork给父进程返回的是子进程的pid,所以对于创建一个子进程的情况我们可以采用wait的返回值是否等于fork的返回值进行判断
- 如果当前进程根本没有fork创建子进程,即根本没有子进程,那么wait等待个鸡毛子进程?所以这种情况下wait会等待子进程失败,wait返回-1
wait的使用
- 那么下面小编带领大家实际的去使用这个wait让父进程去等待子进程的退出
- 如下程序,子进程会循环间隔1秒打印信息,共2秒,即共打印2次,打印完成后子进程会进行退出,与此同时父进程并行的会休眠4秒,所以当子进程打印完成之后,父进程休眠的4秒还剩余2秒,在这2秒期间,父进程并不会等待子进程,所以此时子进程变成僵尸进程,当父进程休眠完成这剩余的2秒后,小编让父进程调用了wait去等待子进程,那么此时父进程就会检测子进程状态是否为Z僵尸状态,等待子进程退出,由于子进程已经退出,并且状态为Z僵尸状态,所以父进程就会回收子进程,释放子进程资源,这里小编并没有获取子进程的退出信息,同时还会将子进程的状态调整为X状态,表示子进程死亡,当子进程状态为X之后一瞬间子进程就消失返回了(由于X状态太快了,所以我们并不会观察到)
- 那么为了便于观察父进程等待子进程成功,子进程消失的场景,只有父进程自己运行的场景,小编特地的将父进程等待完成子进程之后再休眠2秒便于我们观察
#include #include #include #include #include #include void RunChild(){ int cnt = 2; while(cnt) { printf(\"i am child process, pid: %d, ppid: %d, cnt: %d\\n\", getpid(), getppid(), cnt); cnt--; sleep(1); }}int main(){ pid_t id = fork(); if(id < 0) { perror(\"fork\"); return 1; } else if(id == 0) { RunChild(); exit(0); } else { //id > 0 sleep(4); int ret = wait(NULL); if(ret == id) { printf(\"我是父进程,我等待子进程成功了, 子进程的pid: %d\\n\", ret); } else { printf(\"等待子进程失败\\n\"); } sleep(2); } return 0;}
运行结果如下
我们可以观察到前2秒子进程和父进程都在运行,然后过了两秒,子进程退出,由于父进程此时在休眠,无暇等待子进程,所以子进程此时会成为僵尸进程,再过两秒,父进程休眠完成,此时等待子进程成功,那么此时子进程的资源就被释放了,子进程被回收,父进程等待子进程完成后,小编将父进程设置成休眠2秒,所以此时会观察到只有父进程在运行的场景
阻塞状态
- 但是当子进程一直不退出,而此时父进程在调用wait等待子进程的时候,那么我们在父进程中调用的wait将会一直检测子进程的状态,进而等待子进程直至子进程退出,那么此时父进程就不会去执行其它代码,而是在系统调用wait中一直进行检测子进程的的状态,检测子进程的状态是否为Z僵尸状态,子进程是否退出,如果子进程一直不退,那么wait就被一直执行,wait一直不返回,此时父进程的状态就会变成阻塞状态,父进程就会阻塞等待而不去执行其他代码了,后面我们可以使用waitpid的非阻塞轮询来巧妙的避免这个问题
- 那么为了演示,小编将子进程执行的RunChild修改一下,让它直接死循环打印,当子进程调用它的时候就会一直死循环打印进程信息,同时父进程就一直使用wait等待子进程退出即可
#include #include #include #include #include #include void RunChild(){ int cnt = 2; while(1) { printf(\"i am child process, pid: %d, ppid: %d, cnt: %d\\n\", getpid(), getppid(), cnt); cnt--; sleep(1); }}int main(){ pid_t id = fork(); if(id < 0) { perror(\"fork\"); return 1; } else if(id == 0) { RunChild(); exit(0); } else { //id > 0 //sleep(4); int ret = wait(NULL); if(ret == id) { printf(\"我是父进程,我等待子进程成功了, 子进程的pid: %d\\n\", ret); } else { printf(\"等待子进程失败\\n\"); } sleep(2); } return 0;}
运行结果如下
- 结果就是父进程永远也等不到永远不会退出的子进程,此时父进程的状态就是阻塞状态,即阻塞等待,那么此时操作系统就会将父进程从CPU的运行队列中拿出,CPU不会调度父进程了,父进程的PCB就会去子进程的PCB中的队列中去排队,去等待子进程的状态,也就是子进程退出,状态为Z僵尸状态
- 我们还可以得出,原来进程阻塞不光是等待硬件设备就绪,比如scanf等待硬件资源键盘的输入,同时进程阻塞还可以等待软件资源就绪,即这里的父进程等待子进程的状态就绪,也就是子进程退出,状态为Z僵尸状态
五、waitpid
概念讲解
- 同样的,我们也使用man去查看二号手册的waitpid,包对应的头文件进行使用#include 以及#include
- waitpid可以说是wait的plus版本,waitpid可以达到和wait的作用,waitpid的参数有三个,第一个参数如果我们传入-1,那么它就可以等待当前进程的任意一个子进程之外,如果第一个参数我们传入当前父进程的子进程的pid,那么它还可以等待指定为pid子进程
- waitpid的第二个参数是一个指针,也就是一个输出型参数,它可以获取子进程的退出信息,开始演示的时候,小编会将其传入NULL,表示我不想获取子进程的退出信息,后续演示小编会讲解如何获取子进程的退出信息
- waitpid第三个参数传入0可以进行类似于wait的阻塞等待之外,第三个参数如果传入WNOHANG还可以进行非阻塞等待,非阻塞等待加上循环还可以实现非阻塞轮询,单纯的非阻塞轮询没有意义,非阻塞轮询加上父进程做自己的事情才最有意义
- 如果waitpid等待子进程成功,同样会返回子进程的pid,如果waitpid等待子进程的pid子进程根本不是当前进程的子进程,是其它进程,那么表示等待子进程失败,waitpid返回-1
waitpid的使用
- 我们在父进程中使用waitpid等待子进程退出,其中第一个参数传入-1表示等待当前父进程的任意一个子进程退出,第二个参数传入NULL,表示不想获取子进程的退出信息,第三个参数传入0,表示进行和wait进行一样的阻塞等待模式
- 同样的我们也使用shell脚本进行检测进程状态
//脚本代码while :; do ps ajx | head -1 && ps ajx | grep testWait | grep -v grep; sleep 1; echo \"-----------------------------\"; done
#include #include #include #include #include #include void RunChild(){ int cnt = 2; while(cnt) { printf(\"i am child process, pid: %d, ppid: %d, cnt: %d\\n\", getpid(), getppid(), cnt); cnt--; sleep(1); }}int main(){ pid_t id = fork(); if(id < 0) { perror(\"fork\"); return 1; } else if(id == 0) { RunChild(); exit(0); } else { //id > 0 sleep(4) int ret = waitpid(-1, NULL, 0); if(ret == id) { printf(\"我是父进程,我等待子进程成功了, 子进程的pid: %d\\n\", ret); } else { printf(\"等待子进程失败\\n\"); } sleep(2); } return 0;}
运行结果如下
我们可以观察到前2秒子进程和父进程都在运行,然后过了两秒,子进程退出,由于父进程此时在休眠,无暇等待子进程,所以子进程此时会成为僵尸进程,再过两秒,父进程休眠完成,此时等待子进程成功,那么此时子进程的资源就被释放了,子进程被回收,父进程等待子进程完成后,小编将父进程设置成休眠2秒,所以此时会观察到只有父进程在运行的场景
waitpid获取子进程的退出信息
- 铺垫了这么久,终于到了讲解使用waitpid的第二个参数status获取子进程的退出信息了
- 有的读者友友可能会想,这简单,一个输出型参数,我传给它不就是了,之后打印一下不就好了,这样就可以轻松获取子进程的退出码了,可是事实果真如此吗?下面小编带领大家探索一下
- 为了更好的演示效果,小编将子进程的退出码设置成2进行区分
#include #include #include #include #include #include void RunChild(){ int cnt = 2; while(cnt) { printf(\"i am child process, pid: %d, ppid: %d, cnt: %d\\n\", getpid(), getppid(), cnt); cnt--; sleep(1); }}int main(){ pid_t id = fork(); if(id < 0) { perror(\"fork\"); return 1; } else if(id == 0) { RunChild(); exit(2); } else { //id > 0 sleep(4); int status = 0; int ret = waitpid(-1, &status, 0); if(ret == id) { printf(\"我是父进程,我等待子进程成功了, 子进程的pid: %d, 子进程的退出码为: %d\\n\", ret, status); } else { printf(\"等待子进程失败\\n\"); } sleep(2); } return 0;}
运行结果如下
- 很多读者友友心中都是纳尼?为啥会这样,子进程的退出码为啥会是512,难道不应该是2吗?难道编译器坏了?
- 实则不然,其实waitpid的第二个参数status的设计大有用工
- waitpid的第二个参数status是要将子进程的退出信息全部获取到的,那么就会有关退出信息,退出码以及core dump标志,关于这个core dump标志小编在此文章并不会进行讲解,请期待小编在后文的信号文章中进行讲解
- 子进程的退出场景有三个,即1. 异常退出,2. 代码执行完成,结果正确,3. 代码执行完成,结果不正确,关于这三个场景的讲解详情请点击<——
- 父进程关心什么?既然我将任务交给你子进程去执行了,那么我要关心子进程有没有完成我交给它的任务,首先子进程有没有发生异常?如果没有发生异常,说明代码执行完毕,那么结果是否正确?就可以通过退出码来进行说明,如果退出码为0,那么说明结果正确,如果退出码不为0,那么说明结果错误,不同的退出码就可以表示不同的出错原因
- 那么父进程获取子进程的退出信息就要将是否发生异常,是否结果正确全部获取到,即那么对应的就是退出信号,退出码分别为多少,可是我们只有一个整形的数,去获取两个信息,可想而知,势必是使用了位运算,一个整形有32个比特位,所以status巧妙的利用好这32个比特位,进而就可以将退出信号,退出码全部获取到
- 虽然一个int整形有32个比特位,但是在status中我们仅仅研究前16个比特位,当进程异常的时候,此时就会受到退出信号,此时退出信号不为0,那么由于出现了异常,那么可能代码未执行完成,所以我们便不关心退出码,退出码对应的比特位也就未进行使用
- 如果进程没有发生异常,此时信号就为0(为什么将信号设置为0呢?是因为在linux的信号中,我们使用kill -l查看之后,发现linux的信号中根本没有0号信号,所以使用0来表示进程没有收到退出信号),那么此时进程没有发生异常,表示代码执行完成,此时我们就应该关心结果是否正确,即使用退出码得出结果是否正确即可
- status的32个比特位的前16位的存储,以及如何进行位运算提取对应的退出码和退出信号如下
- 那么此时我们正确进行位运算提取status对应的退出信号和退出码即可
#include #include #include #include #include #include void RunChild(){ int cnt = 2; while(cnt) { printf(\"i am child process, pid: %d, ppid: %d, cnt: %d\\n\", getpid(), getppid(), cnt); cnt--; sleep(1); }}int main(){ pid_t id = fork(); if(id < 0) { perror(\"fork\"); return 1; } else if(id == 0) { RunChild(); exit(2); } else { //id > 0 sleep(4); int status = 0; int ret = waitpid(-1, &status, 0); if(ret == id) { printf(\"我是父进程, 我等待子进程成功了, 子进程的pid: %d, 退出信号为: %d, 子进程的退出码为: %d\\n\", ret, status & 0x7F, (status >> 8) & 0xFF); } else { printf(\"等待子进程失败\\n\"); } sleep(2); } return 0;}
运行结果如下
此时我们的退出信号和退出码就可以正常提取出来了
- 其实操作系统提供了对应提取退出信号和退出码的宏函数来供我们使用,通常来讲操作系统提供的WIFEXITED是用来提取status的退出信号的,但是这个提取的退出信号会被转化为bool值,因为如果出现了异常,此时提取出来的为假0,那么说明代码有极大可能是没有执行完,子进程收到了退出信号,即子进程就出现了异常错误,退出码也就没有了意义,如果没有出现异常,此时提取出来的退出信号就为真1,此时此时退出码有意义,于是就使用WEXITSTATUS提取status中的退出码,于是我们的代码就可以转化成下面这样去进行使用
#include #include #include #include #include #include void RunChild(){ int cnt = 2; while(cnt) { printf(\"i am child process, pid: %d, ppid: %d, cnt: %d\\n\", getpid(), getppid(), cnt); cnt--; sleep(1); }}int main(){ pid_t id = fork(); if(id < 0) { perror(\"fork\"); return 1; } else if(id == 0) { RunChild(); exit(2); } else { //id > 0 sleep(4); int status = 0; int ret = waitpid(-1, &status, 0); if(ret == id) { printf(\"我是父进程,我等待子进程成功了\\n\"); if(WIFEXITED(status)) { printf(\"子进程的代码是正常跑完的, 子进程的pid: %d, 子进程的退出码为: %d\\n\", ret, WEXITSTATUS(status)); } else { printf(\"子进程出现异常\\n\"); } } else { printf(\"等待子进程失败\\n\"); } sleep(2); } return 0;}
运行结果如下
- 此时我们进行思考,当子进程退出的时候,子进程的内核数据结构task_struct(PCB)中是会存储子进程的退出信息(退出信号exit_signal,退出码exit_code)的,为什么父进程想要拿子进程的状态数据,任意数据,为什么必须要用wait等系统调用去拿呢?我父进程越过操作系统直接去内核空间中去读取子进程的内核数据结构对象task_struct(PCB)的数据可不可以?
- 不可以,因为进程具有独立性,父进程的代码和子进程的数据和代码共享,当父子进程修改对应的数据的时候,会发生写时拷贝,让父子进程对数据各自私有一份,所以子进程修改的数据父进程是无法拿到的,另外,操作系统不相信任何人,父子进程的代码是由用户去进行编写的,那么父进程的代码本质就是代表的用户的意愿,如果用户可以直接去操作系统的内核空间中拿到子进程的数据,那么也就是代表着父进程可以直接读取子进程的内核数据结构对象了,那么父进程对其进行修改,此时不就乱套了,所以操作系统不允许父进程越过操作系统直接去内核空间中拿数据,只能调用wait等系统调用,系统调用是由操作系统提供的那么也就代表着操作系统,即想要获取子进程任意数据需要由操作系统去拿才可以
- 其实父进程在代码层面是可以传数据给子进程的,在后续的文章中,我们还可以学习到父进程通过进程替换进而传入自定义的环境变量给子进程,环境变量我们可以自定义字符串等内容,那么子进程就可以拿到环境变量的数据去进行对应的操作
waitpid等待指定的子进程
- waitpid的第一个参数传入子进程的pid,就可以让父进程等待指定的子进程,但是需要注意,这个指定的子进程必须是当前进程的子进程,如果是其它的进程那么waitpid将会等待子进程失败,返回-1
- 父进程是可以获取到子进程的pid传入waitpid的,因为父进程fork创建子进程,fork给父进程返回的就是子进程的pid
#include #include #include #include #include #include void RunChild(){ int cnt = 2; while(cnt) { printf(\"i am child process, pid: %d, ppid: %d, cnt: %d\\n\", getpid(), getppid(), cnt); cnt--; sleep(1); }}int main(){ pid_t id = fork(); if(id < 0) { perror(\"fork\"); return 1; } else if(id == 0) { RunChild(); exit(2); } else { //id > 0 sleep(4); int status = 0; int ret = waitpid(id, &status, 0); if(ret == id) { printf(\"我是父进程,我等待子进程成功了\\n\"); if(WIFEXITED(status)) { printf(\"子进程的代码是正常跑完的, 子进程的pid: %d, 子进程的退出码为: %d\\n\", ret, WEXITSTATUS(status)); } else { printf(\"子进程出现异常\\n\"); } } else { printf(\"等待子进程失败\\n\"); } sleep(2); } return 0;}
运行结果如下
waitpid等待多个子进程
- 如果当前进程使用for循环去创建多个子进程的时候,我们该如何使用waitpid去进行多个子进程的等待呢?关于使用for循环去创建多个子进程的讲解详情请点击<——
- 那么我们也使用同样的for循环方式去让父进程进行N次等待即可,这里的等待是阻塞等待当前进程的任意子进程
- 那么在这个过程中,我们将使用脚本去监控进程的状态,脚本代码如下
//脚本代码while :; do ps ajx | head -1 && ps ajx | grep testWait | grep -v grep; sleep 1; echo \"-----------------------------\"; done
#include #include #include #include #include #include #define N 5void RunChild(){ int cnt = 2; while(cnt) { printf(\"i am child process, pid: %d, ppid: %d, cnt: %d\\n\", getpid(), getppid(), cnt); cnt--; sleep(1); }}int main(){ for(int i = 0; i < N; i++) { pid_t id = fork(); if(id == 0) { RunChild(); exit(i);//区分子进程的退出码,因为这个i是在父进程循环中的,父进程的 } //代码和数据拷贝给子进程 } for(int j = 0; j < N; j++) { int status = 0; pid_t ret = waitpid(-1, &status, 0);//waitpid阻塞等待当前进程的任意子进程 if(ret > 0) { printf(\"父进程等待一个子进程成功, \"); if(WIFEXITED(status)) { printf(\"子进程代码执行完了, 子进程的pid: %d, 退出码: %d\\n\", ret, WEXITSTATUS(status)); } else { printf(\"子进程出现异常了\\n\"); } } } sleep(2); return 0;}
运行结果如下
- 如上,我们就可以同时创建出来的多个子进程,并且对这多个子进程进行等待,回收
- 这里需要注意,由于子进程执行后父进程一直在等待子进程,所以子进程在退出的瞬间就会被父进程等待成功,进行回收,所以这里我们看不到子进程的进入僵尸状态
- 当然如果读者友友很想观察到子进程的僵尸状态,那么可以在两个for循环之间让父进程休眠比单个子进程的休眠时间长即可,这样子进程打印,休眠结束,此时父进程还在休眠,无暇等待子进程,此时子进程的僵尸状态就可以被我们检测到
六、非阻塞轮询
- 此时小编为大家介绍一下waitpid的第三个参数options(阻塞方式),传0给waitpid的第三个参数可以让waitpid像wait一样让父进程去执行阻塞等待,除了由阻塞等待之外还有非阻塞等待,那么就是传WNOHANG给waitpid的第三个参数让父进程进行非阻塞等待
- 同时对于waitpid的返回值,小编还有一点没有讲出,在此之前,我们知道,waitpid等待子进程成功,那么会返回一个大于0的数,即子进程的pid,如果waitpid等待的子进程不是当前进程的子进程,而是其它进程,那么waitpid就会等待失败,返回一个小于0的数据,即-1
- 那么waitpid的返回值有大于0的了,也有小于0的了,那么等于0的呢?其实waitpid的返回值也有等于0的,等于0的返回值就是为了配合非阻塞实现while循环式的非阻塞,即非阻塞轮询
- waitpid可以和wait一样检测子进程的状态是否为Z僵尸状态如果子进程的状态没有发生变化,那么在阻塞状态下,即传入waitpid的第三个参数为0,以及当使用wait的时候,是阻塞等待,此时父进程会一直在waitpid/wait中不返回,此时父进程就会一直等待子进程的状态改变就绪,此时父进程无法执行其它代码,操作系统会将父进程从运行队列拿走,将父进程链接入子进程的等待队列中,并且将父进程的状态调整为阻塞状态
- 但是如果是非阻塞等待,即给waitpid的第三个参数传入WNOHANG,那么此时就会执行非阻塞等待,即如果父进程调用了waitpid之后,如果子进程的状态不为Z僵尸状态,并且子进程的进程状态未发生改变,那么此时waitpid不会让父进程在那里傻傻的去进行等待子进程状态就绪了,waitpid而是会直接返回,返回一个为0的值,让用户拿到,由用户去决定下一步执行的决策,此时用户就可以嵌套循环实现非阻塞轮询,并且可以执行自己的事情
- 其实对于这个在等待子进程状态就绪间隙执行的事情不能太重,不能上来拷贝个10G的数据,主要的任务还是等待子进程,例如我们可以执行检测或者打印的一些比较轻的任务
- 我们可以将任务封装到函数指针数组(向量数组)中,并且还可以使用函数对这些任务管理起来,即进行增删查改操作
#include #include #include #include #include #include #define TASK_NUM 3 //创建任务的个数void RunChild(){ int cnt = 5; while(cnt) { printf(\"i am child process, pid: %d, ppid: %d, cnt: %d\\n\", getpid(), getppid(), cnt); cnt--; sleep(1); }}typedef void(*task_t)();//函数指针void task1(){ printf(\"这是一个执行打印日志的任务, pid: %d\\n\", getpid());}void task2(){ printf(\"这是一个执行检测网络健康状况的任务, pid: %d\\n\", getpid());}void task3(){ printf(\"这是一个执行绘制图形化界面的任务, pid: %d\\n\", getpid());}task_t tasks[TASK_NUM];//函数指针数组(向量数组)void InitTask()//初始化函数指针数组为NULL{ for(int i = 0; i < TASK_NUM; i++) { tasks[i] = NULL; }}int AddTask(task_t t)//新增任务{ int i = 0; for(; i < TASK_NUM; i++)//找到为空的位置 { if(tasks[i] == NULL) { break; } } if(i == TASK_NUM)//如果成立,那么表示此时函数指针数组满了,没有为NULL的位置,无法插入 { //此时返回-1表示插入失败 return -1; } tasks[i] = t;//如果到这里说明找到了为NULL的位置,进行赋值我们要插入的函数指针即可 return 0;//返回0表示插入成功}void ExecuteTask()//执行函数指针数组中的所有任务{ for(int i = 0; i < TASK_NUM; i++)//循环遍历调用函数指针即可 { if(tasks[i]) { tasks[i](); } }}int main(){ //初始化函数指针数组为NULL,并且插入三个任务 InitTask(); AddTask(task1); AddTask(task2); AddTask(task3); pid_t id = fork(); if(id < 0) { perror(\"fork\"); return errno; } else if(id == 0) { RunChild(); exit(2); } else { //id > 0(父进程) while(1)//循环执行非阻塞+做自己的事情 { int status = 0; int ret = waitpid(id, &status, WNOHANG); if(ret < 0)//表示此时等待失败 { perror(\"waitpid\"); break;//一定要break退出,因为后面可能会有其它的代码要执行 } else if(ret > 0)//等待成功 { printf(\"我是父进程,我等待子进程成功了\\n\"); if(WIFEXITED(status)) { printf(\"子进程的代码是正常跑完的, 子进程的pid: %d, 子进程的退出码为: %d\\n\", ret, WEXITSTATUS(status)); } else { printf(\"子进程出现异常\\n\"); } break;//一定要break退出,因为后面可能会有其它的代码要执行 } else//ret == 0, 此时执行自己的事情即可 { ExecuteTask();//执行所有的任务 sleep(1);//休眠1秒, 否则可能出现子进程无法调度 } } } return 0;}
运行结果如下
- 于是我们就可以观察到我们的父进程一边检测子进程,一边做自己的事情,即父进程创建子进程实现了非阻塞轮询+自己的事情
- 当子进程退出的时候,此时waitpid返回等待的子进程的pid,id接收waitpid的返回值,id > 0,父进程判断子进程是否出现异常,以及结果是否正确
- 并且小编还可以扩展一下,我们在命令行中执行的命令(程序),这个命令(程序)就是bash通过fork创建的子进程,那么bash就会对这个命令进行阻塞等待,并且对于我们执行的其它命令不响应
#include #include #include int main(){ while(1) { printf(\"i am process, pid: %d\\n\", getpid()); sleep(2); } return 0;}
运行结果如下
- 在这个运行过程中,bash会阻塞等待它的子进程,对于用户执行的其它命名将不会响应,小编尝试执行诸如ls,pwd命令都无效,即bash对我们的命令不响应
- 当我们想要退出的时候按下ctrl + c即可终止退出进程
总结
以上就是今天的博客内容啦,希望对读者朋友们有帮助
水滴石穿,坚持就是胜利,读者朋友们可以点个关注
点赞收藏加关注,找到小编不迷路!