守护进程是一个在后台长期运行并且不受任何终端控制的进程。
我们知道linux有许多自带的守护进程,比如syslogd、crond、sendmail等。那用户或开发者自己编写的程序为什么也需要成为守护进程呢?
这主要是因为守护进程的特性。守护进程不受任何终端控制是为了避免进程在的执行的过程中在终端上输出信息,同时避免进程被终端所产生的信息打断(比如在终端输入ctrl+c或直接退出ssh连接导致的进程退出)。一般的服务软件都需要这样的性质,比如nginx,就要长期运行并且不受终端输入的影响。
这一步的作用有两个:
1.非系统守护进程可能是由用户手动执行的,比如在终端shell上执行./nginx。父进程的终止会让shell认为这条命令已经执行完毕。
2.子进程会继承父进程的进程组ID,但子进程肯定不是组长,这个特性是下一步调用setsid的提前。
这一步的作用有三个:
1.使进程成为新会话的首进程。
2.使进程成为新进程组的组长。
3.使进程脱离控制终端。(用户登陆终端就会产生一个新的会话,比如ssh telnet等登陆。这些登陆产生的会话都是可见的,而setsid产生的新会话是不可见的,所以就达到了脱离终端控制的目的。。个人理解,不知道对不对。)
这一步的作用有三个:
1.修改从父进程继承来的文件屏蔽字,避免父进程原先设置的文件屏蔽字对守护进程来说不合理。
2.守护进程应该会需要调用一些库函数,而这些库函数可能并不允许调用者通过一个显式的参数来指定模式,所以最好能在进程中就设置好自己期望的默认文件屏蔽字。
这一步可有可无,主要是让守护进程自己设置自己的工作目录,不是死板的继承父进程的。
这一步同样可有可无,如果守护进程觉得有必要,可以这样做。如果确信没有从父进程继承什么文件描述符,或者需要这些继承的文件描述符,就不需要关闭。
目的很明显,我们不希望在该终端上见到守护进程的输出,用户也不期望他们在终端上的输入被守护进程所读取。
我们以nginx实现守护进程的方式来具体说明一下上面的几个步骤:
ngx_int_t
ngx_daemon(ngx_log_t *log)
{
int fd;
// 1.创建子进程,父进程退出。
switch (fork()) {
case -1:
ngx_log_error(NGX_LOG_EMERG, log, ngx_errno, "fork() failed");
return NGX_ERROR;
case 0:
break;
default:
exit(0);
}
ngx_parent = ngx_pid;
ngx_pid = ngx_getpid();
// 2.创建新会话。
if (setsid() == -1) {
ngx_log_error(NGX_LOG_EMERG, log, ngx_errno, "setsid() failed");
return NGX_ERROR;
}
// 3.设置文件屏蔽字。
umask(0);
// 6.将标准输出和标准输入重定向到/dev/null。
fd = open("/dev/null", O_RDWR);
if (fd == -1) {
ngx_log_error(NGX_LOG_EMERG, log, ngx_errno,
"open(\"/dev/null\") failed");
return NGX_ERROR;
}
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;
}
#if 0 //这里保留了标准错误输出,使得nginx在启动过程中有致命的错误导致不能启动时,输出这些错误。
if (dup2(fd, STDERR_FILENO) == -1) {
ngx_log_error(NGX_LOG_EMERG, log, ngx_errno, "dup2(STDERR) failed");
return NGX_ERROR;
}
#endif
if (fd > STDERR_FILENO) {
if (close(fd) == -1) {
ngx_log_error(NGX_LOG_EMERG, log, ngx_errno, "close() failed");
return NGX_ERROR;
}
}
return NGX_OK;
// 4和5可做可不做。
}
有很多守护进程的实现是两次调用fork,这样做主要是为了避免僵尸进程的产生。
linux里的进程都属于一棵树,树的根结点是init(pid为1)。除了init进程,其它进程都会有一个父进程,父进程负责分配(fork)和回收(wait4)它的子进程资源。
父进程先于子进程退出:其子进程就被init接管,父进程变为init,这种子进程被称为孤儿进程。
子进程先于父进程退出:子进程结束调用exit清理资源后,此时它并没有结束,而是进入僵尸状态,等待它的父进程调用wait4来清理它的task_struct。也就是在其调用exit后和父进程调用wait4前这段时间,子进程被称为僵尸进程。
如果一切正常,子进程僵尸状态只会存在很短的一段时间。但在一些异常情况下,如果父进程长期阻塞在其它业务上而不能调用wait4,则会导致僵尸状态长期存在。只有父进程可以回收子进程的资源,所以父进程不死,没有其它进程能解决这个僵尸进程;父进程死了,则可以由init来接管,僵尸进程就不存在了。僵尸进程是服务器的大忌,大量的僵尸进程会导致服务器宕机。
守护进程两次调用fork就是出于僵尸进程的考虑:父进程生成守护进程后,还有其它事情要做,其『人生意义』不止是创建守护进程。这个时候如果守护进程因为一些意外退出了,而父进程又阻塞在其它业务中无法调用wait4,就会产生僵尸进程。而如果父进程先fork子进程,子进程再立刻fork孙子进程,这样孙子进程成为守护进程,立刻被init接管,无论父进程怎么阻塞,都与守护进程无关了。
是不是需要两次fork主要是看自己设计,上面nginx就没有两次fork,因为设计上很明确,父进程创建守护进程后就立刻退出了,不会存在僵尸进程的问题。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。