Board logo

标题: 那些永不消逝的进程-3 [打印本页]

作者: look_w    时间: 2017-12-19 20:06     标题: 那些永不消逝的进程-3

守护着 Service 的进程在上一章中,笔者详细向读者们介绍了进程组和会话的前世今生、工作原理、以及 nohup 是如何基于这些工作原理来创建出守护进程的;诚然,nohup                的方法并不完美,绝非铁板一块,所以最后,笔者又狠狠的"黑"了一把 nohup。当然这并不代表笔者认为 nohup 不好,恰恰相反,在很多场景,屏蔽 SIGHUP                是非常便捷的实现守护进程的手段;但这个手段并不适合所有的场景,比如:服务(service)。
在 Linux 中,服务是最需要的守护进程的,大部分的服务的生命周期都伴随着 init 进程的开启直到系统重启始终。显而易见,利用 nohup                的手法来实现这样的进程是不靠谱的,毕竟服务进程连控制终端都没有,所谓的屏蔽 SIGHUP 也就无从谈起了。
那么用于服务的守护进程应该如何实现呢?
其实我们什么都不用做,因为 glibc 为已经把实现守护进程所需的绝大部分工作都封装到 daemon 函数中了。
daemon()daemon()的函数原型如下:
int daemon (int nochdir, int                    noclose)
这个函数的使用非常简单,甚至比 fork()还要方便:只要调用一次,当前进程自动变成守护进程。
作为一个喜欢打破沙锅问到底的工程师,我想读者们应该和我一样很好奇 daemon 到底做了些什么?幸而并不长,我们很容易就可以一窥究竟(中文注释为笔者所加):
清单 6 daemon.c
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
int daemon (int nochdir, int noclose)
{
    int fd;

    /* 步骤 1: fork 出一个新的子进程,用以开启新的会话 */
    switch (__fork()) {
    case -1:
        return (-1);
    case 0:
        break;
    default:
        _exit(0);
    }
    /* 步骤 2: 开启一个新的会话 */
    if (__setsid() == -1)
        return (-1);

    if (!nochdir)
        /* 步骤 3: 把进程当前的执行路径换到根目录 */
        (void)__chdir("/");

    if (!noclose) {
        /* 步骤 4: 将当前进程的标准输入、输出和错误都重定向到/dev/null */
        struct stat64 st;

        if ((fd = open_not_cancel(_PATH_DEVNULL, O_RDWR, 0)) != -1
            && (__builtin_expect (__fxstat64 (_STAT_VER, fd, &st), 0)
            == 0)) {
                 /* …… */
                (void)__dup2(fd, STDIN_FILENO);
                (void)__dup2(fd, STDOUT_FILENO);
                (void)__dup2(fd, STDERR_FILENO);
                /* …… */
    }
    }
    return (0);
}




在解读上述代码之前,有一些比较容易让人困惑的坑是要注意一下的:
从代码中我们可以看到 daemon 的实现主要分为四个步骤,简单概括起来就是:一、建立一个新进程(fork)并为之开启一个新的会话(setsid);二、其他。
从笔者的表述中大家应该可以意识到建立一个新的会话的重要性了,由清单 6 可以猜到 setsid 便是用于建立新会话的系统调用。在执行完 setsid                之后,内核会做以下几件事:
但是这里的实现还是有些略微的让人感到匪夷所思,为什么一定要:1、先 fork 出一个新的进程;2、再杀死父进程;3、最后再调用 setsid 呢?
这主要是由于 setsid 的正常调用有一个前提条件:它要求调用它的进程不可以是一个进程组先导(progress group                leader),否则将返回错误;因此,通常在执行 setsid 之前都会先调用一次 fork 并杀死父进程,因为 fork                出的子进程必然和父进程在同一个进程组之内,且进程组先导必然不为子进程(要么是父进程要么是其他进程),因此逻辑上如此创建出的新进程之上运行 setsid                一定能够成功。
另一个值得注意的是当前进程在运行了 setsid()之后不在会关联任何的控制终端,因为由                setsid()创建出的新会话默认是没有控制终端的——这符合我们对于服务进程的预期,但是也带来了一个争议:虽然由                daemon()建立的新会话没有控制终端,但它也没有办法阻止开发者在之后的实现中另开一个。对于一个通用的 API 来说,这的确不是个好事。
要解决这一问题并非没有可能,两部 Linux/Unix 开发方面的经典砖头:TLPI(The Linux Programming Interface,参考文献 4)和                APUE(Advanced Programming in Unix Environment,参考文献 5)都提到了 System-V 下的解决方案:在                setsid()之后再 fork 出一个新的子进程,并杀死原有父进程——这么做之后,根据 System V                的规则,用户便无法再在这样的进程上下文中开启控制终端了。
然而很遗憾,源自 BSD 的 daemon()并没有将这一设计加入进来,因为同样的机制在 BSD 下无效。因此在 BSD 系的 Unix 下(如                FreeBSD,NetBSD 等),我们就只有祈祷开发者会自觉的在开启终端时加上 O_NOCTTY 了;不过由于 Linux 是参照 System-V                的接口定义设计其行为的,所以我们还是可以在调用了 daemon()之后再按照 System-V 的方案做一遍以防不测。
setsid()之外讨论完了 setsid()的话题,现在我们再来讨论一下 daemon()实现中相对不那么重要的"其他"步骤:





欢迎光临 电子技术论坛_中国专业的电子工程师学习交流社区-中电网技术论坛 (http://bbs.eccn.com/) Powered by Discuz! 7.0.0