【APUE】Chapter13 Daemon Processes

这章节内容比较紧凑,主要有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 < 0) { 
        exit(EXIT_FAILURE); 
    } 
    if (pid > 0) { // 1. terminates the parent process
        exit(EXIT_SUCCESS); 
    } 
    if (setsid()<0) { // 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<0) { 
        exit(EXIT_FAILURE); 
    }
    if (pid>0) { // terminates the parents
        exit(EXIT_SUCCESS); 
    }

    umask(0); // 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>0; x--)
    {
        close(x);
    }

    openlog("firstdaemon", LOG_PID, LOG_DAEMON);
}

int main()
{
    skeleton_daemon();
    while (1)
    {
        syslog(LOG_NOTICE, "First daemon started.");
        sleep(20);
        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 (1)
    {
        syslog(LOG_NOTICE, "First daemon started.");
        sleep(20);
        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, 0);
    syslog(LOG_INFO, "This is a syslog test message generated by program '%s'\n", argv[0]);
    closelog();
    return 0;
}

    编译运行,在/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[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);
    sprintf(buf, "%ld", (long)getpid());
    write(fd, buf, strlen(buf)+1);
    return(0);
}

  由于这段代码依赖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 != 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);
}

  这是个代码框架,按顺序分析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然后再处理。

 

以上。

  

posted on 2015-10-31 13:43  承续缘  阅读(717)  评论(0编辑  收藏  举报

导航