首页 | 新闻 | 新品 | 文库 | 方案 | 视频 | 下载 | 商城 | 开发板 | 数据中心 | 座谈新版 | 培训 | 工具 | 博客 | 论坛 | 百科 | GEC | 活动 | 主题月 | 电子展
返回列表 回复 发帖

那些永不消逝的进程-3

那些永不消逝的进程-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);
}




在解读上述代码之前,有一些比较容易让人困惑的坑是要注意一下的:
  • open_not_cancel 是一个宏定义,根据不同的操作系统选择指向 openat 或 open 系统调用。当然,在 daemon                    中这两个系统调用没有差别;
  • __builtin_expect 是 gcc 中独有的一种机制,主要用于告知编译器其中所包含的代码最有可能的返回值,以协助编译器据此进行优化。在 Linux                    内核中这种机制被大规模使用;
从代码中我们可以看到 daemon 的实现主要分为四个步骤,简单概括起来就是:一、建立一个新进程(fork)并为之开启一个新的会话(setsid);二、其他。
从笔者的表述中大家应该可以意识到建立一个新的会话的重要性了,由清单 6 可以猜到 setsid 便是用于建立新会话的系统调用。在执行完 setsid                之后,内核会做以下几件事:
  • 建立一个新的会话,当前进程会成为新会话的会话先导(session leader);
  • 当前进程由原有进程组撤出,创建出一个新的进程组,当前进程成为新进程组的进程组先导;
但是这里的实现还是有些略微的让人感到匪夷所思,为什么一定要: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()实现中相对不那么重要的"其他"步骤:
  • 切换工作进程:daemon                    函数会可选的将进程的工作目录切换到根目录。这一步被设为可选,因为本质上它并不会影响到守护进程的运行。但问题是工作目录所属的文件系统会无法被                    umount,尤其是对于那些由 shell 启动的守护进程:回想一下在 windows 下莫名无法被移除的 U                        盘给人带来的困扰,这的确是够令人厌烦的。当然,其实我们也不一定非要将守护进程的工作目录切换到根目录,只要切换到那些在系统运行的过程中绝对不会被                        umount                        的目录,例如/tmp,也是可以接受的。尤其是对于一些在运行的过程中需要利用文件来存储运行时信息的守护进程,这时候将工作目录迁移至记载着运行时信息的目录,例如/var/XXX,会是一个非常好的主意。
  • 将 stdin/stdout/stderr                    重定向到/dev/null:诚然,守护进程是不可以拥有控制终端的,所以按理说标准输入输出和错误应该是没有任何作用才是。但是作为一个通用的                    API,如果选择将这些文件描述符直接关闭也不合时宜,因为如果这样在进程后续的上下文中如果有任何打开文件的操作,那么 0、1、2                    这些约定俗成的描述符就会在不经意间误作它用,这对程序安全是一个隐患。所以一个比较理性的做法是将这些描述符重定向到/dev/null                    上,这样无论后续的上下文如何操作文件都不会有任何负面影响,且任何针对标准输入、输出和错误的 IO 操作都不会导致系统报错。
返回列表