什么是进程等待
如果子进程结束父进程没有对子进程进行处理的话,子进程就会变成一个僵尸进程处于这个状态的进程无法被kill指令杀死因为你无法杀死一个已经死去的进程,虽然这个进程的数据和代码已经被操作系统删除,但是该进程的PCB中还存储着各种退出信息所以它还一直存储在内存中等待着被父进程处理,如果父进程一直运行并且不进行处理话那么这就是一个内存泄漏的现象因为PCB也是占空间的,所以为了解决内存泄漏的问题为了查看进程的运行结果如何就有了进程等待这个东西,它可以释放PCB所占用的空间,并且还会读取PCB中的数据拿到子进程的退出信息。实现进程等待有两个方式:一个是调用wait函数另外一个是waitpid函数,这两个函数都可以处理子进程,但是他们两的用法和处理的方式有点点不同,那么接下来我们来看看这两个函数的用法。
wait
wait函数的形式如下:
#include<sys/types.h>
#include<sys/wait.h>
pid_t wait(int*status);
使用这个函数得用到两个头文件并且该函数的返回值类型为pid_t,当进程等待成功之后wait函数就会返回读取的子进程pid,wait函数的作用是读取子进程的退出信息,所以我们需要传给他一个参数用来记录他读取的信息,并且这个参数的类型是一个整型指针,如果大家不想读取子进程的运行结果的话就可以直接传递一个空指针给这个函数,比如说下面的代码:
1 #include<stdio.h>
2 #include<unistd.h>
3 #include<sys/types.h>
4 #include<sys/wait.h>
5 #include<stdlib.h>
6 int main()
7 {
8 pid_t id=fork();
9 if(id==0)
10 {
11 int cnt=10;
12 while(cnt)
13 {
14 printf("我是子进程:%d,父进程:%d,cnt:%d\n",getpid(),getppid(),cnt--);
15 sleep(1);
16 }
17 exit(0);//进程退出
18 }
19 else
20 {
21 sleep(15);
22 pid_t id =wait(NULL);
23 if(id>0)
24 {
25 printf("等待成功\n");
26 }
sleep(5);
27 }
28 }
首先通过fork函数创建子进程,然后通过if else语句让父子进程执行后序代码的不同部分,子进程执行if语句里面的内容首先经历while循环,该循环会执行10次内部的内容每次都打印一句话并且休眠一秒,循环结束之后就调用exit函数来结束子进程,所以子进程的执行时间为10秒。父进程执行else语句里面的内容,首先执行sleep函数将父进程休眠15秒,然后再使用wait函数将子进程的内容进行回收并根据变量id值来判断是否回收成功,所以父进程一共会执行15秒钟。通过上面的分析我们知道子进程在前10秒钟会是阻塞状态,执行完代码之后子进程就结束了由于此时的父进程还在休眠没有对子进程的PCB进行处理,所以子进程就由阻塞状态变成了僵尸状态,等父进程的sleep函数执行完之后就会调用wait函数对子进程进行回收,所以这时子进程就会由僵尸状态变成了死亡状态,然后屏幕上就会打印等待成功,这里在运行代码之前我们得先写一段指令来帮我们不停的查看进程的状态,指令
ps ajx | head -1
可以显示环境变量的标题
ps ajx | grep myproc
指令可以帮我们查看所有与myproc有关的进程状态信息:
使用&&就可以将两个指令打印出来的信息连接起来ps ajx|head-1&& ps ajx|grep myproc
使用grep指令时grep也会将自己查找出来,所以这里还可以添加指令grep -v来删除显示的grep进程的信息
ps ajx|head -1&& ps ajx|grep myproc|grep -v grep
使用这条指令只会打印一次信息,但是这里我们得连续查看所以得这个执行进行循环执行,那么这里指令的形式就得是这样:while :; do ps ajx|head -1&& ps ajx|grep myproc|grep -v grep;sleep 1;done
好这里我们就将监视程序状态的指令写完了,这里我们将程序运行起来再启动监视指令就可以看到一开始两个进程的状态都是阻塞状态:
但是过了几秒就可以看到子进程变成了僵尸状态
再过个几秒父进程醒来使用wait函数将子进程进行回收这时显示的程序状态里面就不存在子进程了
那么这就是wait函数的作用他可以帮助我们回收已经结束的子进程,并且父进程使用wait函数回收子进程时采用的是阻塞式等待的方式也就是说当父进程执行到wait函数时如果子进程没有运行结束,那么父进程就会一直在那里等着,直到子进程运行结束wait函数将子进程处理完才执行父进程剩下的代码,比如说下面的代码:
1 #include<stdio.h>
2 #include<unistd.h>
3 #include<sys/types.h>
4 #include<sys/wait.h>
5 #include<stdlib.h>
6 int main()
7 {
8 pid_t id=fork();
9 if(id==0)
10 {
12 sleep(10);
13 exit(0);//进程退出
14 }
15 else
16 {
17 pid_t id =wait(NULL);
18 printf("子进程还没有运行结束\n");
19 if(id>0)
20 {
21 printf("等待成功\n");
22 }
23 }
24 }
这段代码子进程的执行时间为10秒钟但是父进程一开始就执行wait函数,并且wait函数执行完之后就会使用printf函数打印一句话,因为wait函数是阻塞式等待所以这段代码运行起来后边可以发现父进程一开始并不会执行printf语句,而是先等待子进程结束
过了10秒才会显示打印出来的内容:
那么这就是阻塞式等待他会使得父进程进程一直卡在wait函数那里直到子进程结束。上面的代码只创建一个子进程所以wait函数回收的就是那个子进程的数据,当内存中存在多个子进程时wait函数回收的就是最先结束的子进程,比如说下面的代码:
1 #include<stdio.h>
2 #include<unistd.h>
3 #include<sys/types.h>
4 #include<sys/wait.h>
5 #include<stdlib.h>
6 int main()
7 {
8 pid_t id=fork();
9 if(id==0)
10 {
11 int i=5;
12 while(i)
13 {
14 printf("我是先结束的子进程我的pid为:%d\n",getpid());
15 --i;
16 sleep(1);
17 }
18 }
19
20 else
21 {
22 pid_t id1=fork();
23 if(id1==0)
24 {
25 int i=10;
26 while(i)
27 {
28 printf("我是后结束的子进程我的pid为:%d\n",getpid());
29 --i;
30 sleep(1);
31
32 }
33 }
34 else
{
37 pid_t id =wait(NULL);
38 printf("子进程的pid为:%d\n",id);
41 }
42 }
43 }
这段代码的运行结果如下:
那么这里就可以证明wait函数的作用就是回收最先结束的子进程,wait函数有个参数,当我们不想查看子进程的运行结果时这个参数就可以传一个空指针,如果你想查看子进程的运行结果的话你就得先创建一个整型变量然后将这个变量的地址传递给函数,然后他就会将子进程退出的信息放到你给的这个变量里面,我们把这种参数称之为输出型参数,比如说下面的代码
1 #include<stdio.h>
2 #include<unistd.h>
3 #include<sys/types.h>
4 #include<sys/wait.h>
5 #include<stdlib.h>
6 int main()
7 {
8 pid_t id=fork();
9 if(id==0)
10 {
11 exit(0);//进程退出
12 }
13 else
14 {
15 int num=0;
16 pid_t id =wait(&num);
17 printf("子进程的pid为:%d\n",id);
18 printf("子进程的信息为:%d\n",num);
19
20 }
21 }
这段代码的运行结果如下:
因为在子进程的代码里面直接使用exit函数结束了进程并且参数是0所以这里显示的信息就为0,我们知道exit函数和_exit函数里面的参数表示的是进程运行结束的退出码并且退出码0表示的意思也恰好是运行没有错误,那这里打印的num的值代表的是退出码吗?我们将代码修改一下将exit(0)改成exit(10)再来运行一下程序就会发现这里打印的结果不是10而是一个非常大的数:
那么这就说明这个变量里面装的信息并不是进程的退出码而是一些其他信息,一个整型变量的大小为4个字节,每个字节的大小为8个比特位,所以一个整型变量的大小为32个字节,其中操作系统只使用32个字节的前的16位是被用来记录数据的
并且后这16个比特位也不是全部用来记录退出信息而是第9到第16位记录退出码的信息:
第8位用来记录core dump标志,这里大家暂时不用管
第1位到第七位用来记录程序的终止信号,因为程序在执行的过程的中可能会遇到异常或者错误导致程序终止,所以这里的后七位就记录着程序异常的信息,如果没有异常的话后七位都为0如果有异常的话后七位就会记录对应的异常信息并且记录退出码的位置就全部变成了0,那这里我们要想查看进程的退出码和终止信号的话就得对这个变量进行拆分,将变量按位与上0x7F就可以得到变量的后七位status&0x7F
,将变量往右移动8个比特位然后再按位与上0xFF就可以得到变量的第9位到第16位(status>>8)&0xFF
那这里我们对上述的代码进行修改改成这样:
1 #include<stdio.h>
2 #include<unistd.h>
3 #include<sys/types.h>
4 #include<sys/wait.h>
5 #include<stdlib.h>
6 int main()
7 {
8 pid_t id=fork();
9 if(id==0)
10 {
11 exit(10);//进程退出
12 }
13 else
14 {
15 int num=0;
16 pid_t id =wait(&num);
17 printf("子进程的pid为:%d\n",id);
18 printf("子进程的异常信号为:%d\n",num&0x7F);
19 printf("子进程的退出码位:%d\n",(num>>8)&0xFF);
20
21 }
22 }
这段代码的运行结果如下:
我们可以看到这里打印出来退出码确实变成了10并且异常信号也为0,我们将上述子进程的代码进行修改让其故意爆出异常:
8 pid_t id=fork();
9 if(id==0)
10 {
11 int *p=NULL;
12 *p=10;//越界访问
13 }
然后再将代码运行一下就可以看到这里退出码变成了0,异常信号变成了一个数
使用kill -l指令就可以看到所有异常所对应的数字:
11对应的是SIGSEGV表示的就是段异常也就是因为子进程中的越界访问导致的异常,其中数字9就是我们平时使用的杀死进程的指令,将子进程进行修改改成一个死循环:
9 if(id==0)
10 {
11 while(1)
12 {
13 printf("我是子进程的pid为:%d\n",getpid());
14 sleep(1);
15 }
16 }
执行程序就可以看到屏幕上面在不停的打印数据:
然后使用kill -9 16820将子进程杀死就可以看到父进程打印出来的数据:退出码为0进程异常码为9
那么这就是wait函数的全部使用方法希望大家能够理解。
waitpid
wait函数默认是哪个子进程先结束就回收哪个子进程,那么waitpid函数则是回收指定的子进程,我们来看看这个函数的参数
waitpid函数有三个参数,第一个参数表示的是你要回收哪个子进程,第二个参数就是输出型参数该函数会将读到的信息全部放到这个参数里面,与wait函数一样如果你不想获取退出信息的话这里的参数就传空指针,第三个参数表示的是等待方式如果你想使用阻塞等待的话第三个参数就传0,如果想要非阻塞等待的话这里就传WNOHANG,我们来通过下面的代码来理解一下这里的函数
7 {
8 pid_t id1=fork();
9 if(id1==0)
10 {
11 int i=5;
12 while(i)
13 {
14 printf("我是先结束的子进程我的id为:%d\n",getpid());
15 sleep(1);
16 --i;
17 }
18 }
19 else
20 {
21 pid_t id2=fork();
22 if(id2==0)
23 {
24 int i=10;
25 while(i)
26 {
27 printf("我是后结束的子进程我的id为:%d\n",getpid());
28 sleep(1);
29 --i;
30 }
31 }
32 else
33 {
34 pid_t pid1=waitpid(id2,NULL,0);
printf("先回收的进程为:%d\n",pid1);
36 pid_t pid2=waitpid(id1,NULL,0);
37 printf("后回收的进程为:%d\n",pid2);
38 }
39 return 0;
40 }
41 }
这里创建了两个子进程,但是在父函数里面却先使用waitpid函数以阻塞等待的方式回收后结束的子进程,所以这就会导致就算子进程已经跑完了也得等待waitpid函数将稍慢的子进程回收完之后再回收先结束的子进程,那么这段代码的运行结果如下:
那么这就是waitpid函数的用法希望大家能够理解。
使用宏来查看退出信息
上面是对变量进行转换来查看进程退出的信息
printf("子进程的异常信号为:%d\n",num&0x7F);
printf("子进程的退出码位:%d\n",(num>>8)&0xFF);
但是这样的查看方式很有点麻烦所以c语言就提供了两个宏方便我们查看进程退出的信息,这两个宏分别为:WIFEXITED
和WEXITSTATUS
,把输出型参数传递给两个宏,如果子进程的运行结果没有异常的话WIFEXITED
则返回真反之则为假,当判断完进程是否异常之后就可以使用WEXITSTATUS
来查看进程的进程运行结束的退出码,大家可以通过下面的代码来查看这里执行的过程:
3 #include<sys/types.h>
4 #include<sys/wait.h>
5 #include<stdlib.h>
6 int main()
7 {
8 pid_t id=fork();
9 if(id==0)
10 {
11 sleep(5);
12 printf("我是子进程我的id为:%d\n",getpid());
13 }
14 else
15 {
16 int num =0;
17 waitpid(id,&num,0);
18 if(WIFEXITED(num))
19 {
20 printf("子进程的退出码为:%d\n",WEXITSTATUS(num));
21 }
22 }
23 }
这段代码的运行结果如下:
那么这就是两个宏使用的方法希望大家能够理解。
非阻塞等待
上面讲的处理子进程的方法都是阻塞等待,这种方法处理子进程有个特点就是如果子进程没有运行完,那么父进程就会一直卡在wait函数或者waitpid函数那里,这样做的话会影响父进程的效率,所以为了解决这个问题就有了一个新的处理进程的方式叫做非阻塞等待,当子进程没有执行完时父进程不会一直卡在waitpid函数那里,而是继续往下执行其他的代码并且这个函数就会返回0,如果子进程结束了这个函数就会返回子进程的pid。这里可以用下面的例子带着大家理解一下这里的区别,在平时的生活中大家一定用过手机给其他人打电话,我们知道手机打电话的时候是没网的,所以在通话的时候手机不能干事情,我们平时在生活当中肯定等过一些人比如说等人一起出去玩,等人一起去上课等等,但是很多时候我们和另外一个人是见不到面的所以就通过打电话的方式来询问别人好了没,那么这里打电话就有两个方式一个是一直打电话并且不挂断电话,这时手机是没有网的如果我们要想使用手机干其他事情的话就只能等着另外一个人跟我说我好了我们出发吧这时再挂断电话用手机去干其他事情,那么这种情况就相当于阻塞等待,父进程的wait或者waitpid函数只能等着子进程运行完才能去干其他事情,而还有一种情况就是隔一段时间打一个电话,比如说现在打一个电话问好了没?他跟我说没好这时就挂断电话拿着手机去干其他事情,过了10分钟再打电话问好了没,他说没好的话就再挂断电话用手机去干其他的事情,过10分钟再打一个电话问好了没这样不停的循环下去,这就是非阻塞等待,当发现子进程还在运行时父进程就去干其他的事情不会一直在那等着,这样就可以提高父进程的效率不会占用父进程的资源,我们可以通过下面的代码来看看非阻塞等待的使用:
1 #include<stdio.h>
2 #include<unistd.h>
3 #include<sys/types.h>
4 #include<sys/wait.h>
5 #include<stdlib.h>
6 #include<string.h>
7 #define NUM 10
8 typedef void (*func_t)();
9 func_t handelerTask[NUM];
10 void task1()//具体的事情
11 {
12 printf("handler task1\n");
13 }
14 void task2()
15 {
16 printf("handler task1\n");
17 }
18 void task3()
19 {
20 printf("handler task1\n");
21 }
22 void loadTask()
23 {
24 memset(handelerTask,0,sizeof(handelerTask));
25 handelerTask[0]=task1;
26 handelerTask[1]=task2;
27 handelerTask[2]=task3;
28 }
29 int main()
30 {
pid_t id=fork();
32 if(id==0)
33 {
34 int cnt=2;
35 while(cnt)
36 {
37 printf("child running,pid:%d,cnt=%d\n",getpid(),cnt--);
38 sleep(1);
39 }
40 exit(10);
41 }
42 loadTask();
43 int status=0;
44 while(1)
45 {
46 pid_t ret =waitpid(id,&status,WNOHANG);
47 if(ret==0)
48 {
49 printf("wait done,but child is running,parent running other things\n");
50 for(int i=0;handelerTask[i]!=NULL;i++)
51 {
52 handelerTask[i]();
53 }
}
55 else if(ret>0)
56 {
57 printf("wait success,exit code:%d,sig:%d\n",(status>>8)&0xFF,status&0x7F);
58 break;
59 }
60 else
61 {
62 printf("waitpid call failed\n");
63 }
64 sleep(1);
65 }
66 }
这段代码的运行结果如下:
我们可以看到这里通过循环的方式不停的查看子进程运行的状态,当子进程还在运行时父进程就可以执行其他的事情,这里创建了一个函数指针数组,数组的每个元素都指向一个函数,在父进程等待子进程的时候就可以执行数组中的函数来执行其他的功能,一旦子进程运行结束就会执行else if语句里面的内容并通过break语句结束循环,那么这就是非阻塞等待的用法希望大家能够理解。