守护进程是生存期长的一种进程。他们常常在系统引导装入时启动,仅在系统关闭时才终止。因为他们没有控制终端,所以说它们是在后台运行的。
守护进程的特征
系统进程依赖于操作系统实现。父进程ID为0的各进程通常是内核进程,他们作为系统引导装入过程的一部分而启动。内核进程是特殊的,通常存在于系统的整个生命期中。他们以超级 用户特权运行,无控制终端,无命令行。
编程规则
在编写守护进程程序时需要遵循一些基本规则,以防止产生不必要的交互作用。
1. 首先要做的是调用umask将文件模式创建屏蔽字设置为一个已知值。由继承得来的文件模式创建屏蔽字可能会被设置为拒绝某些权限。如果守护进程要创建文件,那么它可能要设置特定的权限。
2. 调用fork,然后使父进程exit。这样做实现了下面几点。第一,如果该守护进程是作为一条简单的shell命令启动的,那么父进程终止会让shell认为这条命令已经执行完毕。第二,虽然子进程继承了父进程的进程组ID,但获得了一个新的进程ID,这就保证了子进程不是一个进程组的组长进程。这是下面将要进行的setsid调用的先决条件。
3. 调用setsid创建的一个新会话。是调用进程:a.称为新会话的首进程,b.称为一个新进程组的组长进程,c.没有控制终端
4. 将当前工作目录更改为根目录。从父进程处继承过来的当前工作目录可能在一个挂载的文件系统中。因为守护进程通常在系统再引导之前是一直存在的,所以如果守护进程的当前工作目录在一个挂载文件系统中,那么该文件系统就不能被卸载。
或者,某些守护进程还可能会把当前工作目录更改到某个指定位置,并在此位置进行它们的全部工作。
5. 关闭不再需要的文件描述符。这使守护进程不再持有从其父进程继承来的任何文件描述符。可以使用open_max函数或getrlimit函数来判定最高文件描述符值,并关闭直到该值的所有描述符。
6. 某些守护进程打开/dev/null使其具有文件描述符0、1和2,这样,任何一个试图读标准输入、写标准输出或标准错误的库例程都不会产生任何效果。因为守护进程并不与终端设备相关联,所以其输出无处显示,也无处从交互式用户那里接受输入。即使守护进程是从交互式会话启动的,但是守护进程是在后台运行的,所以登录会话的终止并不影响守护进程。如果其他用户在同一终端设备上登录,我们不希望在该终端上见到守护进程的输出,用户也不期望他们在终端上的输入被守护进程读取。
初始化一个守护进程
#include "apue.h"
#include <syslog.h>
#include <fcntl.h>
#include <sys/resource.h>
void daemonize(const char *cmd)
{
int i, fd0, fd1, fd2;
pid_t pid;
struct rlimit r1;
struct sigaction sa;
/*Clear file creation mask*/
umask(0);
/*Get maximum number of file descriptors*/
if(getrlimit(RLIMIT_NOFILE, &r1) < 0)
err_qiut("%s:can‘t get file limit", cmd);
/*Become a session leader to lose controlling TTY*/
if((pid = fork()) < 0)
err_qiut("%s: can‘t fork", cmd);
else if(pid != 0)
exit(0);
setsid();
/*Ensure future opens won‘t allocate controlling TTYs*/
sa.sa_handler = SIG_IGN;
sigemptyset(&sa.sa_mask);
if(sigaction(SIGHUP, &sa, NULL) < 0)
err_quit("%s: can‘t ignore SIGHUP", cmd);
if((pid = fork()) < 0)
err_quit("%s: can‘t fork", cmd);
else if(pid != 0)
exit(0);
/*Change the current working directory to the root so
*wo won‘t prevent file systems from being unmounted
*/
if(chdir("/") < 0)
err_quit("%s: can‘t change directory to /", cmd);
/*Close all open file descriptors*/
if(r1.rlim_max == RLIM_INFINITY)
r1.rlim_max = 1024;
for(i = 0; I < r1.rlim_max; i++)
close(i);
/*Attach file descriptors 0,1 and 2 to /dev/null*/
fd0 = open("/dev/null", O_RDWR);
fd1 = dup(0);
fd2 = dup(0);
/*Initialize the log file*/
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);
}
}
出错记录
守护进程存在的一个问题是如何处理出错信息,因为它本就不应该有控制终端,所以不能只是简单地写到标准错误上。
有以下3种产生日志消息的方法。
1. 内核例程可以调用log函数。任何一个用户进程都可以通过打开并读取/dev/klog设备来读取这些信息。
2. 大多数用户进程(守护进程)调用syslog(3)函数来产生日志消息。这使消息被发送至UNIX域数据报套接字/dev/log。
3. 无论一个用户进程是在此主机上,还是在通过TCP/IP网络连接到此主机的其他主机上,都可将日志消息发向UDP端口514。注意,syslog函数从不产生这些UDP数据报,它们要求产生此日志消息的进程进行显示的网络编程。
通常,syslogd守护进程读取所有3种格式的日子消息。此守护进程在启动时读一个配置文件,其文件名一般为/etc/syslog.conf,该文件决定了不同种类的消息应送向何处。例如,紧急消息可发送至系统管理员,并在控制台上打印,而警告消息则可记录到 一个文件中。
#include <syslog.h>
void openlog(const char *ident, int option, int facility);
void syslog(int priority, const char *format, ...);
void closelog(void);
int setlogmask(int maskpri);
//返回值:前日志记录优先级屏蔽字值
调用openlog是可选择的。如果不调用openlog,则在第一次调用syslog时,自动调用openlog。调用closelog也是课选择的,因为它只是关闭层被用于与syslogd守护进程进行通信的描述符。
调用openlog使我们可以指定一个ident,以后,此ident将被加至每则日志消息中。ident一般是程序的名称。option参数是指定各种选项的位屏蔽。
option | 说明 |
---|---|
LOG_CONS | 若日志消息不能通过UNIX域数据报送至syslogd,则将该消息写至控制台 |
LOG_NDELAY | 立即打开至syslogd守护进程的UNIX域数据报套接字,不要等到第一条消息已经被记录时再打开。通常,在记录第一条消息之前,不打开该套接字 |
LOG_NOWAIT | 不要等待在将消息记入日志过程中可能已创建的子进程。 |
LOG_ODELAY | 在第一条消息被记录之前延迟打开至syslogd守护进程的连接 |
LOG_PERROR | 除将日志消息发送给syslogd以外,还将它写至标准出错 |
LOG_PID | 记录每条消息都要包含进程ID。此选项可供对每个不同的请求都fork一个子进程的守护进程使用。 |
openlog的facility参数值选自如下
facility | 说明 |
---|---|
LOG_AUDIT | 审计设施 |
LOG_AUTH | 授权程序:login、su、getty等 |
LOG_ AUTHPRIV | 与LOG_AUTH相同,但写日志文件时具有权限限制 |
LOG_CONSOLE | 消息写入/dev/console |
LOG_CRON | cron和at |
LOG_DAEMON | 系统守护进程:inetd、routed等 |
LOG_FTP | FTP守护进程 |
LOG_KERN | 内核产生的消息 |
LOG_LOCAL0~7 | 保留由本地使用 |
LOG_LPR | 行式打印机系统:lpd、lpc等 |
LOG_MAIL | 邮件系统 |
LOG_NEWS | Usenet网络新闻系统 |
LOG_ NTP | 网络时间协议系统 |
LOG_SECURITY | 安全子系统 |
LOG_SYSLOG | stslogd守护进程本身 |
LOG_USER | 来自其他用户进程的消息 |
LOG_UUCP | UUCP系统 |
设置facility参数的目的是可以让配置文件说明,来自不同设施的消息将以不同的方式进行处理。如果不调用openlog,或者以facility为0来调用它,那么在调用syslog时,可将facility作为priority参数的一部分进行说明。
调用syslog产生一个日志消息。其priority参数是facility和level 的组合
level | 说明 |
---|---|
LOG_EMERG | 紧急 |
LOG_ALERT | 必须立即修复的情况 |
LOG_CRIT | 严重情况 |
LOG_ERR | 出错情况 |
LOG_WARNING | 警告情况 |
LOG_NOTICE | 正常但重要的情况 |
LOG_INFO | 信息性消息 |
LOG_DEBUG | 调试消息 |
将format参数以及其他所有参数传至vsprintf函数以便进行格式化。在format中,每个出现的%m字符都先被代换成errno值对应的出错消息字符串。
srtlogmask函数用于设置进程的记录优先级屏蔽字。
单实例守护进程
保证只运行一个守护进程的一个副本
#include <unistd.h>
#include <stdlib.h>
#include <fcntl.h>
#include <syslog.h>
#include <string.h>
#include <errno.h>
#include <stdio.h>
#include <sys/stat.h>
#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;
fd = open(LOCKFILE, O_RDWR | 0_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);
sprint(buf, "%ld", (long)getpid());
write(fd, buf, strlen(buf) +1);
return 0;
}
守护进程的惯例
- 若守护进程使用锁文件,那么该文件通常存储在/var/run目录中。然而需要注意的是,守护进程可能需要具有超级用户权限才能在此目录下创建文件。锁文件的名字通常是name.pid,其中,name是该守护进程或服务的名字。
- 若守护进程支持配置选项,那么配置文件通常存放在/etc目录中。配置文件的名字通常是name.conf,其中,name是该守护进程或服务的名字。
- 守护进程可用命令行启动,但通常它们是由系统初始化脚本之一启动的。
- 若一个守护进程有一个配置文件,那么当该守护进程启动时会读该文件,但在此之后一般就不会再查看它。若某个管理员更改了配置文件,那么该守护进程可能需要被停止,然后再启动,以使配置文件的更改生效。为了避免此种麻烦,某些守护进程将捕捉SIGHUP信号,当它们接受到该信号时,重新读配置文件。
守护进程重读配置文件
#include "apue.h"
#include <pthread.h>
#include <syslog.h>
sigset_t mask;
extern int already_running(void);
void reread(void)
{
/*...*/
}
void thr_fn(void *arg)
{
int arr,signo;
for( ; ; ){
err = sigwait(&mask, &signo);
if(err != 0){
syslog(LOG_ERR, "sigwait failed");
exit(1);
}
switch(signo){
case SIGHUP:
syslog(LOG_INFO, "Re-reading configuration file");
reread();
break;
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++;
/*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);
}
/*Restore SIGHUP default and block all signals*/
sa.sa_handler = SIG_DFL;
sigemptyset(&sa.sa_mask);
sa.sa_flags = 0;
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");
/*Create a thread to handle SIGHUP and 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);
}
客户进程-服务器进程模型
守护进程尝尝用作服务器进程。
一般而言,服务器进程等待客户进程与其联系,提出某种类型的服务要求。由syslogd服务器进程提供的服务是将一条出错消息记录到日志文件中。
在服务器进程中调用fork然后exec另一个程序来向客户进程提供服务是很常见的。这些服务器进程通常管理着多个文件描述符:通信端点、配置文件、日志文件和类似的文件。最好的情况下,让子进程中的这些文件描述符保持打开状态并无大碍,因为它们很可能不会被在子进程中执行的程序所使用,尤其是那些与服务器端无关的程序。最坏情况下,保持它们的打开状态会导致安全问题——被执行的程序可能有一些恶意行为,如更改服务器端配置文件或欺骗客户端程序使其认为正在与服务器端通信,从而获取未授权的信息。
解决此问题的一个简单方法是对所有被执行程序不需要的文件描述符设置执行时关闭标志