本节我们主要介绍一下nginx中ngx_daemon.c将进程后台化,变成守护进程的实现。

1. os/unix/ngx_daemon.c源文件

源文件内容如下:

/*
 * Copyright (C) Igor Sysoev
 * Copyright (C) Nginx, Inc.
 */


#include <ngx_config.h>
#include <ngx_core.h>


ngx_int_t
ngx_daemon(ngx_log_t *log)
{
    int  fd;

    switch (fork()) {
    case -1:
        ngx_log_error(NGX_LOG_EMERG, log, ngx_errno, "fork() failed");
        return NGX_ERROR;

    case 0:
        break;

    default:
        exit(0);
    }

    ngx_pid = ngx_getpid();

    if (setsid() == -1) {
        ngx_log_error(NGX_LOG_EMERG, log, ngx_errno, "setsid() failed");
        return NGX_ERROR;
    }

    umask(0);

    fd = open("/dev/null", O_RDWR);
    if (fd == -1) {
        ngx_log_error(NGX_LOG_EMERG, log, ngx_errno,
                      "open(\"/dev/null\") failed");
        return NGX_ERROR;
    }

    if (dup2(fd, STDIN_FILENO) == -1) {
        ngx_log_error(NGX_LOG_EMERG, log, ngx_errno, "dup2(STDIN) failed");
        return NGX_ERROR;
    }

    if (dup2(fd, STDOUT_FILENO) == -1) {
        ngx_log_error(NGX_LOG_EMERG, log, ngx_errno, "dup2(STDOUT) failed");
        return NGX_ERROR;
    }

#if 0
    if (dup2(fd, STDERR_FILENO) == -1) {
        ngx_log_error(NGX_LOG_EMERG, log, ngx_errno, "dup2(STDERR) failed");
        return NGX_ERROR;
    }
#endif

    if (fd > STDERR_FILENO) {
        if (close(fd) == -1) {
            ngx_log_error(NGX_LOG_EMERG, log, ngx_errno, "close() failed");
            return NGX_ERROR;
        }
    }

    return NGX_OK;
}

这里ngx_daemon()较为简单,主要有如下步骤:

  • 调用fork()产生子进程,同时父进程退出

  • 调用setsid()产生一个新的会话

  • 调用umask()将文件模式创建屏蔽字设置为一个已知值

  • 将标准输入、标准输出attach到/dev/null中

  • 关闭打开的/dev/null文件句柄fd

说明:

1. 此处通过ngx_pid = ngx_getpid();将子进程的进程ID保存到了全局变量ngx_pid中

2. 这里并未显示的将标准错误attach到/dev/null中,因此在一般情况下标准错误输出仍可以输
出到控制台(除非打开的/dev/null句柄fd恰巧等于STDERR_FILENO)。

2. Unix高级环境编程–守护进程

在编写守护进程程序时需遵循一些基本规则,以防止产生不必要的交互作用。下面先说明这些规则,然后给出一个按照这些规则编写的函数daemonize。

(1) 首先要做的是调用umask将文件模式创建屏蔽字设置为一个已知值(通常是0)。由继承得来的文件模式创建屏蔽字可能会被设置为拒绝某些权限。如果守护进程要创建文件,那么它可能要设置特定的权限。例如,守护进程要创建组可读、组可写的文件,继承的文件模式创建屏蔽字可能会屏蔽上述两种权限中的一种,而使其无法发挥作用。另一方面,如果守护进程调用的库函数创建了文件,那么将文件模式创建屏蔽字设置为一个限制性更强的值(如007)可能会更明智,因为库函数可能不允许调用者通过一个显示的函数参数来设置权限。

(2) 调用fork,然后使父进程exit。这样做实现了下面几点。第一,如果该守护进程是作为一条简单的shell命令启动的,那么父进程终止会让shell认为这条命令已经执行完毕。 第二,虽然子进程继承了父进程的进程组ID,但获得了一个新的进程ID,这就保证了子进程不是一个进程组的组长进程。这是下面要进行的setsid调用的先决条件。

(3) 调用setsid创建一个新会话。使调用进程:

  • 成为新会话的首进程

  • 成为一个新进程组的组长进程

  • 没有控制终端

在基于System V的系统中,有些人建议在此时再调用一次fork,终止父进程,继续使用子进程中的守护进程。
这就保证了该守护进程不是会话首进程,于是按照System V规则可以防止它取得控制终端。为了避免取得控制
终端的另一种方法是,无论何时打开一个终端设备,都一定要指定O_NOCTTY。

(4) 将当前工作目录更改为根目录。从父进程处继承过来的当前工作目录可能在一个挂载的文件系统中。因为守护进程通常在系统再引导之前是一直存在的,所以如果守护进程的当前工作目录在一个挂载文件系统中,那么该文件系统就不能被卸载。

或者,某些守护进程还可能会把当前工作目录更改到某个指定位置,并在此位置进行它们的全部工作。例如,行式打印机假脱机守护进程可能将其工作目录更改到它们的spool目录上。

(5) 关闭不再需要的文件描述符。这使守护进程不再持有从其父进程继承来的任何文件描述符(父进程可能是shell进程,或是某个其他进程)。可以使用open_max函数或getrlimit函数来判定最高文件描述符值,并关闭直到该值的所有描述符。

(6) 某些守护进程打开/dev/null使其具有文件描述符0,1和2,这样任何一个试图读标准输入、写标准输出或标准错误的库例程都不会产生任何效果。因为守护进程并不与终端设备相关联,所以其输出无处显示,也无处从交互式用户那里接收输入。即使守护进程是从交互式会话启动的,但是守护进程是在后台运行的,所以登录会话的终止并不影响守护进程。如果其他用户在同一终端设备上登录,我们不希望在该终端上见到守护进程的输出,用户也不期望他们在终端上的输入被守护进程读取。


如下给出一个例子(test.c):

#include <stdio.h>
#include <stdlib.h>
#include <syslog.h>
#include <fcntl.h>
#include <sys/resource.h>
#include <signal.h>
#include <unistd.h>

void daemonize(const char *cmd)
{
    int i,fd0,fd1,fd2,fd3;
    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)
    {
       printf("can't get file limit\n");
       exit(-1);
    }

    /*
     * Become a session leader to lose controlling TTY
    */
    if((pid = fork()) < 0)
    {
       printf("%s can't fork\n",cmd);
       exit(-2);
    }
    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)
   {
      printf("%s: can't ignore SIGHUP\n",cmd);
      exit(-3);
   }
   if((pid = fork()) < 0)
   {
      printf("%s: can't fork\n",cmd);
      exit(-4);
   }
   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)
   {
      printf("%s: can't change directory to /\n",cmd);
      exit(-5);
   }


   /*
    * Close all open file descriptions.
   */ 
   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\n",
        fd0, fd1, fd2);
      exit(-6);
   }
}


int main(int argc,char *argv[])
{
    daemonize(argv[0]);
    while(1)
    {
      syslog(LOG_INFO, "%s running daemonized\n",argv[0]);
      usleep(1000*1000);
    }
    return 0;
}

编译运行:

[root@localhost test-src]# gcc -o test test.c
[root@localhost test-src]# ./test
[root@localhost test-src]# pgrep -lf test
101892 test

[root@localhost test-src]# echo $?
0

[root@localhost test-src]# tail -f /var/log/messages
Nov 14 18:42:22 localhost ./test: ./test running daemonized
Nov 14 18:42:23 localhost ./test: ./test running daemonized
Nov 14 18:42:24 localhost ./test: ./test running daemonized
Nov 14 18:42:25 localhost ./test: ./test running daemonized
Nov 14 18:42:26 localhost ./test: ./test running daemonized
Nov 14 18:42:27 localhost ./test: ./test running daemonized
Nov 14 18:42:28 localhost ./test: ./test running daemonized
Nov 14 18:42:29 localhost ./test: ./test running daemonized
Nov 14 18:42:30 localhost ./test: ./test running daemonized
Nov 14 18:42:31 localhost ./test: ./test running daemonized
Nov 14 18:42:32 localhost ./test: ./test running daemonized

[root@localhost test-src]# pgrep -lf test | awk '{print $1}' | xargs kill -9