全部博文(51)
分类: 系统运维
2008-11-23 03:18:13
守护进程()是UNIX系统中的一种特殊进程,它通常以某种特殊用户身份运行,父进程通常是init ,永远不占有控制终端,没有任何与标准输入输出的交互。它在启动成功后将在系统内永久驻留,除非被强行终止。典型的守护进程随系统自举而启动,在系统关闭时终止。
但很多开源程序都有很好的用于守护进程创建的daemonize函数代码可以作为学习参考,例如lighttpd(8)。可以用svn(1)程序下载它的最新开发版本的:
svn co svn://svn.debian.org/pkg-lighttpd/lighttpd/trunk
执行fork(2),使父进程退出,一来可以使启动命令正常退出,二来可以使得子进程不是进程组组长,不会占有当前shell,并使子进程变成由init进程接管的孤儿进程。
if (0 != fork()) exit(0);
通过setsid(2)创建新会话并保证没有控制终端;
if (-1 == setsid()) exit(0);
再次fork并退出,使得子进程不是该会话首进程,从而保证不能获得tty;
if (0 != fork()) exit(0);
清umask为0,避免守护进程受到继承的umask的权限的干扰;
umask(0);
若需要只生成单实例的进程,创建一个固定的pidfile(通常放在/var/run下)并锁住它,如果此文件已存在并被锁定,则认为已经有进程实例。lighttpd尽管也使用了pidfile,但并不实现单实例的daemon。无论是否实现单daemon实例,代码量都不是几行就能完成的,此处略。可参考sysklogd(8)的pidfile.c。
若有安全等考虑,进入工作目录并进行chroot(2)(这里我对原来的代码进行了简化);
if (-1 == chroot(rootdirp)) {
log_error_write("chroot failed: ", strerror(errno));
return -1;
}
用chdir(2)到根目录;
if (0 != chdir("/")) exit(0);
关闭已打开不需要的所有fd(若需要);
for (i = 0; i < limit.rlim_max; i++)
{
close(i);
}
紧接着使标准输入、标准输出、标准出错指向/dev/null,使它们不能使用:
/* close stdin and stdout, as they are not needed */
/* move stdin to /dev/null */
if (-1 != (fd = open("/dev/null", O_RDONLY))) {
close(STDIN_FILENO);
dup2(fd, STDIN_FILENO);
close(fd);
}
/* move stdout to /dev/null */
if (-1 != (fd = open("/dev/null", O_WRONLY))) {
close(STDOUT_FILENO);
dup2(fd, STDOUT_FILENO);
close(fd);
}
这一段在4.4BSD-lite的daemon(3)函数实现中要简洁一些:
if (!noclose && (fd = open(_PATH_DEVNULL, O_RDWR, 0)) != -1) {
(void)dup2(fd, STDIN_FILENO);
(void)dup2(fd, STDOUT_FILENO);
(void)dup2(fd, STDERR_FILENO);
if (fd > 2)
(void)close (fd);
}
由于守护进程已经没有标准输入输出,若需要监视进程执行情况,应使用syslog(3)机制;
openlog(daemonstring, LOG_CONS, LOG_DAEMON);
if (errstring = capture_some_errors())
{
syslog(LOG_ERROR, errstring);
}
若需要,注册对SIGHUP的信号捕捉函数,用于执行重新读取配置文件;
开始daemon例程;
产生日志信息的方法有:
通过本地syslog(3) API;
通过远程的syslog服务器(UDP端口514);
使用logger(1)工具;
使用syslog(3)需要系统守护进程syslogd已经启动,守护进程事实上是通过套接字/dev/log与syslogd进程进行通信的。相关的API包括了
#include
void openlog(const char *ident, int option, int facility);
void syslog(int priorty, const char *format, ...);
void vsyslog(int priorty, const char *format, va_list ap);
void closelog(void);
int setlogmask(int maskpri);
其中的参数ident为日志消息的前缀字符串,通常为程序名,option为6种选项的逻辑和,facility为消息的功能分类,包括LOG_DAEMON, LOG_KERN, LOG_USER等。而priority是功能分类与严重等级的逻辑和。严重等级按严重程度顺序包括了从LOG_EMERG到LOG_DEBUG共8个等级。syslog函数将把严重性大于等于指定的等级以上的事件记录到对应的日志文件中。format用于vsprintf的格式化字符串,其中的%m格式转换为strerror(errno)的结果。函数setlogmask可以屏蔽掉某些优先等级;
以下程序将前缀字符串“test log”和PID的日志信息送到标准出错,同时记录到记录LOG_INFO以上等级的日志文件中:
#include
int main(void)
{
openlog("test log", LOG_PID | LOG_PERROR, LOG_USER);
syslog(LOG_INFO, "Log me, man.\n");
closelog();
return 0;
}
编译运行之,可见/var/log下的文件messages、syslog、user.log都被更新:
$ cc -o logit logit.c
$ ./logit
test log[25226]: Log me, man.
$ ls -lt /var/log/ | head -5
total 21M
-rw-r----- 1 syslog adm 3.7M 2008-11-22 01:08 messages
-rw-r----- 1 syslog adm 6.4M 2008-11-22 01:08 syslog
-rw-r----- 1 syslog adm 127K 2008-11-22 01:08 user.log
-rw-r----- 1 syslog adm 543K 2008-11-21 23:35 debug
-rw-r--r-- 1 root root 23K 2008-11-21 22:49 Xorg.0.log
$ tail -1 /var/log/messages
Nov 22 01:08:46 mjxian test log[25226]: Log me, man.
$ tail -1 /var/log/syslog
Nov 22 01:08:46 mjxian test log[25226]: Log me, man.
$ tail -1 /var/log/user.log
Nov 22 01:08:46 mjxian test log[25226]: Log me, man.
daemon(3)函数可以直接使得进程变成一个守护进程。但该函数不存在于POSIX标准中,而是4.4BSD以来BSD家族都提供的一个函数接口。使用GNU的glibc库的Linux也提供了此函数,但Solaris的库不提供。
#include
int daemon(int nochdir, int noclose);
该函数将使进程变成一个守护进程,即脱离控制终端并在后台执行。参数nochdir为0时,进程的工作目录将切换到根目录,否则是当前工作目录;noclose为0时,不使用标准输入、标准输出和标准出错。
测试程序:
#include
#include
#include
int main(void)
{
printf("Before turn to a daemon process.\n");
printf("Current directory: %s\n", getcwd(NULL, 1024));
#ifdef _NOCHROOT_
daemon(1, 1);
#else
daemon(0, 0);
#endif
printf("Now I am a daemon process.\n");
fprintf(stderr, "A message sent to standard error\n");
openlog("[the daemon cwd log]", LOG_PID | LOG_INFO, LOG_USER);
syslog(LOG_INFO, "Current directory: %s.\n", getcwd(NULL,1024));
sleep(360);
return 0;
}
编译并执行:
$ cc -o daemon daemon.c
$ ./daemon
Before turn to a daemon process.
Current directory: /home/pub/workspace/apuetest
$ jobs
$ tail -1 /var/log/messages
Nov 23 01:05:56 mjxian [the daemon cwd log][21069]: Current directory: /.
$ ps -o pid,ppid,stat,cmd 21069
PID PPID STAT CMD
21069 1 Ss ./daemon
$ cc -D_NOCHROOT_ -o daemon daemon.c
$ ./daemon
Before turn to a daemon process.
Current directory: /home/pub/workspace/apuetest
Now I am a daemon process.
A message sent to standard error
$ jobs
$ tail -1 /var/log/messages
Nov 23 01:20:06 mjxian [the daemon cwd log][22048]: Current directory: /home/pub/workspace/apuetest.
$ ps -o pid,ppid,stat,cmd 22048
PID PPID STAT CMD
22048 1 Ss ./daemon
可见,调用daemon(0, 0)之后已经不能输出到当前tty,工作目录已经变更到“/”,且父进程变成了init(即PID为1的进程)。而调用daemon(1,1)则可以正常使用标准输入和标准输出,且不会改变工作目录。
如果使用锁定文件的方法实现单一实例的守护进程,该文件一般放在/var/run下,文件名为{守护进程名}.pid,同时文件内容为该进程的PID。如/var/run/syslogd.pid;
如果使用配置文件,则配置文件默认放在/etc目录下,名为{守护进程名}.conf;如/etc/syslog.conf;
守护进程的启动和关闭脚本放在/etc/rc.d(4.4BSD及其后代或者追随者的风格,如FreeBSD)或者/etc/init.d(SVR4及其后代或者追随者的风格,如Solaris)中。并链接到通过inittab(5)(Linux的Ubuntu发行版不使用这个方式而用了包)配置它在某一运行级别中的行为。