守护进程(daemon process,又称精灵进程),是一种运行在后台,不需要与用户进行交互的程序,其特性与Windows中的服务类似。Linux/Unix系统中运行着很多这样的进程,且很多此类进程的进程名都采用named的形式,比如httpd, vsftpd, inetd等。但这只是一种命名惯例,你完全可以采用其它的命名形式。
守护进程的特性
守护进程有种种特性,使之成为守护进程。
运行周期较长。守护进程通常在系统启动时由/etc/rc*.d中的启动脚本或者用户在终端手动启动,在系统结束时停止或由用户手动停止。不像普通进程那样在用户退出终端后就被停止,守护进程会一直运行(下面会看到守护进程在启动后会脱离执行它的终端)。
脱离其运行环境。守护进程没有控制终端。进程在创建时(fork)会继承父进程的PCB(进程控制块),因此会同时拥有父进程的许多资源和关系,比如打开的文件描述符、socket、环境变量、控制终端、进程组、登录会话等。而守护进程需要关闭这些资源、脱离这些关系。
创建守护进程
鉴于守护进程的这些特性,一个普通进程若想成为守护进程,就需要做一些特殊的操作。
脱离控制终端
多数用户进程都是从终端运行的,它由与终端关联的shell(用户登录时由login执行的,比如bash程序)经过fork/exec启动。用户程序启动后shell会等待该进程(wait或waitpid等)结束,如果该进程为后台运行,shell会立即返回命令提示符等待用户的其它操作,否则shell会在用户进程结束时才打印命令提示符。
所以,为了能使守护进程运行后shell立即返回,通常在程序的入口采用下面的代码来完成:
if( fork() > 0) exit(0);
程序fork产生新进程,父进程立即返回(shell等待的就是父进程),shell进程的wait也会成功返回,而子进程则会继续执行。注意,此时子进程的父进程已经过继成为init进程(进程号为1)的子进程了。
另立门户
执行的上面的操作后,用户进程(子进程)并未真正脱离控制终端。因为子进程继承了父进程的关系,属于父进程所在的进程组,和父进程同属于一个作业,还隶属于控制终端所关联的登录会话(login session)。关于进程组、作业、登录会话等概念,请参考《高级Unix环境编程》(Advanced Programming of Unix Environment),及Google。
若想脱离这些复杂的关系,操作却十分简单,只须在子进程一开始,调用函数setsid()即可。setsid做了以下工作:脱离了原来的进程组和登录会话,创建新的进程组和会话,并担当其进程组的Group Leader和会话的Session Leader.
这时候,该进程已经真正脱离了其控制终端,即是说,该进程在其原控制终端退出登录后仍可以在系统中运行。
关闭继承打开的文件描述符
控制终端,准确说是shell默认打开了三个文件,0代表标准输入,1代表标准输出,2代表标准错误输出。由该shell执行的进程也会继承打开这三个文件,所以它的所有子进程默认的输入和输出也都关联到此终端。因为守护进程也可能有其他用户进程通过exec系列函数执行,守护进程同样会继承这些文件描述符。因此在守护进程中需要关闭继承而来的所有描述符,APUE中stevens的代码:
int close_all_fd(void) { struct rlimit lim; unsigned int i; if (getrlimit(RLIMIT_NOFILE, &lim) < 0) return -1; if (lim.rlim_cur == RLIM_INFINITY) lim.rlim_cur = 1024; for (i = 0; i < lim.rlim_cur; i ++) { #ifdef MYPERF if (i == 1) continue; #endif if (close(i) < 0 && errno != EBADF) return -1; } return 0; }
改变当前工作目录
每个进程都有自己的当前工作目录,进程创建时默认继承父进程的工作目录。守护进程需要将当前工作目录改变,否则其所在目录所挂载的文件系统将无法卸载(可能是你不想要的),通常将其改变之根目录,方法是chdir(“/”);
重设文件创建掩码
为了能够自主控制文件创建权限位,常将文件创建掩码重设为0000,即umask(0);
忽略SIGCHLD信号
由于守护进程运行周期长,为了避免因为等待子进程而产生僵尸进程,通常需要忽略SIGCHLD信号:signal(SIGCHLD, SIG_IGN);
总结
方便起见,通常把创建守护进程的一系列操作包装为一个函数,在需要的地方调用即可:
void daemonize(); int main() { daemonize(); //~ other operations here. return 0; } void daemonize() { if( fork() > 0) exit(0); setsid(); close(0); //~ im so lazy a guy. close(1); close(2); //~ you can use I/O functions, but to/from the black hole... int fd = open("/dev/null", O_RDWR); dup2(fd, 1); dup2(fd, 2); chdir("/"); umask(0); signal(SIGCHLD, SIG_IGN); }