daemon 守护进程

2015-01-06 13:15:09   最后更新: 2015-01-13 21:38:33   访问数量:1453




近日研读 nginx 源码,读到 daemon 进程的创建

觉得守护进程的创建过程还是有很多地方需要注意的,所以单独拿出来写一篇博客

 

在Unix 环境下,守护进程是相当重要的,大多数提供服务的进程都是以这种方式运行的

他们独立于控制终端,并且循环、周期的执行工作,完成了很多系统日常性的管理功能和各种服务,例如 apache 的 httpd 服务、nginx 的 master 进程、执行日常工作的 crontab 进程、打印进程 lpd 等

守护进程是一种纯粹的后台进程,与运行前环境完全隔离,包括未关闭的文件描述符、控制终端、会话、进程组、工作目录以及文件创建掩码等

很多守护进程是父进程 fork 产生,所以会继承所有的父进程地址空间中的环境,所以必须在守护进程诞生之初,断绝这些相关环境,当然,守护进程也可以在 linux 系统启动时从启动脚本 /etc/rc.d 中启动,也可以由 crontab 启动

事实上,守护进程与普通进程的编写并没有特别大的区别

 

后台运行

让守护进程在后台启动最简单的方式就是通过父进程 fork 然后让父进程退出

这样做的好处是容易实现,同时父进程还可以在此之前进行一些必要的初始化工作,最重要的是,子进程的进程组ID是父进程ID,这样保证了子进程不是组长进程,组长进程是无法通过调用 setsid 脱离控制终端的

nginx 就是采用这样的方式启动守护进程的

脱离终端

在linux中,进程属于一个进程组,进程组号(GID)就是进程组长的进程号 (PID)。登录会话可以包含多个进程组。这些进程组共享一个控制终端。这个控制终端通常是创建进程的登录终端。 控制终端,登录会话和进程组通常是从父进程继承下来的。

守护进程必须摆脱他们,不受他们的影响

因此,需要调用系统调用 setsid 使进程成为会话组组长

在此之后,进程成为新的会话组组长,与原来的会话组和进程组脱离关系

 

在 linux 中,终端会话组必须独占终端,因此,已经脱离终端会话组的进程也随即与控制终端脱离关系

当然,作为会话组长,还是可以再打开一个新的控制终端的

重设文件创建掩码

进程从创建他的父进程那里继承了文件创建掩码,它可能会修改守护进程创建的文件的存取位

因此需要调用:

umask(0)

 

如果需要在 fork 后立即改变其文件掩码,则最好在父进程 fork 前调用

关闭打开的文件描述符

进程从创建它的父进程那里继承了打开的文件描述符

如不关闭,将会浪费系统资源,造成进程所在的文件系统无法卸下以及引起无法预料的错误

复制标准输出、标准输入、错误输出文件描述符

同时,为了标准输入、标准输出、错误输出的正确使用,需要调用系统调用 dup2 复制这些文件描述符

if (dup2(fd, STDIN_FILENO) == -1) { ngx_log_error(NGX_LOG_EMERG, log, ngx_errno, "dup2(STDIN) failed"); return NGX_ERROR; } if (dup2(fd, STDOUT_FILENO) == -1) { ngx_log_error(NGX_LOG_EMERG, log, ngx_errno, "dup2(STDOUT) failed"); return NGX_ERROR; }

 

也可以通过 open("/dev/null"),然后使用 dup2 让标准输入、标准输出、错误输出重定向到空文件

改变当前工作目录

守护进程一般还需要改变当前工作目录到根目录或特定目录,当然,也可以保持在原来目录,调用:

int chdir (const char *path);

 

处理信号

虽然信号处理不是必须的,但是为了让守护进程能够稳定运行,屏蔽一些信号还是很有必要的

同时,如果不对 SIGCHLD 信号进行处理,子进程退出后会变成僵尸进程,从而占用系统资源

在 nginx 中,守护进程在 fork 子进程前,屏蔽了 SIGCHLD、SIGALRM、SIGIO、SIGINT、SIGHUP、SIGINFO、SIGWINCH、SIGTERM、SIGQUIT、SIGXCPU 等信号

/** * author: 龙泉居士 * file: main.c * date: 2015-01-13 */ #include <stdio.h> #include <fcntl.h> #include <unistd.h> #include <stdlib.h> // for exit #include <signal.h> // for sigaction #include <syslog.h> // for syslog #include <sys/stat.h> // for umask #include <sys/resource.h> void daemonize (const char *cmd) { int i, fd0, fd1, fd2; pid_t pid; struct rlimit rl; struct sigaction sa; // 更改文件权限屏蔽字 => 不屏蔽 umask(0); if (getrlimit(RLIMIT_NOFILE, &rl) < 0) { perror("getrlimit"); exit(-1); } if ((pid = fork()) < 0) { perror("fork"); exit(-1); } else if (pid != 0) { // 父进程退出 exit(0); } // 1. 脱离控制终端 // 2. 成为组长进程 // 3. 成为会话组首个进程 setsid(); // 忽略 SIGHUP 信号 // 当终端退出,会发送该信号给会话组长进程,默认处理方式为退出 sa.sa_handler = SIG_IGN; sigemptyset(&sa.sa_mask); sa.sa_flags = 0; if (sigaction(SIGHUP, &sa, NULL) < 0) { perror("sigaction"); exit(-1); } // 再次 fork 让守护进程不再担当会话组长,防止他重新打开终端 if ((pid = fork()) < 0) { perror("fork"); exit(-1); } if (pid != 0) { // 父进程退出 printf("%d\n", pid); exit(0); } if (chdir("/") < 0) { perror("chdir"); exit(-1); } // 关闭所有文件描述符 if (rl.rlim_max == RLIM_INFINITY) rl.rlim_max = 1024; for (i=0; i<rl.rlim_max; ++i) close(i); // 重定向标准输入、标准输出、错误输出到 /dev/null fd0 = open("/dev/null", O_RDWR); fd1 = dup(0); fd2 = dup(0); openlog(cmd, LOG_CONS, LOG_DAEMON); if (fd0 != 0 || fd1 != 1 || fd2 != 2) { syslog(LOG_ERR, "unexpected file descriptors %d %d %d", fd0, fd1, fd2); exit(-1); } while (1) { // TODO } } int main (int argc, char *argv[]) { daemonize(argv[0]); return 0; }

 

执行该程序后,执行 ps axj 命令可以看到:

 

可以看到,TTY 一项中,值为 ?,这意味着,该进程是没有对应的终端的

同时,并不存在一个进程ID为 20121 的进程,这意味着,该进程为孤儿进程,也因此不会有任何机会被分配到一个新的控制终端,这得益于我们函数中的第二个 fork 函数

 






技术帖      linux      unix      龙潭书斋      chdir      技术分享      fork      signal      posix      nginx      daemon      umask     


京ICP备15018585号