daemon进程原理及实现 信号 终端 为何两次Fork

APUE 上的定义如下: 守护进程也称 daemon 进程,是生存期较长的一种进程,它们常常在系统自举时启动,仅在系统关闭时才终止。因为它们没有控制终端,所以说它们是再后台运行的。 
Daemon 功能实现】

      首先,讲解下 daemon 实现的基本原则。事实上,编写守护进程程序时是存在一些基本规则的,目的是防止产生不需要的交互作用(比如与终端的交互)。规则如下:

  1. 调用 umask 将文件模式创建屏蔽字设置为 0 原因防止继承得来的文件模式创建屏蔽字会拒绝设置某些权限的情况。
  2. 调用 fork ,然后使父进程退出(exit)。原因第一,令启动 daemon 进程的 shell 认为命令已经执行完毕;第二,令产生的子进程不是其所在进程组的组长。
  3. 调用 setsid 以创建一个新会话。原因使调用进程,第一,成为新会话的首进程;第二,成为新进程组的组长进程;第三,没有控制终端(在基于 System V 的系统中可以通过 fork 两次来达到防止取得控制终端的效果的,其不再需要下面的规则6)。
  4. 将当前工作目录更改为根目录。原因防止出现不能 umount 的问题。
  5. 关闭不再需要的文件描述符。原因令守护进程不再持有从父进程继承来的某些文件描述符。
  6. 某些守护进程打 /dev/null 使其具有文件描述符012原因防止守护进程与终端设备相关联。

 

有了上面的原则,现在对照下 MySQL Proxy 中的代码: 

/**
 * start the app in the background 
 * 
 * UNIX-version
 */
void chassis_unix_daemonize(void) {
#ifdef _WIN32
	g_assert_not_reached(); /* shouldn't be tried to be called on win32 */
#else
#ifdef SIGTTOU
	signal(SIGTTOU, SIG_IGN);
#endif
#ifdef SIGTTIN
	signal(SIGTTIN, SIG_IGN);
#endif
#ifdef SIGTSTP
	signal(SIGTSTP, SIG_IGN);
#endif
	if (fork() != 0) exit(0);
	
	if (setsid() == -1) exit(0);

	signal(SIGHUP, SIG_IGN);

	if (fork() != 0) exit(0);
	
	chdir("/");
	
	umask(0);
#endif
}


从上面的实现代码中,可以看出以下几点:

  • 代码执行的先后顺序有的是必须的(如setsid 之前的 fork),有的不是必须的(如 umask 放在最后执行)。
  • 实现中使用了两次 fork ,为 System V 中理念。
  • 在 setsid 和第二次 fork 之间插入了 signal 处理,用于对 SIGHUP 执行 SIG_IGN 处理。

      在上述  daemon 编程规则中没有提到 signal 处理的问,那么针对 SIGHUP 的处理代表的是什么意思呢?还是参阅 APUE

      如果终端接口检测到一个连接断开,则将此信号发送给与该终端相关的控制进程(会话首进程)。仅当终端的 CLOCAL 标志没有设置时,上述条件下才产生此信号。

      有别于由终端正常产生的信号(如中断、退出和挂起)-- 这些信号总是传递给前台进程组 --  SIGHUP 信号可以发送到位于后台运行的会话首进程。SIGHUP 信号的默认处理动作是终止当前进程。通常会使用该信号来通知守护进程,以重新读取它们的配置文件,因为守护进程不会有控制终端,而且通常决不会收到这种信号。

      从上面这段文字可以看出,这里增加了 signal 信号处理的原因是,在 setsid 和第二次 fork 之间,当前的子进程仍旧是会话首进程,有可能会在收到 SIGHUP 信号时终止,所以这里通过设置  SIG_IGN 进行忽略。

至此,一个 daemon-mode 的守护进程就启动了。

 

在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),不是退出进程。不过按照上面写也不会造成什么问题。

    在编写精灵进程程序时需要遵循一些基本规则,以便防止产生并不希望的交互作用。下面先说明这些规则,然后是一个按照规则编写的函数daemon_init。

    

    1.  首先做的是调用fork,然后使父进程exit。这样做实现了下面几点:

    第一:如果该精灵进程是由一条简单shell命令启动的,那么使父进程终止使得shell认为这条命令已经执行完成;(之后的所有工作都在子进程中完成,而用户在Shell终端里则可以执行其他命令,从而在形式上做到了与控制终端的脱离。)

    第二:在Linux中父进程先于子进程退出会造成子进程成为孤儿进程,而每当系统发现一个孤儿进程,就会自动由1号进程(init)收养它,这样,原先的子进程就会变成init的子进程;

    第三:子进程继承了父进程的进程组ID,但具有一个新的进程ID,这就保证了子进程不是一个进程组的首进程。这对于下面就要做的setsid调用是必要的前提条件

   

    2.  调用setsid以创建一个新对话期。于是执行9.5节中列举的三个操作,使调用进程:

    第一:成为新对话期的首进程;

    第二:成为一个新进程组的首进程;

    第三:没有控制终端。

    在SVR之下,有些人建议在此时再调用fork,并使父进程终止。第二个子进程作为精灵进程继续运行。这样就保证了该精灵进程不是对话期的首进程,于是按照SVR规则(见9.6节)可以防止它取得控制终端。另一方面,为了避免取得控制终端,无论何时打开一个终端设备都要指定O_NOCTTY

 

    3.  将当前工作目录更改为根目录。父进程继承过来的当前工作目录可能在一个装配的文件系统中。因为精灵进程通常在系统再引导之前是一直存在的,所以如果精灵进程的当前工作目录在一个装配文件系统中,那么该文件系统就不能被拆卸。

    另外,某些精灵进程可能会把当前工作目录更改到某个指定位置,在此位置做它们的工作。

    例如,行式打印机假脱机精灵进程常常将其工作目录更改到它们的spool目录上。

 

    4.  将文件方式创建屏蔽字设置为0。由继承得来的文件方式创建屏蔽字可能会拒绝设置某些许可权。例如,若精灵进程要创建一个组可读、写的文件,而继承的文件方式创建屏蔽字,屏蔽了这两种许可权,则要求的组可读、写就不能起作用。

 

    5.  关闭不在需要的文件描述符。这样使精灵进程就不再持有从其父进程继承来的某些文件描述符(父进程可能是shell进程,或某个其他进程)。但是,究竟关闭那些描述符则与具体的精灵进程有关,所以在下面的例子中不包含此步骤。可以使用程序2-3中的open_max函数来决定最高文件描述符值,并关闭直到该值的所有描述符。

 

 

 
 

daemon进程原理及实现(来源于网络,后整理)  

为何两次Fork

这是创建Daemon进程的大致步骤及作用:

  • 第一次fork(产生父子进程,父进程退出,子进程与父进程控制终端脱离)
  • 子进程setsid(子进程成为会话组长,与父进程会话脱离)
  • 第二次fork(产生子孙进程,子进程退出,孙进程成为Daemon最终进程)
  • 切换工作目录、设置文件创建掩模、关闭所有打开文件句柄等

原则上来说在第一次fork后,子进程即与父进程的控制终端脱离关系,父进程退出后,子进程被init接管,基本达到了Daemon进程要求。但还差了一点,子进程与父进程还在同一个会话组,因此子进程需要调用setsid以达到与父进程会话脱离。也就是这个setsid,使子进程成为了新的会话组组长,却导致了新的问题产生:在Linux中会话组长可以重新申请打开一个控制终端。为了彻底与控制终端断绝关系,我们需要一个非会话组长的进程,子进程的子进程正是我们要的。

 

 

 

 

        守护进程(Daemon)是运行在后台的一种特殊进程。它独立于控制终端并且周期性地执行某种任务或等待处理某些发生的事件。守护进程是一种很有用的进 程。Linux的大多数服务器就是用守护进程实现的。比如,Internet服务器inetd,Web服务器httpd等。同时,守护进程完成许多系统任务。 比如,作业规划进程crond,打印进程lpd等。

        守护进程的编程本身并不复杂,复杂的是各种版本的Unix的实现机制不尽相同,造成不同 Unix环境下守护进程的编程规则并不一致。需要注意,照搬某些书上的规则(特别是BSD4.3和低版本的System V)到Linux会出现错误的。下面将给出Linux下守护进程的编程要点和详细实例。

一. 守护进程及其特性

        守护进程最重要的特性是后台运行。在这一点上DOS下的常驻内存程序TSR与之相似。其次,守护进程必须与其运行前的环境隔离开来。这些环 境包括未关闭的文件描述符,控制终端,会话和进程组,工作目录以及文件创建掩模等。这些环境通常是守护进程从执行它的父进程(特别是shell)中继承下 来的。最后,守护进程的启动方式有其特殊之处。它可以在Linux系统启动时从启动脚本/etc/rc.d中启动,可以由作业规划进程crond启动,还可以由用户终端(通常是 shell)执行。

        总之,除开这些特殊性以外,守护进程与普通进程基本上没有什么区别。因此,编写守护进程实际上是把一个普通进程按照上述的守护进程的特性改造成为守护进程。如果对进程有比较深入的认识就更容易理解和编程了。

二. 守护进程的编程要点

        不同Unix环境下守护进程的编程规则并不一致。所幸的是守护进程的编程原则其实都一样,区别在于具体的实现细节不同。这个原则 就是要满足守护进程的特性。同时,Linux是基于Syetem V的SVR4并遵循Posix标准,实现起来与BSD4相比更方便。编程要点如下;

1. 在后台运行。

        为避免挂起控制终端将Daemon放入后台执行。方法是在进程中调用fork使父进程终止,让Daemon在子进程中后台执行。

    if(pid=fork())

exit(0); //是父进程,结束父进程,子进程继续

2. 脱离控制终端,登录会话和进程组

        有必要先介绍一下Linux中的进程与控制终端,登录会话和进程组之间的关系:进程属于一个进程组,进程组号(GID)就是进程组长的进程号(PID)。登录会话可以包含多个进程组。这些进程组共享一个控制终端。这个控制终端通常是创建进程的登录终端。 控制终端,登录会话和进程组通常是从父进程继承下来的。我们的目的就是要摆脱它们,使之不受它们的影响。方法是在第1点的基础上,调用setsid()使进程成为会话组长:

    setsid(); //设置为会话组长

        说明:当进程是会话组长时setsid()调用失败。但第一点(fork())已经保证进程不是会话组长。setsid()调用成功后,进程成为新的会话组长和新的进程组长,并与原来的登录会话和进程组脱离。由于会话过程对控制终端的独占性,进程同时与控制终端脱离。

3. 禁止进程重新打开控制终端

        现在,进程已经成为无终端的会话组长。但它可以重新申请打开一个控制终端。可以通过使进程不再成为会话组长来禁止进程重新打开控制终端(再fork一次):

if(pid=fork()) 

    exit(0); //结束第一子进程,第二子进程继续(第二子进程不再是会话组长)

4. 关闭打开的文件描述符

        进程从创建它的父进程那里继承了打开的文件描述符。如不关闭,将会浪费系统资源,可能会造成进程所占用的文件系统无法卸下以及引起无法预料的错误。按如下方法关闭它们:

for(i=0;i < 3; ++i)

close(i); //0, 1, 2 分别表示标准输入、标准输出和标准错误

      当然,此处关闭哪个文件描述符与实际需要相关,也可以关闭以后再重新打开。

5. 改变当前工作目录

        进程活动时,如果该目录是一个挂载的目录,将导致其文件系统不能卸载。一般需要将工作目录改变到根目录(chdir("/"))。对于需要转储核心,写运行日志的进程将工作目录改变到特定目录如 /tmp

6. 重设文件创建掩模

        进程从创建它的父进程那里继承了文件创建掩模。由继承得来的文件方式创建屏蔽字可能会拒绝设置某些许可权。例如,若daemon进程要创建一个组可读、写的文件,而继承的文件方式创建屏蔽字,屏蔽了这两种许可权,则要求的组可读、写就不能起作用。为防止这一点,将文件创建掩模清除:

umask(0);

7. 处理SIGCHLD信号

        处理SIGCHLD信号并不是必须的。但对于某些进程,特别是服务器进程往往在请求到来时生成子进程处理请求。如果父进程不等待子进程结 束,子进程将成为僵尸进程(zombie)。如果父进程等待子进程结束,将增加父进程的负担,影响服务器进程的并发性能。在Linux下 可以简单地将 SIGCHLD信号的操作设为SIG_IGN。

signal(SIGCHLD,SIG_IGN);

        这样,内核在子进程结束时不会产生僵尸进程。这一点与BSD4不同,BSD4下必须显式等待子进程结束才能释放僵尸进程。

三. 守护进程实例

        fork()两次,将孙子进程“过继”给1号进程init,如果孙子进程成功,其父进程将会是系统1号进程init,如果失败,将由init负责收拾残局。

void InitAsDaemon()

{

if (fork() > 0 ) exit(0);

setsid();

signal(SIGINT, SIG_IGN);

signal(SIGHUPSIG_IGN);

signal(SIGQUIT, SIG_IGN);

signal(SIGPIPE, SIG_IGN);

signal(SIGTOU, SIG_IGN);

signal(SIGTIN, SIG_IGN);

signal(SIGCHLD, SIG_IGN);

if (fork() > 0) exit(0);

chdir("/");

umusk(0);

}

四. wait和waitpid

        两者相同点都是用于在创建子进程之后,阻塞自己,然后检查自己的子进程中是否有僵尸进程存在,如果存在,该父进程就释放僵尸进程占用的资源,并返回,如果没有这样的僵尸进程,就一直阻塞。

        不同点是后者还有两个pid和option参数。

        pid:当pid > 0时,只等待进程号为pid的进程退出;pid == -1时,等待任意子进程,作用等同于wait;pid == 0时,等待同一进程组中任意子进程;pid < -1 时,等待某个进程组的任意进程,该进程组的组号为pid的绝对值。

        option:有两个值,WNOHANG和WUNTRACED,前者使得waitpid立即返回而不管有没有子进程退出;后者是指其子进程集合中,如果有子进程是STOPED状态,就立即返回,如果该进程是被traced的,那么即使不提供WUNTRACED参数,也立即返回。

       于是可以看出,wait是等待第一个退出的子进程,而waitpid是等待指定子进程退出。因此对于多进程服务器,使用waitpid可以避免因为第一个子进程退出调用了wait,而造成剩余子进程没有wait来处理,引起的僵尸进程的问题。

五. 僵尸进程的处理

        僵尸进程是无法使用kill或者killall来杀死的,虽然僵尸进程并不占用很多系统资源(只是占用进程表process table中的一个项),但是过多的僵尸进程还是会对系统性能造成影响(达到系统进程数上限),因此应尽可能避免:

        1、改写父进程:如上所述,fork两次,将init作为其父进程,由系统负责清理

        2、kill掉僵尸进程的父进程,交由系统处理。

        使用

        ps auwx可以查看系统中僵尸进程,僵尸进程的状态会被标注为“Z”。或者

        ps axf   以树形展示进程表

        ps axm    列出线程,linux下进程线程一致

        ps aux   列出进程的详细信息

 

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值