目录
1、概念说明
僵尸进程
在之前提到了进程状态中,有提到“子进程资源未被及时回收,就会变成僵尸进程”。那么僵尸进程是什么意思呢?
僵尸进程(Zombie Process)是指已经完成了执行任务的子进程,但是由于其父进程没有及时处理该子进程的完成状态,该子进程的进程控制块仍然被保留在系统中,占用了系统资源,但是无法被调用和执行任何任务,也无法被清除。
僵尸进程会一直停留在系统中,直到其父进程终止或者处理该子进程的完成状态。如果系统中存在大量的僵尸进程,可能会导致系统资源的浪费和性能下降。
为了避免僵尸进程,所以需要及时回收子进程资源。
2、进程回收wait
函数说明
##include<sys/types.h>//wait所需头文件
#include<sys/wait.h>//wait所需头文件
pid_t wait(int *status);
参数:
status:用于保存子进程的返回值和结束方式,是一个指向保存地址的指针。
(如果不关心子进程返回值,可以设为NULL,即不接收返回值)
tips:这里提到的返回值,指的是函数退出函数“void exit(int XXX)”中的XXX(可以理解为子进程完成任务(进程退出)时,会给你留下一封信(返回值:XXX),你可以利用wait()将子进程安葬,并指定一个盒子(status)来装留下的信,这样你就可以看看子进程留下了什么。)
返回值:
成功时,返回回收的子进程的进程号;失败时返回EOF
注意点:
需要注意的是:wait()是一个阻塞函数。
简单来说:如果子进程一直没结束,父进程会一直阻塞。(就和堵车一样,前面的车不走,后面的车就只能一直等待)
而且如果有多个子进程运行时,哪个先结束就先回收哪个子进程。
运行实例:
#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>//exit所需头文件
#include<sys/types.h>//wait所需头文件
#include<sys/wait.h>//wait所需头文件
int main(void){
pid_t pid;
int i;
int status;
pid = fork();
if(pid > 0){
//以下为父进程A内容
wait(&status);//用&status,存放子进程返回值
printf("This is father pthread ID = %d\n", getpid());
printf("ret = %d", status);//打印子函数返回值
}else if(pid == 0){
//以下为子进程B内容
for(i = 0; i < 3; i++){
printf("sleep %d\n", i);
sleep(1);
};//休眠3秒,检查父进程是否阻塞
printf("This is child pthread ID = %d\n", getpid());
exit(22);//子函数返回值设为22
}
程序思路:
1、利用fork()函数,先让父进程(简称A)创建子进程(简称B)。
2、子进程B:休眠3秒后,利用getpid()函数获取子进程进程号。然后利用exit()函数结束子进程B,并设置返回值为22。
3、父进程A:利用wait()函数回收子进程B,并用&status获取函数返回值(由于阻塞的原因,父函数A直到子函数B结束后,才会继续执行)。然后利用getpid()函数获取进程号,打印子进程返回值。
运行结果如下:
结果显示:
1、 父进程在等待子进程结束后,才运行。哪怕子进程休眠了3s。
2、ret = 5632不等于子进程设置的返回值22,那是函数出错了吗?
答:其实并不是,就像之前举的例子:盒子(status)确实装了留下里的信(返回值),但是盒子里面还有其他的信息,需要宏函数来判断取值(也就是说:信被压在盒子底,需要依次打开隔间才能找到它)。
补充说明:
wait函数中的status:当子进程正常回收时,返回相应返回值;异常回收时,返回相应信号值。
可以通过三组宏函数(盒子有三个隔间),每组都需要先判断(是否为真),再利用后续函数打印取值。他们分别是:
//第一组(重点):
WIFEXITED(status) //如果这个宏为真,代表: 进程正常结束,W表示wait,IF为如果
WEXITSTATUS(status) //如果上面的宏为真,可使用此宏来:获取进程退出状态 (exit的参数)
//第二组(重点):
WIFSIGNALED(status) //如果这个宏为真,代表:进程异常终止
WTERMSIG(status) //如果上面的宏为真,可使用此宏来:取得使进程终止的那个信号的编号。
//第三组(了解即可):
WIFSTOPPED(status) //如果这个宏为真,代表:进程处于暂停状态
WSTOPSIG(status) //如果上面的宏为真,可使用此宏来:取得使进程暂停的那个信号的编号。
WIFCONTINUED(status) //如果这个宏为真,代表:进程暂停后已经继续运行
修改后代码如下:
#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>//exit所需头文件
#include<sys/types.h>//wait所需头文件
#include<sys/wait.h>//wait所需头文件
int main(void){
pid_t pid;
int i;
int status;
pid = fork();
if(pid > 0){
//以下为父进程A内容
wait(&status);
printf("This is father pthread ID = %d\n", getpid());
//此段进行修改
if(WIFEXITED(status)){//判断第一个宏是否为真
printf("ret = %d\n", WEXITSTATUS(status));//利用宏函数打印子函数返回值
}
}else if(pid == 0){
//以下为子进程B内容
for(i = 0; i < 3; i++){
printf("sleep %d\n", i);
sleep(1);
};//休眠3秒,检查父进程是否阻塞
printf("This is child pthread ID = %d\n", getpid());
exit(22);//子函数返回值设为22
}
return 0;
}
程序思路:
对第一份代码进行修改,如下:
对父进程内容(21-23段)进行修改,采用WIFEXITED(status)判断是否为正常退出,使用WEXITSTATUS(status)获取子函数返回值
运行结果如下:
结果显示:
ret = 22,为预定的返回值。
3、线程回收waitpid
在上面我们介绍了使用wait()函数来进行进程回收,实际上是有很多不足的。还是拿堵车举例子:
1、阻塞问题
使用wait()回收进程时,子进程(B车)就相当于堵在了父进程(A车)前面。如果这个时候,B车设备故障熄火在路上了(即子进程出现bug,无法正常结束),A车就得一直在原地等(父进程阻塞)。
这种事情显然不是我们所希望看到的,那么能否有办法让父进程查看子进程是否退出?如果子进程退出就回收子进程,如果没退出就继续运行父进程呢?(即不阻塞)
2、无法指定回收的子进程
现有父进程(A车)、1号子进程(B车)、2号子进程(C车),1号子进程运行时间比2号子进程运行时间短(B车需走5min,走完后打电话给A车;C车需走10min,走完后打电话给A车)。
要求:父进程需要先回收2号子进程、再回收1号子进程(A车得先接C车的电话,再接B车的电话)。
可以看到wait由于B车先打电话,所以A车肯定先接到B车电话。那我们能否在B车打电话的时候,不接他的电话,只等C车的电话呢?(即父进程,只回收指定的子进程,忽略其他子进程)
那这就得介绍本次的主角:waitpid()函数
函数说明
##include<sys/types.h>//wait所需头文件
#include<sys/wait.h>//wait所需头文件
pid_t waitpid(pid_t pid, int *status, int option);
参数说明
pid | pid>0时,只等待进程ID等于pid的子进程。不管其它已经有多少子进程运行结束退出了,只要指定的子进程还没有结束,waitpid就会一直等下去。 pid=-1时,等待任何一个子进程退出,没有任何限制,此时waitpid和wait的作用一模一样。 pid=0时,等待同一个进程组中的任何子进程,如果子进程已经加入了别的进程组,waitpid不会对它做任何理睬。 pid<-1时,等待一个指定进程组中的任何子进程,这个进程组的ID等于pid的绝对值。 |
status | 采用宏函数调用,用法和wait()中相同,就不过多赘述了 |
options | WNOHANG :若由pid指定的子进程未发生状态改变(没有结束),则waitpid()不阻塞,立即返回0 WUNTRACED: 返回终止子进程信息和因信号停止的子进程信息 0:不设置 |
tips:wait(&status) 等价于 waitpid(-1, &status, 0)。其中int status
返回值
成功时,返回回收子进程的进程号;失败时,返回-1
运行实例:
#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>//exit所需头文件
#include<sys/types.h>//wait所需头文件
#include<sys/wait.h>//wait所需头文件
int main(void){
pid_t pid;
int i;
int status;
pid = fork();
if(pid > 0){
//以下为父进程A内容
waitpid(pid, &status, WNOHANG);//指定子进程号(pid返回子进程进程号)
//status:存放子进程返回值
//WNOHANG:设置为不阻塞
printf("This is father pthread ID = %d\n", getpid());
if(WIFEXITED(status)){//判断第一个宏是否为真
printf("ret = %d\n", WEXITSTATUS(status));//利用宏函数打印子函数返回值
}
}else if(pid == 0){
//以下为子进程B内容
for(i = 0; i < 3000; i++){
printf("sleep %d\n", i);
sleep(1);
};//休眠5min,检查父进程是否阻塞
printf("This is child pthread ID = %d\n", getpid());
exit(22);//子函数返回值设为22
}
return 0;
}
程序思路:
对第二份函数进行修改如下:
对父进程内容(19-23段)进行修改,采用waitpid(pid, &status,WNOHANG)设置为:指定回收子进程(父进程中,pid的是返回子进程进程号)、非阻塞(WNOHANG,若指定函数退出,则回收该函数;若指定函数还未退出,则忽略waitpid()函数)。
对子进程内容进行修改,将休眠时间延长至5min
由于子进程会休眠5min,所以父进程会忽略waitpid()函数。导致子进程无法被回收,父进程先于子进程结束后,子进程成为孤儿进程
运行结果如下:
再新开一个终端,使用ps -elf|grep XXX的方式,查看XXX的进程状况。
结果显示:
第一张图片显示:父进程先于子进程完成,非阻塞。而且waitpid()返回值为0。
第二张图片显示:
1、此时sleep仍然在打印,已经打印到了51
2、此时子进程已被接管(由于我是用的不是root账号,所以接管父进程编号为1679,而不是1),成为了孤儿进程
3、此时使用常规手段ctrl+c,已经无法关闭该进程
第三张图片显示:
需要使用kill -9 XXX(XXX为进程号)命令,来结束该进程。由于我这里的进程号为17899,所以在新开的终端里输入kill -9 17899,就可以结束该进程。
补充说明:
可以看到,使用非阻塞方式回收进程有好也有坏,所以在编程时一定得注意提前规划好,避免临时调整导致bug不断。
很多时候,调试bug比重新写程序花的时间还多。写出一个逻辑顺畅的程序,也是程序员的价值所在。