1、创建daemon步骤:

要变成daemon,一个程序需要完成下面的步骤:

  1. 执行一个fork(),之后父进程退出,子进程继续执行。(结果就是daemon成为了init进程的子进程。)之所以要做这一步是因为下面两个原因:
  • 假设daemon是从命令行启动的,父进程的终止会被shell发现,shell在发现之后会显示出另一个shell提示符并让子进程继续在后台运行。
  • 子进程被确保不会称为一个进程组首进程,因为它从其父进程那里继承了进程组ID并且拥有了自己的唯一的进程ID,而这个进程ID与继承而来的进程组ID是不同的,这样才能够成功地执行下面一个步骤。
  1. 子进程调用setsid()开启一个新回话并释放它与控制终端之间的所有关联关系。
  2. 如果daemon从来没有打开过终端设备,那么就无需担心daemon会重新请求一个控制终端了。如果daemon后面可能会打开一个终端设备,那么必须要采取措施来确保这个设备不会成为控制终端。这可以通过下面两种方式实现:
  • 在所有可能应用到一个终端设备上的open()调用中指定O_NOCTTY标记。
  • 或者更简单地说,在setsid()调用之后执行第二个fork(),然后再次让父进程退出并让孙子进程继续执行。这样就确保了子进程不会称为会话组长,因此根据System V中获取终端的规则,进程永远不会重新请求一个控制终端。(多一个fork()调用不会带来任何坏处。)
  1. 清除进程的umask以确保当daemon创建文件和目录时拥有所需的权限。
  2. 修改进程的当前工作目录,通常会改为根目录(/)。这样做是有必要的,因为daemon通常会一直运行直至系统关闭为止。如果daemon的当前工作目录为不包含/的文件系统,那么就无法卸载该文件系统。或者daemon可以将工作目录改为完成任务时所在的目录或在配置文件中定义一个目录,只要包含这个目录的文件系统永远不会被卸载即可。
  3. 关闭daemon从其父进程继承而来的所有打开着的文件描述符。(daemon可能需要保持继承而来的文件描述的打开状态,因此这一步是可选的或者可变更的。)之所以这样做的原因有很多。由于daemon失去了控制终端并且是在后台运行的,因此让daemon保持文件描述符0、1和2的打开状态毫无意义,因为它们指向的就是控制终端。此外,无法卸载长时间运行的daemon打开的文件所在的文件系统。因此,通常的做法是关闭所有无用的打开着的文件描述符,因为文件描述符是一种有限的资源。
  4. 在关闭了文件描述符0、1和2之后,daemon通常会打开/dev/null并使用dup2()(或类似的函数)使所有这些描述符指向这个设备。之所以要这样做是因为下面两个原因:
  • 它确保了当daemon调用了在这些描述符上执行I/O的库函数时不会出乎意料地失败。
  • 它防止了daemon后面使用描述符1或2打开一个文件的情况,因为库函数会将这些描述符当做标准输出和标准错误来写入数据(进而破坏了原有的数据)。

2、示例代码:

#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdlib.h>


bool start_daemon()
{
    int fd;

    switch (fork()) {
        case -1:
            printf("fork() failed\n");
            return false;

        case 0:
            break;

        default:
            exit(0);
    }

    /*
    pid_t setsid(void);
    进程调用setsid()可建立一个新对话期间。
    如果调用此函数的进程不是一个进程组的组长,则此函数创建一个新对话期,结果为:
        1、此进程变成该新对话期的对话期首进程(session leader,对话期首进程是创建该对话期的进程)。
           此进程是该新对话期中的唯一进程。
        2、此进程成为一个新进程组的组长进程。新进程组ID就是调用进程的进程ID。
        3、此进程没有控制终端。如果在调用setsid之前次进程有一个控制终端,那么这种联系也被解除。
    如果调用进程已经是一个进程组的组长,则此函数返回错误。为了保证不处于这种情况,通常先调用fork(),
    然后使其父进程终止,而子进程继续执行。因为子进程继承了父进程的进程组ID,而子进程的进程ID则是新
    分配的,两者不可能相等,所以这就保证了子进程不是一个进程组的组长。
    */
    if (setsid() == -1) {
        printf("setsid() failed\n");
        return false;
    }

    switch (fork()) {
        case -1:
            printf("fork() failed\n");
            return false;

        case 0:
            break;

        default:
            exit(0);
    }

    umask(0);
    chdir("/");

    long maxfd;
    if ((maxfd = sysconf(_SC_OPEN_MAX)) != -1)
    {
        for (fd = 0; fd < maxfd; fd++)
        {
            close(fd);
        }
    }

    fd = open("/dev/null", O_RDWR);
    if (fd == -1) {
        printf("open(\"/dev/null\") failed\n");
        return false;
    }

    /*
    // Standard file descriptors.
    #define STDIN_FILENO    0   // Standard input.
    #define STDOUT_FILENO   1   // Standard output.
    #define STDERR_FILENO   2   // Standard error output.
    */

    /*
    int dup2(int oldfd, int newfd);
    dup2()用来复制参数oldfd所指的文件描述符,并将它拷贝至参数newfd后一块返回。
    如果newfd已经打开,则先将其关闭。
    如果oldfd为非法描述符,dup2()返回错误,并且newfd不会被关闭。
    如果oldfd为合法描述符,并且newfd与oldfd相等,则dup2()不做任何事,直接返回newfd。
    */
    if (dup2(fd, STDIN_FILENO) == -1) {
        printf("dup2(STDIN) failed\n");
        return false;
    }

    if (dup2(fd, STDOUT_FILENO) == -1) {
        printf("dup2(STDOUT) failed\n");
        return false;
    }

    if (dup2(fd, STDERR_FILENO) == -1) {
        printf("dup2(STDERR) failed\n");
        return false;
    }

    if (fd > STDERR_FILENO) {
        if (close(fd) == -1) {
            printf("close() failed\n");
            return false;
        }
    }

    return true;
}


int main(int argc, char** argv)
{
    start_daemon();

    while (true)
    {
        sleep(100);
    }

    return 0;
}