潜水于CU(www.chinaunix.net),看到了大家对Zombie Process和Daemon Process的理解,同样也意识到以前自己对这两个概念理解的偏颇,想在这篇Blog中将之纠正。

一、Zombie Process
Zombie Process,译成中文为僵尸进程,以前我一直认为父进程先结束,子进程就变成了僵尸进程,事实上这与正确的理解恰恰相反,真惭愧,只是从字面理解了而并未深入研究。下面重新理解一下:

父子进程的退出次序无非两种:(这里的父进程并不等待子进程)
(1) 父进程先,子进程后
在《Unix环境高级编程》中Stevens是这样说的:“对于其父进程已经终止的所有进程,它们的父进程都改变为init进程。我们称这些进程由init进程领养。其操作过程大致是:在一个进程终止时,内核逐个检查所有活动进程,以判断它是否是正要终止的进程的子进程,如果是,则该进程的父进程ID就更改为1 ( init进程的ID )。这种处理方法保证了每个进程有一个父进程”。这样子进程退出后的“善后”工作就由init进程来完成了,不会产生Zombie Process,在后面Stevens谈到了避免子进程成为Zombie Process的一个技巧就是利用init进程托管。

(2) 子进程先,父进程后
用CU上一个网友的形象理解就是“小孩死了老爸不管就变僵尸了”。其实进程的退出应该分成两个阶段:
   a) 进程主程序退出,此时进程进入TASK_ZOMBIE状态。此时大部分与该进程相关的资源都已被释放了,包括该进程的运行的地址空间已不存在了,它拥有的东西包括内核进程栈信息、线程相关信息等其父进程可能需要知道的信息。
   b) 当其父进程获取上述进程留下的信息后(调用wait or waitpid)或者其父进程通知内核对该进程的信息不感兴趣(调用signal(SIGCHLD,SIG_IGN); )时,该进程在内核中的资源才被释放。只有这两部都完成了,该进程才算是真正意义上的优美退出。而产生Zombie Process的本质就在于只完成了a)步骤,而b)的步骤却迟迟没有进程来完成(这本来是fork该子进程的父进程的责任)。这样的话,该进程在内核中占用的资源始终不能得到释放,一旦系统内部Zombie Process多了,系统运行就会受到影响了。

二、Daemon Process
Daemon Process,译为守护进程、后台进程或精灵进程。其定义这里引用Stevens的话“守护进程是生存期长的一种进程。它们独立于控制终端并且周期性的执行某种任务或等待处理某些发生的事件。他们常常在系统引导装入时启动,在系统关闭时终止。unix系统有很多守护进程,大多数服务器都是用守护进程实现的”。

守护进程可以通过一步一步的改造普通进程而得来。创建守护进程的步骤很固定,但是想要完全理解为什么要这么做的话,要了解的东西还不少。我们先来看看Stevens的做法:
int daemon_init(void) {
        int pid;
        pid = fork();             ———–(1)
        if (pid < 0) {
                return -1;
        } else if (pid > 0) {
                exit(0);
        }

        /* child process */
        setsid();          ———-(2) 注[1]

        chdir("/"); 

        umask(0);

        关闭相关文件描述符(根据具体的系统而定)

        return 0;
}
由于在书中Stevens对这些已经说的很详细,这里只是简单说明:
(1) 这里父进程退出,子进程为init进程托管,所以你用ps -fj察看会发现其ppid == 1。这里子进程从亲生父进程那继承了进程组ID、会话(session)ID和控制终端。子进程由于派生于父进程所以不可能成为进程组首进程,这为其成为Daemon创造了先天的条件(可以调用setsid成为新的session的首进程)。而后天的条件则需其自己创造了。
(2) 而子进程要想成为Daemon,就必须建立新的会话(Session)。由于会话对控制终端的独享性,一旦子进程创建了新的会话,就会自动脱离原先继承的控制终端。由于已经是新的会话所以进程组ID和Session ID都为该子进程的PID,该进程也成为新的进程组的首进程。

在CU的讨论中,又有如下一些问题:
a) 如何禁止进程重新打开控制终端?
现在,进程已经成为无终端的会话首进程,但它可以重新申请打开一个控制终端。如何来做来阻止其重新打开一个控制终端呢?可以通过使进程不再成为会话组长来禁止进程重新打开控制终端。这个话题有时被说成“创建一个Daemon进程到底需要一次fork还是二次fork”
int daemon_init(void) {
        int pid;
        pid = fork();            
        if (pid < 0) {
                return -1;
        } else if (pid > 0) {
                exit(0);
        }

        /* new session founder process */
        setsid();        

        pid = fork();
       if (pid < 0) {
          return -1;
       } else if (pid > 0) {
          exit(0);
       }

        /* child process */
        chdir("/"); 

        umask(0);

         关闭相关文件描述符(根据具体的系统而定)

        return 0;
}

b) 是否处理SIGCHLD信号?
很多Daemon进程在运行过程中还会fork出很多子进程,如果父进程不等待这些子进程,它们结束后就会变成Zombie Process,仍然占用了系统的资源,简单的调用signal(SIGCHLD, SIGIGN);就可以避免这种事情的发生,这个根据程序的需要可选。

三、总结
在实际的开发中,Zombie Process的产生往往是由于设计不当造成的。而创建Daemon Process也是不局限于上面Stevens的做法,当然必要的步骤是不能省略的。

四、参考资料
1、《Unix环境高级编程》
2、《深入理解Linux内核》

[注1]进程组、会话(Session)和控制终端(Control Terminal)之间的关系
理解Daemon Process涉及到进程组、会话(Session)和控制终端(Control Terminal)等多个概念,下面是它们的概念和之间的关系:
进程组:进程组是一个或多个进程的集合。每个进程组有一个唯一的进程组ID;
会话:一个或多个进程组的集合;
控制终端:通常是我们在登录的终端设备(终端登录情况)或伪终端设备(网络登录情况)。

一个会话若干个进程组(一般一个前台进程组和若干个后台进程组) 0或1个控制终端

© 2005, bigwhite. 版权所有.

Related posts:

  1. 理解dup和dup2
  2. APR源代码分析-进程篇
  3. APR源代码分析-信号篇
  4. APR源代码分析-内存篇
  5. Effective Java阅读笔记-item16