在创建守护进程时执行双fork的原因是什么?

我试图在python中创建一个守护进程。我已经找到了以下问题,其中有一些很好的资源,我目前正在跟踪,但我很好奇为什么双分叉是必要的。我对谷歌进行了研究,发现了大量的资源,这些资源都表明这是必要的,但不知道为什么。

有人提到这是为了防止守护进程获取控制终端。如果没有第二个分叉,它将如何做到这一点?后果是什么?

68476 次浏览

摘自坏与原:

在某些类型的Unix上,为了进入守护进程模式,你不得不在启动时执行双叉操作。这是因为单分叉不能保证与控制终端分离。”

如果daemon()调用成功,则有父调用_exit()。最初的动机可能是让父母在孩子守护的时候做一些额外的工作。

这也可能是基于一种错误的信念,认为这是必要的,以确保守护进程没有父进程,并被重新父化给init——但在单分叉的情况下,一旦父进程死亡,无论如何都会发生这种情况。

所以我认为这一切最终都归结为传统——只要父母在短时间内死亡,一个分叉就足够了。

根据Stephens和Rago的“Unix环境中的高级编程”,第二个fork更多的是一个建议,它是为了保证守护进程不会在基于System v的系统上获得控制终端。

查看问题中引用的代码,理由是:

Fork第二个child并立即退出以防止僵尸。这 使第二个子进程成为孤儿,从而使init . conf进程成为孤儿 负责其清理的进程。因为第一个孩子是 会话领导没有控制终端,这是可能的 它将通过在未来开设一个终端来获得一个(系统V- 基于系统)。第二个fork保证子节点是no 更长的会话领导者,阻止守护进程获取

因此,这是为了确保守护进程被重新父化到init上(以防启动守护进程的进程是长期存在的),并消除守护进程重新获得控制tty的任何机会。因此,如果这两种情况都不适用,那么一个分叉就足够了。"Unix网络编程-史蒂文斯"在这方面有一个很好的部分。

一个原因是父进程可以立即为子进程wait_pid(),然后忘记它。当孙子孙女死亡时,它的父节点是init,它将wait()等待它——并将它从僵尸状态中取出。

结果是,父进程不需要知道被分叉的子进程,这也使得从库等中分叉长时间运行的进程成为可能。

一个像样的讨论似乎在http://www.developerweb.net/forum/showthread.php?t=3025

引用mlampkin的话:

...将setsid()调用视为做事的“新”方式(与终端分离),而[第二个]fork()调用作为处理SVr4的冗余…

我试图理解这个双叉,无意中发现了这个问题。经过大量研究,我得出了这个结论。希望它能帮助那些有同样问题的人更好地澄清事情。

在Unix中,每个进程都属于一个组,而这个组又属于一个会话。这是层次结构……

会话(SID)→进程组(PGID)→进程(PID)

进程组中的第一个进程成为进程组组长,会话中的第一个进程成为会话组长。每个会话都可以有一个与之关联的TTY。只有会话领导者才能控制TTY。为了使进程真正被守护(在后台运行),我们应该确保会话领导者被杀死,这样会话就不可能控制TTY。

我从这个网站在我的Ubuntu上运行了Sander Marechal的python示例守护程序。以下是结果和我的评论。

1. `Parent`    = PID: 28084, PGID: 28084, SID: 28046
2. `Fork#1`    = PID: 28085, PGID: 28084, SID: 28046
3. `Decouple#1`= PID: 28085, PGID: 28085, SID: 28085
4. `Fork#2`    = PID: 28086, PGID: 28085, SID: 28085

注意,该进程是Decouple#1之后的会话领导者,因为它是PID = SID。它仍然可以控制TTY。

注意,Fork#2不再是会话领导者PID != SID。这个过程永远不能控制TTY。真正的监控。

我个人认为术语fork-twice令人困惑。一个更好的习语可能是fork- decoupling -fork。

其他感兴趣的链接:

严格地说,双叉与将守护进程作为init的子进程重新养育无关。重父进程所需要的就是父进程必须退出。这只需要一个分叉就可以完成。另外,单独执行双fork并不会将守护进程重新父化到init;该守护进程的父进程必须退出。换句话说,当派生一个正确的守护进程时,父进程总是退出,以便守护进程被重新父化到init

那么为什么要用双叉呢?posix . 1的授权- 2008第11.1.3节,“控制终端”,有答案(强调添加):

会话的控制终端以实现定义的方式是由会话领导者分配。如果会话领导者没有控制终端,并且在没有使用O_NOCTTY选项(参见open())的情况下打开一个尚未与会话关联的终端设备文件,则该终端是否成为会话领导者的控制终端是由实现定义的。如果一个过程不是会话领导者打开一个终端文件,或用于open() O_NOCTTY选项,那么该终端将不会成为调用进程的控制终端

这告诉我们,如果一个守护进程做了这样的事情…

int fd = open("/dev/console", O_RDWR);

... 然后守护进程可能获取/dev/console作为它的控制终端,这取决于守护进程是否是会话领导者,以及取决于系统实现。该程序可以保证上面的调用不会获得控制终端,如果该程序首先确保它不是会话领导。

通常,当启动一个守护进程时,会调用setsid(从调用fork后的子进程调用)来将守护进程与其控制终端分离。然而,调用setsid也意味着调用进程将是新会话的会话领导者,这为守护进程重新获得控制终端留下了可能性。双叉技术确保守护进程不是会话领导者,这就保证了对open的调用,如上面的例子所示,不会导致守护进程重新获得控制终端。

双叉技术有点偏执。如果你知道,守护进程将永远不会打开终端设备文件,这可能没有必要。此外,在某些系统上,即使守护进程确实打开了终端设备文件,也可能没有必要这样做,因为这种行为是由实现定义的。然而,有一点不是由实现定义的,那就是只有会话领导者才能分配控制终端。因此,如果你想要变得偏执,并且确定守护进程不会无意中获得一个控制终端,不管任何实现定义的细节,那么double-fork技术是必不可少的。

这样也许更容易理解:

  • 第一个fork和setsid将创建一个新的会话(但是进程ID ==会话ID)。
  • 第二个fork确保进程ID !=会话ID。