守护进程是在后台运行且不与任何控制终端关联的进程。Unix系统通常由很多守护进程在后台运行(约在20到50个的量级),执行不同的管理任务。
守护进程启动方法:
由系统初始化脚本启动,通常位于/etc/rc或/etc
目录下,由这些脚本启动的守护进程一开始拥有超级用户特权。
许多网络服务器由inetd超级服务器启动。
cron守护进程定期执行一些程序,由它执行的程序同样作为守护进程运行。
at命令指定将来某个时刻的程序执行。当这些程序的执行时间到了由cron来启动。
可以从用户终端前台和后台启动。
由于守护进程没有控制终端,通常使用syslog函数来输出消息,并通过syslogd守护进程接收消息。
syslogd
守护进程由某个系统初始化脚本启动,并且在系统工作期间一直运行。可以在终端输入ps -aux | grep syslog
查看。当syslog守护进程启动时一般执行下列步骤:
读取配置:通常为/etc/syslog.conf
指定syslogd
守护进程可能接收到的各种日志消息和怎样处理。当这些消息被添加到/dev/console
中时,则消息打印到控制台。
创建Unix域套接字,捆绑路径名/var/run/log(或者/dev/log)
。
创建udp套接字,端口号为514,通过命令cat /etc/services | grep "syslog"
查看。
打开路径名/dev/klog
。内核的错误消息看着像是这个设备的输入。
监听:调用select,监听上面几个步骤。(收到信息,读入并安装配置进行处理;收到SIGHUP信号,重新配置文件)。
注:新的syslogd实现禁止创建UDP套接字,会造到Dos攻击。
守护进程没有控制终端,因此我们不能把消息fprintf
到stderr
上,那么就可以使用syslog
函数:
#include
void syslong(int priority,const char *message,...);
//priority:级别(level):默认:LOG_NOTICE,和设施(facility):默认:LOG_USER,两者的组合,如下两图所示。
rename函数调用意外失败时,守护进程可以执行以下调用:
syslog(LOG_IOFO|LOG_LOCAL2,"rename(%s,%s):%m",file1,fil);
//facitily和level:允许/etc/syslog.conf文件统一配置来自同一给定设施的所有信息,或者统一配置具有相同级别的所有消息。
//例:该配置文件可能含有以下两行:
kern.* /dev/debug //内核的所有消息发送控制台
loca17.debug /var/log/cisco.log //来自loca17的所有消息添加到文件cisco.log的尾部
当syslog被应用进程首次调用时,会创建一个Unix域数据报套接字,然后调用connect连接由syslogd守护进程创建的Unix域数据报套接字的路径名(/var/run/log)。这个套接字一直保持打开,直到进程终止为止。进程也可以调用openlog和closelog
。
//openlog可以在首次调用syslog前调用,closelog可以在应用进程不再需要发送日志消息时调用。
#include
void openlog(const char *ident,int options,int facility);
//ident:日志前缀,通常为程序名
//options:如下图所示
//facility:设置默认的打印设施的值
void closelog(void);
int setlogmask(int maskpri);
//为当前的进程设置这个日志掩码,并返回先前的掩码。如果掩码参数为0,那么当前的日志掩码不会被修改。
openlog
被调用时,通常并不立即创建Unix域套接字。相反,该套接字直到首次调用syslog
时才打开。
LOG_NDELAY
选项迫使该套接字在openlog
被调用时就创建。
openlog
的facility
参数为没有指定设施的后续syslog
调用指定一个默认值。
有些守护进程通过调用openlog
指定一个设施 (对于一个给定守护进程,设施通常不变),然后在每次调用syslog
时只指定级别(因为级别可随错误性质改变)。
日志消息也可以由logger
命令产生。例:logger
命令可用在shell脚本中以向syslogd
发送消息。
将一个普通进程转变为守护进程。(daemon的C库函数,实现类似功能)。
#include "unp.h"
#include
#define MAXFD 64
extern int daemon_proc; /* defined in error.c */
int daemon_init(const char *pname, int facility)
{
int i;
pid_t pid;
//首先调用fork, 然后终止父进程,留下子进程继续运行。子进程继承了父进程的进程组ID,不过它有自己的进程ID。
//保证子进程不是进程组的头进程。
if ( (pid = Fork()) < 0)
return (-1);
else if (pid)
_exit(0); /* parent terminates */
/* child 1 continues... */
//创建新的会话,当前进程变为新会话的会话头进程已及新进程组的进程组头进程,从而不再有控制终端。
if (setsid() < 0) /* become session leader */
return (-1);
//忽略SIGHUP信号并再次调用fork。该函数返回时,父进程实际上是上一次调用fork产生的子进程,它被终止掉,留下新的子进程继续运行。
//再次fork的目的是确保本守护进程将来即使打开了一个终端设备,也不会自动获得控制终端。
//当没有控制终端的一个会话头进程打开一个终端设备时(该终端不会是当前某个其他会话的控制终端),该终端自动成为这个会话头进程的控制终端。
//然而再次调用fork之后,我们确保新的子进程不再是一个会话头进程,从而不能自动获得一个控制终端。
//这里必须忽略SIGHUP信号,因为当会话头进程(即首次fork产生的子进程)终止时,其会话中的所有进程(即再次fork产生的子进程)都收到SIGHUP信号。
Signal(SIGHUP, SIG_IGN);
if ( (pid = Fork()) < 0)
return (-1);
else if (pid)
_exit(0); /* child 1 terminates */
/* child 2 continues... */
//非0值改为调用syslog,取代fprintf到标准错误输出。
daemon_proc = 1; /* for err_XXX() functions */
//把工作目录改到根目录。
chdir("/"); /* change working directory */
//关闭本守护进程从它的进程继承来的所有打开着的描述符。
/* close off file descriptors */
for (i = 0; i < MAXFD; i++)
close(i);
//打开/dev/null作为本守护进程的标准输入、标准输出和标准错误输出。
/* redirect stdin, stdout, and stderr to /dev/null */
open("/dev/null", O_RDONLY);
open("/dev/null", O_RDWR);
open("/dev/null", O_RDWR);
//第一个参数来自调用者,通常是程序名称。第二个:指定把进程ID加到每个日志消息中。第三个:调用者决定
openlog(pname, LOG_PID, facility);
return (0); /* success */
}
如果想要一个程序作为守护进程运行,我们就得避免调用诸如printf和fprintf
之类函数,改而调用我们的err_msg
函数。
#include "unp.h"
#include
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 [] " );
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);
}
}
Unix系统存在许多服务器,只等待客户请求的达到。例FTP、Telnet、Rlogin、TFTP
等。服务都有一个进程相关联。进程在/etc/rc
文件中启动,且执行相同任务:
存在问题:
所有这些守护进程含有几乎相同的启动代码,既表现在创建套接字上,也表现在演变成守护进程上(类似daemon_init函数)。
inetd守护进程(因特网超级服务器简化上面问题,基于TCP或UDP的服务器都可以使用这个守护进程。
通过由inetd处理普通守护进程的大部分启动细节以简化守护程序的编写。每个服务器不再有调用daemon_init
函数的必要。
单个进程就能为多个服务等待外来的客户请求,以此取代每个服务一个进程的做法。这么做减少了系统中的进程总数。
inetd进程
使用我们随daemon_init
函数讲解的技巧把自己演变成一个守护进程。它接着读入并处理自己的配置文件。通常是/etc/inetd.conf
的配置文件指定本超级服务器处理哪些服务以及当一个服务请求到达时该怎么做。文件包含如下内容:
inetd.conf文件中作为例子的若干行:
当inetd调用exec执行某个服务器程序时,该服务器的真实名字总是作为程序的第一个参数传递。
①TCP:
select返回可读后,若套接字是TCP套接字,其wait-flag为nowait
,则调用accept接受新的连接,inetd守护进程调用fork派生进程,并由子进程处理服务请求,关闭相应的描述符。子进程关闭除了要处理的套接字描述符为的所有描述符,并根据配置文件切换相应的用户,用exec执行相应的操作。若设置为wait,TCP中父进程用FD_CLR
禁止这个套接字,保存子进程ID,子进程终止(SIGCHILD
)后监听。
②UDP:
数据报服务器只有一个套接字,所以只能按步处理,通过子进程的SIGCHLD
信号后才进行监听,因为fork后一个子进程,而父进程优先子进程再次执行,则会由于没有读取数据而再次触发select事件,从而再次fork个无用的子进程。
注:Linux系统是,xinetd提供和inetd一致的基本服务,还提供了许多其他特性。
为错误处理函数设置daemon_proc标志。
#include "unp.h"
#include
extern int daemon_proc;
void daemon_inetd(const char *pname, int facility)
{
daemon_proc = 1; /* for our err_XXX() functions */
openlog(pname, LOG_PID, facility);
}
#include "unp.h"
#include
int main(int argc, char **argv){
socklen_t len;
struct sockaddr *cliaddr;
char buff[MAXLINE];
time_t ticks;
daemon_inetd(argv[0],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);
}
//1.所有套接字创建代码消失。步骤由inetd执行。
//2.描述符0(标准输入)指代已由inetd接收的TCP连接。
//循环消失,因为针对每个客户连接启动一次。服务完当前客户后进程终止。
//例:
//运行程序在/etc/services文件添加服务器名和端口:
mydaytime 9999/tcp
//在/etc/inetd.conf中添加:
mydaytime stream tcp nowait andy
/home/andy/daytimetcpsrv3 daytimetcpsrv3
//然后给inetd发送SIGHUP信号,告知它重新读入其配置文件。