如何编写daemon程序
时间:2006-01-12 来源:dclroom
24.3 如何编写daemon程序
Q: 在FreeBSD下用"ps auxw"查看进程列表时,注意到某些进程没有控制终端,也就
是说TT列显示??。知道这是所谓的daemon进程,如果我想自己编写这样的程序,
该如何做。 A: Andrew Gierth <[email protected]> 这个回答来自著名的<<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 <[email protected]> 以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 <[email protected]> 以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: [email protected] 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()。
是说TT列显示??。知道这是所谓的daemon进程,如果我想自己编写这样的程序,
该如何做。 A: Andrew Gierth <[email protected]> 这个回答来自著名的<<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 <[email protected]> 以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 <[email protected]> 以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: [email protected] 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()。
相关阅读 更多 +