fork两次 避免僵尸进程

前几天接到某互联网公司的电话面试,面试官问我两次fork()的作用,我一头雾水,说不知道。知识面还是太窄了。下面就总结下两次fork()的作用。

首先,要了解什么叫僵尸进程,什么叫孤儿进程,以及服务器进程运行所需要的一些条件。两次fork()就是为了解决这些相关的问题而出现的一种编程方法。

孤儿进程

孤儿进程是指父进程在子进程结束之前死亡(return 或exit)。如下图所示:

但是孤儿进程并不会像上面画的那样持续很长时间,当系统发现孤儿进程时,init进程就收养孤儿进程,成为它的父亲,child进程exit后的资源回收就都由init进程来完成。 

僵尸进程

僵尸进程是指子进程在父进程之前结束了,但是父进程没有用wait或waitpid回收子进程。如下图所示:

     

父进程没有用wait回收子进程并不说明它不会回收子进程。子进程在结束的时候会给其父进程发送一个SIGCHILD信号,父进程默认是忽略SIGCHILD信号的,如果父进程通过signal()函数设置了SIGCHILD的信号处理函数,则在信号处理函数中可以回收子进程的资源。

事实上,即便是父进程没有设置SIGCHILD的信号处理函数,也没有关系,因为在父进程结束之前,子进程可以一直保持僵尸状态,当父进程结束后,init进程就会负责回收僵尸子进程。

但是,如果父进程是一个服务器进程,一直循环着不退出,那子进程就会一直保持着僵尸状态。虽然僵尸进程不会占用任何内存资源,但是过多的僵尸进程总还是会影响系统性能的。黔驴技穷的情况下,该怎么办呢?

这个时候就需要一个英雄来拯救整个世界,它就是两次fork()技法。

两次fork()技法

两次fork()的流程如下所示:

《UNIX环境高级编程》(下载见http://www.linuxidc.net/thread-2063-1-1.html)这本书里提供了两次fork的一个例子,代码如下:

  1. int main(void)  
  2. {  
  3.     pid_t        pid;  
  4.   
  5.     if ( (pid = fork()) < 0)  
  6.           err_sys("fork error");  
  7.     else if (pid == 0)   
  8.         {                /* first child */  
  9.            if ( (pid = fork()) < 0)  
  10.                         err_sys("fork error");  
  11.            else if (pid > 0)  
  12.                  exit(0);        /* parent from second fork == first child */  
  13.   
  14.                 /* We're the second child; our parent becomes init as soon 
  15.                    as our real parent calls exit() in the statement above. 
  16.                    Here's where we'd continue executing, knowing that when 
  17.                    we're done, init will reap our status. */  
  18.   
  19.             sleep(2);  
  20.             printf("second child, parent pid = %d\n", getppid());  
  21.             exit(0);  
  22.         }  
  23.   
  24.     if (waitpid(pid, NULL, 0) != pid)        /* wait for first child */  
  25.             err_sys("waitpid error");  
  26.   
  27.         /* We're the parent (the original process); we continue executing, 
  28.            knowing that we're not the parent of the second child. */  
  29.   
  30.     exit(0);  
  31. }  

理所当然,第二个子进程的父进程是进程号为1的init进程。

 一言以蔽之,两次fork()是为了防止系统中产生僵尸进程。 

参考文献:《UNIX环境高级编程》下载见http://www.linuxidc.net/thread-2063-1-1.html 与 http://www.linuxidc.com/Linux/2011-04/34662.htm

linux

 

 

 

 


 

 

 

daemon进程是后台守护进程,有时候也叫精灵进程(agent).linux 下server都是daemon进程。相信大部分开发人员都知道如何去写一个daemon进程。但是另一方面,大部分人不知道为什么要这么做,不少人是从某个地方copy一个函数,拿来主义。但是具体为什么这么实现,却不是很透彻。

 
  见过一些面试官或被面试人。很多人解释daemon进程存在的理由是因为僵死进程。或者输入输出。其实和这些东西一毛钱关系都没有。daemon函数存在的原因是因为控制终端由于某些原因(如断开终端链接)会发送一些信号的原因。而接收进城处理这些信号缺省动作会让进程退出。这些信号会由于终端上敲一些特殊按键而产生。
 
  贴一个daemon函数常见的实现:
  1. int daemon(void)
  2. {
  3.     pid_t pid = fork();
  4.     if( pid != 0 ) exit(0);//parent
  5.     //first children
  6.     if(setsid() == -1)
  7.     {
  8.        printf("setsid failed\n");
  9.        assert(0);
  10.        exit(-1);
  11.     }
  12.     umask(0);
  13.     pid = fork();
  14.     if( pid != 0) exit(0);
  15.   
  16.     //second children 
  17.     chdir ("/");
  18.     for (int i = 0; i < 3; i++)
  19.     {
  20.         close (i);
  21.     }
  22.     int stdfd = open ("/dev/null", O_RDWR);
  23.     dup2(stdfd, STDOUT_FILENO);
  24.     dup2(stdfd, STDERR_FILENO);
  25.     return 0;
  26. }
    1 、第一次fork的作用是让shell 认为本条命令 已经终止,不用挂在终端输入上。还有一个作用是为后面setsid服务。setsid的调用者不能是进程组组长(group leader). 此时父进程是进程组组长。
    
    2 、setsid() 是本函数最重要的一个调用。它完成了daemon函数想要做的大部分事情。调用完整个函数。子进程是会话组长(sid==pid),也是进程组组长(pgid == pid),并且脱离了原来控制终端。到了这一步,基本上不管控制终端如何怎么样。新的进程都不会收到那些信号。
 
    3  、经过前面2个步骤,基本想要做的都做了。第2次fork不是必须的。也看到很多开源服务没有fork第二次。fork第二次主要目的是。防止进程再次打开一个控制终端。因为打开一个控制终端的前台条件是该进程必须是会话组长。再fork一次,子进程ID != sid(sid是进程父进程的sid)。所以也无法打开新的控制终端。
 
    daemon目的就是防止终端产生的一些信号让进程退出。上面函数并没有直接调用signal函数去处理它。而是间接通过fork和setsid函数使用更少代码优雅处理。而被有些人误以为是僵死进程的原因需要这样处理。
 
    当然,也有很多程序不是像上面函数那样去实现。而是直接通过忽略信号方式处理。这样其实也不错,因为这些信号很少会有用到的价值。直接忽略基本上不存在误杀的情况。反正达到最终目的就可以。条条大路通罗马。
 
   下面罗列一下控制终端会产生哪些信号。程序中只要处理好这些信号,同样能达到上面函数实现的目的。
 
   //后台进程读取/写入终端输入产生下面两个信号,或者控制终端不存在情况读取和写入会产生
   signal(SIGTTOU, SIG_IGN);
   signal(SIGTTIN, SIG_IGN);
 
   //按CTRL-C ,CTRL-\ CTRL-Z会向前台进程组发送下面这些信号
   signal(SIGINT,  SIG_IGN );
   signal(SIGQUIT, SIG_IGN );
   signal(SIGTSTP, SIG_IGN );
   
   //终端断开,会给会话组长或孤儿进程组所有成员发送下面信号
   signal(SIGHUP,  SIG_IGN );
 
   还有有些信号也可以由终端shell产生,需要关注
   signal(SIGCONT, SIG_IGN );
   signal(SIGSTOP, SIG_IGN );
 
 
  上面这些信号,应该有些程序缺省处理(SIG_DFL)本身动作就是忽略(SIG_IGN),不是退出进程。不过按照上面写也不会造成什么问题。

 

 

 

 

posted @ 2013-06-27 14:31  tangr206  阅读(2038)  评论(0编辑  收藏  举报