为什么我们需要创建新流程?


Answers:


61

简而言之,fork是在Unix中,因为当时它很容易安装到现有系统中,并且伯克利前身系统使用了fork的概念。

摘自Unix分时系统的演进(相关文本已突出显示):

几天之内就设计并实施了现代形式的过程控制。令人惊讶的是它很容易安装到现有系统中;同时,很容易看到设计中某些稍微不寻常的功能是如何呈现的,因为它们代表了对存在的内容的细微,易于编码的更改。一个很好的例子是fork和exec函数的分离。创建新流程的最常见模型涉及为该流程执行指定程序。在Unix中,派生进程将继续与其父进程运行相同的程序,直到执行显式exec。这些功能的分离当然不是Unix独有的,事实上,它存在于汤普森众所周知的伯克利分时系统中。。仍然可以合理地假设它存在于Unix中,主要是因为可以很容易地实现fork而不改变其他内容。系统已经处理了多个(即两个)过程;有一个进程表,并且进程在主内存和磁盘之间交换。fork的初始实现只需要

1)扩展过程表

2)添加一个fork调用,该调用使用现有的swap IO原语将当前进程复制到磁盘交换区域,并对进程表进行一些调整。

实际上,PDP-7的fork调用恰好需要27行汇编代码。当然,还需要对操作系统和用户程序进行其他更改,其中一些更改非常有趣且出乎意料。但是,仅仅因为不存在这样的exec,合并的fork-exec就会复杂得多。它的功能已经由外壳使用显式IO执行。

从那篇论文开始,Unix就发展了。fork其次exec不再是运行程序的唯一方法。

  • vfork的创建是为了在新进程打算在该分支之后立即执行exec的情况下提高效率。执行vfork之后,父进程和子进程共享相同的数据空间,并且父进程被挂起,直到子进程执行程序或退出。

  • posix_spawn在一个系统调用中创建一个新进程并执行一个文件。它带有许多参数,可让您有选择地共享调用者的打开文件,并将其信号处置和其他属性复制到新进程。


5
很好的答案,但我要补充一点,即不应再使用vfork。现在的性能差异很小,使用起来可能很危险。请参阅此SO问题stackoverflow.com/questions/4856255/…,此站点ewontfix.com/7和有关vfork的“高级Unix编程”第299页
Raphael Ahrens 2014年

4
posix_spawn()执行相同的叉后重排作业所需的机械设计(数据结构设置),可以轻松使用fork()内联代码完成,并且内联代码令人信服,理由fork()是其易于使用。
Jonathan Leffler 2014年

34

[我将从这里重复部分答案。]

为什么不仅仅拥有一个从头开始创建新进程的命令? 复制仅将立即替换的内容是荒谬且效率低下的吗?

实际上,由于以下几个原因,效率可能不那么高:

  1. fork()因为内核使用写时复制系统,所以产生的“副本” 有点抽象。真正需要创建的只是一个虚拟内存映射。如果该副本随后立即调用exec(),则实际上如果已被流程的活动修改了该副本的大部分数据,则实际上不必复制/创建该副本,因为流程不需要执行任何需要使用的操作。

  2. 子进程的各个重要方面(例如,其环境)不必根据上下文的复杂分析等单独进行复制或设置。仅假设它们与调用过程的相同,并且这是我们熟悉的相当直观的系统。

为了进一步解释#1,至少在大多数情况下,“复制”但从未随后访问的内存永远不会真正被复制。在这种情况下,例外情况可能是,如果您分叉了一个流程,然后在子级将自己替换为子级之前退出了父级进程exec()。我之所以说是可能的,是因为如果有足够的可用内存,那么很多父对象都可以被缓存,而且我不确定这将被利用到什么程度(取决于操作系统的实现)。

当然,从表面上看,这并不比使用空白板有效地使用副本-除了“空白板”从字面上看不是什么,而必须涉及分配。该系统可以具有通用的空白/新流程模板,该模板以相同的方式复制1,但是与写时复制fork相比并不会真正节省任何东西。因此,#1仅演示了使用“新”空进程不会更有效。

第二点确实解释了为什么使用分叉可能更有效。子级环境是从其父级继承的,即使它是完全不同的可执行文件。例如,如果父进程是一个外壳程序,子进程是一个Web浏览器,$HOME则两者都相同,但是由于随后任何一个都可以更改它,因此它们必须是两个单独的副本。孩子中的一个是原始的fork()

1.一种策略可能并没有多大的意义,但我的观点是,创建进程所涉及的不仅仅是将其映像从磁盘复制到内存中。


3
这两点都是正确的,但都没有支持为什么选择派生方法而不是从给定的可执行文件重新创建新进程的原因。
2014年

3
我认为这确实回答了这个问题。之所以使用Fork,是因为在创建新流程是最有效的方法中,使用fork的成本微不足道(可能不到流程创建成本的1%)。另一方面,在许多地方,fork的API效率更高或更简单(例如处理文件句柄)。Unix决定只支持一种API,从而使规范更简单。
Cort Ammon 2014年

1
@SkyDan没错,它是对为什么不这样做而不是为什么的答案,马克· 普洛特尼克对此作了更直接的回答-我认为这不仅意味着这是最简单的选择,而且可能是最有效的选择选择(根据Dennis Richie的话:“ PDP-7的fork调用恰好需要27行汇编... exec这样就不存在;它的功能已经执行了”)。因此,这种“为什么不”实际上是对两种策略的一种沉思,一种策略表面上看起来更简单,更有效,而有时却并非如此(见证……的可疑命运……
goldilocks

1
金发姑娘是正确的。在某些情况下,分叉和修改比从头创建新的便宜。当然,最极端的例子是任何时候您想要分叉行为本身。 fork()可以非常快地完成它(如GL所述,大约27条装配线)。从另一个方向看,如果您想“从头开始创建流程”,则fork()花费仅比从空白创建的流程开始花费的成本高(27行汇编+关闭文件句柄的成本)。因此,fork既处理好分叉又创造得很好,而create只能处理好创造。
Cort Ammon 2014年

2
您的答案涉及硬件改进:虚拟内存,写时复制。在此之前,fork实际上复制了所有进程内存,这非常昂贵。
2014年

6

我认为Unix仅fork具有创建新进程的功能的原因是Unix哲学的结果

他们构建了一个功能出色的功能。它创建一个子进程。

然后,由新程序员决定如何处理新过程。他可以使用exec*功能之一并启动其他程序,或者他不能使用exec并使用同一程序的两个实例,这很有用。

因此,您可以使用,因此拥有更大的自由度

  1. 没有exec *的分叉
  2. 用exec *进行分叉
  3. 只是exec *不带fork

此外,您只需要记住forkexec*函数调用,就可以在1970年代完成。


3
我了解叉子的工作方式以及如何使用它们。但是,当我可以用更少的精力做同样的事情时,为什么还要创建一个新的流程呢?例如,我的老师给了我一个作业,我必须为传递给argv的每个数字创建一个过程,以检查数字是否为质数。但这不就是最终要做同样的事情吗?我可以只使用一个数组并为每个数字使用一个函数...那么为什么我们创建子进程,而不是在主进程中进行所有处理?
user1534664

2
我敢说您了解分叉的工作原理以及使用方法,因为您曾经有一位老师给您分配作业,您必须创建一堆流程(在运行时指定数量),控制它们,协调它们,并在它们之间进行交流。当然,没有人会在现实生活中做些琐碎的事情。但是,如果您有一个很大的问题,很容易分解为可以并行处理的部分(例如,图像中的边缘检测),则派生使您可以同时使用多个CPU内核。
斯科特,

5

流程创建有两种哲学:带有继承的派生和带有参数的创建。显然,Unix使用fork。(例如,OSE和VMS使用create方法。)Unix具有许多可继承的特性,并且会定期添加更多特性。通过继承,可以添加这些新特性而无需更改现有程序!使用带参数创建模型,添加新特征将意味着向create调用添加新参数。Unix模型更简单。

它还提供了非常有用的fork-without-exec模型,其中一个进程可以将自身分成多个部分。当没有异步I / O形式时,这是至关重要的,并且在系统中利用多个CPU时很有用。(预线程。)这些年来,甚至最近,我已经做了很多事情。本质上,它允许将多个“程序”容器化为单个程序,因此绝对没有损坏或版本不匹配等的空间。

fork / exec模型还为特定的孩子提供了继承在fork和exec之间建立的根本怪异环境的能力。诸如继承文件描述符之类的事情尤其如此。(stdio fd的扩展。)create模型不提供继承create调用的创建者未想到的任何内容的功能。

一些系统还可以支持本地代码的动态编译,该过程实际上是在编写自己的本地代码程序。换句话说,它需要一个新的程序,它可以即时编写自己的程序,而不必经历源代码/编译器/链接器的周期,而无需占用磁盘空间。(我相信有一个Verilog语言系统可以做到这一点。)fork模型支持这一点,而create模型通常不支持。


文件描述符不是“ stdio的扩展”。stdio文件指针是文件描述符的包装。文件描述符首先出现,它们是基本的Unix I / O句柄。但是,否则,这是一个好主意。
斯科特,

2

fork()函数不仅复制父进程,还返回一个值,该值表示该进程是父进程还是子进程,下图说明了如何使用fork()作为父进程和父进程。儿子:

在此处输入图片说明

如进程是父进程时所示,fork()返回子进程ID,PID 否则返回0

例如,如果您有一个接收请求的进程(Web服务器),并且可以在每个请求上创建一个son process来处理该请求,则可以使用它,此处父级和子级都有不同的工作。

因此,没有运行进程副本不是fork()的确切含义。


5
的确如此,但这并不能回答问题。如果要运行其他可执行文件,为什么创建过程需要进行分叉?
2014年

1
我同意SkyDan –这不能回答问题。posix_spawn是30年前(在Posix存在之前)可以想象fork_execve函数的更高级版本。创建新进程的过程,从可执行文件初始化其映像,甚至不暗示要复制父进程的映像(参数列表,环境和进程属性(例如,工作目录)除外),然后返回给调用方(父进程)的新进程的PID
斯科特(Scott)

1
还有其他方法可以将“父母”信息传递给孩子。返回值技术恰好是从做的最有效的方法fork ,如果你认为你要fork摆在首位
科特阿蒙

0

在fork之后和exec之前,最容易实现I / O重定向。孩子知道自己是孩子,可以关闭文件描述符,打开新的文件描述符,使用dup()或dup2()将它们添加到正确的fd编号等,而所有这些都不影响父对象。完成此操作之后,也许任何所需的环境变量都会发生变化(也不会影响父级),它可以在定制的环境中执行新程序。


您在这里要做的只是稍微重复一下Jim Cathey回答的第三段。
斯科特,

-2

我想这里的每个人都知道fork是如何工作的,但问题是为什么我们需要使用fork创建父级的精确副本? 答案 ==>以服务器为例(不带fork),当client-1正在访问服务器时,如果同时有第二个client-2到达并想要访问服务器,但服务器未向新到达的服务器授予权限client-2,因为服务器忙于为client-1提供服务,因此client-2必须等待。到client-1的所有服务完成后,client-2现在可以访问服务器了。现在考虑是否同时客户端3到达,因此客户端3必须等到对客户端2的所有服务完成。以这种情况为例,成千上万的客户端需要同时访问服务器...然后所有客户端必须等待(服务器正忙!)。

通过创建(使用fork)服务器的精确副本(即子节点)来避免这种情况,其中每个子节点(即其父节点(即服务器)的精确副本)专用于新到达的客户端,因此所有客户端同时访问同一客户端服务器。


这就是为什么服务器进程不应是单线程的,而是在可以同时处理客户端请求时(例如,在单独的进程中)连续处理客户端请求。但是,多线程服务器模型可以通过侦听器进程轻松实现,该侦听器进程接受来自客户端的请求,并创建一个全新的进程来运行客户端服务程序。复制父进程的调用提供的唯一好处是,您不必拥有两个单独的程序,但是拥有单独的程序(例如)可以使系统更加模块化。forkinetd
斯科特,
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.