守护进程(daemon)是在后台运行且不与任何控制终端关联的进程。unix系统有很多守护进程在后台工作(有20到50的数量级),执行不同的管理任务。
守护进程没有控制终端通常源于它们由系统初始化脚本启动。然而守护进程也可能从某个终端由用户在shell提示符下键入命令行启动,这样的守护进程必须亲自脱离于控制终端的关联,从而避免与作业的控制、终端会话管理、终端产生信号等发生任何不期望的交互,也可以避免在后台运行的守护进程非预期低输出到终端。
1) 在系统启动阶段,由系统初始化脚本启动。这些脚本通常位于/etc目录或以/etc/rc开头的某个目录下。由这些脚本启动的守护进程一开始就拥有超级用户权限。有若干个网络服务器通常从这些脚本启动:inetd超级服务器、web服务器、邮件服务器。
2) 由inetd超级服务器启动,inetd自身由上一条中的某个脚本启动。inetd监听网络请求,每当有一个请求到达时,启动相应的实际服务器(telnet、ftp服务器)。
3) cron守护进程按照规则定期执行一些程序,而由它启动执行的程序同样作为守护进程运行。其自身由某个脚本启动。
4) at命令用于指定将来某个时刻的程序执行。这些程序的执行时刻到来时,通常有cron守护进程启动执行他们,因此这些程序同样作为守护进程运行。
5) 守护进程还可以从用户终端或在前台或在后台启动。这么做往往是为了测试守护程序或重启因某种原因而终止了的某个守护进程。
因为守护进程没有控制终端,所以当有事发送时他们得有输出消息的某种方式可用,而这些消息即可能是普通的通告性消息,也可能是需由系统管理员处理的紧急事件消息。syslog函数时输出这些消息的标准方式,它把消息发送给syslogd守护进程。
此守护进程通常由某个系统初始化脚本启动。源自berkeley的syslogd实现在启动时执行以下步骤:
1) 读取配置文件。通常为/etc/syslog.conf的配置文件指定本守护进程可能收取的各种日志消息应该如何处理。这些消息坑内被添加到一个文件/dev/console文件时一个特例,它把消息写到控制台上),或被写到指定用户的登录窗口,或被转发给另一个主机上的syslogd进程。
2) 创建一个unix域数据报套接字,给它捆绑路径名/var/run/log(有的是/dev/log)
3) 创建一个udp套接字,给他捆绑端口514(syslog服务使用的端口号)。
4) 打开路径名/dev/klog、来自内核中的任何出错消息看着像是这个设备的输入。
此后syslogd守护进程在一个无限循环中进行,调用select以等到它的三个描述符(来自上述第2、3、4步)之一变为可读,读取日志消息,并按照配置文件进行处理。如果守护进程收到sighup信号,那就是重新读取配置文件。
通过创建一个unix域数据报套接字,我们就可以从自己的守护进程中通过syslogd绑定的路径名发送我们的消息达到发送日志消息的目的,更简单的房还是是使用syslog函数。
守护进程没有控制终端,不能把消息fprintf到stderr上。从守护进程中登记消息常用技巧是调用syslog函数。
#include
void syslog(intpriority,const char *message,…..);
进程也可以使用openlog和closelog:
#include
void openlog(const char *ident,intoptions,int facility);
void closelog(void);
openlog可以在首次调用syslog前调用,closelog可以在应用进程不再需要发送日志消息时调用。
调用该函数(通常从服务器程序中),能够将一个普通进程变为守护进程。bsd、linux以及有些unix变种提供了名为daemon的c库函数,实现类似功能。
int
daemon_init(const char *pname, int facility)
{
int i;
pid_t pid;
if ( (pid = fork()) < 0)
return (-1);
else if (pid)
_exit(0); /* parent terminates */
/* child 1 continues... */
if (setsid() < 0) /* become session leader */
return (-1);
signal(sighup, sig_ign);
if ( (pid = fork()) < 0)
return (-1);
else if (pid)
_exit(0); /* child 1 terminates */
/* child 2 continues... */
daemon_proc = 1; /* for err_xxx() functions */
chdir("/"); /* change working directory */
/* close off file descriptors */
for (i = 0; i < maxfd; i )
close(i);
/* redirect stdin, stdout, and stderr to /dev/null
将stdin、stdout和stderr重定向到/dev/null,打开/dev/null作为本守护进程
的标准输入/输出/错误输出.
*/
open("/dev/null", o_rdonly);
open("/dev/null", o_rdwr);
open("/dev/null", o_rdwr);
openlog(pname, log_pid, facility); //使用sylogd处理错误
return (0); /* success */
}
如该程序一样,创建守护进程的过程为:
(1)首先调用fork,然后终止父进程,留下子进程继续在后台运行。
(2)脱离控制终端,登录会话和进程组(创建新会话)
有必要先介绍一下linux中的进程与控制终端,登录会话和进程组之间的关系:进程属于一个进程组,进程组号(gid)就是进程组长的进程号(pid)。会话(session)是一个或多个进程组的集合,登录会话可以包含多个进程组,这些进程组共享一个控制终端。这个控制终端通常是创建进程的登录终端。 控制终端,登录会话和进程组通常是从父进程继承下来的。我们的目的就是要摆脱它们,使之不受它们的影响。
方法是在第1点的基础上,调用setsid()函数建立新会话,使进程成为会话组长:setid()函数,用于创建一个新会话。
(3)忽略sigup信号并禁止进程重新打开控制终端
忽略sigup信号并在此调用fork。现在,进程已经成为无终端的会话组长。但它可以重新申请打开一个控制终端。 可以通过使进程不再成为会话组长来禁止进程重新打开控制终端。将父进程(上一次调用fork产生的子进程)终止,留下其新的子进程继续运行。再次fork目的是确保本守护进程将来即使打开一个终端设备,也不会自动获得控制终端。当没有控制终端的一个会话头进程打开一个终端设备时(该终端不会是当前某个其他会话的控制终端),该终端自动成为这个会话头进程的控制终端。然而,再次调用fork后,我们确保新的子进程不再是一个会话头进程,从而不能自动获得一个控制终端。这里必须忽略sigup信号,因为当会话头进程(即首次fork产生的子进程)终止时,其会话中所有进程(即再次fork产生的子进程)都收到sigup信号。
(4)改变工作目录
守护进程可能是在某个任意的文件系统中启动,如果仍在其中,那么该文件系统就无法拆卸(unmounting),除非使用潜在破坏性的强制措施。
(5)关闭所有打开的描述符
关闭守护进程从执行它的进程(通常是一个shell)继承来的所有打开的描述符。进程从创建它的父进程那里继承了打开的文件描述符。如不关闭,将会浪费系统资源,造成进程所在的文件系统无法卸下以及引起无法预料的错误。
问题是怎样检测正在使用的最大描述符:没有现成的unix函数提供该值。检测当前进程能够打开的最大描述符数目自有办法,然而由于这个限制可以是无限的,这样的监测也变得复杂。我们的解决办法是干脆关闭前64个描述符,及时其中大部分没有打开。
(6)重设权限掩码
进程从创建它的父进程那里继承了文件创建掩码。它可能修改守护进程所创建的文件的存取位。为防止这一点,将文件创建掩模清除:
umask(0);
典型的unix系统可能存在许多服务器,它们只是等待客户请求的到达,如ftp、telnet、rlogin、tftp等。最开始,所有的这些服务都与一个进程相关联,这些进程在系统启动时开始运行,而且执行几乎相同的启动任务:创建一个套接口,把本服务的众所周知的端口捆绑到该套接口,等待一个连接(tcp)或是一个数据报(udp),然后派生子进程。
子进程为客户提供服务,父进程则等待下一个客户请求。这个模型存在两个问题:
(1) 所有这些守护进程含有几乎相同的启动代码(套接口的创建以及成为守护进程)。
(2) 每个守护进程在进程表中占据一项,并且大部分时间处于睡眠状态。
inetd超级服务器使上述的问题得到简化:
(1) 通过
inetd处理普通进程的大部分细节以简化守护程序的编写。
(2) 单个进程(inetd)就能为多个服务等待外来的客户请求,以此取代每个服务一个进程的做法,减少了系统中的进程数。
(1)在启动阶段,读入配置文件(/etc/inetd.conf /etc/xinetd.conf),对于配置文件中的每个服务创建一个适当类型(tcp或udp)的套接口。新创建的每个套接口都被加入到将由某个select调用使用的一个描述字集中。
(2)为每个套接口调用bind(根据/etc/services中的配置项)。这个tcp或udp端口号通过getservbyname获得,作为函数参数的是相应服务器在配置文件章的service-name字段和protocol字段。
(3) 对于每个tcp套接口,调用listen以接受外来的连接请求;对数据报套接字不执行该步骤。
(4)创建完毕所有套接口后,调用select等待其中任何一个套接口变为可读。inetd的大部分时间阻塞于select调用内部,等待某个套接口变为可读。
(5)当select返回指出某个套接口可读以后,如果该套接口是tcp套接口,而且其服务器为nowait类型,则调用accept接受这个连接。
(6)inetd调用fork派生进程,并由子进程处理服务请求。
子进程关闭要处理的套接口描述字之外的所有描述字(对于tcp为accept返回的套接口,对于udp为最初创建的套接口),子进程三次调用dup2,把待处理套接口的描述字复制到描述字0、1、2上;然后关闭原套接口描述字。因此,子进程打开的描述字只有0、1、2。子进程从标准输入读,相当于从所处理的套接口读;子进程往标准输出或标准错误上写,相当于往所处理套接口写。
子进程根据login-name(user)的配置值,如果不是root,子进程则调用setgid和setuid把自身改为指定的用户。
子进程调用exec执行由配置文件指定的程序( 如上例中的/root/echo)来具体处理请求。
(7)如果5中返回的是tcp套接口,则父进程先关闭接受请求产生的连接套接口。父进程在此调用select,等待下一个变为可读的套接口。
inetd通常不适合提供大量密集服务的服务器,如mail服务以及web服务,因为inetd对每次请求都执行一次fork exec,对于频繁的数据请求来说,开销相当大。web服务器使用各种技术(如多线程等)尽量将每个连接处理的开销降到最小。