一、进程等待是什么
通过系统调用wait/waitpid,来进行对子进程进行状态检测与回收的功能
二、进程等待必要性
子进程退出,父进程如果不管不顾,就可能造成"僵尸进程"的问题,进而造成内存泄漏。另外,进程一旦变成僵尸状态,那就没有办法去杀死,kil -9 也无能为力,因为谁也没有办法杀死一个已经死去的进程。
最后,父进程派给子进程的任务完成的如何,我们需要知道。如,子进程运行完成,结果对还是不对,或者是否正常退出。
父进程通过进程等待的方式,回收子进程资源,获取子进程退出信息
三、进程等待的方法
3.1wait
通过让父进程调用wait/waitpid对僵尸进程的等待来达到对僵尸进程的一个回收
wait函数返回值:
成功返回子进程的pid,失败则返回-1
wait函数参数:
输出型参数,获取子进程退出状态,不关心则可以设置为NULL
1 #include <stdio.h>
2 #include <unistd.h>
3 #include <stdlib.h>
4 #include <sys/types.h>
5 #include <sys/wait.h>
6 int main()
7 {
8 pid_t id=fork();
9 if(id<0)
10 {
11printf("进程创建失败\n");
12 }
13 else if(id==0)
14 {
15//子进程
16int cnt=5;
17while(cnt--)
18{
19 printf("i am child pid is %d, ppid is %d,cnt is %d\n",getpid(),getppid(),cnt);
20 sleep(1);
21}
22exit(0);
23 }
24 else if(id>0)
25 {
26//父进程
27int cnt =10;
28while(cnt--)
29{
30 printf("i am father pid is %d, ppid is %d,cnt is %d\n",getpid(),getppid(),cnt);
31 sleep(1);
32}
33pid_t ret=wait(NULL);
34 if(ret==id)
35 {
36 printf("recycle success, ret is %d\n",ret);
37 }
38 sleep(5);
39 }
40 return 0;
41 }
父子进程先运行五秒,子进程先于父进程退出,子进程陷入僵尸状态,父进程再运行五秒钟后退出while函数,回收子进程
目前为止,进程等待是必须的
3.2wait等待多个子进程
wait是等待任意一个子进程退出的
1 #include <stdio.h>
2 #include <unistd.h>
3 #include <stdlib.h>
4 #include <sys/types.h>
5 #include <sys/wait.h>
6 #define N 5
7 void runchild()
8 {
9 int cnt=5;
10 while(cnt)
11 {
12 printf("i am child pid is %d, ppid is %d\n",getpid(),getppid());
13 sleep(1);
14 cnt--;
15 }
16 }
17 int main()
18 {
19 for(int i=0;i<N;i++)
20 {
21 pid_t id=fork();
22 if(id==0)
23 {
24 runchild();
25 exit(0);
26 }
27 printf("run child success child pid is%d\n",id);
28 }
29 sleep(10);
30 for(int i=0;i<N;i++)
31 {
32 pid_t ret=wait(NULL);
33 if(ret)
34 {
35 printf("wait success pid is %d\n",ret);
36 }
37 }
38 sleep(5);
39 return 0;
40 }
通过循环来创建一批子进程,同时子进程去执行自己的任务,子进程结束之后退出,否则子进程会继续进入循环创建子进程,为了观察僵尸状态我们要先让父进程等待几秒钟时间,再回收
3.2.1阻塞等待
上面所述的是子进程会僵尸的情况,那么子进程永远不死呢
如果子进程不退出父进程也不退出,父进程在默认调用wait的时候,也就不返回,默认叫做阻塞状态
3.3waitpid
wait所能提供的作用是waitpid的子集
waitpid返回值
成功返回子进程的pid
如果options设置为WORNING,而调用中waitpid发现没有已退出的子进程可收集,则返回0
如果调用中出错,则返回-1,这时errno会被设置成相应的值以指示错误所在
参数
pid=-1,等待任意一个子进程
pid>0,等待pid=子进程ID
status
WIFEXITED:若为正常终止子进程返回的状态,则为真.(查看进程是否是正常退出)
WEXITSTATUS(若WIFEXITED非零,提取子进程退出码.(查看进程的退出码)
options
WNOHANG: 若pid指定的子进程没有结束,则waitpid()函数返回0,不予以等待。若正常结束,则返回该子进程的ID。
pid_t ret=waitpid(-1,NULL,0);
由此可见waitpid和wait的效果是类似的
3.4status
status保存着进程退出时候的退出信息
pid_t waitpid(pid_t pid, int *status, int options);
接下来就让我来带大家详细剖析一下这个status
首先父进程等待子进程退出之前,我们需要先定义一个status整型变量用来存储子进程的退出信息(退出码)
waitpid函数里面的status是int*,由此可见里面保存着子进程退出信息的地址,取整型的地址,它的类型就是int*
41 int main()
42 {
43 pid_t id=fork();
44 if(id<0)
45 {
46printf("进程创建失败\n");
47 }
48 else if(id==0)
49 {
50//子进程
51int cnt=5;
52while(cnt--)
53{
54 printf("i am child pid is %d, ppid is %d,cnt is %d\n",getpid(),getppid(),cnt);
55 sleep(1);
56}
57exit(1);
58 }
59 else if(id>0)
60 {
61//父进程
62int cnt =10;
63while(cnt--)
64{
65 printf("i am father pid is %d, ppid is %d,cnt is %d\n",getpid(),getppid(),cnt);
66 sleep(1);
67}
68int status=0;
69pid_t ret=waitpid(id,&status,0);
70 if(ret==id)
71 {
72 printf("recycle success, ret is %d,status is %d\n",ret,status);
73 }
74 sleep(5);
75 }
76 return 0;
77 }
子进程的退出码为1,status的退出信息却是256,这是什么一回事呢
3.4.1父进程等待,期望得到子进程的哪些信息呢
进程退出有三种场景,运行成功结果正确,运行成功结果不正确,代码异常
父进程关心子进程
1.子进程代码是否异常
2.没有异常,结果是否正确,退出码不对是因为什么原因
不同的退出码,表示不同的出错原因
所以父进程关心如此多的子进程退出信息这就注定status不能单单被看作是一个整数要被划分为多个部分
3.4.2status的划分
整数一共有32个bit位,我们谈论的是后16位
首先异常终止的信息保存到了第7位,core dump等到了信号再做处理,
这就能解释我们信号为什么只有64位以及为什么没有0号信号
1.算到2^6刚好是64位
2.没有0号信号的原因就是进程先检查status的后7位是否为0,通过按位与等方法,不等于0进程异常退出就可以检验是收到了几号信号,等于0就可以检验正常终止情况下的退出信息也就是进程的退出码是多少
3.由于此次设置的退出码为1,在2^8刚好就是256对应了退出码为1
3.4.3为什么一定要调用waitpid
如果通过定义全局变量的方式,子进程对status进行修改,但是父子进程是具有独立性的,子进程对变量修改父进程是看不到的,所以要通过系统调用也就是操作系统来拿
3.4.4正确划分status
41 int main()
42 {
43 pid_t id=fork();
44 if(id<0)
45 {
46printf("进程创建失败\n");
47 }
48 else if(id==0)
49 {
50//子进程
51int cnt=5;
52while(cnt--)
53{
54 printf("i am child pid is %d, ppid is %d,cnt is %d\n",getpid(),getppid(),cnt);
55 sleep(1);
56}
57exit(1);
58 }
59 else if(id>0)
60 {
61//父进程
62int cnt =10;
63while(cnt--)
64{
65 printf("i am father pid is %d, ppid is %d,cnt is %d\n",getpid(),getppid(),cnt);
66 sleep(1);
67}
68int status=0;
69pid_t ret=waitpid(id,&status,0);
70 if(ret==id)
71 {
72 printf("recycle success, ret is %d,exit sig is %d,exit code is %d\n",id,status&0x7F,(status>>8)&0xFF);
73 }
74 sleep(5);
75 }
76 return 0;
77 }
0x7F是0111 1111按位与可以得到后7位上为1的数字
0xFF是1111 1111右移八位可以得到退出码上为1的数字
我们再来验证异常
[wyx@hcss-ecs-000a proc_wait]$ kill -9 7329
很显然这是收到了9号信号
3.4.5status原理
子进程退出的时候代码和数据可以释放,但是子进程的PCB要保留下来,里面保存着子进程的退出信息和异常信息,sigcode和exitcode通过按位与等操作拼成了status,
CPU调度父进程的时候,父进程通过调用waitpid系统调用去检查子进程是否是Z状态(僵尸状态)是僵尸状态,如果是,将Z状态改为X状态并提取错误信息和错误码
父进程不直接去拿子进程的,而是通过操作系统的手的原因是进程间都是独立的,父进程本质也是用户的操作,让用户直接去操作本身就是不安全的
3.4.6等待失败
当父进程等待不属于自己的子进程pid是就会等待失败
41 int main()
42 {
43 pid_t id=fork();
44 if(id<0)
45 {
46printf("进程创建失败\n");
47 }
48 else if(id==0)
49 {
50//子进程
51int cnt=5;
52while(cnt--)
53{
54 printf("i am child pid is %d, ppid is %d,cnt is %d\n",getpid(),getppid(),cnt);
55 sleep(1);
56}
57exit(1);
58 }
59 else if(id>0)
60 {
61int cnt =10;
62while(cnt--)
63{
64 printf("i am father pid is %d, ppid is %d,cnt is %d\n",getpid(),getppid(),cnt);
65 sleep(1);
66}
67int status=0;
68pid_t ret=waitpid(id+2,&status,0);
69 if(ret==id)
70 {
71 printf("recycle success, ret is %d,exit sig is %d,exit code is %d\n",id,status&0x7F,(status>>8)&0xFF);
72 }
73 else{ 74 printf("wait failed\n");
75 }
76 sleep(2);
77 }
78 return 0;
79 }
3.4.7status的宏函数
WIFEXITED:若为正常终止子进程返回的状态,则为真.(查看进程是否是正常退出)
WEXITSTATUS(若WIFEXITED非零,提取子进程退出码.(查看进程的退出码)
因为0x7F和0xFF有可能我们也不是很熟悉
WIFEXITED就相当于检查进程是否有异常退出
WEXITSTATUS就是检查进程的退出码
if(WIFEXITED(status))
70 {
71 printf("exit success,exit code is %d\n",WEXITSTATUS(status));
72 // printf("recycle success, ret is %d,exit sig is %d,exit code is %d\n",id,status&0x7F ,(status>>8)&0xFF);
73 }
else
74 printf("进程出异常了\n");
75}
单进程是在你waitpid已经将子进程的Z状态改成了X状态,等待成功了,再检验status里面的信息
多进程也是如此
7 void runchild()
8 {
9 int cnt=3;
10 while(cnt)
11 {
12 printf("i am child pid is %d, ppid is %d\n",getpid(),getppid());
13 sleep(1);
14 cnt--;
15 }
16 }
17 int main()
18 {
19 for(int i=0;i<N;i++)
20 {
21 pid_t id=fork();
22 if(id==0)
23 {
24 runchild();
25 exit(0);
26 }
27 printf("run child success child pid is%d\n",id);
28 }
29 sleep(3);
30 for(int i=0;i<N;i++)
31 {
32 int status=0;
33 pid_t ret=waitpid(-1,&status,0);
34 if(ret>0)
35 {
36 if(WIFEXITED(status))
37 {
38 printf("wait success pid is %d,exit code is %d\n",ret,WEXITSTATUS(status));
39
40 }
41 }
42 }
43 sleep(2);
44 return 0;
45 }
如果是bash进程看的就是你main函数的退出码
3.5options–非阻塞轮询
WNOHANG: 若pid指定的子进程没有结束,则waitpid()函数返回0,不予以等待。若正常结束,则返回该子进程的ID。
在默认option是0的情况下,当子进程循环持续运行的时候,父进程只能阻塞等待,等待子进程运行结束回收,那么这种情况下父进程就会被投入子进程的等待队列里面,子进程结束的时候操作系统会将父进程从子进程的等待队列中唤醒
3.5.1非阻塞轮询
那么干等这种情况是非常拖慢运行效率的
父进程可以向子进程发起询问,询问子进程是否结束,检查不成功立马返回这叫做非阻塞,一直向子进程询问这就是非阻塞轮询
但是非阻塞轮询也是十分少见的,由上面的话语我们也可以感觉到这也是拖慢运行效率的
我们父进程可以隔一段时间向子进程发起询问,在这一段时间父进程可以去执行一些其他的任务
waitpid的返回值大于0,表示父进程等待成功,子进程结束
waitpid的返回值小于0,表示父进程等待失败
waitpid的返回值等于0,表示父进程问过子进程还没结束,返回0,表示还要再等
46 int main()
47 {
48 pid_t id=fork();
49 if(id<0)
50 {
51printf("进程创建失败\n");
52 }
53 else if(id==0)
54 {
55//子进程
56int cnt=5;
57while(cnt--)
58{
59 printf("i am child pid is %d, ppid is %d,cnt is %d\n",getpid(),getppid(),cnt);
60 sleep(1);
61}
62exit(1);
63 }
64 else if(id>0)
65 {
66int status=0;
67while(1)
68{
69pid_t ret=waitpid(id,&status,WNOHANG); 70if(ret>0)
71{
72 if(WIFEXITED(status))
73 {
74 printf("exit success,exit code is %d\n",WEXITSTATUS(status));
75 // printf("recycle success, ret is %d,exit sig is %d,exit code is %d\n",id,status&0x7F,(status>>8)&0xFF);
76 break;
77 }
78 else
79 printf("进程出异常了\n");
80}
81 else if(ret<0){
82 printf("wait failed\n");
83 break;
84 }
85 else if(ret==0)
86 {
87 printf("子进程还没结束,再等\n");
88 }
89 sleep(2);
90 }
91 }
92 return 0;
93 }
3.5.2测试任务
46 #define NUM 5
47 typedef void(*task_t)();
48 task_t TASK[NUM];
49 void task1()
50 {
51 printf("测试网络,pid is %d\n",getpid());
52 }
53 void task2()
54 {
55 printf("写日志,pid is %d\n",getpid());
56 }
57 void task3()
58 {
59 printf("检查内存,pid is %d\n",getpid());
60 }
61 int AddTask(task_t t);
62 void InitTask()
63 {
64 for(int i=0;i<NUM;i++) TASK[i]=NULL;
65 AddTask(task1);
66 AddTask(task2);
67 AddTask(task3);
68 }
69 int AddTask(task_t t)
70 {
71 int pos=0;
72 for(;pos<NUM;pos++)
73 {
74 if(!TASK[pos]) break;
75 }
76 if(pos==NUM) return -1;
77 TASK[pos]=t;
78 return 0;
79 }
80 void EXCUTE()
81 {
82 int pos=0;
83 for(;pos<NUM;pos++)
84 {
85if(TASK[pos])
86 TASK[pos]();
87 }
88 }
89 int main()
90 {
91 pid_t id=fork();
92 if(id<0)
93 {
94printf("进程创建失败\n");
95 }
96 else if(id==0)
97 {
98//子进程
99int cnt=5;
100while(cnt--)
101{
102 printf("i am child pid is %d, ppid is %d,cnt is %d\n",getpid(),getppid(),cnt);
103 sleep(1);
104}
105exit(1);
106 }
107 else if(id>0)
108 {
109int status=0;
110 InitTask();
111while(1)
112{
113pid_t ret=waitpid(id,&status,WNOHANG);
114if(ret>0)
115{
116 if(WIFEXITED(status))
117 {
118 printf("exit success,exit code is %d\n",WEXITSTATUS(status));
119 // printf("recycle success, ret is %d,exit sig is %d,exit code is %d\n",id,status&0x7F,(status>>8)&0xFF);
120 break;
121 }
122 else
123 printf("进程出异常了\n");
124}
125 else if(ret<0){
126 printf("wait failed\n");
127 break;
128 }
129 else if(ret==0)
130 {
131 EXCUTE();
132 usleep(100000);
133// printf("子进程还没结束,再等\n");
134 }
135 }
136 }
137 return 0;
138 }
但是我们只是在父进程等待子进程中执行任务,所以等待子进程才是主要任务,父进程在这期间执行的任务只能是轻量化的任务
注意:waitpid能够保证父进程是最后一个退出的进程(子进程全部被回收)