菜单

QEMU代码中os_daemonize()函数的理解

2015年8月6日 - Linux, Windows

之前是做几年的Windows c++开发,Linux下的经验不够丰富,导致我在看QEMU代码时,有些地方还需要回头学习Linux操作系统的实现机制才能更准确理解。学习Linux操作系统时泛泛地看了很多书籍,好像明白了,但是要深刻理解,以这平庸的智商我觉得还是要多看代码多码代码。闲话少说,来看下os-posix.c中的一个函数,叫os_daemonize(),从名字上我们就知道是要搞一个守护进程,代码如下:

void os_daemonize(void)
{
if (daemonize) {
pid_t pid;
int fds[2];

if (pipe(fds) == -1) {
exit(1);
}

pid = fork();
if (pid > 0) {
uint8_t status;
ssize_t len;
close(fds[1]);
do {
len = read(fds[0], &status, 1);
} while (len < 0 && errno == EINTR);
/* only exit successfully if our child actually wrote
* a one-byte zero to our pipe, upon successful init */

exit(len == 1 && status == 0 ? 0 : 1);
} else if (pid < 0) {
exit(1);
}
close(fds[0]);
daemon_pipe = fds[1];
qemu_set_cloexec(daemon_pipe);
setsid();
pid = fork();

if (pid > 0) {
exit(0);
} else if (pid < 0) {
exit(1);
}
umask(027);
signal(SIGTSTP, SIG_IGN);
signal(SIGTTOU, SIG_IGN);
signal(SIGTTIN, SIG_IGN);
rcu_after_fork();
}

}

我们主要关注fork和setsid函数的调用。第一个fork调用之后,父进程调用exit退出,然后子进程调用setsid,接着子进程又调用fork产生一个孙进程(是我发明的叫法吗?),然后子进程退出,显然孙进程变成了最后的守护进程。问题是为什么要这么做?

首先我们知道守护进程是在后台运行的,没有控制终端(Controlling Terminal)和它关联,当我们执行QEMU命令时,一般是从Shell里敲命令执行,这时候这个进程就是有了控制终端。第一个fork后子进程调用setsid,把自己放到了一个新的session里,和父进程不属于一个session,也就是没有和控制终端有联系了。父进程退出后,子进程会重新找父进程,这个父进程就是init进程,这样也不会有僵尸进程的出现。其实这个时候子进程已经是个守护进程了,但它为啥不辞劳苦又去调用一次fork生成孙进程然后自尽呢?原因是子进程在调用setsid后,它成为了新的session的leader,虽然目前它没有和控制终端关联了,但是作为一个session的leader,它是有机会再去关联上一个控制终端的,如果代码里不小心的某些操作使得子进程又和某个控制终端关联上,那就不是守护进程本来该有的样子了。所以为了万无一失,子进程就fork了一个孙进程,孙进程和子进程同属一个session,但孙进程自己没有调用setsid,它不是session的leader,子进程退出后,孙进程再也没有机会去关联一个控制终端了,因为能关联上控制终端的只能是session的leader进程,这样一来,孙进程就满足了守护进程的一些特性,作为最后的守护进程继续运行。

总结一下,os_daemonize()做的就是两件事:

1. 生成一个子进程,和控制终端撇开关系,通过调用fork和setsid函数来达到;

2. 子进程生成一个孙进程,孙进程没有调用setsid,子进程退出,因为孙进程不是session的leader,它再也没有机会和控制终端关联上,它就是我们想要的守护进程;

关于daemon两次fork的讨论,可参考这里:

http://stackoverflow.com/questions/881388/what-is-the-reason-for-performing-a-double-fork-when-creating-a-daemon

关于session,可参考这里:

http://www.informit.com/articles/article.aspx?p=397655&seqNum=6