一个进程在终止时会关闭所有文件描述符,释放在用户空间分配的内存,但它的PCB还保留着,内核在其中保存了一些信息:如果是正常终止则保存着退出状态,如果是异常终止则保存着导致该进程终止的信号是哪个。
父进程可以调用wait或waitpid获取这些信息,然后彻底清除掉这个进程。
#include <sys/types.h>
#include <sys/wait.h>
pid_t wait(int *status);
pid_t waitpid(pid_t pid, int *status, int options);
例如:一个进程的退出状态(比如:return 0;)可以在Shell中用特殊变量$?查看,因为Shell是它的父进程,当它终止时Shell调用wait或waitpid得到它的退出状态的同时彻底清除掉这个进程。
如果一个进程已经终止,但是它的父进程尚未调用wait或waitpid对它进行清理,这时的进程状态称为僵尸(Zombie)进程,可以用ps u查看进程状态。
父进程调用wait或waitpid时可能会:
1.阻塞(如果它的所有子进程都还在运行)。ps:Shell就是阻塞等着子进程运行完毕。
2.带子进程的终止信息立即返回(如果一个子进程已终止,正等待父进程读取其终止信息)。
3.出错立即返回(如果它没有任何子进程)。
wait和waitpid区别:
如果父进程的所有子进程都还在运行,调用wait将使父进程阻塞,而在调用waitpid时如果在options参数中指定WNOHANG可以使父进程不阻塞而立即返回0。wait等待第一个终止的子进程,而waitpid可以通过pid参数指定等待哪一个子进程。
下面这段程序,子进程先死了,变成僵尸进程,父进程一直在睡觉:
#include "./common/head.h" /*功能: *子进程先死,变成僵尸进程,父进程不给它收尸,一直在睡觉。 */ int main() { pid_t pid = fork(); if(pid < 0){ perror("fork"); exit(1); } if(pid){ //父进程 //一直睡觉,不给子进程收尸 while(1) sleep(1); }else{ //子进程 exit(1); //子进程先死,变成僵尸进程 } return 0; }
过程解析:
子进程先于父进程死,父进程却没给它收尸,子进程变成了僵尸进程。当我们再打开一个终端,用ps u命令查看,发现父进程状态是S(睡眠),子进程状态是Z(僵尸)。如果用Ctrl+c结束父进程,则父进程会被Shell收尸,这时,子进程变成了孤儿僵尸,会被1号进程回收。值得一提的是,用kill -9命令杀不掉僵尸进程,当kill -9杀掉僵尸的父进程后,父进程会被父进程的父进程(在这里就是Shell收尸),孤儿僵尸因为父亲死了,会被1号进程收养,此时用ps u已经查看不到父进程和子进程了。
用waitpid函数获取子进程死亡原因:
#include "./common/head.h"
/*功能:
*用waitpid函数获取子进程死亡原因。
*/
int main()
{
pid_t pid = fork();
if(pid < 0){
perror("fork");
exit(1);
}
if(pid == 0){ //子进程
int n = 5;
while(n--){ //子进程5秒后结束
printf("this is child process\n");
sleep(1);
}
exit(3); //父进程中打印正常退出代码为3
}else{ //父进程
int stat_val;
//等待子进程退出
wait(pid, &stat_val, 0); //pid为-1时代表等待所有子进程结束,子进程结束的信息存储在stat_val中,可以用以下的宏来进行判断
if(WIFEXITED(stat_val)){ //子进程正常退出
//打印正常退出代码,即上面返回的3
printf("Child exited with code %d\n", WEXITSTATUS(stat_val));
}else if(WIFSIGNALED(stat_val)){ //子进程是被信号终止的
//打印信号
printf("Child terminated abnormally, signal %d\n", WTERMSIG(stat_val));
}
}
return 0;
}