APUE(Unix 环境高级编程)——守护进程

今天补了一下APUE的13章,守护进程部分。这里简单记录一下,当个笔记吧。
我的理解:守护进程就是后台进程,没有控制终端,所以无法与用户进行交互,就只是在后台默默执行。

守护进程的实现步骤

  1. 使用umask将文件模式创建屏蔽字设置为一个已知值。因为守护进程是一个子进程,他会继承父进程的文件模式创建屏蔽字,所以本身它是一个不确定的值。我们需要为守护进程设置合适的值。文件模式创建屏蔽字用于决定创建新文件或新目录时,新文件或新目录的默认访问权限。
  2. 需要调用fork,来产生子进程,然后父进程调用exit结束。为什么需要创建子进程呢,原因有两个:
    (1)守护进程本来就是由一条shell命令启动的,父进程负责对这条命令的执行做准备,然后返回,让shell认为这条命令执行完毕,接着准备读取下一条命令;
    (2)因为守护进程需要一个新的会话,在这个会话中没有控制终端,所以后面必须要使用setsid来创建一个新的会话,并将调用进程放入到这个新会话中。setsid成功创建新会话的必要条件是调用进程不能是原来进程组的组长,而父进程有可能就是进程组的组长。新创建一个子进程,这个子进程与父进程是一个进程组的,所以子进程必不可能是进程组的组长,这就满足了setsid的前置条件。
  3. 调用setsid创建一个新会话,使得调用进程成为:1)新会话的首进程;2)新进程组的组长进程;3)没有控制终端。
  4. 将当前工作目录改为根目录。因为子进程是继承父进程的工作目录,一般都是在一个挂载的文件系统中。因为守护进程通常在系统再引导前是一直存在的,所以如果守护进程的当前工作目录在一个挂载文件系统中,那么该文件系统就不能被卸载。
  5. 关闭不再需要的文件描述符。因为子进程会从父进程那里继承很多已经打开的文件描述符,但是基本都用不上,所以需要关闭。
  6. 打开/dev/null,使守护进程拥有文件描述符0、1、2。在守护进程中如果写出到标准输出、标准错误或者从标准输入中读取时,因为相应的文件描述符重定向到/dev/null上,所以不会产生任何的效果。这本身是契合守护进程的定义的,因为守护进程不会与用户终端进行交互,所以肯定也无法对标准输出、标准输入进行操作。

具体实现方法与上述步骤有一点区别,尤其是在代码中是使用孙子进程作为守护进程,而不是使用儿子进程,主要原因在于:
前面的setsid已经让子进程所在的会话(session)中没有了控制终端(controlling terminal),但是在某些条件下还是会给它自动分配控制终端,这些条件包括:1)这个进程是session leader;2)“这个进程调用了open,并且没有设定O_NOCTTY flag”或“调用了ioctl函数”。所以也就是说如果当前进程是session leader,系统还是有可能给它分配控制终端,那么就违反“守护进程没有控制终端”的规定。所以创建孙子进程之后,孙子进程与儿子进程都在同一个会话中,儿子进程是session leader,孙子进程就不可能是session leader了,因此就可以保证系统不会自动分配控制终端。(当然这里有个疑问,如果儿子进程结束了,session leader是否会传给会话中的唯一进程孙子进程?这个我也不清楚,大家可以在评论中提出自己的见解。)
下面是实现代码:

#include 
#include 
#include 
#include 
#include 
#include 

// 根据命令创建一个守护进程
void daemonize(const char* cmd) {

    pid_t pid;
    struct rlimit rl;
    int fd0, fd1, fd2;
    struct sigaction sa;

    // 将文件模式创建屏蔽字设置为一个已知值,防止继承而来得到的未知值
    umask(0);

    // 获取当前进程可以打开的文件描述符的最大数目
    if (getrlimit(RLIMIT_NOFILE, &rl) < 0) {
        printf("can't get file limit.\n");
        exit(0);
    }

    // 创建子进程,在子进程中执行命令
    if ((pid = fork()) < 0) {
        printf("can't fork.\n");
        exit(0);
    }
    // 父进程,直接退出
    else if (pid > 0) {
        exit(0);
    }

    // 创建一个新的会话
    // 子进程会成为新会话的受进程,并且没有控制终端
    // 相当于这个子进程独占这个会话,才不会收到会话内部控制终端的影响
    setsid();

    // 确保未来不会获取一个控制终端
    sa.sa_handler = SIG_IGN;
    sigemptyset(&sa.sa_mask);
    sa.sa_flags = 0;
    if (sigaction(SIGHUP, &sa, NULL) < 0) {
        printf("can't sigaction.\n");
        exit(0);
    }
    
    // 解释1:现在,子进程已经成为无终端的会话组长。但它可以重新申请打开一个控制终端。可以通过使进程不再成为会话组长来禁止进程重新打开控制终端,也就是新建一个孙子进程

    // 解释2:这里需要fork一次的原因是:
    // 通过setsid()已经让进程所在的session没有了controlling terminal,按理说是是符合daemon的要求了;
    // 但是某些条件下,系统会给这些没有controlling terminal的进程 “allocating the controlling terminal for a session”。
    // 只要一个process虽然所在的session是没有controlling terminal的;
    // 但是只要这个process是session leader,那么还是有可能在某种情况下被触发,让系统给其分配controlling terminal,打破了daemon process的禁区。
    // 那么新建一个孙子进程,并且让子进程结束,这个孙子进程就不是session leader了(这个新的session的leader是子进程)

    if ((pid = fork()) < 0 ) {
        printf("can't fork.\n");
        exit(0);
    }
    else if (pid > 0) {
        exit(0);
    }

    // 切换当前目录为根目录
    // 防止如果文件系统被卸载之后,守护进程会停止
    if (chdir("/") < 0) {
        printf("can't chdir.\n");
        exit(0);
    }

    // 关闭继承得到的所有文件描述符
    // 因为本身就不需要这些
    if (rl.rlim_max == RLIM_INFINITY)
        rl.rlim_max = 1024;
    for (int i = 0; i < rl.rlim_max; i++) {
        close(i);
    }


    // 将描述符0、描述符1、描述符2重定向到/dev/null
    // 因为重定向到/dev/null,相当于不从标准输入读入也不写出到标准输出
    // 这样就可以确保守护进程不输出到终端上,也不从终端上获取输入
    fd0 = open("/dev/null", O_RDWR);
    fd1 = dup(fd0);
    fd2 = dup(fd0);

    // 初始化log file
    // 因为所有消息要输出到log file中
    openlog(cdm, LOG_CONS, LOG_DAEMON);
    if (fd0 != 0 || fd1 != 1 || fd2 != 2) {
        // 错误信息写到log file上
        syslog(LOG_ERR, "unexpected file descriptors %d %d %d", fd0, fd1, fd2);
        exit(1);
    }

}

你可能感兴趣的:(C++,Linux,C)