daemon
)是生存期长的一种进程。它们常常在系统引导装入时启动,仅在系统关闭时才终止。因为它们没有控制终端,所以说它们是在后台运行的。d
结尾,如inetd
提供网络服务,sshd
提供ssh
登录服务,httpd
提供web
服务等待。
setsid
的结果(setsid
会断开与控制终端的联系)。init(1)
进程。#include "apue.h"
#include
#include
#include
void
daemonize(const char *cmd)
{
int i, fd0, fd1, fd2;
pid_t pid;
struct rlimit rl;
struct sigaction sa;
/*(1)清空文件模式创建屏蔽字*/
umask(0);
/*
* 获取最大文件描述符
*/
if (getrlimit(RLIMIT_NOFILE, &rl) < 0)
err_quit("%s: can't get file limit", cmd);
/*(2)调用fork,然后使父进程exit退出*/
if ((pid = fork()) < 0)
err_quit("%s: can't fork", cmd);
else if (pid != 0) /* parent */
exit(0);
/*(3)调用setsid创建一个新会话*/
setsid();
/*(3`)再次调用fork,终止父进程,继续使用子进程中的守护进程。
这就保证了该守护进程不是会话首进程,可以防止它取得控制终端*/
sa.sa_handler = SIG_IGN;
sigemptyset(&sa.sa_mask);
sa.sa_flags = 0;
if (sigaction(SIGHUP, &sa, NULL) < 0)//忽略SIGHUP信号,见9.10节孤儿进程组
err_quit("%s: can't ignore SIGHUP", cmd);
if ((pid = fork()) < 0)
err_quit("%s: can't fork", cmd);
else if (pid != 0) /* parent */
exit(0);
/*(4)将当前工作目录更改为根目录*/
if (chdir("/") < 0)
err_quit("%s: can't change directory to /", cmd);
/*(5)关闭所有不再需要的文件描述符*/
if (rl.rlim_max == RLIM_INFINITY)
rl.rlim_max = 1024;
for (i = 0; i < rl.rlim_max; i++)
close(i);
/*(6)将文件描述符0/1/2指向/dev/null*/
fd0 = open("/dev/null", O_RDWR);
fd1 = dup(0);
fd2 = dup(0);
/*初始化log文件*/
openlog(cmd, LOG_CONS, LOG_DAEMON);
if (fd0 != 0 || fd1 != 1 || fd2 != 2) {
syslog(LOG_ERR, "unexpected file descriptors %d %d %d",
fd0, fd1, fd2);
exit(1);
}
}
umask
将文件模式创建屏蔽字设置为一个已知值(通常是0),见4.8
节相关内容。由继承(如fork
)得来的文件模式创建屏蔽字可能会被设置为拒绝某些权限。如果守护进程要创建文件,那么它可能要设置特定的权限。fork
,然后使父进程exit
退出,这样会实现以下几点:
shell
命令启动的,那么父进程终止会让shell
认为这条命令已经执行完毕。ID
,但是获得了一个新的进程ID
,因此子进程不是该进程组的组长进程,这是接下来进行setsid
调用的先决条件。setsid
创建一个新会话,见9.5节相关内容,这样会使调用进程:
fork
,终止父进程,继续使用子进程中的守护进程,这就保证了该守护进程不是会话首进程,可以防止它取得控制终端。open
函数打开终端设备时,设置O_NOCTTY
标志。getrlimit
函数判定最高文件描述符值,并关闭直到该值的所有描述符。/dev/null
使文件描述符0/1/2
指向该文件。这样使得任何一个试图读标准输入、写标准输出或标准错误的例程都不会产生任何效果。因为守护进程不与终端设备关联,因此其输出无处显式,也无处从交互式用户那里接收输入。
/dev/null
文件:
EOF
。/dev/null
被称为位桶(bit bucket
)或者黑洞(black hole
)。空设备通常被用于丢弃不需要的输出流,或作为用于输入流的空文件。这些操作通常由重定向完成。/dev/zero
文件:
/dev/random
和/dev/urandom
文件:
/dev/random
产生随机数据依赖系统中断,当系统中断不足时,/dev/random
设备会“挂起”,因而产生数据速度较慢,但随机性好;/dev/urandom
不依赖系统中断,数据产生速度快,但随机性较低。syslog
设施
syslog
设施,其组织结构如下:syslog
函数产生日志消息,该函数将消息发送至UNIX域数据报套接字/dev/log
。/dev/log
是一个套接字类型文件TCP/IP
网络连接到此主机的其他主机上,都可以将日志消息发送到UDP
端口514
log
函数产生日志消息syslogd
是一个守护进程。不同的进程(client
)都可以将log
输送给syslogd
(server
),由syslogd
集中收集。syslogd
可以将log
保存到本地,也可以发送到共享内存或远程服务器。syslogd
守护进程读取所有3种格式的日志消息。syslogd
在启动时读一个配置文件/etc/syslog.conf
,该文件决定了不同种类消息应该送往何处。如一个紧急消息可在控制台上打印,而警告信息记录到一个文件中。syslog
设施的接口函数void openlog(const char *ident, int option, int facility);
void syslog(int priority, const char *format, ...);
void closelog(void);
int setlogmask(int mask);
调用openlog
是可选择的。如果不调用openlog
,则在第一次调用syslog
时,自动调用openlog
。
closelog
也是可选的,因为它只是关闭曾被用于与syslogd
守护进程进行通信的描述符
openlog
函数
ident
参数:此参数是一个字符串,将被加至每一则日志消息中。(类比perror
函数)option
参数:指定的标志用来控制openlog()
操作和syslog()
的后续调用。他的值为下列值或运算的结果:
LOG_CONS
:若日志消息不能通过UNIX
域数据报套接字送至syslogd
守护进程,则将该消息写至控制台LOG_NDELAY
:立即打开至syslogd
守护进程的UNIX
域数据包套接字,不要等到第一条消息已经被记录时再打开。(通常在记录第一条消息前不打开该套接字文件)LOG_NOWAIT
:在记录日志信息时,不等待可能的子进程的创建LOG_ODELAY
:在第一条消息被记录之前延迟打开至syslogd
守护进程的连接LOG_PERROR
:除了将日志消息发送给syslogd
以外,还将它写至标准错误stderr
LOG_PID
:每条消息都包含进程PID
facility
参数:指定记录消息程序的类型。syslogd
通过指定的配置文件,将以不同的方式来处理来自不同设施的消息(即这个要与syslogd
守护进程的配置文件对应,日志信息会写入syslog.conf
文件的指定位置)。openlog
或者该参数值为0
,则在调用syslog
时,可以将facility
参数作为syslog
的priority
参数的一部分。syslog
函数:产生一个日志消息
setlogmask
函数
mask
参数:日志优先级掩码,在该掩码中的消息才会被真正记录。该掩码是level
中各个常量的按位或openlog("lpd",LOG_PID,LOG_LPR);
syslog(LOG_ERR,"open error for %s:%m",filename);
ident
字符串设置为程序名"lpd"
,LOG_PID
参数指定该进程ID要始终被打印,并且将系统默认的facility
设定为行式打印机系统(LOG_LPR
参数)。对syslog
制定一个出错条件(LOG_ERR
)和一个消息字符串。openlog
,该程序可以有第二个调用形式syslog(LOG_LPR|LOG_ERR,"open error for %s:%m",filename);
priority
参数指定为level
和facility
的组合。有时候需要在任意时刻只运行该守护进程的一个副本。如果同时运行该守护进程的多个实例,则会出现错误。
可以通过文件和记录锁机制,该方法保证一个守护进程只有一个副本在运行(见14.3
节)。如果每一个守护进程创建一个有固定名字的文件,并在该文件的整体上加一个写锁,那么只允许创建一把这样的写锁。在此之后创建写锁的尝试都会失败,这向后续守护进程副本指明已有一个副本正在运行。
文件和记录锁提供了一种方便的互斥机制。如果守护进程在一个文件的整体上得到一把写锁,那么在该是守护进程终止时,这把锁将被自动删除。这就简化了复原所需的操作。
实例:以下程序说明了如何使用文件和记录锁来保证只运行守护进程的一个副本。
#include
#include
#include
#include
#include
#include
#include
#include
#define LOCKFILE "/var/run/daemon.pid"
#define LOCKMODE (S_IRUSR|S_IWUSR|S_IRGRP|S_IROTH)
extern int lockfile(int);
int
already_running(void)
{
int fd;
char buf[16];
fd = open(LOCKFILE, O_RDWR|O_CREAT, LOCKMODE);
if (fd < 0) {
syslog(LOG_ERR, "can't open %s: %s", LOCKFILE, strerror(errno));
exit(1);
}
if (lockfile(fd) < 0) {
if (errno == EACCES || errno == EAGAIN) {
/*守护进程实例已经存在*/
close(fd);
return(1);
}
syslog(LOG_ERR, "can't lock %s: %s", LOCKFILE, strerror(errno));
exit(1);
}
/*能运行到此处,说明该进程是守护进程的唯一副本*/
ftruncate(fd, 0);
/*得到进程id*/
sprintf(buf, "%ld", (long)getpid());
/*将进程ID写入该文件*/
write(fd, buf, strlen(buf)+1);
return(0);
}
PID
写入到指定文件中。如果该文件已经加了锁,那么lockfile
函数将会返回失败,errno
设为EACCES
或EAGAIN
(lockfile
函数的具体实现见见14.3
节)。函数返回1
,表明该守护进程已在运行。0
,其原因是之前的守护进程ID
字符串可能长于调用此函数的当前进程的进程ID
字符串。比如之前是12345
,现在是9999
,那么在文件中留下的就是99995
,将文件截断为0
就可解决该问题。若守护进程使用锁文件,那么该文件通常存储在/var/run
目录中(/var
包括系统运行时要改变的数据)。守护进程可能需要超级用户权限才能在此目录下创建文件。锁文件的名字通常是name.pid
,其中name
是该守护进程或服务的名字。如cron
守护进程锁文件的名字就是/var/run/crond.pid
若守护进程支持配置选项,那么配置文件通常存放在/etc
目录中。配置文件的名字通常是name.conf
,其中name
是该守护进程或服务的名字。例如syslogd
守护进程的配置文件通常是/etc/syslog.conf
守护进程可用命令行启动,但通常它们是由系统初始化脚本之一(/etc/rc*
或/etc/init.d/*
)启动的。如果在守护进程终止时,应当自动地重新启动它,则我们可以在/etc/inittab
中为该守护进程包括respawn
记录项,这样init
就重新启动该守护进程。
若一个守护进程有一个配置文件,那么当该守护进程启动时会读该文件,但在之后一般就不会再查看它。若某个管理员更改了配置文件,那么该守护进程可能需要被停止,然后再启动,以使配置文件生效。为避免这种麻烦,某些守护进程将捕捉SIGHUP
信号,当它们收到该信号时重新读配置文件。
SIGHUP
时在信号捕捉函数内进行配置文件重读;SIGHUP
信号阻塞,当有阻塞的SIGHUP
时,sigwait
函数返回,然后执行配置文件重读。实例:说明守护进程可以重读其配置文件的一种方法,该程序使用sigwait
以及多线程,具体用法见12.8节。
#include "apue.h"
#include
#include
sigset_t mask;
extern int already_running(void);
void
reread(void)
{
/* ... */
}
void *
thr_fn(void *arg)
{
int err, signo;
for (;;) {
err = sigwait(&mask, &signo);
if (err != 0) {
syslog(LOG_ERR, "sigwait failed");
exit(1);
}
switch (signo) {
/*当收到SIGHUP信号,该线程调用reread函数重读它的配置文件*/
case SIGHUP:
syslog(LOG_INFO, "Re-reading configuration file");
reread();
break;
/*当收到SIGTERM信号,会记录消息并退出*/
case SIGTERM:
syslog(LOG_INFO, "got SIGTERM; exiting");
exit(0);
default:
syslog(LOG_INFO, "unexpected signal %d\n", signo);
}
}
return(0);
}
int
main(int argc, char *argv[])
{
int err;
pthread_t tid;
char *cmd;
struct sigaction sa;
if ((cmd = strrchr(argv[0], '/')) == NULL)
cmd = argv[0];
else
cmd++;
/*调用13.4节中的daemonize函数来初始化守护进程*/
daemonize(cmd);
/*调用13.5节中的already_running函数以确保该守护进程只有一个副本在运行*/
if (already_running()) {
syslog(LOG_ERR, "daemon already running");
exit(1);
}
/*
* Restore SIGHUP default and block all signals.
*/
sa.sa_handler = SIG_DFL;
sigemptyset(&sa.sa_mask);
sa.sa_flags = 0;
/*此时SIGHUP信号仍被忽略,所以需要恢复对信号的默认处理方式,
否则调用sigwait的线程绝不会见到该信号*/
if (sigaction(SIGHUP, &sa, NULL) < 0)
err_quit("%s: can't restore SIGHUP default");
sigfillset(&mask);
/*阻塞所有信号,为何要这样做?防止主线程响应信号,允许其他线程响应*/
if ((err = pthread_sigmask(SIG_BLOCK, &mask, NULL)) != 0)
err_exit(err, "SIG_BLOCK error");
/*创建一个线程去处理信号,该线程唯一工作是等待SIGHUP和SIGTERM。*/
err = pthread_create(&tid, NULL, thr_fn, 0);
if (err != 0)
err_exit(err, "can't create thread");
/*
* Proceed with the rest of the daemon.
*/
/* ... */
exit(0);
}
实例:单线程守护进程捕捉SIGHUP
并重读其配置文件
#include "apue.h"
#include
#include
extern int lockfile(int);
extern int already_running(void);
void
reread(void)
{
/* ... */
}
void
sigterm(int signo)
{
syslog(LOG_INFO, "got SIGTERM; exiting");
exit(0);
}
void
sighup(int signo)
{
syslog(LOG_INFO, "Re-reading configuration file");
reread();
}
int
main(int argc, char *argv[])
{
char *cmd;
struct sigaction sa;
if ((cmd = strrchr(argv[0], '/')) == NULL)
cmd = argv[0];
else
cmd++;
/*
* Become a daemon.
*/
daemonize(cmd);
/*
* Make sure only one copy of the daemon is running.
*/
if (already_running()) {
syslog(LOG_ERR, "daemon already running");
exit(1);
}
/*
* Handle signals of interest.
*/
sa.sa_handler = sigterm;
sigemptyset(&sa.sa_mask);
sigaddset(&sa.sa_mask, SIGHUP);
sa.sa_flags = 0;
if (sigaction(SIGTERM, &sa, NULL) < 0) {
syslog(LOG_ERR, "can't catch SIGTERM: %s", strerror(errno));
exit(1);
}
sa.sa_handler = sighup;
sigemptyset(&sa.sa_mask);
sigaddset(&sa.sa_mask, SIGTERM);
sa.sa_flags = 0;
if (sigaction(SIGHUP, &sa, NULL) < 0) {
syslog(LOG_ERR, "can't catch SIGHUP: %s", strerror(errno));
exit(1);
}
/*
* Proceed with the rest of the daemon.
*/
/* ... */
exit(0);
}
SIGHUP
和SIGTERM
配置了信号处理程序。可以将重读逻辑放在信号处理程序中,也可以只在信号处理程序中设置一个标志,并由守护进程的主线程完成所有的工作。syslogd
进程就是服务器进程,而用户进程(客户进程)用UNIX
域数据报套接字向其发送消息。syslogd
服务器进程提供的服务就是将一条出错消息记录到日志文件中。