> 🍃 本系列为Linux的内容,如果感兴趣,欢迎订阅🚩
> 🎊个人主页:【小编的个人主页】
>小编将在这里分享学习Linux的心路历程✨和知识分享🔍
>如果本篇文章有不足,还请多多包涵!🙏
> 🎀 🎉欢迎大家点赞👍收藏⭐文章
> ✌️ 🤞 🤟 🤘 🤙 👈 👉 👆 🖕 👇 ☝️ 👍
目录
🐼前言
🐼进程创建
🐼进程终止
🐬相关背景知识
🐬进程常见退出方法
编辑
🐼进程等待
🐬为什么要有进程等待
🐬解决子进程"僵尸"问题
1️⃣方式一:通过系统调用wait
2️⃣方式二:waitpid方法
🐬最佳实践
🐬非阻塞等待
🐼前言
上一篇文章我们认识了进程优先级,进程优先级其实主要由NI值决定的。然后我们又谈论了进程O(1)调度算法,通过活跃队列和过期队列,让每个进程都能够合理的享有CPU资源。本节内容将对进程进一步补充,包括进程创建,进程终止os会做什么,进程等待等等话题,话不多说,直接开冲!
🐼进程创建
进程创建我们在进程入门指南已经分享了绝大部分内容,包括调用fork()创建一个新进程,fork的返回值,为什么给父进程返回子进程的pid,给子进程返回0,返回值为什么有两个,为什么id即>0,又==0。我们也知道了:在创建子进程后,内核将做这些事情包括:分配新的内存块和内核数据结构给子进程,将父进程部分数据结构内容拷贝给子进程, 添加子进程到系统进程列表当中,fork返回,开始调度器调度等四件事。
这次,我们主要谈基于程序地址空间进程在创建时,写实拷贝的问题:
在没创建子进程之前,父进程的进程数据结构片段是这样的:
其中虚拟内存包括两个字段,数据段和代码段,父进程页表代码段的权限是只读的,数据段的内容是读写的。而创建子进程时,父进程会将页表数据段的权限改为只读, 由于fork()创建出来子进程,子进程也会被创建出task_struct,mm_struct,子进程页表等,共享父进程的代码和数据,所以,在修改内容之前,父子进程的数据段权限都是只读的,如图:
然而,在父子进程任意一方进行写入的时候,由于此时数据段的权限是只读(r),不让写入,就会触发"报错",操作系统会检查这个报错进行分类讨论:1.如果该报错真的是越界访问,比如野指针等,那么操作系统会杀死该进程,2.如果并不是野指针,写入的区域是合理的,有对应的映射关系,那么操作系统就会认为只是读写权限的报错,于是操作系统就会对该进程进行写实拷贝,让父进程指向不同的数据段,并修改权限只读(r)为读写(rw)。如图:
所以父进程创建子进程,并且子进程发生写入的完整大致过程如图:
因为有写时拷贝技术的存在,所以父子进程得以彻底分离!完成了进程独立性的技术保证!
需要注意:即使写实拷贝,但是代码段还是共享的,因为它本身是只读的!
我们现在要解决两个问题
1️⃣为什么创建子进程时,需要把数据分开呢,我们为什么不直接拷贝一份给子进程呢,为什么要写实拷贝???
因此当父子进程任何一方都没有发生写入时,无脑的拷贝一份数据给子进程,就是在浪费内存资源和时间。而当子进程确实需要写入时,按需申请就好了,本质是一种"惰性申请",为了节省内存的使用率和效率。
2️⃣子进程真要写入了,为什么要把数据拷贝给子进程呢,给子进程直接申请空间不就好了?
因为子进程不一定是覆盖性的写入,有可能在原有数据上做修改,并且子进程和父进程都很多相似的字段,只需要做微调即可。
同样的思想:我们在之前学习C/C++申请malloc ,new时,需要在物理内存开辟空间吗?
答案是一定不在物理内存上开辟空间,因为物理内存资源就那么多,我们申请时并不是立即使用,只有真正用的时候,os会自已判断,才会对操作系统进行内存级申请,构建完整的映射关系,所以我们mallo/new,都是在虚拟地址空间开辟的!这本质上也是一种"惰性空间开辟"。而这个过程对用户是"透明"的。我们以为os真的给我们申请物理内存,开了空间,其实只是操作系统给我们画了一张大饼!
🐼进程终止
🐬相关背景知识
首先回答我们之前学习过的两个问题。
进程终止时,操作系统要做什么?
进程终止就是进程创建时的逆过程,进程在被创建出来时os会给进程创建task_struct,虚拟地址空间,页表等内核数据结构以及内存会加载代码和数据。所以进程终止os一定也会对进程做资源管理的回收和利用,安全清理掉该进程。
第二个问题:我们之前在写C/C++代码时main函数总会在结尾写一个return 0,可不可以不写return 0?return 1可不可以?return 0给谁了?
我们来验证一下:比如我们返回100
#include<stdio.h>2 3 int main()4 {5 int a = 10,b = 20;6 int c = a+b;7 printf("%d + %d = %d\n",a,b,c);8 return 100; 9 }
返回100依然可以执行,那我们为什么要写return 0呢?
其实我们返回的这个数字叫做该进程退出时的退出码。我们可以在内核数据结构 task_struct中查看到:
而退出码会被"系统"(父进程)获得,让系统甄别,该进程完成的怎么样 。(这里是bash)
想想为什么要创建子进程,就是为了帮助父进程执行某种任务或者自已执行特定任务,而该任务完成的怎么样,谁需要知道,父进程优先需要知道,因此父进程需要拿到子进程的退出码。我们写的程序是bash创建出来的子进程,bash需要知道我们这个进程完成的怎么样,因此我们要给父进程返回一个退出码,也就是当前main函数的返回值是该进程的退出码。
我们可以通过echo $?查看最近一个进程的退出码。比如我们依然给bash返回100,用echo $?查看:
父进程bash确实可以拿到我当前进程的退出码。 我们在命令行执行的每一个命令都是一个进程,我们依旧可以查看其退出码。
那为什么一定要有退出码呢?退出码代表着什么?
依旧是我们所说的,子进程完成父进程交给子进程做的事情怎么样,父进程需要知道。比如,子进程返回0,代表完成成功,return !0,也要告诉父进程失败原因,比如return 1,2,3,4...都要有对应的原因告诉父进程。
👇打个比方:
假设父进程是一位“老板”👬,子进程是被派去完成任务的“员工”👭。老板把任务交给员工后,员工去执行任务。任务完成后,员工会向老板汇报情况:
-
如果员工顺利完成了任务,他会告诉老板:“老板,任务搞定了😄,没问题!”(子进程返回0,代表成功)。
-
如果任务没完成,员工会说:“老板,任务没做好😓,原因是……”(子进程返回非0值,比如1、2、3等,每个数字代表一种失败的原因,比如1代表找不到文件,2代表权限不够,等等)。
老板(父进程)根据员工(子进程)的汇报,就知道任务的执行情况,然后决定下一步怎么做。
在C语言中提供了一种内置的错误原因,errno,比如系统调用strerror这个函数:
我们可以尝试一下输入错误码,看看内置的错误信息,比如1-150的错误码对应的信息:
#include<stdio.h> 2 #include<string.h> 3 4 int main() 5 { 11 int i = 1; 12 for(;i<=150;i++) 13 { 14 printf("%d %s\n",i,strerror(i)); 15 } 16 17 return 0; 18 19 }
一共有134条退出码对应的错误原因。其中0表示该进程任务完成成功。
这样我们就能理解了,我们在命令行输入不存在的命令比如 ls -dddddd,系统会报错下图:
而当我们查看该进程的退出码时:
错误码2对应的错误信息是谁啊?
这样我们就也能理解了,main函数的返回值是该进程的退出码,退出码由父进程拿到,决定父进程下一步要做什么,其中退出码对应的退出信息我们可以echo $?看到。
所以进程退出,无非就对应三种场景:
- 代码跑完了,结果对。
- 代码跑完了,结果不对。
- 代码没跑完,进程异常了。
而对或不对都是由退出码决定,而当进程异常时,退出码本身就无意义了,就要考虑进程异常的原因,为什么异常?这都需要让管理者操作系统先知道,而操作系统知道后,一般通过信号杀掉该进程!我们在后面进程信号详说退出信号。
🐬进程常见退出方法
我们已经知道进程退出时的退出场景。而让进程退出时常见退出方法有三种:
1️⃣方法一:main函数进行return n,n表示进程的退出码
#include<stdio.h>2 #include<unistd.h>3 4 int main()5 {6 printf("hello world\n");7 sleep(1);8 return 1; 9 }
父进程获取退出码:
1️⃣方法二:直接调用exit(n),n表示进程的退出码
1 #include<stdio.h>2 #include<unistd.h>3 #include<stdlib.h>4 5 int main()6 {7 printf("hello world\n");8 sleep(1);9 10 exit(2); 11 12 }
父进程获取退出码:
3️⃣方法三: 直接调用系统调用_exit(n),n表示进程的退出码
父进程获取退出码:
下面区分两个问题:
1️⃣return 和exit 有什么区别?
return 表示函数调用结束,在main函数中的return表示进程退出。
比如我们在自已定义的函数return,我们的程序并不会直接退出。比如下面这个代码:
#include<stdio.h>2 #include<unistd.h>3 #include<stdlib.h>4 5 6 int func()7 {8 printf("func()\n");9 return 10;10 }11 int main()12 {13 printf("我的进程开始了\n");14 func();15 sleep(3); 16 printf("我的进程正常退出了\n");17 return 1;18 }
我们从退出码就可以看出我们在普通函数内部返回不影响进程退出。
而exit表示进程结束,在代码中,任何地方,调用,都会导致进程直接退出。
我们把func()中的return 改为exit:
#include<stdio.h> 2 #include<unistd.h> 3 #include<stdlib.h> 4 5 6 int func() 7 { 8 printf("func()\n"); 9 10 exit(7); 11 } 12 int main() 13 { 14 printf("我的进程开始了\n"); 15 func(); 16 sleep(3); 17 printf("我的进程正常退出了\n"); 18 return 1; 19 }
在任何地方,进程直接退出,我们根据退出码就知道:
所以最佳实践:终止进程,我们使用exit
2️⃣exit和_exit有什么区别?
我们先来看一下代码:
#include<stdio.h>2 #include<unistd.h>3 #include<stdlib.h>4 5 int main()6 {7 printf("你猜我会被输出吗?");//不会直接刷新,而是会放在输出缓冲区中8 sleep(3);9 exit(10); 10 }
输出结果:
对于同样一段代码,我们把exit换成_exit试试
什么都没有输出:
我们可以得出结论,exit在终止进程时,会刷新缓冲区的内容,而_exit则不会刷新缓冲区的内容。就像如图所示:
exit这点和在main函数中的return很相似,都会在进程结束后刷新缓冲区,但是_exit()就不会刷新缓冲区的内容,所以大家甄别。
这里我们需要再补充一下,我们之前知道,操作系统的结构是层状结构的,并且库和系统调用是上下层的关系,又因为exit为库函数,_exit为系统调用,如图:
终止进程,是不是在终止内核的源代码?是的。这样我们就能得出结论:进程终止,必须要调用系统调用,必须让操作系统完整的让进程退出。又因为库函数和系统调用是上下层关系,终止进程又必须调用系统调用,所以exit内一定封装了_exit,才能完成真正的进程终止。
这里又能扩展一个问题:在我们之前学习时,一直有一个问题,就是输出缓冲区在哪???
首先缓冲区一定是一段内存空间,既然库函数exit可以刷新缓冲区,系统调用_exit不可以刷新缓冲区,而exit内部又封装了_exit,所以我们说的缓冲区都在库缓冲区中!假设在操作系统内部,那么_exit会接触操作系统,则_exit也会刷新缓冲区,但是我们上面验证了_exit不会刷新缓冲区,所以缓冲区一定不在操作系统内部。(至于缓冲区的具体问题我们会在文件系统详谈)
🐼进程等待
我们已经知道进程如何创建的,也能终止进程了。
🐬为什么要有进程等待
一方面,在之前,我们分享过,子进程退出,父进程如果不管不顾,就可能造僵尸进程的问题,进而造成内存泄漏。而进程⼀旦变成僵尸状态,那就刀枪不入,“杀人不眨眼”的kill -9 也无能为力,因为谁也没有办法杀死⼀个已经死去的进程,只能等父进程来回收子进程了,这也是进程等待的原因之一。
另外一方面:父进程派给子进程的任务完成的如何,我们需要知道。比如子进程运行完成,结果对还是不对,或者是否正常退出,而判定子进程把任务完成的怎么样我们可以通过退出码来判断。
所以,父进程通过进程等待的方式,一方面回收子进程资源(这时必须做的),另一方面获取子进程退出信息(这是父进程可选的)
所以进程等待就是让父进程通过等待的方式,回收子进程(处在Z状态)的task_struct,如果有需要,可以获取子进程的退出信息。
🐬解决子进程"僵尸"问题
下面我们就来做如何解决子进程处在僵尸时,父进程如何回收的问题。
1️⃣方式一:通过系统调用wait
wait的用法是,父进程调用wait,表示等待任意一个子进程。
- 如果子进程没有退出,父进程就会一直wait,处于阻塞。
- 如果子进程退出,父进程wait的时候,wait就会返回了,让系统自动解决子进程的僵尸问题。如果成功,返回子进程的pid,否则,返回-1。
下面我们就来验证父进程等待回收Z状态的子进程:
#include<stdio.h>2 #include<unistd.h>3 #include<stdlib.h>4 #include<sys/types.h>5 #include<sys/wait.h>6 7 int main()8 {9 pid_t id = fork();10 if(id == 0)11 {12 //子进程13 int count =10;14 while(count--)15 {16 printf("我是子进程,我的pid: %d,ppid: %d,count: %d\n",getpid(),get ppid(),count);17 sleep(1); 18 }19 exit(100);20 } 21 22 //父进程23 printf("父进程开始等待ing...\n");24 pid_t cid = wait(NULL);25 if(cid>0)26 { 27 printf("父进程等待子进程成功,子进程pid %d\n",cid);28 } 29 30 return 0;31 }
前10秒有父子两个进程,父进程一直在等待子进程,直到子进程退出,父进程等待成功,父进程也结束了,现象:
如果我们想看到子进程的"僵尸"状态,再让父进程回收。即让子进程运行完,先处于"僵尸"状态(即子进程退出,父进程还没有退出也没有立即回收),我们可以让子进程退出时父进程休眠一会再回收,代码:
#include<stdio.h>2 #include<unistd.h>3 #include<stdlib.h>4 #include<sys/types.h>5 #include<sys/wait.h>6 7 int main()8 {9 pid_t id = fork();10 if(id == 0)11 {12 //子进程13 int count =10;14 while(count--)15 {16 printf("我是子进程,我的pid: %d,ppid: %d,count: %d\n",getpid(),get ppid(),count);17 sleep(1);18 }19 exit(100);20 }21 22 sleep(15);//父进程先休眠15秒等待子进程退出再回收 23 //父进程24 printf("父进程开始等待ing...\n");25 pid_t cid = wait(NULL);26 if(cid>0)27 {28 printf("父进程等待子进程成功,子进程pid %d\n",cid);29 }30 31 return 0;
}
前10秒子进程运行完,父进程还在休眠,这5秒由于父进程还在休眠,没人回收子进程,所以我们看到了子进程的Z状态,父进程休眠后,父进程成功回收处于Z状态的子进程。现象:
那往往在实际应用中,我们会创建多个进程为父进程办事,那么对应多进程,我们应该怎么回收呢?下面我们一次性创建10个子进程并让他们全部处于"僵尸"状态等待父进程回收。代码:
#include<stdio.h>2 #include<unistd.h>3 #include<stdlib.h>4 #include<sys/types.h>5 #include<sys/wait.h>6 7 wait多进程8 9 #define N 1010 11 int main()12 {13 int i=0;14 for(;i<N;i++)15 {16 pid_t id = fork();17 if(id == 0)18 {19 //子进程20 int count =10;21 while(count--)22 {23 printf("我是子进程,我的pid: %d,ppid: %d,count: %d\n",getpid() ,getppid(),count);24 sleep(1);25 }26 exit(100); 27 }28 }29 30 sleep(15);//父进程先休眠15秒等待子进程退出再回收31 //父进程printf("父进程开始等待ing...\n");33 for(i = 0;i<N;i++)34 {35 pid_t cid = wait(NULL);36 if(cid>0)37 {38 printf("父进程等待子进程成功,子进程pid %d\n",cid);39 }40 }41 42 return 0;43 }
观察到10个子进程都处于僵尸状态等待父进程回收,最后父进程将10个"僵尸"都回收成功!
在上面例子中,我们可以得出一个结论:父进程往往是最先创建出来,最后退出的,因为他既要创建子进程也要回收子进程。
2️⃣方式二:waitpid方法
我们借助man 2 waitpid查看相关信息和返回值
waitpid有三个参数,其返回值和参数含义见下:
返回值:
- 当正常返回的时候waitpid返回收集到的子进程的进程ID;
- 如果设置了选项WNOHANG,而调用中waitpid发现没有已退出的子进程可收集,则返回0;
- 如果调用中出错,则返回-1,这时errno会被设置成相应的值以指示错误所在;
参数:
- pid:
- Pid=-1,等待任一个子进程。与wait等效。Pid>0.等待其进程ID与pid相等的子进程。
- status: 输出型参数(可以带出子进程的退出码和其退出信号,怎么带出来的下面会说)
- WIFEXITED(status): 若为正常终止进程返回的状态,则为真。(查看进程是否是正常退出)
- WEXITSTATUS(status): 若WIFEXITED非零,提取子进程退出码。(查看进程的退出码)
- options:默认为0,表示阻塞等待
- WNOHANG: 若pid指定的子进程没有结束,则waitpid()函数返回0,不予以等待。若正常结束,则返回该子进程的ID。
waitpid的参数使用方式有点多样,我们下面一个个谈:
我们这里先默认将参数pid设置为-1,表示等待任意一个子进程,options设置为0,表示阻塞等待,status是一个输出型参数(和我们在C语言中学的二级指针目的一样)都是要带值出来的(这里先认为是status是子进程的退出码),我们先看看带出来的值怎么回事?
#include<stdio.h>2 #include<unistd.h>3 #include<stdlib.h>4 #include<sys/types.h>5 #include<sys/wait.h>6 7 wait多进程8 9 #define N 1010 11 int main()12 {13 int i=0;14 for(;i<N;i++)15 {16 pid_t id = fork();17 if(id == 0)18 {19 //子进程 20 int count =3; 21 while(count--) 22 { 23 printf("我是子进程,我的pid: %d,ppid: %d,count: %d\n",getpid(),getppid(),count);24 sleep(1); 25 } 26 printf("子进程退出\n"); 27 exit(1); 28 } 29 } 30 31 sleep(5);//父进程先休眠5秒等待子进程退出再回收 //父进程 33 printf("父进程开始等待ing...\n");34 for(i = 0;i<N;i++)35 {36 //pid_t cid = wait(NULL);37 int status = 0;38 pid_t cid = waitpid(-1,&status,0); 39 if(cid>0)40 {41 printf("父进程等待子进程成功,子进程pid %d,status: %d\n",cid,status);42 }43 }44 sleep(2);//父进程再休眠两秒再退出45 return 0;46 }
这段代码和我们上面写的代码很相似,就是把status的值带出来让我们看看,我们这里故意将子进程的退出码设置为1,观察现象:
为什么status的值是256呢? 我们设置子进程的退出码不是1吗?
其实,status不仅仅是子进程退出时的退出码。status不能简单的当作整形来看待,可以当作位图来看待,具体细节如下图(只研究status低16比特位,高16位不考虑):
当子进程正常终止时,只需要考虑status的低16位的次低8位,所以子进程退出码范围一般是[0-255],所以我们的子进程退出码为1,status对应的二进程是100000000,刚好是2^8 = 256。如下图所示:
而我们想要获取到子进程的退出码也就是获取到我红色圈的8位,只需要对status进行位运算,让status先右移8位,再和0xFF进行&运算,即(status>>8)&0xFF 就可以得到子进程的退出码。
这次为了让子进程退出码特殊一点,我们改为100,看能不能正确得到子进程退出码,代码改动:
我们来试试:
确实可以让status通过位运算可以获得子进程退出码.
那么如果子进程异常退出时,比如说被信号所杀时,那么我们只需要考虑Int的最低7位,为什么不考虑第8位,我们在信号会谈,这里仅需要知道低7位表示退出信号。其中这7位我们叫做终止信号(退出信号),而不需要考虑次低8位获取退出码了,因为此时由于进程异常退出,退出码不重要了。所以我们总结一下,低16位的高8位是退出码,低7位是退出信号,如图:
而进程为什么出现异常,是因为出现了问题,所以操作系统会给所在进程发信号!我们查看系统给异常退出的进程的信号有哪些:
所以,子进程如果正常结束,信号为0,而完成的怎么样,由退出码来决定!
这样就能对应起来之前说过,子进程退出无非就三种情况
代码跑完,结果对。代码跑完,结果不对。代码没跑完。它们分别对应如图:
因此我们用两个数字来表示子进程的执行情况,表示子进程的退出信息,进程退出信号和进程退出码。
我们既然已经知道如何用waitpid获取退出码和退出信号,下面我们分别来模拟子进程退出的三种情况:
1️⃣代码运行完毕,结果正确:
int main()12 {13 int i=0;14 for(;i<N;i++)15 {16 pid_t id = fork();17 if(id == 0)18 {19 //子进程20 int count =3;21 while(count--)22 {23 printf("我是子进程,我的pid: %d,ppid: %d,count: %d\n",getpid(),getppid(),count);24 sleep(1);25 }26 printf("子进程退出\n");27 int ret = 0; 28 if(ret == 0)29 exit(0); 30 else 31 exit(1);32 } 33 }34 35 sleep(5);//父进程先休眠5秒等待子进程退出再回收36 //父进程37 printf("父进程开始等待ing...\n");for(i = 0;i<N;i++)39 {40 //pid_t cid = wait(NULL);41 int status = 0;42 pid_t cid = waitpid(-1,&status,0); 43 if(cid>0)44 {45 printf("父进程等待子进程成功,子进程pid %d,status: %d,exit code: %d ,exit signal: %d\n",cid,status,(status>>8)&0xFF,status&0x7F);46 }47 }48 sleep(2);49 50 return 0;51 }
输出:
2️⃣代码运行完毕,结果不正确
我们只需要让上面代码ret = 1即可。
3️⃣代码异常终止:
比如说故意让我们的子进程发生除0错误,或者野指针,让我们的子进程崩溃,发生异常。如图:
我们查看一下11号信号时什么?
段错误
那如果除0呢?
8号信号是浮点异常错误!
在之前,进程执行完,如果我们想知道进程完成的对不对,完成的怎么样,我们用exit code(退出码),有退出编号和描述。
所以根据上面三种场景对应,我们能得出另一个信息,就是如果进程出现异常,我们同样也想知道,什么原因导致的异常,所以,信号也有不同的数字,对应不同的描述!
我们就能理解了,子进程退出时,会向自已的task_struct中写入exit_code和exit_signal(我们在源码中也能看到)而父进程调用系统调用waitpid()的第二个输出型参数*status,获取子进程task_struct内的属性和数据,就跟geipid()没区别。当父进程调用完成的时候,会让os释放目标的task_struct。父进程等待获取子进程的相关属性(退出码,退出信号)如图所示:
我们根据waitpid的参数,下面我们再扩展一下waitpid的用法
比如等待指定的子进程,并且根据退出信号和退出码判断子进程的执行情况
代码:
#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>
#include<sys/types.h>
#include<sys/wait.h>
#include<iostream>
#include<vector>
wait多进程
#include<string.h>#define N 10int main()
{std::vector<pid_t> sids;int i=0;for(;i<N;i++){pid_t id = fork();sids.push_back(id);if(id == 0){//子进程int count =3;while(count--){printf("我是子进程,我的pid: %d,ppid: %d,count: %d\n",getpid(),getppid(),count);sleep(1);}//int * p = NULL;// *p = 100;//野指针//int a = 10;//a /= 0;//除0printf("子进程退出\n");int ret = 1;if(ret == 0)exit(0);else exit(1);}}sleep(5);//父进程先休眠5秒等待子进程退出再回收//父进程for(auto& sid:sids){//pid_t cid = wait(NULL);printf("父进程开始等待ing...,等待的子进程pid:%d \n",sid);int status = 0;pid_t cid = waitpid(sid,&status,0);int exit_code = (status>>8)&0xFF;//退出码int exit_signal = status&0x7F;if(cid>0){if(exit_code == 0 && exit_signal == 0){printf("子进程跑完,任务完成的正确,exit_code: %d,exit_signal: %d\n",exit_code,exit_signal);}else if(exit_code !=0 && exit_signal == 0){printf("子进程跑完,任务完成的不正确,错误原因:%s\n,exit_code: %d,exit_signal: %d",strerror(exit_code),exit_code,exit_signal);}else{printf("子进程异常,exit_signal: %d\n",exit_signal);}//printf("父进程等待子进程成功,子进程pid %d,status: %d,exit code: %d ,exit signal: %d\n",cid,status,(status>>8)&0xFF,status&0x7F);}else{printf("wait failed\n");}}return 0;
}
比如说,代码跑完,结果不正确,并告诉错误原因输出:
🐬最佳实践
我们上述的操作获取exit_code和exit_signal还要用位操作,显得有点繁琐了。我们不就关心这两个数字吗?Linux提供了两个宏
这个宏帮助我们判断子进程是否正常退出,就相当于我们上面的退出信号。status&0x7F
在进程正常退出的前提下,这个宏帮助我们提取进程的退出码。这个宏和(status>>8)&0xFF类似
最佳实践:
#include<stdio.h>2 #include<unistd.h> 3 #include<stdlib.h>4 #include<sys/types.h>5 #include<sys/wait.h>6 #include<string.h>7 8 int main()9 {10 pid_t id = fork();11 if(id == 0)12 {13 //子进程14 int cnt = 3;15 while(cnt--)16 {17 printf("子进程:%d\n",getpid());18 sleep(1);19 }20 exit(1);21 }22 23 //父进程24 int status = 0;25 pid_t rid = waitpid(id,&status,0);26 if(rid>0)27 {28 printf("wait success\n");29 if(WIFEXITED(status))30 {31 printf("正常运行结束,exit_code: %d\n",WEXITSTATUS(status));}else{33 printf("进程异常了\n");34 }35 }36 37 return 0;38 }
🐬非阻塞等待
我们在上面都将waitpid的第三个参数options设置为0,表示阻塞等待.而如果我们把第三个参数设置为WNOHANG,就表示非阻塞等待。它们有什么区别呢?
给大家打个比方 :👇
阻塞等待:张三和李四是好朋友👫,张三需要联系李四,但李四可能正在忙或者不在家。此时张三拿起电话,拨通了李四的号码,然后等待李四接听。这就好比程序调用waitpid,并且options设置为 0,表示程序会阻塞等待子进程结束。如果李四正在忙或者没有接听电话,张三只能一直等待,什么也不能做。他不能挂断电话去干别的事情,只能一直等待李四接听或者电话自动挂断😝。如果李四接听了电话,张三就可以和他交流了。这就好比子进程结束,父进程从阻塞状态中恢复,继续执行后续的任务,阻塞等待就不要挂电话。
而非阻塞等待,如果李四没有接听电话,张三不会一直等待,而是会立即挂断电话,去做别的事情,过一会再打一个电话看李四接不接,如果不接,再打,再挂,再打...😉。他不会被“阻塞”在等待电话接听的状态。他可以在之后的某个时刻再次尝试联系李四。所以非阻塞等待不会因为条件不就绪而阻塞,而是立即返回(其本质就是检测子进程状态,退出,回收,没有退出,立即返回)
既然我们知道了非阻塞等待,那我们先见一下非阻塞等待再谈谈为什么要有非阻塞等待,我们将waitpid第三个参数设置为WNOHANG,根据waitpid的返回值
如果返回值>0,表示等待子进程成功,返回值==0,表示子进程没有退出,<0表示等待子进程失败。因此对于非阻塞等待,我们可以这样写:
#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>
#include<sys/types.h>
#include<sys/wait.h>
#include<string.h>int main()
{pid_t id = fork();if(id == 0){//子进程int cnt = 3;while(cnt--){printf("子进程:%d\n",cnt);sleep(1);}exit(0);}//父进程pid_t rid = waitpid(id,NULL,WNOHANG);if(rid>0){printf("wait success\n");}else if(rid == 0){printf("child no quit\n");}else{printf("wait failed\n");}return 0;
}
我们发现运行时,直接会打印出子进程没有退出,并且观察到只有一个进程,他的父进程是1号进程,并且该进程是一个后台进程,这是怎么回事?
因为,父进程执行完waitpid就退了,子进程运行3秒,没有退出呢,父进程先先退出了,所以子进程被称为"孤儿进程"了,被1号进程领养了 ,变成后台进程了,我们也不能直接用ctrl+c杀掉该进程了。
如果我们想让父进程等待子进程先不退,并且要求非阻塞等待,所以我们就需要让父进程在子进程退出之后退,不能让比子进程先退,这种等待方式叫做非阻塞轮询等待。代码:
#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>
#include<sys/types.h>
#include<sys/wait.h>
#include<string.h>int main()
{pid_t id = fork();if(id == 0){//子进程int cnt = 3;while(cnt--){printf("子进程:%d\n",cnt);sleep(1);}exit(0);}//父进程while(1){pid_t rid = waitpid(id,NULL,WNOHANG);if(rid>0){printf("wait success\n");break;}else if(rid == 0){printf("child no quit\n");//此时父进程不光只在等待子进程,也能干自已的悄悄事sleep(1);}else{printf("wait failed\n");break;}}return 0;
}
运行结果:
这个过程就好像张三不断给李四打电话,问他好了没,没好,挂电话,干自已事情,等一会再问....。
为什么要有非阻塞轮询等待,我们显而易见,因为非阻塞轮询等待不会卡住父进程,父进程就可以在等待的子进程的间隙干自已的事情,所以比阻塞等待稍微高效一点,可以让父进程干更多的事情。
如果我们想具体实现一下子进程在完成任务的时候,也不要让父进程闲着,父进程也在周期性的推进其他任务,让他们并发来执行,该怎么办?我们当然可以直接在父进程代码逻辑直接添加一些方法方便父进程执行,但是这样代码耦合度太高了。我们专门设计一个工具箱Tool.hpp(里面可能有很多功能,比如能添加任务和执行任务的功能)和父进程的所有任务(task.hpp)。以后想添加任务直接在task.hpp中添加就行了。具体代码如下:
Tool.hpp
#pragma once//防止头文件被重复包含#include<iostream>
#include<vector>
#include<functional>using fun_t = std::function<void()>;//typedef std::functional<void()> fun_t;//工具箱
class Tool
{public:Tool(){}void PushFunc(fun_t f){_funcs.push_back(f);}void Execute(){for(auto&f: _funcs){f();//执行任务}}~Tool(){}private:std::vector<fun_t> _funcs;//方法集
};
task.hpp
#pragma once2 3 #include<iostream>4 5 6 void DownLoad()7 {8 std::cout << "我是一个下载任务" <<std::endl;9 }10 11 void PrintLog()12 {13 std::cout << "我是一个打印日志的任务" << std::endl;14 }15 16 void FlushData()17 {18 std::cout << "我是一个刷新数据的任务" << std::endl; 19 }
myproc.cc
#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>
#include<sys/types.h>
#include<sys/wait.h>
#include<string.h>
#include<iostream>
#include"Tool.hpp"
#include"task.hpp"int main()
{Tool tool;tool.PushFunc(DownLoad);tool.PushFunc(PrintLog);tool.PushFunc(FlushData);pid_t id = fork();if(id == 0){//子进程int cnt = 3;while(cnt--){printf("子进程:%d\n",cnt);sleep(1);}exit(0);}//父进程while(1){pid_t rid = waitpid(id,NULL,WNOHANG);if(rid>0){printf("wait success\n");break;}else if(rid == 0){printf("child no quit\n");//此时父进程不光只在等待子进程,也能干自已的悄悄事tool.Execute();sleep(1);}else{printf("wait failed\n");break;}}return 0;
}
执行代码,让父子进程周期性的同时推进,并发运行,现象:
综上:非阻塞等待的优点就是不知道子进程何时退出,父进程必须要回收子进程,但不能够干等,在等待子进程的同时也能做自已的事情,直到等待子进程退出。
感谢你耐心地阅读到这里,你的支持是我不断前行的最大动力。如果你觉得这篇文章对你有所启发,哪怕只是一点点,那就请不吝点赞👍,收藏⭐️,关注🚩吧!你的每一个点赞都是对我最大的鼓励,每一次收藏都是对我努力的认可,每一次关注都是对我持续创作的鞭策。希望我的文字能为你带来更多的价值,也希望我们能在这个充满知识与灵感的旅程中,共同成长,一起进步。如果本篇文章有错误,还请大佬多多指正,再次感谢你的陪伴,期待与你在未来的文章中再次相遇!⛅️🌈 ☀️