为什么 fork 后面经常跟着 exec?

ale*_*vak 12 unix process

为什么“fork 后面经常跟着 exec”?不能在 UNIX 中创建一个新进程吗?

raj*_*raj 43

fork()创建一个新进程,它是父进程的副本。因此,如果您仅执行此操作fork(),则会运行两个相同的进程。因此,为了用另一个代码替换分叉进程,您需要执行exec()用指定的可执行文件替换当前正在运行的进程的操作。

Linux 内核就是这样组织的。您没有一个系统调用可以同时创建新进程并加载新的可执行文件。您必须分两步完成 - 首先创建新进程,然后将新的可执行文件加载到该新进程中。(尽管您的编程语言中可能有一个结合了这两者的库函数- 例如spawn()在许多 C 变体中都有)。

有时exec()不需要,如果您只需要创建当前进程的另一个副本即可。例如,许多守护进程都会这样做。

  • @fraxinus 我很确定这来自原始的 UNIX 系统,所以这已经有 50 多年的历史了。 (19认同)
  • 答案并不是 Linux 特有的。我认为 BSD 也是这样做的。 (6认同)
  • “Linux 内核就是这样组织的”,这绝对不是真的。这是 Unix 方式。 Linux 有许多其他方法来生成新进程 (4认同)
  • @gerrit 可能是的。我不太热衷于数值编程;但例如网络守护进程(如 Web 服务器或 SSH 服务器)始终会执行此操作。它们有一个主进程,当网络请求到来时,该主进程会分叉,并且分叉的进程会为该请求提供服务,如果一段时间后没有更多请求,则会退出。 (2认同)

dav*_*bak 33

这是因为历史原因:太初只有forkexec。因为它很容易实现(根据 DMR:只有 27 行 PDP-7 汇编代码fork! - 参见路上的Afork() (Baumann, Appavoo, Krieger, Roscoe, 2019) - 次要来源,尽管它引用了主要来源来源The Evolution of the Unix time-sharing system (Ritchie, 1979). 不管怎样,真正的从头开始直接进程创建是后来才出现的。(可能不在 POSIX 中?)

事实上,真正的直接进程创建 API 的出现要晚得多,这一事实至今仍影响着 Unix 编程。因为数百本书籍、手册、教程、幻灯片和课程都是为了解释而编写的,fork并且exec几十年来它们一直被教授给学生和程序员,作为Unix 中进行进程创建/控制的方法,并且广泛的遗产在代码方式中持续存在写到今天


哦,这是《Unix 分时系统的演变》(Ritchie,1979)。向下滚动到第 6 页可以看到:“现代形式的流程控制是在几天内设计和实现的。......事实上,PDP-7 的fork调用精确地需要 27 行汇编代码。”

  • @JeremyFriesner`posix_spawn`。在许多 POSIX 实现中,它是 `fork` 和 `exec` 之上的库函数,因为它们无论如何都必须存在,并且不能在 `posix_spawn` 之上实现。 (9认同)
  • 有谁知道 *ab initio* 直接进程创建 API 在 Unix 中被称为什么?我想读一下它,如果它存在的话。 (2认同)
  • @JeremyFriesner 不是叫“启动”吗?内核启动第一个进程(例如/bin/init)。 Init 通过 fork 和 exec 启动其他一切。 (2认同)

use*_*198 13

因为 exec 不会创建进程,并且 linux 没有用于创建进程和加载可执行文件的单个系统调用,因为这只适用于使用没有预先存在的资源的新可执行文件创建进程的简单情况。如果您想做的不仅仅是琐碎的事情,复杂性会迅速增加,并且更容易拥有单独的“创建进程”和“启动可执行文件”步骤,并且能够在两者之间操纵进程。有关于此的讨论,请参阅https://lwn.net/Articles/360556/ 。

Unix,回到最早的版本已经解决了这个问题,方法是使用 fork 创建父进程的副本,专用于设置环境,然后在完成后加载新的可执行文件。然后,子进程处于一种临时状态,可以访问父进程的所有资源,但在子进程中运行。这种方法有几个优点:

  1. 您可以使用现有的进程内操作 API 来设置子进程。这意味着您不需要一整套 API 调用来操作子进程来设置资源。
  2. 如果创建新进程后父进程不再需要存在,您可以单独使用 exec。
  3. 如果您想要同一可执行文件的第二个进程,您可以在不使用 exec 的情况下进行 fork。

  • 这不仅仅是一种合理化,更是一种设计理念。 Unics [原文如此] 简单性的灵感来自于与 Multics 复杂性的对抗,因此,如果当您想要复制时,“fork”是复制进程的好方法,而“exec”是当您想要复制时替换可执行映像的好方法。替换它,为什么要浪费时间和资源以另一种方式来做同样的事情呢? https://multicians.org/unix.html (11认同)
  • 另请参见:clone() 和 posix_spawn() (3认同)
  • @BowlOfRed posix_spawn 不是 Linux 系统调用。它是被模仿的。克隆基本上是一个分支,能够更有效地处理一些极其常见的配置情况,并且具有更好的多线程安全性。它不具备一步执行 fork + exec 的能力。 (3认同)
  • @user253751 我没有看到有人说“这是在任何操作系统中设计的理想方式,并且永远不应该改变”,他们说“这是当时导致这一决定的合理推理”。说“好吧,这是我认为更好的第三种工作方式”,这绝不意味着所选择的设计比其他操作系统的实现方式具有明显优势的事实。 (3认同)

Ano*_*noE 7

与大多数“分叉羞辱”现有答案略有不同的观点......;)

最初,正如 @davidbak 提到的,这样做可能非常简单。但是,在多次使用fork/进行工作(并且经常使用 only进行多处理)之后,这种工作方式仍然存在且活跃,并且没有被委托给历史的迷雾,这肯定是有原因的:execfork

  • 从使用任何编程语言的程序员的角度来看,它仍然非常简单。我在哪里编码并不重要 - 任何语言都可以信任语义的极其简单的含义fork并将其作为语言的一部分提供。因此,每种语言都有一个相对简单的(与进程内多线程相比)方法来至少为其用户提供多处理。(注意:一个例外是,如果您在程序中使用多线程 -在分叉之后,只有一个线程在运行;除了最微不足道的多线程应用程序之外,这可能会导致所有明显的问题。)
  • 作为用户(程序员),我可以用几行代码编写我的多重处理,而不必担心互斥体、信号量、非法覆盖任何程序变量的状态等等。同时,父子之间的“初始通信”也为我轻松处理 - 子级确实可以完全访问父级拥有的任何变量或 RAM,并且可以继续使用它。在实践中,这意味着,如果我需要与主程序并行执行一些简短的 I/O 或网络进程,我可以用几行代码来完成;一切都集中在一处,一目了然。之后我就可以接孩子,然后就可以愉快地上路了。没有“工作线程”,我不需要注意仅使用线程安全的方法或数据结构。
  • 同样,由于内存内容开始相同但实际上是独立的,因此在父/子进程之间覆盖任何内容的风险为零。是的,我确实必须找到其他方法来实现父子之间的 IPC,但这些方法也不是那么难;通常,语言提供像“open3”这样的标准函数或类似的函数,它自动提供基于管道的双向文件句柄进行通信以避免死锁等。
  • 具体来说,在编程语言之间切换时,一旦理解了语义fork,就不再需要学习有关新环境的更多信息 - 它总是像任何其他语言一样简单。
  • 无论如何,拥有它真是太好了exec。它允许我们用不同的东西替换当前的进程映像(即正在执行的可执行文件)。比如说,让一些脚本或程序准备某种环境,然后执行其他东西,同时从场景中消失,这就变得很干净。它不仅释放资源(RAM,还释放进程表中的空间等),而且让任何参与或查看它的人都非常清楚,以前的父进程在未来将不再扮演任何角色。您经常在编写良好的bash脚本中发现这一点,这些脚本在启动“有效负载”时释放bash解释器的资源。

此外:

  • 它完全符合 Unix 哲学,即拥有许多可以相互交互的小工具,而不是非常有限或需要大量参数或 API 才能真正使用的胖黑盒子。
  • 如上所示,在某些只有单一功能会受到限制的场景下,它是强大的;fork但让+exec相互跟随仍然很容易。除非您需要,否则您不必在中间做很多事情(或者根本不需要做任何事情)。
  • 根据手册页,在一些现代 Unix(即 Linux)中,fork本身只是更现代、更强大的克隆调用的包装,这确实有点像fork+exec. 请注意,在这里我们看到复杂性已经露出了丑陋的头;Linux 还有一个clone3函数,它可以取代clone并使界面变得更简单或更方便(使用structs而不是那么多标志)。