概述
守护进程(daemon)是在后台运行且不与任何控制终端关联的进程。Unix系统通常由很多守护进程在后台运行(约在20到50个的量级),执行不同的管理任务。
守护进程有多种启动方法:
- 在系统启动阶段,许多守护进程由系统初始化脚本启动。这些脚本通常位于/etc目录或以/etc/rc开头的某个目录中,它们 的具体位置和内容确实实现相关的。由这些脚本启动的守护进程一开始时拥有超级用户特权。有若干个网络服务器通常从这些脚本启动:inetd超级服务器、Web服务器、邮件服务器;
- 许多网络服务器由inetd超级服务器启动。inetd自身由上一条中的某个脚本启动。inetd监听网络请求(Telnet、FTP等),每当有一个请求到达时,启动相应的实际服务器;
- cron守护进程按照规则定期执行一些程序,而由它启动执行的程序同样作为 守护进程运行。cron自身由第一条启动方法中的某个脚本启动;
- at命令用于指定将来某个时刻的程序执行。这些程序的执行时刻到来时,通常由cron守护进程启动执行它们,因此这些程序同样作为守护进程运行;
- 守护进程还可以从用户终端或在前台或在后台启动。这么做往往是为了测试守护程序或重启因某种原因而终止了的某个守护进程;
syslogd守护进程
Unix系统中的syslogd守护进程通常由某个系统初始化脚本启动,而且在系统工作期间一直运行。源自Berkeley的syslogd实现在启动时执行以下步骤:
- 读取配置文件。通常为/etc/syslog.conf的配置文件指定本守护进程可能收取的各种日志消息(log message)应该如何处理。这些消息可能被添加到一个文件(/dev/console文件是一个特例,它把消息写到控制台上),或被写到指定用户的登陆窗口,或被转发给另一个主机上的syslogd进程;
- 创建一个Unix域数据报套接字,给它捆绑路径名var/run/log(在某些系统上是/dev/log);
- 创建一个UDP套接字,给它捆绑端口514(syslog服务使用的端口号);
- 打开路径名/dev/klog。来自内核中的任何出错消息看着像是这个设备的输入;
通过创建一个Unix域数据报套接字,我们就可以从自己的守护进程中通过往syslogd绑定的路径名发送我们的消息达到发送日志消息的目的,然后更简单的接口是使用syslog函数。另外,我们也可以创建一个UDP套接字,通过往回环地址(127.0.0.1)和端口514发送我们的消息达到发送日志消息的目的。
syslog函数
#include <syslog.h>
void syslog(int priority, const char *message, ...);
日志消息的level可从0到7,若发送者未指定level值,那就默认为LOG_NOTICE。
日志消息还包含一个用于标识消息发送进程类型的facility。下图列出了facility的各种值。如果发送者未指定facility值,那就默认为LOG_USER。
举例来说,当rename函数调用意外失败时,守护进程可以执行以下调用:
syslog(LOG_INFO | LOG_LOCAL2, "rename(%s, %s): %m",file1, file2);
facility和level的目的在于:允许在/etc/syslog.conf文件中统一配置来自同一给定设施的所有消息,或者统一配置具有相同级别的所有消息。举例来说,该配置文件可能含有以下两行:
kern.* /dev/console
local7.debug /var/log/cisco.log
这两行指定所有内核消息登记到控制台,来自local7设施的所有debug消息添加到文件/var/log/cisco.log的末尾。
当syslog被应用进程首次调用时,它创建一个Unix域数据报套接字,然后调用connect连接到由syslogd守护进程创建的Unix域数据报套接字的众所周知路径名(譬如/var/run/log)。这个套接字一直保持打开,直到进程终止为止。作为替换,进程也可以调用openlog和closelog。
#include <syslog.h>
void openlog(const char *ident, int options, int facility);
void closelog(void);
openlog可以在首次调用syslog前调用,closelog可以在应用进程不再需要发送日志消息时调用。
【参数解析】
- ident :是一个由syslog冠于每个日志消息之前的字符串。它的值通常是程序名;
- options : 由下图所示的一个或多个常值的逻辑或构成:
daemon_init函数
作用:通过调用它(通常从服务器程序中),我们能够把一个普通进程转变为守护进程。该函数在所有Unix变体上都应该适合使用,不过有些Unix变体提供一个名为daemon的C库函数,实现类似的功能。
#include "unp.h"
#include <syslog.h>
#define MAXFD 64
int daemon_init(const char *pname, int facility){
int i;
pid_t pid;
if((pid = fork()) < 0)
return -1;
else if(pid)
_exit(0);
if(setsid() < 0)
return -1;
signal(SIGHUP, SIG_IGN);
if((pid = fork()) < 0)
return -1;
else if(pid)
_exit(0);
daemon_proc = 1;
chdir("/");
for(i=0;i<MAXFD;i++)
close(i);
open("/dev/null",O_RDONLY);
open("/dev/null",O_RDWR);
open("/dev/null",O_RDWR);
openlog(pname, LOG_PID, facility);
return 0;
}
- 首先调用fork,然后终止父进程,留下子进程继续运行,子进程继承了父进程的进程组ID,并且拥有自己的进程ID;
- setsid是一个POSIX函数,用于创建一个新的会话(session)。当前进程变为新会话的会话头进程以及新进程组的进程组头进程,从而不不再有控制终端;
- 忽略SIGHUP信号并且再次调用fork。该函数返回时,父进程实际上是上一次调用fork产生的子进程,它被终止掉,留下新的子进程继续运行。再次fork的目的是确保本守护进程将来即使打开了一个终端设备,也不会自动获得控制终端。当没有控制终端的一个会话头进程打开了一个终端设备时(该终端不会是当前某个其他会话的控制终端),该终端自动成为这个会话头进程的控制终端;
例子:作为守护进程运行的时间获取服务器程序
#include "unp.h"
#include <time.h>
int main(int argc, char **argv){
int listenfd,connfd;
socklen_t addrlen,len;
struct sockaddr *cliaddr;
char buff[MAXLINE];
time_t ticks;
if(argc < 2|| argc >3)
err_quit("usage: daytimecpsrv2 [<host>] <service or port>");
daemon_init(argv[0],0);
if(argc == 2)
listenfd = tcp_listen(NULL, argv[1], &addrlen);
else
listenfd = tcp_listen(argv[1], argv[2], &addrlen);
cliaddr = malloc(addrlen);
for( ; ; ){
len = addrlen;
connfd = accept(listenfd, cliaddr, &len);
err_msg("connection from %s", sock_ntop(cliaddr, len));
ticks = time(NULL);
snprintf(bbuff, sizeof(buff),"%.24s\r\n",ctime(&ticks));
write(connfd, buff, strlen(buff));
close(connfd);
}
}
【注】如果想要一个程序作为守护进程运行,我们就得避免调用诸如printf和fprintf之类的函数,改而调用我们的err_msg函数。
inetd守护进程
因特网超级服务器(inetd守护进程)可以解决两个问题。
问题:
- 所有的守护进程(ftp、tftp、telnet等)含有几乎相同的启动代码,既表现在创建套接字上,也表现在演变成守护进程上(类似我们的daemon_init函数);
- 每个守护进程在进程表中占据一个表项,然而它们大部分时间处于睡眠状态;
解决:
- 通过inetd处理普通守护进程的大部分启动细节以简化守护程序的编写。这么一来每个服务器不再有调用daemon_init函数的必要;
- 单个进程(inetd)就能为多个服务等待外来的客户请求,以此取代每个服务一个进程的做法。这么做减少了系统中的进程总数;
inetd随着自己演变成一个守护进程,接着读入并处理自己的配置文件。通常是/etc/inetd.conf的配置文件指定本超级服务器处理哪些服务以及当一个服务请求到达时该怎么做。该文件中每行包含的字段如下图:
下面是inetd.conf文件中作为例子的若干行:
当inetd调用exec执行某个服务器程序时,该服务器的真实名字总是作为程序的第一个参数传递。
【参数解析】
- wait-flag:指定由inetd启动的守护进程是否有意接管与之关联的监听套接字。
- UDP没有分离的监听和接受套接字,所以总是设置成wait;
- TCP则可以设置成wait或者是nowait
daemon_inted函数
#include "unp.h"
#include <syslog.h>
extern int daemon_proc; //defined in error.c
void daemon_inetd(const char *pname, int facility){
daemon_proc = 1;
openlog(pname, LOG_PID, facility);
}
例子:由inetd作为守护进程启动的时间获取服务器程序
#include "unp.h"
#include <time.h>
int main(int argc, char **argv){
socklen_t len;
struct sockaddr *cliaddr;
char buff[MAXLINE];
time_t ticks;
daemon_inetd(argv[0],0);
cliaddr = malloc(sizeof(struct sockaddr_storage));
len = sizeof(struct sockaddr_storage);
getpeername(0, cliaddr, &len);
err_msg("connection from %s",sock_ntop(cliaddr, len));
ticks = time(NULL);
snprintf(buff,sizeof(buff),"%.24s\r\n",ctime(&ticks));
write(0,buff,strlen(buff));
close(0);
exit(0);
}
【改动】
所有套接字创建代码(即对tcp_listen和accept的调用)都消失了。这些步骤改由inetd执行,我们使用描述符0(标准输入)指代已由inetd接受的TCP连接。其次,无限的for循环也显示了,因为本服务器程序将针对每个客户连接启动一次,服务完当前客户后进程就终止。
【书上的例子】