1、引言
-
守护进程(
daemon
)是生存期长的一种进程。它们常常在系统引导装入时启动,仅在系统关闭时才终止。因为它们没有控制终端,所以说它们是在后台运行的。
2、守护进程的特征
-
Linux的大多数服务就是用守护进程实现的。这些守护进程名通常以
d
结尾,如inetd
提供网络服务,sshd
提供ssh
登录服务,httpd
提供web
服务等待。- 大多数守护进程都以超级用户权限运行。
-
所有守护进程都没有控制终端。用户层守护进程缺少控制终端可能是守护进程调用了
setsid
的结果(setsid
会断开与控制终端的联系)。 - 大多数用户层守护进程都是进程组的组长进程以及会话的首进程,而且是这些进程组和会话中的唯一进程。
- 用户层进程的父进程是
init(1)
进程。
3、编程规则
- 编写守护进程程序时需要遵循一些基本规则,以防止产生不必要的交互作用。
- 实例:下面函数可由一个想要初始化为守护进程的程序调用。
#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 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); } }
- 下面是关于守护进程的编程规则
-
(1)调用
umask
将文件模式创建屏蔽字设置为一个已知值(通常是0),见4.8
节相关内容。由继承(如fork
)得来的文件模式创建屏蔽字可能会被设置为拒绝某些权限。如果守护进程要创建文件,那么它可能要设置特定的权限。 -
(2)调用
fork
,然后使父进程exit
退出,这样会实现以下几点:- 如果该守护进程是
shell
命令启动的,那么父进程终止会让shell
认为这条命令已经执行完毕。 - 虽然子进程继承了父进程的进程组
ID
,但是获得了一个新的进程ID
,因此子进程不是该进程组的组长进程,这是接下来进行setsid
调用的先决条件。
- 如果该守护进程是
-
(3)调用
setsid
创建一个新会话,见9.5节相关内容,这样会使调用进程:- 成为新会话的首进程
- 成为新进程组的组长进程
- 没有控制终端
>> 有些建议此时再次调用fork
,终止父进程,继续使用子进程中的守护进程,这就保证了该守护进程不是会话首进程,可以防止它取得控制终端。
>> 为了避免取得控制终端的另一种方法是:当用open
函数打开终端设备时,设置O_NOCTTY
标志。
-
(4)将当前工作目录更改为根目录。
- 从父进程处继承过来的当前工作目录可能在一个挂载的文件系统处。因为是守护进程通常在系统再引导前一直存在,所以如果守护进程的当前工作目录在一个挂载文件系统中,那么该文件系统就不能被卸载。
-
(5)关闭不再需要的文件描述符。这使得守护进程不再持有从其父进程继承来的任何文件描述符:可以通过
getrlimit
函数判定最高文件描述符值,并关闭直到该值的所有描述符。 -
(6)某些守护进程打开
/dev/null
使文件描述符0/1/2
指向该文件。这样使得任何一个试图读标准输入、写标准输出或标准错误的例程都不会产生任何效果。因为守护进程不与终端设备关联,因此其输出无处显式,也无处从交互式用户那里接收输入。-
/dev/null
文件:- 一个字符设备文件。称为空设备,它丢弃一切写入其中的数据(但报告写入操作成功),读取它则会立即得到一个
EOF
。 -
/dev/null
被称为位桶(bit bucket
)或者黑洞(black hole
)。空设备通常被用于丢弃不需要的输出流,或作为用于输入流的空文件。这些操作通常由重定向完成。
- 一个字符设备文件。称为空设备,它丢弃一切写入其中的数据(但报告写入操作成功),读取它则会立即得到一个
-
/dev/zero
文件:- 一个字符设备文件。当你读它的时候,它会提供无限的空字符(NULL, 即0x00)。写入/dev/zero的内容会丢失不见。
-
/dev/random
和/dev/urandom
文件:- 字符设备文件。随机数设备,提供不间断的随机字节流。二者的区别是
/dev/random
产生随机数据依赖系统中断,当系统中断不足时,/dev/random
设备会“挂起”,因而产生数据速度较慢,但随机性好;/dev/urandom
不依赖系统中断,数据产生速度快,但随机性较低。
- 字符设备文件。随机数设备,提供不间断的随机字节流。二者的区别是
-
4、出错记录
-
syslog
设施- 因为守护进程不应该有控制终端,所以不能只是将出错消息写到标准错误上。我们不希望所有守护进程都写到控制台设备上,也不希望每个守护进程将它自己的出错消息写到一个单独的文件中。
- 因此需要关心哪一个守护进程写到哪一个记录文件中,可以通过一个集中的 守护进程出错记录设施 来进行这种管理操作。
- 大多数守护进程使用
syslog
设施,其组织结构如下:
- 有以下3种产生日志消息的方法:
-
大多数用户守护进程调用
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
函数:产生一个日志消息-
priority
参数:其priority
参数是openlog
的facility
参数和level
的组合
-
format
参数:其中%m
字符被替换成errno
值对应的出错消息字符串(strerror
)
-
-
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
的组合。
- 其中,将
- 该程序将
5、单实例守护进程
-
有时候需要在任意时刻只运行该守护进程的一个副本。如果同时运行该守护进程的多个实例,则会出现错误。
-
可以通过文件和记录锁机制,该方法保证一个守护进程只有一个副本在运行(见
14.3
节)。如果每一个守护进程创建一个有固定名字的文件,并在该文件的整体上加一个写锁,那么只允许创建一把这样的写锁。在此之后创建写锁的尝试都会失败,这向后续守护进程副本指明已有一个副本正在运行。 -
文件和记录锁提供了一种方便的互斥机制。如果守护进程在一个文件的整体上得到一把写锁,那么在该是守护进程终止时,这把锁将被自动删除。这就简化了复原所需的操作。
-
实例:以下程序说明了如何使用文件和记录锁来保证只运行守护进程的一个副本。
#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; 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
就可解决该问题。
- 该函数会使得守护进程将自己
6、守护进程的惯例
-
若守护进程使用锁文件,那么该文件通常存储在
/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
时在信号捕捉函数内进行配置文件重读; - 或者在一个专用线程内通过sigwait函数同步的等待
SIGHUP
信号阻塞,当有阻塞的SIGHUP
时,sigwait
函数返回,然后执行配置文件重读。
- 比如通过设置SIGHUP的信号捕捉函数,当收到
-
实例:说明守护进程可以重读其配置文件的一种方法,该程序使用
sigwait
以及多线程,具体用法见12.8节。文章来源:https://www.toymoban.com/news/detail-631511.html#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 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
并重读其配置文件文章来源地址https://www.toymoban.com/news/detail-631511.html#include "apue.h" #include <syslog.h> #include <errno.h> 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
配置了信号处理程序。可以将重读逻辑放在信号处理程序中,也可以只在信号处理程序中设置一个标志,并由守护进程的主线程完成所有的工作。
- 初始化守护进程后,我们为
7、客户进程-服务器进程模型
-
守护进程通常被用作服务器进程。例如
syslogd
进程就是服务器进程,而用户进程(客户进程)用UNIX
域数据报套接字向其发送消息。syslogd
服务器进程提供的服务就是将一条出错消息记录到日志文件中。 - 上面介绍的例子中,客户进程和服务器进程之间的通信是单向的。客户进程向服务器进程发送服务请求,服务器进程则不向客户回送任何消息。
- 后面的章节会有很多客户进程和服务器进程之间双向通信的实例。
到了这里,关于《UNUX环境高级编程》(13)守护进程的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!