荔园在线

荔园之美,在春之萌芽,在夏之绽放,在秋之收获,在冬之沉淀

[回到开始] [上一篇][下一篇]


发信人: sh.bbs@bbs.sjtu.edu.cn (尝试蜕变), 信区: Linux
标  题: QA:怎样使一个进程以daemon方式运行
发信站: 饮水思源 (Tue Apr 12 12:07:17 2005)
转信站: SZU!news.szu.edu.cn!bbsnews.sdu.edu.cn!SJTU

24.3 如何编写daemon程序

Q: 在FreeBSD下用"ps auxw"查看进程列表时,注意到某些进程没有控制终端,也就
   是说TT列显示??。知道这是所谓的daemon进程,如果我想自己编写这样的程序,
   该如何做。

A: Andrew Gierth <andrew@erlenstar.demon.co.uk>

这个回答来自著名的<<Unix Programming FAQ ver 1.37>>,由Andrew Gierth负责维
护,其它细节请参看原文1.7小节。

通常将一个不与任何终端相关联的后台进程定义为daemon进程。下面是通常所需的七
个步骤:

a. fork()之后父进程退出。子进程确保不是process group leader,这是成功调用
   setsid()所要求的。

b. setsid(),创建新的session和process group,成为其leader,并脱离控制终端。

c. 再次fork()之后父进程退出,子进程确保不是session leader,将永远不会重获
   控制终端。这是SVR4的特性所致。

d. chdir( "/" ),减少管理员卸载(unmount)文件系统时可能遇上的麻烦。这一步可
   选,也可chdir()到其它目录。

e. umask( 0 ),使当前进程对自己所写文件拥有完全控制权,避免继承的umask()设
   置带来困挠。这一步可选。

f. 关闭0、1、2三个句柄。许多daemon程序用sysconf()获取_SC_OPEN_MAX,并在一
   个偱环中关闭所有可能打开的文件句柄。目的在于释放不必要的系统资源,它们
   是有限资源。

g. 出于安全以及健壮性考虑,即使当前进程不使用stdin、stdout、stderr,也应重
   新打开0、1、2三个句柄,使之对应/dev/null。当然,你也可以根据需要使之对
   应不同的(伪)文件。总之,保持0、1、2三个句柄呈打开状态,并使之指向无害文
   件。

D: scz <scz@nsfocus.com>

以FreeBSD 4.5-RELEASE为例进行讨论。

注意,存在与终端相关联的后台进程,比如在支持作业控制的bash上以&符结尾启动
的进程。当用"ps auxw"查看时,这种后台进程的TT列不为??。用"ps -p pid -jfl"
查看这种后台进程,可以看到其PGID与父进程的PGID不同,属于另外一个进程组。支
持作业控制的现代shell对&符的解释一般都是fork/setpgid。前台进程组、后台进程
组是终端的属性,不是进程本身的属性,没有控制终端的进程无所谓前台、后台,一
定要算就都算是后台进程。

非作业控制型的shell对&符的解释一般只是fork,而没有setpgid,这样启动的进程
与shell属于同一进程组。后面的讨论都假设使用支持作业控制的现代shell。

当在控制终端上按下Ctrl-C,终端驱动程序产生SIGINT信号(可用stty设置)并分发至
前台进程组的所有进程。

APUE 10.2中提到,当session leader终止时,系统会向该session前台进程组中所有
进程分发SIGHUP信号。我的疑问是,如果某session没有控制终端,也就没有所谓前
台进程组,当session leader终止时,系统会向该session中所有进程分发SIGHUP信
号吗。UNP 12.4的例子正是这种情形,可是Stevens没有在其它地方进一步阐述,也
永远不可能得到他本人的解释了。

APUE 13.3所给的daemon_init()与UNP 12.4所给不同,没有做二次fork()。因为二次
fork()只是SVR4的要求。从最广泛兼容角度出发,如果daemon进程企图打开一个(伪)
终端设备,无论是否二次fork()过,open()时都应该指定O_NOCTTY。由于daemon程序
是自己完全可控的,将来是否会打开终端是已知的,如果确认将来不会打开终端,就
完全不必考虑重获控制终端的问题,换句话说,二次fork()很大程度上是不必要的。

关于Andrew Gierth所提第七步骤,1987年Henry Spencer在setuid(7)手册页中做了
相关建议,1991年,在comp news上有人重贴了这份文档。1992年Richard Stevens建
议daemon进程应该关闭所有不必要的文件句柄,并将stdin、stdout、stderr指向
/dev/null。参看<<x86/FreeBSD 4.5-RELEASE IO Smash及S/Key机制分析>>。第七步
骤严格意义上来说,不是可选的,而是必须的。

参看<<19.0 如何将stdin、stdout、stderr重定向到/dev/null>>。

A: W. Richard Stevens

一般我会使用类似daemon_init()这样的函数,使当前进程成为daemon进程。

--------------------------------------------------------------------------
static void daemon_init ( const char *workdir, mode_t mask )
{
    int i, j;

    /*
     * change working directory, this step is optional
     */
    chdir( "/tmp" );
    if ( 0 != Fork() )
    {
        /*
         * parent terminates
         */
        exit( EXIT_SUCCESS );
    }
    /*
     * first child continues
     *
     * become session leader
     */
    setsid();
    Signal( SIGHUP, SIG_IGN );
    if ( 0 != Fork() )
    {
        /*
         * first child terminates
         */
        exit( EXIT_SUCCESS );
    }
    /*
     * second child continues
     *
     * change working directory, chdir( "/" )
     */
    chdir( workdir );
    /*
     * clear our file mode creation mask, umask( 0 )
     */
    umask( mask );
    j = Open( "/dev/null", O_RDWR );
    Dup2( j, 0 );
    Dup2( j, 1 );
    Dup2( j, 2 );
    j = getdtablesize();
    for ( i = 3; i < j; i++ )
    {
        close( i );
    }
    return;
}  /* end of daemon_init */
--------------------------------------------------------------------------

调用setsid(),如果成功,导致三个结果:

a. 创建一个新的session,当前进程成为session leader,也是新session中的惟一
   进程。

b. 当前进程成为一个新进程组的组长(process group leader)。

c. 如果当前进程以前有一个控制终端,现在将脱离这个控制终端。

对于SVR4,一个session leader调用open()打开一个(伪)终端设备,如果这个终端不
是其它会话的控制终端,而open()时又未指定O_NOCTTY,则这个终端成为当前会话的
控制终端。第二次fork()后,孙子进程将确保不是session leader。于是以后不会再
有任何控制终端,彻底脱离。

必须在第二次fork()之前显式忽略SIGHUP信号。孙子进程将继承子进程所设置的信号
句柄。Stevens是这样解释的,当session leader终止时,系统会向该session中所有
进程分发SIGHUP信号。即这里的子进程终止时,系统会向孙子进程分发SIGHHUP信号。
前面有关于这个问题的讨论。

getdtablesize()返回的也就是sysconf( _SC_OPEN_MAX )返回的值。

D: scz <scz@nsfocus.com>

以FreeBSD 4.5-RELEASE为例进行讨论。

做为Guru of the Unix gurus,Andrew Gierth与Richard Stevens在各类文档或书籍
中对"进程"进行了相当广泛、深入的解释,其中可能引发困惑的一个问题是,父子进
程关系与信号分发的关系。

有相当多的人认为父进程终止时,子进程应该收到一个SIGHUP信号。即使熟练的Unix
程序员参与某些讨论时,也可能忘记几分钟前TA还在fork(),并立即让父进程退出的
事实。一般来说,有两种典型的与SIGHUP信号相关的情形。

假设某session有控制终端,当session leader终止时,系统会向该session前台进程
组中所有进程分发SIGHUP信号。

如果某进程组中有一个进程,其父进程属于同一会话(session)的另一个进程组,则
该进程组不是"孤儿进程组",反之该进程组称为"孤儿进程组"。

APUE 9.10指出,当某进程的终止导致一个新的"孤儿进程组"产生,系统会向这个新
的"孤儿进程组"中处于"停止"状态的每个进程分发SIGHUP信号,然后分发SIGCONT信
号。那些未处于"停止"状态的进程不会收到这两个信号。

启动"tcpdump -i lnc0 udp &",此时tcpdump成为后台进程组成员。退出当前shell,
此时tcpdump成为孤儿进程组成员,但它处于"运行"状态。重新登录后会发现该进程
仍然存在,它不是daemon进程,TT列不为??。它没有收到SIGHUP信号,手动kill -1
是可以杀掉它的。

启动"nohup tcpdump -i lnc0 udp",此时tcpdump仍为前台进程组成员。从另一shell
执行"kill -1"杀掉前一shell,此时tcpdump成为孤儿进程组成员。有SIGHUP信号分
发到tcpdump,因为session leader终止了。nohup确保tcpdump继续运行。对比没有
使用nohup时的情形。

有一个bindshell,它只是简单fork()了一次,父进程立即退出。并未处理SIGHUP信
号,也未调用setsid()。它已经达到目的了。fork()之后产生一个后台孤儿进程组,
并未脱离控制终端,但再也不会有SIGHUP信号分发到bindshell。前述两种情形都不
会出现。在控制终端上按Ctrl-C产生的SIGINT信号不会分发到后台进程组。一般入侵
中要的就是这个效果,并不需要复杂的daemon_init()。

还有一种情形,简单fork()一次,父进程调用setpgid()使子进程自己成为进程组长,
然后父进程退出。这只是确保产生后台孤儿进程组,setpgid()不是必须的。子进程
仍然过继给init进程。

两位先生给出的daemon化步骤考虑得相当周全。但更多的入侵者、临时跳板工具并不
需要daemon化,最省事的办法就是fork()一次。最后再强调一次,脱离控制终端、彻
底脱离控制终端与不受SIGHUP信号影响是两回事,绝大多数时候要的只是后者的效果。

此外,Linux可能在某些细节上与上述讨论有出入,但最后的结论一样,最省事的办
法就是fork()一次。

个人推荐严肃的Unix/C程序员在需要这类效果时,统一使用daemon_init(),并捕捉
相关信号。

D: lskuangren@bbs.apue.net 2003-07-11

FreeBSD和Linux直接提供了一个函数,DAEMON(3)

--------------------------------------------------------------------------
DAEMON(3)          FreeBSD库函数手册                             DAEMON(3)

名字

    daemon - 在后台运行程序



    标准C库(libc, -lc)

语法

    #include <stdlib.h>

    int daemon ( int nochdir, int noclose );

描述

    daemon()用于脱离控制终端、转入后台运行程序(守护进程)。

    如果第一形参nochdir为零,daemon()最终执行chdir( "/" )。

    如果第二形参noclose为零,daemon()最终将stdin、stdout、stderr重定向到
    /dev/null。

错误

    失败时返回-1,并设置errno,errno的值与fork(2)、setsid(2)的情形一致。

参看

    fork(2), setsid(2)

历史

    4.4BSD首次引入了daemon()。
--------------------------------------------------------------------------

从man手册可以看出daemon()都做了些什么。在清楚自己到底需要何种效果的前提下,
可以不使用复杂的daemon_init()而直接使用daemon()。

AIX、Solaris未直接提供daemon(),如编写最广泛兼容程序,应避免使用daemon()。

--
   .   ● .  .       .   /\/\  .
 .   .  .        .     =(    )|\|\     今晚的月光好美呀~
      .    .        .    |   \    )=
    .      ______________\___/\___)_________________________________________
 . .   .  |_____|_____|_____|\/___|_____|_____|_____|_____|_____|_____|_____|
          |__|_____|_____|_____|_____|_____|_____|_____|_____|_____|_____|__|
※ 来源:·饮水思源 bbs.sjtu.edu.cn·[FROM: 202.120.5.*]


[回到开始] [上一篇][下一篇]

荔园在线首页 友情链接:深圳大学 深大招生 荔园晨风BBS S-Term软件 网络书店