APUE - 守护进程

编程规则

在编写守护进程程序时需遵循一些基本规则,以便防止产生并不需要的交互作用。

  1. 首先要做的是调用 umask 将文件模式创建屏蔽字设置为 0。由继承得来的文件模式创建屏蔽字可能会拒绝某些权限。
  2. 调用 fork,然后使父进程退出。这样做实现了下面几点:第一,如果该守护进程是作为一条简单 shell 命令启动的,那么父进程终止使得 shell 认为这条命令已经执行完毕;第二,子进程继承了父进程的进程组 ID,但具有一个新的进程 ID,这就保证了子进程不是一个进程组的组长进程。这对于下面就要做的 setsid 调用是必要的前提条件。
  3. 调用 setsid 以创建一个新会话。使调用进程:(a)成为新会话的首进程,(b)成为一个新进程组的组长进程,(c)没有控制终端。
  4. 将当前工作目录更改为根目录。从父进程处继承过来的当前工作目录可能在一个装配文件系统中。因为守护进程通常在系统再引导之前一直存在的,所以如果守护进程的当前工作目录在一个装配文件系统中,那么该文件系统就不能被拆卸。
  5. 关闭不再需要的文件描述符。这使守护进程不再持有从其父进程继承来的某些文件描述符。
  6. 某些守护进程打开 /dev/null 使其具有文件描述符 0、1 和 2,这样,任何一个试图读标准输入、写标准输出或标准出错的库例程都不会产生任何效果。

初始化一个守护进程

void daemonize(const char *cmd)
{
    int                 i, fd0, fd1, fd2;
    pid_t               pid;
    struct rlimit       rl;
    struct sigaction    sa;

    /*
     * Clear file creation mask.
     */
    umask(0);

    /*
     * Get maximum number of file descriptors.
     */
    if (getrlimit(RLIMIT_NOFILE, &rl) < 0)
        err_quit("%s: can't get file limit", cmd);

    /*
     * Become a session leader to lose controlling TTY.
     */
    if ( (pid = fork() ) < 0)
        err_quit("%s: can't fork", cmd);
    else if (pid != 0)  /* parent */
        exit(0);
    setsid();

    /*
     * Ensure future opens won't allocate controlling TTYs.
     */
    sa.sa_handler = SIG_IGN;
    sigemptyset(&sa.sa_mask);
    sa.sa_flags = 0;
    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)  /* parent */
        exit(0);

    /*
     * Change the current working directory to the root so
     * we 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 (rl.rlim_max == RLIM_INFINITY)
        rl.rlim_max = 1024;
    for (i = 0; i < rl.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);
    }
}

出错记录

#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);

#include <syslog.h>
#include <stdarg.h>
void vsyslog(int priority, const char *format, va_list arg);

单实例守护进程

#define LOCKFILE "/var/run/daemon.pid"
#define LOCKMODE (S_IRUSR|S_IWUSR|S_IRGRP|S_IROTH)

int lockfile(int fd)
{
    struct flock    fl;

    fl.l_type   = F_WRLCK;
    fl.l_start  = 0;
    fl.l_whence = SEEK_SET;
    fl.l_len    = 0;

    return fcntl(fd, F_SETLK, &fl);
}

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;
}

守护进程的每个副本都将试图创建一个文件,并将其进程 ID 写到该文件中。这使管理人员易于标识该进程。如果该文件已经加了锁,那么 lockfile 函数将失败,errno 设置为 EACCES 或 EAGAIN,函数返回 1,这表明该守护进程已经运行。否则将文件长度截短为 0,将进程 ID 写入该文件,函数返回 0。

守护进程的惯例

在 UNIX 系统中,守护进程遵循下列公共惯例:

  • 若守护进程使用锁文件,那么该文件通常存放在 /var/run 目录中。锁文件的名字通常是 name.pid,其中,name 是该守护进程或服务的名字。
  • 若守护进程支持配置选项,那么配置文件通常存放在 /etc 目录中。配置文件的名字通常是 name.conf,其中,name 是该守护进程或服务的名字。
  • 守护进程可用命令行启动,但通常它们是由系统初始化脚本之一启动的。如果在守护进程终止时,应当自动地重新启动它,则我们可在 /etc/inittab 中为该守护进程包括 _respawn 记录项,这样,init 就将重启动该守护进程。
  • 若一守护进程有一配置文件,那么当该守护进程启动时,它读该文件,但在此之后一般就不会再查看它。若一管理员更改了配置文件,那么该守护进程可能需要被停止,然后再启动,以使配置文件的更改生效。为避免此种麻烦,某些守护进程将捕捉 SIGHUP 信号,当它们接收到该信号时,重读配置文件。因为守护进程并不与终端相结合,它们或者是无控制终端的会话首进程,或者是孤儿进程组的成员,所以守护进程并不期望接收 SIGHUP。于是,它们可以安全地重复使用它。