这章节内容比较紧凑,主要有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然后再处理。

以上。

  

最新文章

  1. iphone 尺寸and字体
  2. OC中NSDictionary(字典)、NSMutableDictionary(可变字典)、NSSet(集合)、NSMutableSet(可变集合)得常用方法
  3. lintcode 中等题:partition array 数组划分
  4. Eclipse配置
  5. php __set()和__get()函数
  6. 旺财速啃H5框架之Bootstrap(六)
  7. Eclipse 项目导入 Studio Debug运行卡死在进入界面
  8. PHP常用的三种设计模式
  9. 一步一步设置Joomla!开发环境
  10. Hexo使用细节及各种问题
  11. A1003. Emergency
  12. NodeJs 学习笔记(一)Wedding 项目搭建
  13. brew装snappy
  14. 确认OHS版本的方法
  15. Linux文件系统命令 split
  16. C/C++中带可变参数的函数
  17. ES6中的import与export对class操作相关用法举例
  18. 利用SHELL的PROMPT_COMMAND添加日志审计功能,实时记录任何用户的操作到日志文件中
  19. Erlang的Web库和框架
  20. CentOS系统命令

热门文章

  1. 未启用当前数据库的 SQL Server Service Broker,请为此数据库启用 Service Broker
  2. 【转】Mac本地生成SSH Key 的方法
  3. [转]Android Studio启动时出现unable to access android sdk add-on list
  4. 封装方法到对象(javascript)
  5. HDU 1254 推箱子(BFS加优先队列)
  6. notepad++括号自动补全插件: XBracket Lite
  7. lucene&amp;solr学习——分词器
  8. autofac 注册
  9. Linux中软件使用笔记
  10. linux命令进阶及和windows进行文件传输的所有方式