前言

  守护进程可以运行在后台,不受某个特定终端的限制,安全稳定地持续运行,一般会作为程序中的“监管进程存在”。
  守护进程有以下特点:
  1.不属于任何终端的会话分组,自己拥有会话分组。
  2.没有显示终端,一般不与用户交互。
  3.一般持续运行在后台。

源代码

//描述:守护进程初始化
//执行失败:返回-1,   子进程:返回0,父进程:返回1
int ngx_daemon()
{
    //(1)创建守护进程的第一步,fork()一个子进程出来
    switch (fork())  //fork()出来这个子进程才会成为咱们这里讲解的master进程;
    {
    case -1:
        //创建子进程失败
        return -1;
    case 0:
        //子进程,走到这里直接break;
        break;
    default:
        //父进程以往 直接退出exit(0);现在希望回到主流程去释放一些资源
        return 1;  //父进程直接返回1;
    } //end switch
    
    //只有fork()出来的子进程才能走到这个流程
    ngx_parent = ngx_pid;     //ngx_pid是原来父进程的id,因为这里是子进程,所以子进程的ngx_parent设置为原来父进程的pid
    ngx_pid = getpid();       //当前子进程的id要重新取得
    
    //(2)脱离终端,终端关闭,将跟此子进程无关
    if (setsid() == -1)  
    {
        return -1;
    }

    //(3)设置为0,不要让它来限制文件权限,以免引起混乱
    umask(0); 

    //(4)打开黑洞设备,以读写方式打开
    int fd = open("/dev/null", O_RDWR);
    if (fd == -1) 
    {
        return -1;
    }
    if (dup2(fd, STDIN_FILENO) == -1) //先关闭STDIN_FILENO[这是规矩,已经打开的描述符,动他之前,先close],类似于指针指向null,让/dev/null成为标准输入;
    {        
        return -1;
    }
    if (dup2(fd, STDOUT_FILENO) == -1) //再关闭STDIN_FILENO,类似于指针指向null,让/dev/null成为标准输出;
    {
        return -1;
    }
    if (fd > STDERR_FILENO)  //fd应该是3,这个应该成立
     {
        if (close(fd) == -1)  //释放资源这样这个文件描述符就可以被复用;不然这个数字【文件描述符】会被一直占着;
        {
            return -1;
        }
    }
    return 0; //子进程返回0
}

分析

  创建一个守护进程的方式比较巧妙。首先,在一个终端运行的程序中,无论它fork()多少次,创建了多少子进程,最原始的父进程是不能离开终端的会话组的。
  也就是说,一开始在终端运行的进程,无法成为守护进程。
  因此,成为守护进程的愿望只能由它的子进程实现。

创建子进程

//(1)创建守护进程的第一步,fork()一个子进程出来
    switch (fork())  //fork()出来这个子进程才会成为咱们这里讲解的master进程;
    {
    case -1:
        //创建子进程失败
        return -1;
    case 0:
        //子进程,走到这里直接break;
        break;
    default:
        //父进程以往 直接退出exit(0);现在希望回到主流程去释放一些资源
        return 1;  //父进程直接返回1;
    } //end switch

  首先应该明白fork()函数有三种返回值:
  1.返回值为-1:说明子进程创建失败了,这种情况很罕见,基本是因为计算机内存不够继续分配导致的,这就是硬件资源和总体设计的问题了。
  返回值为0:说明是子进程。
  返回值是个正数:说明是父进程,更确切的说,返回值是子进程的pid号。pid号肯定是一个正数。
  通过返回值的区分,我们可以实现在一个程序文件中对子进程和父进程进行分流。
  在上面的代码中,子进程使用break关键字退出switch循环,继续向函数的下面运行;父进程则直接return退出该函数,继续回到调用该函数的区域工作。

//只有fork()出来的子进程才能走到这个流程
    ngx_parent = ngx_pid;     //ngx_pid是原来父进程的id,因为这里是子进程,所以子进程的ngx_parent设置为原来父进程的pid
    ngx_pid = getpid();       //当前子进程的id要重新取得

  随后要更新该进程的pid,因为调用该函数的是父进程,所以原先的pid一定是父进程的。

子进程分离会话

//(2)脱离终端,终端关闭,将跟此子进程无关
    if (setsid() == -1)  
    {
        return -1;
    }

  setsid()函数实现了子进程与原先父进程所在终端会话的脱离,这样子进程就已经成为了一个守护进程,在后台运行。

设置权限掩码

//(3)设置为0,不要让它来限制文件权限,以免引起混乱
    umask(0);

  在创建守护进程之后,需要为其设置一次权限掩码,不要限制文件的权限。

输出与输出的重定向

  这里需要补充一个概念:空设备。
  空设备的路径是:/dev/null
  空设备是一个特殊的设备文件,它会丢弃一起写入其中的内容。
  一般称其为“黑洞设备”,专门吸收你不想要的内容。
  如果我们将守护进程的标准输入与标准输出重定向到空设备,就能实现守护进程与用户的零交互,不受影响地在后台运行。

//(4)打开黑洞设备,以读写方式打开
    int fd = open("/dev/null", O_RDWR);
    if (fd == -1) 
    {
        return -1;
    }
    if (dup2(fd, STDIN_FILENO) == -1) //先关闭STDIN_FILENO[这是规矩,已经打开的描述符,动他之前,先close],类似于指针指向null,让/dev/null成为标准输入;
    {        
        return -1;
    }
    if (dup2(fd, STDOUT_FILENO) == -1) //再关闭STDIN_FILENO,类似于指针指向null,让/dev/null成为标准输出;
    {
        return -1;
    }
    if (fd > STDERR_FILENO)  //fd应该是3,这个应该成立
     {
        if (close(fd) == -1)  //释放资源这样这个文件描述符就可以被复用;不然这个数字【文件描述符】会被一直占着;
        {
            return -1;
        }
    }
    return 0; //子进程返回0

  最后注意一下,打开文件的句柄需要在函数的最后进行回收,不然文件描述符会被一直占用。