这章节内容比较紧凑,主要有5部分:
1. 守护进程的特点
2. 守护进程的构造步骤及原理。
3. 守护进程示例:系统日志守护进程服务syslogd的相关函数。
4. Singe-Instance 守护进程。
5. 其他相关内容
1. 守护进程的特点
守护进程也是unix系统中的一种进程。有大量的系统守护进程,其最主要的特点有两个:
(1)系统启动的时候守护进程就跟着起来;只有当系统关闭的时候守护进程才跟着关闭
(2)没有controlling terminal,运行在background
直观上,可以用ps(1)命令先去查看各种进程,执行ps -efj > o (结果重定向到文件里方便处理)
e(every)代表所有进程;f(full)代表现实全部信息;j(job)job模式,结果如下图:
(1)/sbin/init是一个user-level的守护进程,系统启动的时候kernel让它跟着起来
(2)带中括号的进程都是kernel级别的进程,kernel process跟着系统一起起来执行周期是entire lifetime;而且这些进程没有controlling terminal & command line,正经的Daemon Processes
(3)其中[kthread]是'kernel的kernel',其他的kernel来产生其他的kernel进程,这一点从其他kernel进程的PPID就能够看出来(kthread的PID是2,其余的kernal processes的PPID都是2)
除了kernel process还有一些常见的非kernel的daemons processes,比如xinted(监听网络服务),crond(定时任务),sshd(远程链接),rsyslogd(系统日志服务)等
(1)注意到这些守护进程,大都是有root权限的;而且TTY的选项都是'?' (即没有controlling terminal)。
(2)这些非kernel的daemon processes的parent process都是init进程
(3)除了rsyslogd之外,一般的daemon processes都是独占session和process group,并且都是leader。在这个方面rsyslogd算是一个特例,后面单独拎出来讲rsyslogd。
2. 守护进程的构造步骤及原理
系统自带了大量的守护进程,如果我们要自己构建一个守护进程,需要按照特定的方式一步步来完成。
先上一个代码(并非书上的例子,而是stackoverflow找的http://stackoverflow.com/questions/17954432/creating-a-daemon-in-linux)
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <signal.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <syslog.h> static void skeleton_daemon()
{
pid_t pid; pid = fork(); // 1. fork off the parent process
if (pid < ) {
exit(EXIT_FAILURE);
}
if (pid > ) { // 1. terminates the parent process
exit(EXIT_SUCCESS);
}
if (setsid()<) { // 2. child process becomes session leader
exit(EXIT_FAILURE);
}
signal(SIGCHLD, SIG_IGN);
signal(SIGHUP,SIG_IGN); pid = fork(); // 3. fork off the second time
if (pid<) {
exit(EXIT_FAILURE);
}
if (pid>) { // terminates the parents
exit(EXIT_SUCCESS);
} umask(); // 4. set new file permissions chdir("/"); // 5. change the working directory int x; // 6. close all open file descriptors
for (x=sysconf(_SC_OPEN_MAX); x>; x--)
{
close(x);
} openlog("firstdaemon", LOG_PID, LOG_DAEMON);
} int main()
{
skeleton_daemon();
while ()
{
syslog(LOG_NOTICE, "First daemon started.");
sleep();
break;
} syslog(LOG_NOTICE, "First daemon terminated.");
closelog(); return EXIT_SUCCESS;
}
按照上述代码的注释中的标号 (代码注释中的标号对应着下面分析的标号)& APUE Chapter 13.3的内容,分析如下(要想看懂上面的代码,必须有APUE前面章节关于fork和进程的知识):
(1)构造孤儿进程
做第一次fork,结束parent process,留下child process;这样留下的child process就成了孤儿进程。
为什么要做这一步呢?
构造孤儿进程的目的:后续构建步骤要调用setsid(),而调用setsid()的进程要求必须不是process group leader,孤儿进程肯定不是group leader
(2)脱离原来的session
就是一句调用setsid()就可以了,产生 了一个新的session。
为什么要做这一步呢?
回顾APUE(英文原版)P295上的内容,当非process group leader调用setsid()的时候会产生如下的作用:
a. 这个孤儿进程成为了新的session的唯一进程,并且也是session leader
b. 这个孤儿进程成为了所在process group的leader
c. 这个孤儿进程没有相对应的controlling terminal
这一步的原因就是,新产生的session是没有controlling terminal的,这个是daemons process要求的
(3)* 再次构造孤儿进程
做第二次fork,结束parent process,留下child process;这样上次刚刚调用setsid()函数的进程,由产下第二个孤儿进程。
为什么要做这一块呢?
在(2)中,通过setsid()已经让进程所在的session没有了controlling terminal,按理说是是符合daemon的要求了;但是某些条件下,系统会给这些没有controlling terminal的进程 “allocating the controlling terminal for a session”。
回顾APUE书上P297中Controlling Terminal的部分,上面说的“某些条件下”包括:
a. 首先这个process是session leader
b. "这个process调用了open,并且没有设定O_NOCTTY flag"或"调用了ioctl函数"
简单来说,只要一个process虽然所在的session是没有controlling terminal的;但是只要这个process是session leader,那么还是有可能在某种情况下被触发,让系统给其分配controlling terminal,打破了daemon process的禁区。
这一步骤,在APUE书上并不是必须的,但是既然说的有道理,就应该当成是一个必须的步骤。
(4)设置文件权限
unmask(0),这个不太明白,先记个“权限”
(5)修改进程工作的目录
由于child process的工作目录是从parent process中继承获得的,修改工作目录,给process增加“权限”
(6)关闭所有的file descriptors
这个也是从parent process继承过来的,但是daemon process并不需要。
daemon process不能与stdout stderr stdin发生交互;所以如果之前有打开的,就必须给关上才行。
代码编译运行后,在终端并没有什么输出。
执行ps xj,结果如下:
可以看到a.out的PPID是1(归init管了);TTY是‘?’(没有controlling terminal了);SID PGID都是一样的;但是PID与PGID不一样(这就是第二次fork的作用,不是session leaders了,不会触发某些条件使得所在session被allocating controlling terminal了)。
一个daemon process的例子就完成了。
3. 系统日志守护进程服务:syslogd及其相关函数
我们检查一下/var/log/messages文件(需要root权限),发现多了如下的两行:
我们关注2中main函数中的代码:
int main()
{
skeleton_daemon();
while ()
{
syslog(LOG_NOTICE, "First daemon started.");
sleep();
break;
} syslog(LOG_NOTICE, "First daemon terminated.");
closelog(); return EXIT_SUCCESS;
}
main的开始先调用skeleton_daemon()构建了一个daemon process;后面syslog函数中包含输出结果中的文本;这个过程中发生了什么?
(1)首先说一下motivation。回顾1中提到的daemon process的特点:没有controlling terminal,不能写到标准输入输出中。那么如果daemon process出了问题,或者想输出一些log信息便于调试该怎么办?那么多的daemon process存在,总不能来一个daemon process就制定一个separate file作为输出的容器吧。更好的做法是把daemon process统一管理起来,于是就产生了上面提到的一种专门负责日志输出的daemon process,就是rsyslogd。
(2)系统已经提供了rsyslogd服务来处理日志,那么我们只需要学会调用就可以了。具体的流程图如下(P469):
通过上面的流程图,我们对发生了什么可以了解个大概:
a. 在main中调用syslog函数
b. 调用kernel中的unix domain datagrom socket相关的内容
c. 再调用rsyslogd服务,完成了日志内容的输出
再由上图的内容,扩展一下unix系统处理日志输出的kernel框架:
a. 有一个分支专门管kernal routines的输出的
b. 有一个分支专门管TCP/IP network来的
c. 处理user process中各种log需求的(最左边的)
(3)了解了大致流程后,我们看openlog和syslog函数的具体参数。
openlog(const char *ident, int option, int facility)
ident : 用于标示是哪个程序产生的,一般都用程序的名;在上面的例子中就是"firstdaemon"
option : 书上写的也比较模糊,直接man openlog查看,如下:
大概也看懂了,就是可以用or的运算逻辑把这些选项添加到option中
我们改变一下原来的代码,把option中的LOG_PID换成LOG_CONS,运行结果如下:
可以看到PID的信息就没有打入log中。
facility:还是man openlog
大概意思就是说,标示什么类型的程序要logging message;显然在这个背景下,是应用的LOG_DAEMON
syslog(int priority, const char *format, ...)
priority:限定log message重要级别的
自上而下,级别逐渐降低,例子中用的是NOTICE级别的日志(回想实习中用到的也是NOTICE级别的日志)
format:这个从例子中可以看到了,就是输出什么格式的信息,跟printf的那种差不多。
(4)还剩一个问题没有解决,为什么例子中的日志就打入了/var/log/message中呢?
回顾一下(2)中的流程图,user process调用syslog后,最终还是交给rsyslogd守护进程去具体操作了。因此,答案就在rsyslogd怎么去判断往哪个文件写上了。那么可以猜测,rsyslogd会读取一个配置文件,判断log要往哪里写。
我用的系统中,这个配置文件是/etc/rsyslog.conf。查看一下:
原来,上面提到的syslog函数中的priority确定了日志的importance level之后,rsyslogd会从conf文件中读取配置,哪个level的log信息该写到哪里。这个文件一般不能被修改,影响的面非常广,就不做破坏性试验了。但是还可以通过一个小栗子感受一下:
栗子代码如下:
#include <syslog.h> int main(int argc, char **argv)
{
openlog("test error", LOG_CONS | LOG_PID, );
syslog(LOG_INFO, "This is a syslog test message generated by program '%s'\n", argv[]);
closelog();
return ;
}
编译运行,在/var/log/messages中多了如下的信息:
如果把上述代码中的LOG_INFO改成LOG_DEBUG,则/var/log/messages中则不会有新的内容。至于这个输出到哪里去了,我还没弄明白。
4. Singe-Instance 守护进程
书上首先给出了这部分内容的一些概述:
(1)某些daemons可能有多个copy,但是某些时候必须保证只有一个copy在运行。
(2)比如,cron这个定时任务调度daemon,如果多个cron都在running,那么调度任务肯定要乱套的;再比如多个daemons操作一个文件,如果有写操作,也是需要类似同步的机制来保护的。
(3)有些情况下,有机制可以保证daemon只能有一个copy instance在执行(比如访问某些device,这时候device driver就会保证同一时间,只有一个daemon能操作device);但是如果没有现成的保护机制,那么就得靠程序员自己实现
想想threads那一章已经提到了mutex,condition variable等互斥锁机制来保持同步,为什么这个地方还要单独拎出来呢?之前提到的多线程同步毕竟都是在同一个进程中的(寻址空间、多个线程之间可以共享全局互斥变量);而daemons别说同一个process了,即使是同一个daemon的不同copies,都在不同的session中,按照我个人的理解,用之前全局互斥锁的方法是行不通的(毕竟不同进程的寻址空间不一样),所以这个单独拎出来了。
上面(3)中提到的例子中,file- & record-locking(具体实现在14.3节,先不去纠结具体实现,当成是现成的了)就是非常典型的一个。
file-locking & record-locking大概要实现的就是:如果daemon的多个copies向同一个资源进行写操作,并且都会请求一个write lock,那么只能有一个wirte lock满足请求,其余再有请求这个write lock的,都让他们知难而退了。
本质上,file-locking & record-locking就是一种便捷的文件锁:daemon先请求加锁,daemon退出的时候锁自动解开。
为什么要搞这种文件锁?书上说的是“This simplifies recovery, eliminating the need for us to clean up from the previous instance of the daemon”。我并没有太理解,只能简单猜测,处理锁是容易的而处理daemon是困难的,所以宁愿去玩儿锁而不去搞daemon。
书上给了一个already_running函数的实现如下:
#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[]; fd = open(LOCKFILE, O_RDWR|O_CREAT, LOCKMODE);
if (fd < ) {
syslog(LOG_ERR, "can't open %s: %s", LOCKFILE, strerror(errno));
exit();
}
if (lockfile(fd) < ) {
if (errno == EACCES || errno == EAGAIN) {
close(fd);
return();
}
syslog(LOG_ERR, "can't lock %s: %s", LOCKFILE, strerror(errno));
exit();
}
ftruncate(fd, );
sprintf(buf, "%ld", (long)getpid());
write(fd, buf, strlen(buf)+);
return();
}
由于这段代码依赖lockfile的实现(14.3节),所以先不去编译运行,只是分析实现思路。
为了达到single-instance daemon的效果:
(1)如果file已经被lock,那么请求write lock的daemon就会fail,并且errno为EACCES(Premission Denied)或EAGAIN(Resource temporarily unavaliable)
(2)如果获得了lock,则先ftruncate file(这里相当于清空file,防止上次写入文件的内容还留着),执行写操作。
5. 其他相关内容
(1)lockfile一般放在/var/run/xxx.pid
(2)如果daemon支持configuration,则配置文件一般放在/etc/XXX.conf
(3)daemon可以从命令行启动,但系统daemon一般从系统脚本启动
(4)daemon如果有配置文件,则只在daemon启动时读一次,如果中间修改了配置则需要重读才行;一种做法就是让daemon接收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 err, signo; for (;;) {
err = sigwait(&mask, &signo);
if (err != ) {
syslog(LOG_ERR, "sigwait failed");
exit();
} switch (signo) {
case SIGHUP:
syslog(LOG_INFO, "Re-reading configuration file");
reread();
break; case SIGTERM:
syslog(LOG_INFO, "got SIGTERM; exiting");
exit(); default:
syslog(LOG_INFO, "unexpected signal %d\n", signo);
}
}
return();
} int
main(int argc, char *argv[])
{
int err;
pthread_t tid;
char *cmd;
struct sigaction sa; if ((cmd = strrchr(argv[], '/')) == NULL)
cmd = argv[];
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();
} /*
* Restore SIGHUP default and block all signals.
*/
sa.sa_handler = SIG_DFL;
sigemptyset(&sa.sa_mask);
sa.sa_flags = ;
if (sigaction(SIGHUP, &sa, NULL) < )
err_quit("%s: can't restore SIGHUP default");
sigfillset(&mask);
if ((err = pthread_sigmask(SIG_BLOCK, &mask, NULL)) != )
err_exit(err, "SIG_BLOCK error"); /*
* Create a thread to handle SIGHUP and SIGTERM.
*/
err = pthread_create(&tid, NULL, thr_fn, );
if (err != )
err_exit(err, "can't create thread"); /*
* Proceed with the rest of the daemon.
*/
/* ... */
exit();
}
这是个代码框架,按顺序分析main中的代码内容:
(1)处理cmd命令
(2)让进程变成daemon
(3)保证single-instance daemon
(4)恢复SIGHUP的信号处理方式,并在main线程中屏蔽所有信号(这一步需要回顾之前daemonize的实现,中间有一步骤是屏蔽SIGHUP信号,所以在这里要恢复对SIGHUP的处理方式)
(5)开一个新线程,在新线程中专门开一个sigwait来处理SIGHUP和SIGTERM信号(这里需要用到12.8中sigwait的知识):如果是SIGHUP信号,则reread()配置文件;如果是SIGTERM信号,则直接退出
书上还提到了,并不是所有的daemons都是支持多线程的。对于这样的daemon则就用传统单线程方式,注册signal handler然后再处理。
以上。