创建守护进程时执行双叉的原因是啥?
Posted
技术标签:
【中文标题】创建守护进程时执行双叉的原因是啥?【英文标题】:What is the reason for performing a double fork when creating a daemon?创建守护进程时执行双叉的原因是什么? 【发布时间】:2010-10-27 06:04:47 【问题描述】:我正在尝试在 python 中创建一个守护进程。我找到了following question,其中有一些我目前正在关注的好资源,但我很好奇为什么需要双叉。我在 google 上四处搜寻,发现大量资源声明这是必要的,但不是为什么。
有人提到这是为了防止守护进程获取控制终端。如果没有第二个分叉,它将如何做到这一点?有什么影响?
【问题讨论】:
***.com/questions/10932592/why-fork-twice/… 双分叉的一个难点是父进程无法轻易获取孙进程的PID(fork()
调用将子进程的PID返回给父进程,因此很容易得到子进程的PID,但不是那么容易得到grandchild进程的PID)。
【参考方案1】:
我试图理解双叉并在这里偶然发现了这个问题。经过大量研究,这是我想出来的。希望它能帮助有相同问题的人更好地澄清问题。
在 Unix 中,每个进程都属于一个组,该组又属于一个会话。这是层次结构……
会话(SID)→进程组(PGID)→进程(PID)
进程组中的第一个进程成为进程组组长,会话中的第一个进程成为会话组长。每个会话都可以有一个与之关联的 TTY。只有会话负责人可以控制 TTY。对于真正被守护的进程(在后台运行),我们应该确保会话领导者被杀死,这样会话就不可能控制 TTY。
我在我的 Ubuntu 上从 this site 运行了 Sander Marechal 的 python 示例守护程序。这是我的 cmets 的结果。
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-decouple-fork。
其他感兴趣的链接:
Unix 进程 - http://www.win.tue.nl/~aeb/linux/lk/lk-10.html【讨论】:
分叉两次还可以防止在父进程运行较长时间时创建僵尸,并且由于某种原因删除通知该进程死亡的信号的默认处理程序。 但是 second for 也可以调用解耦成为 session leader 然后获取终端。 这不是真的。第一个fork()
已经阻止创建僵尸,前提是您关闭父级。
产生上述引用结果的最小示例:gist.github.com/cannium/7aa58f13c834920bb32c
在之前调用setsid()
会有什么好处吗?其实我猜this question的答案是这样回答的。【参考方案2】:
严格来说,双叉与将守护进程重新设置为init
的子级无关。重新养育孩子所需要的只是父母必须退出。这可以只用一个叉子来完成。此外,自己进行双叉不会将守护进程重新设置为init
;守护进程的父进程必须退出。换句话说,当派生一个适当的守护进程时,父进程总是退出,以便守护进程重新成为init
的父进程。
那么为什么是双叉呢? POSIX.1-2008 第 11.1.3 节,“The Controlling Terminal”,有答案(已添加重点):
会话的控制终端由会话负责人以实现定义的方式分配。如果会话领导者没有控制终端,并且在不使用
O_NOCTTY
选项(参见open()
)的情况下打开尚未与会话关联的终端设备文件,则终端是否成为控制终端由实现定义会议负责人。如果一个不是会话负责人的进程打开了一个终端文件,或者在open()
上使用了O_NOCTTY
选项,那么该终端不应成为调用进程的控制终端。
这告诉我们,如果一个守护进程做这样的事情......
int fd = open("/dev/console", O_RDWR);
...那么守护进程可能获取/dev/console
作为它的控制终端,这取决于守护进程是否是会话领导者,并且取决于系统实现。如果程序首先确保它不是会话领导者,则程序可以保证上述调用不会获取控制终端。
通常,在启动守护程序时,会调用setsid
(从调用fork
后的子进程)以将守护程序与其控制终端分离。但是,调用setsid
也意味着调用进程将成为新会话的会话领导者,这为守护进程重新获取控制终端提供了可能性。双叉技术确保守护进程不是会话领导者,从而保证调用open
(如上例所示)不会导致守护进程重新获取控制终端。
双叉技术有点偏执。如果您知道守护程序永远不会打开终端设备文件,则可能没有必要。此外,在某些系统上,即使守护程序确实打开了终端设备文件,也可能没有必要,因为该行为是实现定义的。然而,没有实现定义的一件事是只有会话负责人才能分配控制终端。 如果一个进程不是会话领导者,它就不能分配控制终端。因此,如果你想偏执并确保守护进程不会无意中获取控制终端,无论任何实现定义的细节,那么双叉技术是必不可少的。
【讨论】:
+1 太糟糕了,这个答案是在提出问题四年后才出现的。 但这仍然不能解释为什么守护进程无法重新获取控制终端如此重要 关键字是“不经意间”获取控制终端。如果该进程碰巧打开了一个终端,并且它成为控制终端的进程,那么如果有人从该终端发出 ^C ,它可能会终止该进程。因此,保护一个进程免于无意中发生这种情况可能会很好。就我个人而言,我会坚持使用一个 fork 和 setsid() 来编写我知道不会打开终端的代码。 @BobDoolittle 这怎么会“不经意间”发生?如果没有写成这样,一个进程将不会最终打开终端。如果您的程序员不知道代码并且不知道它是否可以打开一个 tty,那么双分叉可能会很有用。 @Marius 想象一下,如果您在守护程序的配置文件中添加这样的行:LogFile=/dev/console
,会发生什么。程序并不总是能够在编译时控制它们可能打开哪些文件;)【参考方案3】:
查看问题中引用的代码,理由是:
分叉第二个孩子并立即退出以防止僵尸。这 导致第二个子进程成为孤立的,使 init 负责其清理的进程。而且,由于第一个孩子是 没有控制终端的会话负责人,可能 将来通过打开终端来获得一个(系统V- 基于系统)。这第二个叉子保证孩子没有 不再是会话领导者,阻止守护进程获取 一个控制终端。
因此,这是为了确保守护进程重新成为 init 的父级(以防启动守护进程的进程长期存在),并消除守护进程重新获取控制 tty 的任何机会。因此,如果这两种情况都不适用,那么一个分叉就足够了。 “Unix Network Programming - Stevens”对此有一个很好的部分。
【讨论】:
这并不完全正确。创建守护进程的标准方法是简单地执行p=fork(); if(p) exit(); setsid()
。在这种情况下,父进程也退出并且第一个子进程被重新设置。双叉魔法仅用于防止守护进程获取 tty。
所以,据我了解,如果我的程序启动并且forks
是child
进程,那么第一个子进程将是session leader
并且能够打开一个TTY 终端。但是,如果我再次从这个孩子分叉并终止第一个孩子,第二个分叉的孩子将不是session leader
并且将无法打开 TTY 终端。这个说法正确吗?
@tonix:简单的分叉不会创建会话负责人。这是由setsid()
完成的。因此,第一个分叉的进程在调用setsid()
后成为会话领导者,然后我们再次分叉,这样最终的双分叉进程就不再是会话领导者。除了setsid()
要求成为会议负责人之外,您还可以。
@parasietje 有没有办法阻止守护进程在不分叉两次的情况下获取 tty?如果单叉没有,为什么双叉会阻止它获得一个?
会话领导者是一个连接到tty的控制进程。如果您想阻止进程获取 tty,则该进程不应该是会话领导者。所以,我认为唯一的方法是通过双分叉。【参考方案4】:
取自Bad CTK:
“在某些版本的 Unix 上,您必须在启动时执行双叉,才能进入守护程序模式。这是因为单叉不能保证与控制终端分离。”
【讨论】:
单叉如何不脱离控制终端而双叉可以这样做?这发生在哪些 unix 上? 一个守护进程必须关闭它的输入和输出文件描述符(fds),否则,它仍然会连接到它启动的终端。一个分叉的进程从父进程继承那些。显然,第一个孩子关闭了 fds,但这并没有清理所有内容。在第二个 fork 上,fds 不存在,因此第二个孩子无法再连接到任何东西。 @Aaron: 不,守护进程通过在初始分叉后调用setsid
正确地从其控制终端“分离”自身。然后,它通过再次分叉并让会话领导者(调用 setsid
的进程)退出来确保它保持与控制终端分离。
@bdonlan:不是fork
与控制终端分离。是setsid
做到了。但是如果setsid
是从进程组负责人那里调用的,它就会失败。因此,必须在setsid
之前完成初始fork
,以确保从不是进程组领导的进程调用setsid
。第二个fork
确保最终进程(将成为守护进程的进程)不是会话领导者。只有会话领导者才能获得控制终端,所以第二个分叉保证守护进程不会无意中重新获得控制终端。任何 POSIX 操作系统都是如此。
@DanMoulding 这并不能保证第二个孩子不会获得控制终端,因为它可以调用setsid并成为会话负责人,然后获得控制终端。【参考方案5】:
根据 Stephens 和 Rago 撰写的“Unix 环境中的高级编程”,第二个 fork 更像是一个建议,这样做是为了保证守护进程不会在基于 System V 的系统上获得控制终端。
【讨论】:
【参考方案6】:一个原因是父进程可以立即为子进程wait_pid(),然后忘记它。当孙子去世时,它的父母是 init,它会等待()它 - 并将其从僵尸状态中取出。
结果是父进程不需要知道分叉的子进程,它还可以从库等中分叉长时间运行的进程。
【讨论】:
【参考方案7】:如果 daemon() 调用成功,则父调用 _exit()。最初的动机可能是让父级在子级守护进程时做一些额外的工作。
这也可能是基于一个错误的信念,即有必要确保守护进程没有父进程并被重新设置为 init - 但是一旦父进程在单叉情况下死亡,无论如何都会发生这种情况。
所以我想这一切最终都归结为传统——只要父母在短时间内死亡,一个叉子就足够了。
【讨论】:
【参考方案8】:http://www.developerweb.net/forum/showthread.php?t=3025 似乎对它进行了体面的讨论
从那里引用 mlampkin:
...将 setsid( ) 调用视为做事的“新”方式(与终端分离),然后将 [second] fork( ) 调用视为处理 SVr4 的冗余...
【讨论】:
【参考方案9】:这样理解可能更容易:
第一个 fork 和 setsid 将创建一个新会话(但进程 ID == 会话 ID)。 第二个分叉确保进程 ID != 会话 ID。【讨论】:
以上是关于创建守护进程时执行双叉的原因是啥?的主要内容,如果未能解决你的问题,请参考以下文章