创建守护程序时执行双叉的原因是什么?


165

我正在尝试在python中创建守护程序。我发现了以下问题,该问题中有一些我目前正在关注的很好的资源,但是我很好奇为什么需要双叉。我到处搜寻Google,发现有很多资源宣称有必要,但不是为什么。

有人提到这是为了防止守护程序获取控制终端。没有第二个分叉怎么办?有什么影响?



2
执行双叉的一个困难是,父级无法轻松获取孙子进程的PID(该fork()调用将子进程的PID返回给父级,因此很容易获得子进程的PID,但要实现获取孙子进程的PID )。
克雷格·麦昆

Answers:


105

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

分叉第二个孩子并立即退出以防僵尸。这将导致第二个子进程被孤立,使初始化进程负责其清理。并且,由于第一个孩子是没有控制终端的会话主持人,因此将来有可能通过打开终端(基于System V的系统)来获取一个孩子。第二个分叉确保孩子不再是会话领导者,从而防止守护程序获取控制终端。

因此,这是为了确保该守护程序重新绑定到init上(以防启动该守护程序的进程长期存在),并消除该守护程序重新获得控制tty的任何可能性。因此,如果这两种情况均不适用,那么一个叉子就足够了。“ Unix网络编程-Stevens ”对此有很好的介绍。


28
这并不完全正确。创建守护程序的标准方法是简单地执行p=fork(); if(p) exit(); setsid()。在这种情况下,父级也将退出,并且第一个子进程将被重新设置父级。需要使用双叉魔术师才能防止守护程序获取tty。
parasietje 2015年

1
因此,据我了解,如果我的程序启动并启动forks一个child进程,则第一个子进程将是,session leader并且将能够打开TTY终端。但是,如果我再次从这个孩子那里分叉并终止这个第一个孩子,则第二个分叉的孩子将不会是a session leader并且也无法打开TTY终端。这句话正确吗?
tonix

2
@tonix:简单地分叉不会创建会话领导者。这是由完成的setsid()。因此,第一个分叉的过程在调用后成为会话领导者setsid(),然后我们再次进行分叉,以使最终的双叉过程不再是会话领导者。除了setsid()成为会议负责人的要求之外,您也很会做。
dbmikus

169

我试图理解双叉,在这里偶然发现了这个问题。经过大量研究,这就是我想出的。希望它将有助于为有相同问题的任何人更好地澄清问题。

在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。真正守护。

我个人发现术语叉两次是令人困惑的。更好的习惯用法可能是前叉-后叉-叉子。

其他感兴趣的链接:


当父进程运行时间较长时,两次分叉还可以防止创建僵尸,并且出于某种原因,删除默认处理程序以通知该进程已死亡。
Trismegistos 2014年

但是第二也可以叫解耦成为会话领导者然后获得终端。
Trismegistos 2014年

2
这不是真的。fork()只要您关闭父级,第一个就可以防止创建僵尸。
parasietje

1
产生以上引用结果的一个最小示例:gist.github.com/cannium/7aa58f13c834920bb32c
可以。

1
在下单setsid() 打电话fork()好吗?实际上,我想这个问题的答案就是答案。
Craig McQueen

118

严格来说,双叉与将守护程序重新作为的子代无关init。重新给孩子父母父母所必须要做的就是父母必须退出。这仅需一个叉子即可完成。另外,仅靠自己做双叉也不会使守护进程重新成为父级init;守护程序的父项必须退出。换句话说,在派生适当的守护程序时,父级始终会退出,以便将守护程序进程重新绑定到init

那为什么要双叉呢?POSIX.1-2008第11.1.3节“ 控制终端 ”具有答案(强调):

会话的控制终端由会话负责人以实现定义的方式分配。如果会话负责人没有控制终端,并且在不使用该O_NOCTTY选项的情况下打开了尚未与会话关联的终端设备文件(请参阅参考资料open()),则该终端是否成为会话负责人的控制终端由实现定义。如果不是会话负责人的进程打开了终端文件,或者使用了该O_NOCTTY选项open()则该终端不应成为呼叫进程的控制终端

这告诉我们,如果守护进程执行了以下操作...

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

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

通常,启动守护程序时,会setsid被调用(从调用后的子进程中fork),以将守护程序与其控制终端分离。但是,调用setsid还意味着调用过程将成为新会话的会话负责人,这使守护程序可以重新获取控制终端的可能性成为可能。双叉技术确保守护进程不是会话领导者,然后保证对的调用open(如上例所示)不会导致守护进程重新获取控制终端。

双叉技术有点偏执。如果您知道守护程序将永远不会打开终端设备文件,则可能没有必要。同样,在某些系统上,即使守护程序确实打开了终端设备文件,也不一定需要,因为该行为是实现定义的。但是,未实现定义的一件事是只有会话负责人才能分配控制终端。如果某个进程不是会话负责人,则无法分配控制终端。因此,如果您想变得偏执狂,并确保守护进程不会无意中获得控制终端,而不管任何实现定义的细节如何,那么使用双叉技术是必不可少的。


3
+1太糟糕了,这个答案是在提出问题后的四年左右。
Tim Seguine 2013年

12
但这仍然不能解释为什么守护程序无法重新获得控制终端如此重要的原因
UloPe

7
关键字“无意间”获得控制终端。如果该过程碰巧打开了一个终端,并且它成为了过程控制终端,那么如果有人从该终端发出^ C,则它可以终止该过程。因此,保护​​进程避免意外发生可能是一个不错的选择。就我个人而言,我会坚持使用单一的fork和setsid()来编写我知道不会打开终端的代码。
BobDoolittle 2014年

1
@BobDoolittle这怎么可能“无意间”发生?如果没有编写一个进程,它不会只是故意地终止终端。如果程序员不知道代码并且也不知道它是否可能打开tty,那么双重分叉很有用。
Marius

10
@Marius想象一下,如果将这样的行添加到守护程序的配置文件:中,会发生什么LogFile=/dev/console。程序并不总是可以对打开哪些文件进行编译时控制;)
Dan Molding 2015年

11

摘自Bad CTK

“在某些版本的Unix上,为了进入守护程序模式,您不得不在启动时进行双叉。这是因为不能保证单叉会脱离控制终端。”


3
单叉不能从控制终端上卸下,而双叉怎么能从控制终端上卸下呢?这发生在什么Unix上?
bdonlan

12
守护程序必须关闭其输入和输出文件描述符(fds),否则它将仍附加到启动它的终端。分叉的进程将从父进程继承它们。显然,第一个孩子关闭了fds,但这并不能清除所有内容。在第二个分支上,fds不存在,因此第二个孩子无法再连接任何东西。
2009年

4
@Aaron:不,守护程序可以通过setsid在初始派生之后进行调用来正确地将自身与其控制终端“分离” 。然后,通过再次分叉并让会话负责人(称为的过程)退出,确保它与控制终端保持分离setsid
Dan Molding

2
@bdonlan:不是fork与控制终端分离的。这是setsid该做的。但是setsid如果从流程组负责人那里调用它将会失败。因此,fork必须先进行初始操作,setsid以确保setsid从不是过程组负责人的过程调用该初始值。第二个fork确保最后的过程(将成为守护程序的过程)不是会话负责人。只有会话负责人才能获取控制终端,因此第二个派生可确保守护程序不会无意间重新获取控制终端。任何POSIX OS都是如此。
Dan Molding

@DanMoulding这不能保证第二个孩子不会获得控制终端,因为它可以调用setid并成为会话领导者,然后获得控制终端。
Trismegistos

7

根据Stephens和Rago的“ Unix环境中的高级编程”,第二个fork更为推荐,并且这样做是为了确保守护程序在基于System V的系统上不获取控制终端。


3

原因之一是父进程可以立即为孩子创建wait_pid(),然后将其忽略。然后,当孙子去世时,它的父级是init,它将等待()-并将其带出僵尸状态。

结果是父进程不需要知道派生的子进程,并且还可以从libs等派生长时间运行的进程。


2

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

它也可能基于一种错误的信念,即必须确保该守护进程没有父进程并被重新绑定到init,但是一旦父进程在单个fork实例中死亡,无论如何都会发生这种情况。

因此,我想这一切最终都归结为传统-只要父母在短时间内死亡,一个叉子就足够了。



-1

用这种方式可能更容易理解:

  • 第一个fork和setsid将创建一个新会话(但进程ID ==会话ID)。
  • 第二个派生确保进程ID!=会话ID。
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.