在Unix中,每当我们要创建一个新进程时,我们都会分叉当前进程,并创建一个与父进程完全相同的新子进程。然后执行一个exec系统调用,用新流程替换父流程中的所有数据。

为什么我们首先创建父流程的副本而不创建新的直接处理?

评论

另请参见unix.stackexchange.com/questions/31118/…

#1 楼

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

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

1)
扩展进程表

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

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


自从这篇论文开始,Unix就发展了。 fork和随后的exec不再是运行程序的唯一方法。


创建vfork可以更有效地分叉,以防新进程打算在此之后立即执行exec叉子。执行vfork之后,父进程和子进程共享相同的数据空间,并且父进程被挂起,直到子进程执行程序或退出。
posix_spawn创建一个新进程并在单个系统中执行文件呼叫。它带有一堆参数,可让您有选择地共享调用者的打开文件,并将其信号处置和其他属性复制到新进程。


评论


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

–拉斐尔·阿伦斯(Raphael Ahrens)
2014年6月12日下午4:51

使用posix_spawn()进行相同的叉后重排工作所需的机械设计(数据结构设置),可以使用fork()轻松完成,并且内联代码使fork()的引人注目参数更易于使用。

–乔纳森·莱弗勒(Jonathan Leffler)
2014年6月12日23:41

#2 楼

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

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

实际上,由于以下几个原因,这样做可能效率不高:


fork()产生的“副本”有点抽象,因为内核使用写时复制系统。真正需要创建的只是一个虚拟内存映射。如果该副本随后立即调用exec(),则实际上如果该副本已被流程的活动进行了修改,那么将被复制的大多数数据实际上都不会被复制/创建,因为流程不需要执行任何需要使用的操作。
子进程的各个重要方面(例如,其环境)不必根据上下文的复杂分析等进行单独复制或设置。仅假设它们与调用过程的相同,并且这是我们熟悉的相当直观的系统。

为了进一步解释#1,至少在大多数情况下,“复制”但从未随后访问的内存永远不会真正被复制。在这种情况下,如果您分叉了一个进程,然后在子进程用exec()替换子进程之前退出了父进程,则可能是一个例外。我之所以说是可能的,是因为如果有足够的可用内存,那么很多父对象都可以被缓存,而且我不确定这将被利用到什么程度(这取决于操作系统的实现)。 ,从表面上看,这使使用副本的效率比使用空白面板的效率更高-除非“空白面板”在字面上不是什么,而且必须涉及分配。该系统可以具有通用的空白/新流程模板,该模板以相同的方式进行复制1,但是与写时复制叉子相比,这实际上并不会节省任何东西。因此,#1仅表明使用“新”空过程不会更有效。

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

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

评论


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

– hellodanylo
2014年6月11日在20:06



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

–Cort Ammon
2014年6月12日,下午1:34

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

–金锁
2014年6月12日7:46

金发姑娘是正确的。在某些情况下,分叉和修改比从头创建新的便宜。当然,最极端的例子是任何时候您想要分叉行为本身。 fork()能否很快(如GL所述,在27行汇编的顺序上)。从另一个方向看,如果要“从头开始创建进程”,fork()的成本仅比从空白的已创建进程开始的成本高(27行汇编+关闭文件句柄的成本)。因此,fork既可以很好地处理fork,又可以很好地创建,而create只能很好地处理create。

–Cort Ammon
2014年6月12日在22:57

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

– Barmar
2014年6月19日4:00在

#3 楼

我认为Unix仅具有fork函数来创建新进程的原因是Unix理念的结果。它会创建一个子进程。

然后由程序员来决定如何处理新进程。
他可以使用exec*函数之一并启动另一个程序,
否则他不能使用exec并使用同一程序的两个实例,这可能会很有用。

因此您可以使用

<不带exec *的br />叉
带exec *的for叉或不带fork的仅exec *的br
在1970年代你不得不做。

评论


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

–user1534664
2015年12月3日,下午1:41

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

–斯科特
17年8月11日在18:05

#4 楼

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

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

fork / exec模型还提供了实现在fork和exec之间建立的特定子级继承了一个非常奇怪的环境。诸如继承文件描述符之类的事情尤其如此。 (stdio fd的扩展。)create模型不具备继承create调用的创建者未想到的任何内容的功能。某些系统还可以支持对native的动态编译代码,该过程实际上是在编写自己的本机代码程序。换句话说,它需要一个新的程序,它可以即时编写自己的程序,而不必经历源代码/编译器/链接器的周期,而无需占用磁盘空间。 (我相信有一个Verilog语言系统可以做到这一点。)fork模型支持这一点,而create模型通常不支持。

评论


文件描述符不是“ stdio的扩展”。 stdio文件指针是文件描述符的包装。文件描述符排在第一位,它们是基本的Unix I / O句柄。但是,否则,这是一个好点。

–斯科特
17年8月11日在18:51

#5 楼

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



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

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

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

评论


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

– hellodanylo
2014年6月12日下午4:39

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

–斯科特
2014年6月12日18:55

还有其他方法可以将“父母”信息传递给孩子。如果您假设首先要使用分叉,那么返回值技术恰好是从分叉执行此操作的最有效方法

–Cort Ammon
2014年6月12日22:59

#6 楼

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

评论


您在这里要做的只是重复吉姆·凯西(Jim Cathey)回答的第三段,但要多一点点细节。

–斯科特
17年8月11日在18:38

#7 楼

我想这里的每个人都知道fork是如何工作的,但问题是为什么我们需要使用fork创建父级的完全重复?
Answer ==>以client-1为例(不带fork)的服务器为例正在访问服务器,如果同时有第二个client-2到达并且想访问服务器,但是服务器没有为新到达的client-2授予权限,因为服务器正忙于为client-1服务,因此client-2必须等待。对客户端1的所有服务完成后,客户端2现在可以访问服务器。现在考虑客户端3是否同时到达,因此客户端3必须等到对客户端1的所有服务为止。 client-2已经完成。假设有成千上万的客户端需要同时访问服务器的情况,那么所有客户端都必须等待(服务器正忙!)。

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

评论


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

–斯科特
17年8月11日在18:30