memcached系列一——守护进程总结

#memcached #apue

在memcached的源码学习过程中,又见到了一个版本的daemonize的守护进程创建的实现, 突然想到好像对相关的知识理解也不是非常清晰,借此机会做个总结吧。

###进程组、进程、会话、控制终端关系

要彻底理解linux下的守护进程的运行,非常有必要说说进程组,进程、会话及控制终端这几者间的 关系的。下面具体看看:

####进程组 在linux系统中,每个进程必定会属于且仅属于一个进程组,而一个进程组可以包含多个进程,进程组 的生命周期从进程组内的第一个进程运行开始到最后一个进程终止结束。在进程内可分别使用函数 getpgid,setpgid获取及设置所属的进程组。进程组的首个进程即为该进程组的组长进程,进程组的组号 (gid)与组长进程的进程号(pid)相同。

####会话 会话通常是一个或多个进程组的集合,这些进程组可被分为前台进程组及后台进程组两类,前台进程组 与会话对应的控制终端对应。另通常用户的一次登录即会形成一次会话,其生命周期也随用户的登录与 退出而开始结束。函数setsid可用于新建一个会话。会话的首个进程称为会话首进程(session header)。 仅有会话的首进程才能够创建一个控制终端。而会话与进程组的关系如下图示: session ####控制终端 控制终端与会话是一对一的关系,控制终端由会话的首进程(session header)创建,因此对于控制终端来说, 对应的会话首进程又可称为控制进程。在控制终端产生的中断信号(DELETE或CTRL+C)、退出信号(CTRL+) 均会发送给前台进程组内的进程。当检测到与终端断开时,将会有挂断信号发送到会话的首进程。

###守护进程的要点 守护进程通常需要在后台运行,及需要脱离于会话的前台进程组,而归属于后台进程组。另守护进程还需要独立于 基运行的环境,以减少其对其他资源的依赖,以及其它进程对其的影响。为此在编写守护进程时,需要有一下几个要点:

####(1)屏蔽控制终端的信号

由于控制终端关闭时,会向其对应会话的控制进程发送挂起信号,因此以防万一的方法(后台有方法让,会话脱离控制 终端,因此个人认为屏蔽信号不是必要的)是屏蔽部分可能会因控制终端关闭而发出的干扰守护进程正常运转的信号。 如SIGHUP、SIGTSTP、SIGTTIN、SIGTTOU。对不同的系统有不同的信号屏蔽方法,如sigignore等。

####(2)处理SIGCHILD信号

如果父进程不处理子进程的结束,子进程就会变成僵死进程(积少成多,会占用大量的系统资源),如果等待子进程的 结束,又无形中增加了系统的开销(父进程的运转,父进程检查子进程结束等)。因此通常的处理方法是,守护进程同样 屏蔽掉SIGCHILD信号。

注:父进程不等待子进程结束,子进程结束时变为僵尸进程,父进程屏蔽(ignore)SIGCHILD信号,子进程结时由init进程 接管释放资源;子进程还在运行,父进程退出,子进程称孤儿进程,同样由init接管,完成资源的回收。

注:另关于防止僵尸进程的产生,apue一书中还提到过采用双fork的形式,将具体的任务处理放到孙子进程中,这样在孙子 进程处理完成任务退出时可直接由init进程接管,完成资源的回收。

####(3)后台运行

守护进程必定需要在后台运行,以使前台的控制终端继续可操作,且不受前台控制终端关闭的影响。因此,往往通过 fork的形式,创建子进程,父进程退出,在子进程完成任务处理。

####(4)脱离控制终端、登录会话与进程组

前面介绍的进程组,会话,控制终端与进程的关系,得到进程在运行是受很多方面的影响,大部分是由于其继承自父进程的 与进程组,会话、控制终端的关系。因此为摆脱这些影响,最好的方法就是将进程从这些环境中脱离出来,为此,就可使用 setsid函数新建会话组,来完成。setsid将以当前进程作这会话首进程新建一个会话,这样就脱离了与控制终端,会话,原 进程组的关系。

####(5)禁止进程重新打开终端

在前面的介绍中知道只有会话的首进程才能打开终端,为防止进程再打开终端,以使得进程再度与终端关联,因为保险的方 法是让进程失去打开终端的能力,也即让运行任务的进程不为进程的首进程,即可再进程一次fork的操作。

####(6)重设文件掩膜 ####(7)关闭打开的文件描述符 ####(8)更改当前的工作目录 ####(9)重定向标准错误输出

###守护进程创建的实现

以上的9个要点,并不是建立守护进程所必需的,如下为memcached的守护进程建立函数的实现:

    int daemonize(int nochdir, int noclose)
    {
        int fd;

        switch (fork()) {
        case -1:
            //出错返回
            return (-1);
        case 0:
            //子进程
            break;
        default:
            //父进程
            _exit(EXIT_SUCCESS);
        }

        //为子进程新开会话
        if (setsid() == -1)
            return (-1);

        //切换进程的执行目录,以释放占用的文件系统,以免除一些后续的影响
        if (nochdir == 0) {
            if(chdir("/") != 0) {
                perror("chdir");
                return (-1);
            }
        }

        //重定向stdin, stdout, stderr
        if (noclose == 0 && (fd = open("/dev/null", O_RDWR, 0)) != -1) {
            if(dup2(fd, STDIN_FILENO) < 0) {
                perror("dup2 stdin");
                return (-1);
            }
            if(dup2(fd, STDOUT_FILENO) < 0) {
                perror("dup2 stdout");
                return (-1);
            }
            if(dup2(fd, STDERR_FILENO) < 0) {
                perror("dup2 stderr");
                return (-1);
            }

            if (fd > STDERR_FILENO) {
                if(close(fd) < 0) {
                    perror("close");
                    return (-1);
                }
            }
        }
        return (0);
    }

上面的守护进程的创建的实现涉及的点实际是非常少的,但基本是一个较为可行的守护进程实现方案。