一、引言
目前,Unix系统V在微机上应用比较普遍,如:System V Release 4.2/386、SCOUnix、UnixWare 1.1或UnixWare 2.0等,在Unix系统V中有许多daemon(幽灵)进程,它们是Unix系统的重要组成部分。daemon进程一般有两种运行方式:有的 daemon进程一直在运行并时刻等待某一事件的发生,如cron进程一直在运行并在指定的时间激活某一程序,sendmail进程一直在一个端口上侦听 是否有新邮件的到来等;有的daemon进程周期性地被激活,并且在完成一个任务后终止,如uucp系统连接和文件传输的uucico进程,当一个远程机 器登录时,它能够被一个登录shell激活,calendar进程能够在夜晚被cron进程激活去检查用户的日历和通知用户即将到来的事件等。那么什么是 daemon呢?所谓daemon就是能够自动执行周期性任务的程序,而且通常不为用户所感知。普遍用户可以根据需要编写自己的daemon程序,系统程 序员发现某个管理任务需经常运行时,也可以为它编写一个daemon程序。接触过Unix系统的用户都会觉得写一个daemon程序是比较简单的,但写一 个正确的daemon程序却不容易,因为用户在进行一次普遍的登录会晤时,可以忽略诸如设置用户和组的登录ID、建立会晤和进程组、分配控制终端、作业管 理控制等,这些工作Unix系统都替用户做了,而对于daemon进程则要复杂得多,如果daemon进程是在一次登录会晤现场之外被激活的(如:用 /etc/rc),则它需要自己负责设置作业管理控制等功能;如果是在一次登录会晤现场之内被激活的,则它可能需要取消某些系统为它做的设置。因此一个好 的daemon程序应该可以从任何现场被激活。
本文主要讨论如何编写有效的daemon程序和如何启动daemon进程。
二、daem的编程准则
根据我们工作的体验,要编写一个好的daemon程序一般需遵守以下一些准则。
1.应该使daemon进程不受后台作业I/O的影响
每个在登录会晤期间运行的程序都与一个控制终端相关,该控制终端就是用户从此处登录系统的。如果daemon进程是通过后台方式被激活的,则在读或写控 制终端时可能会使daem on进程出错。解决该问题最简单的办法是将daemon进程与它的控制终端脱联。但是在某些场合下,daemon进程需要执行某些设置检查或在失去它的控 制终端前显示错误信息。在这种情况下不能将daemon进程与它的控制终端脱联,但是如果让daemon进程忽略SIGTTOU信号,则能进行可靠输出。 为了安全起见,daemon进程还要忽略SIGTTIN和SIGTSTP信号,其中忽略SIGTT IN信号是避免daemon进程去读取控制终端的输入。
2.将daemon进程与它的原始进程组和控制终端脱联
一个进程组就是一个进程的集合,它可以通过一个进程组ID被引用,每个进程是某个进程组的成员,可以用进程组ID将一个信号发给该进程组中的所有进程。 例如:终端产生的信号可以发送给与这个终端相联的进程组中的每个进程。daemon在一次登录会晤时被激活,它继承控制终端、进程组和该会晤的现场,只要 该daemon进程还在与控制终端相联的进程组中,它就接收终端产生的信号(如:SIGINT或SIGHUP)。此外,当daemon进程还在它所启动的 进程组中, 当另一个进程给这个进程组发送信号时(如:通过kill系统调用),该daemon进程也将接收该信号。为了防止daemon进程接收这些不需要的信号, 一种简单办法是忽略所有的信号,这样,da emon进程就不能用信号机制进行工作。
例如:进程间通讯等。事实上,这种办法是不可行的, 因为有些信号是不能被忽略的,如:SIGKILL或SIGSTOP,所以必须有一种有效的办法将daemon 进程与它所继承的进程组分离开来。
在Unix系统V中,用setpgrp系统调用可以实现这种功能,此外也可以用setsid系统调用创建一个新的会晤、进程组,与原来的控制终端脱联。 然而为了保证setpgrp或setsid系统调用达到目的,该进程不能是一个进程组的头,所谓进程组的头就是该进程的进程ID与进程组的ID相等。该程 序必须先调用fork系统调用复制自己,这样在调用setpgrp或setsid之前确保已不是一个进程组的头。子进程不是一个进程组的头,因为子进程从 它的父进程处得到进程组的ID,但是从操作系统的核心得到一个新的进程ID,因此子进程的进程ID不可能与进程组的ID相等。
让daemon进程用fork系统调用复制自己是非常有用的。许多daemon进程永远不终止。如果一个用户从shell激活一个程序,但是忘记加后台 命令,shell就会一直等待该daemon的结束。假设该daemon进程屏蔽了键盘信号,则此shell就不可用了。为了防止这种偶然事故的发生, daemon进程需立即创建一个子进程,并且在子进程中运行,父进程不等待子进程的结束就终止,这样的执行顺序使shell确信该daemon进程已经结 束,因为父进程已终止,尽管子进程还在不感知地运行着。
3.daemon进程不要重新获得一个控制终端
一旦daemon进程成为没有控制终端的一个进程组头,它还有可能重新获得一个控制终端。如果这样,其它进程可能不能正确地获得它们的控制终端。在 Unix系统V中,当一个没有控制终端的进程组的头打开一个终端时,将获得一个新的控制终端,如:daemon能够用打开/dev/console来获得 一个控制终端。执行登录和错误输出,尽管daemon进程随后关闭了该终端,但是如果还有其它的进程打开相同的终端,则daemon还可以用此终端。此时 用setpgrp或setsi d系统调用已不起作用,因为调用者是进程组的头,所以必须防止重新获得一个控制终端。一种防止获得新控制终端的简单办法是:在调用setpgrp或 setsid后再创建一个子进程,daemon 在第二个子进程中运行,它的父进程(即第一个子进程)立即终止。因为终止的父进程是一个进程组的头,子进程的进程组设置为0。这样,该daemon进程没 有控制终端,并处在一个新的进程组中,因此可以避免从终端获得信号,也不会重新获得一个新的控制终端。
4.关闭所有打开的文件,特别是标准输入、标准输出和标准错误输出
如果有某些文件是终端设备,则它们必须被关闭,不要维持打开,即使确信打开一个终端设备时,该daemon将不会重新获得一个控制终端。因为这有更深一 层的考虑:终端的状态设置,如:波特率、信号字符的定义等,只有当最近打开的进程关闭它时才能重新设置。如果该daemon进程一直打开此终端,这样在该 终端上进行一次新的登录前,没有重新设置终端的属性。
5.最好将记录错误和状态的信息写入一个磁盘文件
因为该daemon进程没有一个控制终端,所以不能写到任何一个标准输出文件中。写到总控台也不是一个好的办法,因为总控台可能在一个窗口系统下运行。 但是用户也会发现:将错误和状态的信息写入自己定义的磁盘文件也是有困难的,因为用户必须知道哪些文件被哪些daemon所修改,而且用户必须时刻监视这 些文件。那么程序员该如何做呢?使用syslog机制。syslog机制是由4.2BSD引进的,在Unix系统V中也支持这种集中式的log机制。C库 为daemon进程提供使用log机制的函数有:openlog, setlogmask, syslog,closelog。
6.将当前工作目录改为根目录
每个进程都有一个当前工作目录,在该进程的生命周期,操作系统的核心使该目录文件处于打开状态。如果一个daemon进程的当前目录是在一个可装卸的文 件系统上,则该文件系统处于忙状态,它就不能被超级用户所拆卸,只有杀掉该daemon进程才能进行,因此daemon进程的当前工作目录最好不要在一个 可装卸的文件系统上,最可靠的选择是根目录。子进程继承父进程的当前工作目录,但是子进程可以用chdir系统调用来修改自己的当前工作目录。
根据以上讨论的注意事项,下面我们给出一个daemon程序的框架:
例:daemon程序的框架
main(argc.argv)
int argc;
char *argv[];/* argv[1] is the file name of daemon program */
{
/* Try to ensure that execv() will succeed because we won′t
* be able to report any execv() error after daemonsetup()
* closes all our open file descriptors.
*/
if (access(argv[1], X-OK) <0) {
fprintf(stderr, ″%s: Can′t execute/n″, argv[0]);
perror(argv[1]);
exit(2);
}
daemonsetup(); /* setup a correct daemon environment */
openlog(″My own daemon″ ,LOG-PID│LOG-CONS, LOG-DAEMON);
syslog (LOG-INFO, ″Daemon starting...″);
closelog();
execv(argv[1], &argv[1]); /*launch the daemon command */
/* NOTREACHED */
exit(3);
}
/*
* daemonsetup() --Setup a correct daemon environment
*/
daemonsetup()
{
int fd; /* file descriptor */
if (getppid()==1) /* if parent is init */
goto out; /* can skip ahead */
/* igmore background process attempts write signal */
signal(SIGTTOU,SIG-IGN);
/* ignore background process attempts read signal */
signal(SIGTTIN,SIG-IGN);
/* ignore keyboard-generated stop signal */
signal(SIGTSTP,SIG-IGN);
/* fork child process */
if(fork()!=0)
exit(0); /* parent terminates */
/* child code: */
setsid();/* become session leader and group process leader with no controll
ing terminal */
out:
for (fd=0;fd close(fd); /* close them */
chdir(″/″); /* move current directory off mounted file system */
return;
}
三、daemon进程的启动方式
daemon进程的启动方式主要有以下几种。
1.用系统V Unix中的at命令。at命令是从标准输入读取daemon程序名并在以后某个时刻运行该daemon程序。at允许用户指定何时运行该daemon,但日期和时间的格式须遵守环境变量LC-TIME所规定的。
2.用户自己为daemon进程编写一个crontab文件。由cron进程根据crontab文件的内容来激活daemon进程。
3.在shell上使用后台命令(&)。
4.在每次用户登录时通过一个shell启动文件来激活用户的daemon进程(如:cshell中的.login、.cshrc文件;shell的.profile文件等)。这种激活方式主要用于用户的daemon进程。
5.通过init进程。系统初始化init进程直接或间接地负责启动系统中的所有进程(核心的swapper、pageout进程等除外)。init进 程维持着它直接创建的所有进程的踪迹。如果这些进程死亡时,它可以有选择地重新激活它们;或者系统的运行状态改变为一个新状态时,它可以杀死这些进程。 /etc/inittab文件指定了某个运行状态时init程序须激活或者在它们死亡时时须重新激活的程序名。在Unix系统V中,系统的daemon进 程通常放在/etc/rc2.d目录下,当系统运行状态从单用户变为多用户时,由/etc/rc激活。此外,系统管理员也可以将系统daemon程序放在 /etc/inittab文件中,由init进程直接激活。这样,当daemon进程死亡时,ini t进程可以重新激活它;当系统状态改变时,init进程也可以杀死该daemon进程。这种激活方式主要用于系统的daemon进程。
--