在unix/linux中,正常情况下,子进程是通过父进程创建的。子进程的结束和父进程的运行是一个异步过程,即父进程永远无法预测子进程到底什么时候结束。 当一个进程完成它的工作终止之后,它的父进程需要调用wait()或者waitpid()系统调用取得子进程的终止状态。
孤儿进程
当创建子进程的父进程先结束,子进程就会变成孤儿进程,会被孤儿院(init,进程编号为1)收养。
危害:
孤儿进程是没有父进程的进程,这时init进程就站了出来,init进程就好像是一个孤儿院,专门负责处理孤儿进程的善后工作。每当出现一个孤儿进程的时候,内核就把孤儿进程的父进程设置为init,而init进程会循环地wait()子进程。因此孤儿进程并不会有什么危害。
孤儿进程示例:
#include <stdio.h>
#include <unistd.h>
int main(int argc, char const *argv[])
{
if (0 == fork())
{
printf("Son pid:%d father pid:%d\n",getpid(),getppid());//首先打印
sleep(2);//延时2s
printf("Oh no,my father process died.\n");
printf("Son pid:%d Father pid:%d\n",getpid(),getppid());//父进程终止后
return 0;
}
sleep(1);//延时1s,保证父进程未终止前子进程的第一个printf先打印
printf("I am Father, my process id:%d\n",getpid());
return 0;
}
结果:
很明显看到,当父进程死后,子进程的父进程pid变成了1 。
僵尸进程
如果子进程死了,会发送SIGCHLD信号给父进程,而该信号的默认处理动作是忽略,父进程可以自定义SIGCHLD信号的处理函数,这样父进程就可以处理自己的事情,而不必关心子进程的退出,在父进程的处理函数中调用wait清理子进程即可。如果父进程并没有调用wait或waitpid获取子进程的状态信息,那么子进程的进程描述符仍然保存在系统中。其状态为Z(Zombie),称为僵尸进程。通俗的讲,就是子进程死了,而父进程没有给子进程收尸(回收子进程的资源),使得子进程变成了僵尸进程。
危害:
unix提供了一种机制可以保证只要父进程想知道子进程结束时的状态信息, 就可以得到。这种机制就是: 在每个进程退出的时候,内核释放该进程所有的资源,包括打开的文件,占用的内存等。 但是仍然为其保留一定的信息(包括进程号the process ID,退出状态status,运行时间the amount of CPU time taken by the process等)。直到父进程通过wait / waitpid来取时才释放。 但这样就导致了问题,如果进程不调用wait / waitpid的话, 那么保留的那段信息就不会释放,其进程号就会一直被占用,但是系统所能使用的进程号是有限的,如果大量的产生僵尸进程,将因为没有可用的进程号而导致系统不能产生新的进程. 此即为僵尸进程的危害,应当避免。
任何一个子进程(init除外)在exit()之后,并非马上就消失掉,而是留下一个称为僵尸进程(Zombie)的数据结构,等待父进程处理。(系统调用exit,它的作用是使进程退出,但也仅仅限于将一个正常的进程变成一个僵尸进程,并不能将其完全销毁)这是每个子进程在结束时都要经过的阶段。如果子进程在exit()之后,父进程没有来得及处理,这时用ps命令就能看到子进程的状态是“Z”。如果父进程能及时处理,可能用ps命令就来不及看到子进程的僵尸状态,但这并不等于子进程不经过僵尸状态。如果父进程在子进程结束之前退出,则子进程将由init接管。init将会以父进程的身份对僵尸状态的子进程进行处理。
僵尸进程示例:
#include<stdio.h>
#include<unistd.h>
int main(int argc, char const *argv[])
{
if(0 == fork())
{
printf("pid:%d\n",getpid());
return 0;
}
sleep(30);//延时30s才执行父进程,期间子进程已终止,但父进程不会收到
return 0;
}
结果显示为:
子进程pid为21621 通过ps aux|grep 21621 看到其进程状态为Z。
僵尸进程的避免:
1.父进程通过wait和waitpid等函数等待子进程结束,这会导致父进程挂起。
wait函数:
pid_t wait(int *status);
功能:等待子进程结束,并回收。
返回值:成功返回子进程pid,失败返回-1.
waitpid函数:
pid_t waitpid(pid_t pid,int *status,int options)
功能:等待指定的子进程结束,并回收(实际上,wait函数就是经过包装后的waitpid函数)
参数:pid:指定的pid
<-1 等待进程组id等于pid绝对值的任意子进程
-1 等待任意子进程回收,与wait等效
0 等待组id等于调用进程组id的任意进程
>0 等待进程组id与pid相等的子进程
status:用于接受子进程的结束状态
如果不需要状态码,则设置为NULL
options:提供了一些额外的选项来控制waitpid,可以用"|"运算符把它们连接起来使用
0 以阻塞状态等待子进程结束
WNOHANG:如果没有子进程退出会立即返回
WUNTRACED:等待的进程处于暂停状态,并且没有报告,则立即返回
返回值:1.当正常返回的时候,waitpid返回收集到的子进程的进程ID;
2.如果设置了选项WNOHANG,而调用中waitpid发现没有已经退出的子进程可以回收,则返回0;
3.如果调用时出错,则返回-1 。
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
int main(int argc, char const *argv[])
{
int pid1 = fork();
if (0 == pid1)
{
printf("pid1:%d\n",getpid());
sleep(3);
exit(0);
}
pid_t pid;
do
{
pid = waitpid(-1,NULL,WNOHANG);
if(0 == pid)
{
printf("No son process exit.\n");
sleep(1);
}
}while(pid == 0);
if(pid1 == pid)
{
printf("son process: %d release successfully.\n",pid);
}
}
执行结果:
可以看到,经历3s后子进程24998被释放成功。
总结:父进程调用wait/waitpid函数才是有意义的:
1.如果所有子进程都在运行,则父进程阻塞
2.只要有一个子进程结束了,会立即返回子进程的id和结束状态
3.当所有子进程结束运行时,wait返回-1
4.如果在调用wait之前子进程就已经结束(僵尸子进程),执行wait函数时会立即返回并回收僵尸子进程
5.如果不调用wait/waitpid函数,子进程结束后就处于僵尸状态,当父进程结束时,父进程的父进程会把他们统一回收
2. 如果父进程很忙,那么可以用signal函数为SIGCHLD安装handler,因为子进程结束后, 父进程会收到该信号,可以在handler中调用wait回收。
但是如果创建25个子进程呢?还能避免所有的僵尸进程吗?
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
void sigchld(int num)
{
static int i = 0;
printf("receive:%d waiting...%d\n",num,i++);
wait(NULL);
}
int main(int argc, char const *argv[])
{
signal(SIGCHLD,sigchld);
pid_t cid;
for(int i = 0;i < 25;++i)
{
cid = fork();
if(cid == 0) exit(0);
}
if(cid > 0)
{
while(1)
{
printf("father:%d is working!\n",getpid());
sleep(1);
}
}
return 0;
}
执行结果:
发现竟然出现了6个僵尸进程,这是为何?
这涉及到了信号的概念,信号分为可靠信号和不可靠信号。编号小于SIGRGMI(34)的信号都是不可靠的,不可靠信号不支持排队,因此在接收信号时可能会丢失,如果一个信号发给一个进程多次,进程可能只接受到一次,其他的信号就丢失了。
3.如果父进程不关心子进程什么时候结束,那么可以用signal(SIGCHLD,SIG_IGN) 即忽略子进程终止信号,并通知内核,自己对子进程的结束不感兴趣,那么子进程结束后,内核会回收, 并不再给父进程发送信号。
4. 还有一些技巧,就是fork两次,父进程fork一个子进程,然后继续工作,子进程fork一个孙进程后退出,那么孙进程被init接管,孙进程结束后,init会回收。不过子进程的回收还要自己做。