在Unix中,每当我们想要创建一个新进程时,我们都会fork当前进程,创建一个与父进程完全相同的新子进程;然后我们执行 exec 系统调用,将父进程中的所有数据替换为新进程的数据。
为什么我们首先创建父进程的副本而不直接创建新进程?
答案1
简短的答案是,fork
在 Unix 中,因为它很容易融入当时的现有系统,而且因为伯克利的前身系统使用了分叉的概念。
从Unix分时系统的演变(相关文字已突出显示):
现代形式的过程控制在几天内设计并实施。令人惊讶的是它如此容易地融入现有系统;同时很容易看出如何设计中一些稍微不寻常的特征之所以出现,正是因为它们代表了对现有内容的小的、易于编码的更改。一个很好的例子是 fork 和 exec 函数的分离。创建新进程的最常见模型涉及指定要执行的进程的程序;在 Unix 中,分叉进程继续运行与其父进程相同的程序,直到它执行显式 exec。功能的分离当然不是 Unix 所独有的,并且事实上,它存在于汤普森所熟知的伯克利分时系统中。尽管如此,这样的假设似乎是合理的它存在于 Unix 中主要是因为 fork 可以很容易地实现而无需改变太多其他东西。系统已经处理了多个(即两个)进程;有一个进程表,进程在主存和磁盘之间交换。最初只需要fork的实现
1)进程表的扩展
2) 添加了一个 fork 调用,使用现有的交换 IO 原语将当前进程复制到磁盘交换区域,并对进程表进行一些调整。
事实上,PDP-7 的 fork 调用恰好需要 27 行汇编代码。当然,还需要对操作系统和用户程序进行其他更改,其中一些更改相当有趣且出乎意料。但组合的 fork-exec 会更加复杂,如果只是因为 exec 本身不存在;它的功能已经由 shell 使用显式 IO 执行。
自那篇论文以来,Unix 不断发展。fork
其次exec
不再是运行程序的唯一方法。
叉子被创建为一个更有效的 fork,用于新进程打算在 fork 之后立即执行 exec 的情况。进行 vfork 后,父进程和子进程共享相同的数据空间,并且父进程被挂起,直到子进程执行程序或退出。
posix_spawn创建一个新进程并在单个系统调用中执行一个文件。它需要一堆参数,让您有选择地共享调用者打开的文件并将其信号配置和其他属性复制到新进程。
答案2
[我将重复我的部分答案这里.]
为什么不直接使用一个从头开始创建新进程的命令呢? 复制一个马上就会被替换的东西不是很荒谬而且效率低下吗?
事实上,由于以下几个原因,这可能不会那么有效:
产生的“副本”
fork()
有点抽象,因为内核使用写时复制系统;真正需要创建的只是一个虚拟内存映射。如果复制然后立即调用exec()
,则如果进程的活动修改了大部分数据,则实际上不必复制/创建大部分数据,因为进程不执行任何需要使用它的操作。子进程的各个重要方面(例如,其环境)不必单独复制或基于上下文的复杂分析等进行设置。它们只是假设与调用进程相同,并且这是我们熟悉的相当直观的系统。
为了进一步解释#1,被“复制”但随后从未访问过的内存从未被真正复制过,至少在大多数情况下是这样。在这种情况下有一个例外可能如果您分叉了一个进程,然后在子进程将其替换为 之前让父进程退出exec()
。我说可能因为如果有足够的可用内存,大部分父级都可以被缓存,而且我不确定这会被利用到什么程度(这取决于操作系统的实现)。
当然,从表面上看这并不意味着使用副本更多的比使用空白石板更有效——除了“空白石板”实际上并不是什么都没有,并且必须涉及分配。系统可以有一个通用的空白/新流程模板,它以相同的方式复制,1,但与写时复制分支相比,这不会真正保存任何内容。所以#1 只是证明使用“新的”空进程不会更有效。
第 2 点确实解释了为什么使用叉子可能更有效。子级的环境是从其父级继承的,即使它是完全不同的可执行文件。例如,如果父进程是 shell,子进程是 Web 浏览器,则$HOME
它们仍然相同,但由于任何一个都可能随后更改它,因此它们必须是两个单独的副本。子里的那个是原作制作的fork()
。
1. 这种策略可能没有多大字面意义,但我的观点是,创建一个进程不仅仅涉及将其映像从磁盘复制到内存中。
答案3
我认为Unix只有fork
创建新进程的功能的原因是Unix哲学
他们构建一个可以很好地完成一件事的函数。它创建一个子进程。
如何处理新进程就取决于程序员了。他可以使用其中一个exec*
函数并启动另一个程序,或者他不能使用 exec 并使用同一程序的两个实例,这可能很有用。
因此您可以获得更大的自由度,因为您可以使用
- 没有 exec 的 fork*
- 与 exec* 分叉或
- 只需 exec* 而不使用 fork
此外,您只需记住fork
和exec*
函数调用,这在 20 世纪 70 年代是您必须做的。
答案4
fork() 函数不仅仅是复制父进程,它还返回一个值来指示该进程是父进程还是子进程,下图解释了如何使用 fork() 作为父进程和子进程。儿子:
如图所示,当进程是父进程时 fork() 返回子进程 ID PID
,否则返回0
例如,如果您有一个接收请求的进程(Web 服务器),并且在每个请求上创建一个进程来处理该请求,那么您可以使用它son process
,这里父亲和儿子有不同的工作。
所以,不运行进程的副本并不像 fork() 那样。