进程需要等待的原因
- 子进程退出,父进程如果不管,就会导致子进程变成僵尸进程,进而导致内存泄漏
- 进程一旦变成僵尸进程,使用kill -9信号也无法杀死,因为无法对一个死去的进程进行操作。
- 同时,子进程完成自己的任务后,需要父进程获取到子进程任务完成的结果。
所以,父进程可以通过进程等待的方式回收子进程的资源,从而获取子进程的退出信息。
僵尸进程无法被杀死,需要进程等待来杀死,进而解决内存泄漏问题(必须解决)。因此需要通过进程等待来获得子进程的退出情况(可选的需求)。
进度等待的概念
通过系统调用wait()或者waitpid(),来对子进程进行状态检测与回收的功能。
进程等待的方法
cpp
int main()
{
// 创建子进程
pid_t id = fork();
if(id < 0)
{
// 进程创建失败
perror("fork fail!\n");
return 1;
}
else if(id == 0)
{
// 子进程
int cnt = 5;
while(cnt)
{
printf("child process : pid->%d ; ppid->%d ; cnt->%d\n", getpid(), getppid(), cnt);
sleep(1);
cnt--;
}
// 进程终止
exit(0);
}
else
{
// 父进程
while(1)
{
printf("father process : pid->%d ; ppid->%d\n", getpid(), getppid());
sleep(1);
}
}
return 0;
}
【perror知识点】fork创建子进程失败,会修改errno值,perror()可以将错误码描述输出出来。

父进程通过调用wait()或者waitpid()进行僵尸进程的回收问题!

cpp
int main()
{
// 创建子进程
pid_t id = fork();
if(id < 0)
{
// 进程创建失败
perror("fork fail!\n");
return 1;
}
else if(id == 0)
{
// 子进程
int cnt = 5;
while(cnt)
{
printf("child process : pid->%d ; ppid->%d ; cnt->%d\n", getpid(), getppid(), cnt);
sleep(1);
cnt--;
}
// 进程终止
exit(0);
}
else
{
// 父进程
int cnt = 10;
while(cnt)
{
printf("father process : pid->%d ; ppid->%d ; cnt->%d\n", getpid(), getppid(), cnt);
sleep(1);
cnt--;
}
// 回收子进程??参数设置为NULL??返回值为子进程pid
pid_t ret = wait(NULL);
if(ret == id)
{
printf("wait success\n");
}
}
return 0;
}

所以------进程等待是创建进程后必须存在的一种工作。
【注意】如果进程被创建出了很多个,wait会将哪一个进程回收?wait会等待任意一个子进程退出。
wait随机等待任意子进程
cpp
1 #include<stdio.h>
2 #include<unistd.h>
3 #include<stdlib.h>
4 #include<sys/types.h>
5 #include<sys/wait.h>
6
7 #define N 10
8
9 // 子进程需要执行的任务
10 void ChildRun(int i)
11 {
12 int cnt = 5;
13 while(cnt)
14 {
15 printf("child[%d]-running-pid : %d ; ppid : %d ; cnt : %d\n", i, getpid(), getppid(), cnt);
16 sleep(1);
17 cnt--;
18 }
19 }
20
21 int main()
22 {
23 // 父进程创建10个子进程
24 for(int i = 0; i < N; ++i)
25 {
26 pid_t id = fork();
27 if(id == 0)
28 {
29 // 创建子进程成功
30 // 子进程执行任务
31 ChildRun(i);
32 // 终止当前子进程
33 exit(1);
34 }
35 }
// 此时10个子进程均创建成功
38 printf("child fail success\n");
39
40 // 让10个子进程均变成僵尸进程
41 sleep(10);
42
43 // 父进程开始回收子进程,wait()随机等待子进程
44 for(int i = 0; i < N; ++i)
45 {
46 // 随机等待一个子进程
47 pid_t ret = wait(NULL);
48 if(ret > 0)
49 {
50 // 当前子进程回收成功
51 printf("child %d wait success\n", ret);
52 }
53 }
54 sleep(5); // 父进程进行执行
55
56 return 0;
57 }


【注意】父进程是随机等待子进程的
子进程不退出父进程一直等待子进程

子进程中如果有一个或者多个不退出,父进程会一直等待回收子进程,并不会进行后续的工作,父进程会一直在wait进行阻塞等待。
- 如果子进程不退出,父进程默认在wait的时候,在调用这个系统调用的时候,也就不返回,默认叫做阻塞状态。
- 阻塞状态不仅仅只有等待硬件(cin输入流),也会存在等待软件的情况(等待子进程退出)。
子进程的退出结果------waitpid
wait提供的功能是waitpid所提供的功能的子集。

pid_t waitpid(pid_t pid, int *status, int options);
返回值:
- 当正常返回的时候waitpid返回等待成功的子进程的进程ID。
- 如果参数options设置了WHOHANG,而调用waitpid后发现没有子进程可以等待,则返回0;
- 如果调用失败,返回-1,这时会将errno设置成相应的值以指示错误所在。
参数:
- pid :
- pid=-1,等待任意一个子进程,与wait效果相同
- pid>0,等待其进程ID与pid相等的子进程,相当于可以选择某一个子进程退出。
- status:
- WIFEXITED(status):若为正常终止子进程返回的状态,则为真。(查看进程是否正常退出)
- WEXITSTATUS(status):若WIFEXITED非零,提取子进程退出码。(查看进程的退出码)
- options:
- WNOHANG:若pid指定的子进程没有结束,则waitpid()j函数返回0,不予以等待。若正常结束,则返回该子进程的ID。

【测试】将上述代码中的wait替换成waitpid,其中第一个参数设置成-1,效果是相同的。
cpp
99 int main()
100 {
101 // 创建子进程
102 pid_t id = fork();
103 if(id < 0)
104 {
105 // 进程创建失败
106 perror("fork fail!\n");
107 return 1;
108 }
109 else if(id == 0)
110 {
111 // 子进程
112 int cnt = 5;
113 while(cnt)
114 {
115 printf("child process : pid->%d ; ppid->%d ; cnt->%d\n", getpid(), getppid() , cnt);
116 sleep(1);
117 cnt--;
118 }
119 // 进程终止
120 exit(0);
121 }
122 else
123 {
124 // 父进程
125 int cnt = 10;
126 while(cnt)
127 {
128 printf("father process : pid->%d ; ppid->%d ; cnt->%d\n", getpid(), getppid( ), cnt);
129 sleep(1);
130 cnt--;
131 }
132 // 回收子进程??参数设置为NULL??返回值为子进程pid
133 // pid_t ret = wait(NULL);
134 pid_t ret = waitpid(id, NULL, 0);
135 if(ret == id)
136 {
137 printf("wait success\n");
138 }
139 }
140
141 return 0;
142 }
第一个参数设置成子进程id可以选择性的等待进程。
获取子进程的退出信息
是否需要获取资金的退出信息,这件事情是可选的,如果不需要获取,直接在wait或者waitpid的status参数设置成为NULL即可。
++如何理解status指针?++

- status指针是一个输出形参数
- int 是被当作多个部分使用的

++问题1:子进程退出一共会有几种退出场景?++
【答】:三种(1)代码运行完毕,结果正确;(2)代码运行完毕,结果不正确;(3)代码异常终止。
++问题2:父进程等待中,期望获得子进程退出的哪些信息?++
【答】:(1)代码是否异常;(2)没有异常,结果是否正常,通过exitcode来获取退出信息;(3)用1,2,3,4....表示不同的出错原因

status这个退出信息,需要考虑三个因素:(1)退出是否正常;(2)不正常退出,原因是合适;(3)正常退出,结果是否正常。status是int变量,有32个bit位置,只需要关注低16位即可。
- 前0到7个比特位置(除了第8个位置core dump后续讲解),是描述进程异常退出的情况,进程退出一定是收到了某种信号,才会退出。
- 前8到15个bit位是描述进程正常退出的退出状态,也就是子进程退出时exit的退出信息。(上面我们的exit退出码是1,将1放在第8个比特上,也就是256)
++问题3:父进程需要获取子进程的状态数据的任意数据,为什么必须要使用wait系统调用呢?为什么不能使用全局变量?++
【答】:不能使用全局变量,是因为进程具有独立性,当子进程修改了全局变量,父进程的该全局变量的值并不会修改。只能通过系统调用获取相对应的进程状态信息。

【总结】status可以通过位图的操作,反应进程是否异常退出以及退出时的情况
使用系统设置的宏来获取进程退出的信息
【系统设置status的宏】
- WIFEXITED(status):若为正常终止子进程返回的状态,则为真。(查看进程是否正常退出)
- WEXITSTATUS(status):若WIFEXITED非零,提取子进程退出码。(查看进程的退出码)


进程退出的原理

非阻塞轮询
每一个子进程的PCB中也有一个等待队列,当父进程想要回收子进程的时候,子进程就会让父进程处于该等待队列中,waitpid如果一直回收不到子进程,就会一直处于阻塞等待状态。而waitpid第三个参数options可以选择父进程等待的方式。

当options设置成为这个宏WNOHANG的时候,返回值pid_t就会多一种选择:
- pid_t ret > 0 ; 等待成功
- pid_t ret < 0 ; 等待失败
- pid_t ret == 0; 代表所等待的条件还没有就绪
cpp
int main()
{
// 创建子进程
pid_t id = fork();
if(id < 0)
{
// 进程创建失败
perror("fork fail!\n");
return 1;
}
else if(id == 0)
{
// 子进程------10s的操作
int cnt = 10;
while(cnt)
{
printf("child process : pid->%d ; ppid->%d ; cnt->%d\n", getpid(), getppid(), cnt);
sleep(1);
cnt--;
}
// 进程终止
exit(1);
}
else
{
// 父进程
int cnt = 3;
while(cnt)
{
printf("father process : pid->%d ; ppid->%d ; cnt->%d\n", getpid(), getppid(), cnt);
sleep(1);
cnt--;
}
// 回收子进程------参数设置为NULL------返回值为子进程pid
// pid_t ret = wait(NULL);
// 设置子进程的退出信息
while(1)
{
int status = 0;
pid_t ret = waitpid(id, &status, WNOHANG);// 设置轮询等待
if(ret > 0)
{
//printf("wait success, ret : %d , exit sig : %d ; exit code : %d \n",
//ret, status & 0x7F, (status >> 8) & 0xFF);
// 等待成功
// 使用系统的宏来判断进程是否正常终止
if(WIFEXITED(status))
{
printf("进程是正常退出的,退出码:%d\n", WEXITSTATUS(status));
}
else
{
printf("进程异常终止.\n");
}
break;
}
else if(ret < 0)
{
printf("等待子进程失败!\n");
break;
}
else
{
// 子进程还在运行,父进程可以先做自己的事情一会等待
printf("父进程做自己的事情,一会等待子进程.\n");
sleep(1);
}
}
}
printf("所有任务完成\n");
return 0;
}

上述代码演示了父进程轮询等待子进程。父进程等待子进程,子进程没有终止,父进程会返回0,让父进程处于循环中,再次循环等待。
