为什么fork()以它的方式工作

时间:2011-11-28 06:27:45

标签: linux process fork

所以,我使用了fork(),我知道它的作用。作为初学者,我非常害怕它(我仍然不完全理解它)。您可以在网上找到的fork()的一般描述是,它复制当前进程并分配不同的PID,父PID,并且进程将具有不同的地址空间。一切都很好,但是,鉴于这个功能描述,初学者会想知道“为什么这个功能如此重要......为什么我要复制我的过程?”。所以我确实很奇怪,最终我发现你可以通过execve()家族在当前流程中调用其他流程。

我仍然不明白为什么你必须这样做?最合乎逻辑的是拥有一个可以调用的函数,如

create_process("executable_path+name",params..., more params); 

会生成一个新进程并在main()的开头运行它并返回新的PID。

困扰我的是fork / execve解决方案正在进行可能不需要的工作的感觉。如果我的进程使用大量内存怎么办?内核是否复制了我的页面表等。我确信它不会真正分配实际内存,除非我触及它。另外,如果我有线程会发生什么?在我看来,它太乱了。

几乎所有关于fork的描述,说它只是复制进程,新进程在fork()调用后开始运行。这确实发生了什么,但为什么会这样发生?为什么fork / execve是产生新进程的唯一方法,以及从当前创建新进程的最常用的unix方法是什么?是否有其他更有效的方法来产生进程?**不需要复制更多内存。

This线程讨论了同样的问题,但我发现它不太令人满意:

谢谢。

14 个答案:

答案 0 :(得分:19)

这是由于历史原因。正如https://www.bell-labs.com/usr/dmr/www/hist.html所解释的那样,早期的Unix确实既没有fork()也没有exec*(),而且shell执行命令的方式是:

  • 进行必要的初始化(打开 stdin / stdout )。
  • 阅读命令行。
  • 打开命令,加载一些引导代码并跳转到它。
  • 引导程序代码读取打开的命令,(覆盖shell的内存),并跳转到它。
  • 一旦命令结束,它将调用exit(),然后通过重新加载shell(覆盖命令的内存)来工作,并跳转到它,返回到步骤1.

从那里,fork()是一个简单的补充(27条装配线),重复使用其余的代码。

在Unix开发的那个阶段,执行命令变为:

  • 阅读命令行。
  • fork()子进程,等待它(通过向其发送消息)。
  • 子进程加载了命令(覆盖了孩子的内存),然后跳转到它。
  • 一旦命令结束,它将调用exit(),这现在更简单了。它只是清理了它的进程,并放弃了控制。

最初,fork()没有写入副本。由于这会导致fork()费用昂贵,并且fork()经常用于生成新流程(通常会紧跟exec*()),因此出现fork()的优化版本:{{ 1}}共享父和子之间的内存。在vfork()的那些实现中,父项将被暂停,直到孩子vfork()'ed或exec*()'ed,从而放弃父母的记忆。后来,_exit()被优化为在写入时进行复制,只有当它们开始在父和子之间不同时才制作内存页的副本。 fork()后来看到了对MMU系统端口的重新兴趣(例如:如果你有一个ADSL路由器,它可能在一个MMU MIPS CPU上运行Linux),它无法进行COW优化,而且不能有效支持vfork()'ed流程。

fork()中的其他效率低下的原因是它最初复制了父级的地址空间(和页表),这可能使得从大型程序运行的短程序相对较慢,或者可能使操作系统拒绝{ {1}}认为可能没有足够的内存(为了解决这个问题,您可以增加交换空间,或者更改操作系统的内存过载设置)。作为一则轶事,Java 7使用fork()来避免这些问题。

另一方面,fork()使得创建同一进程的多个实例非常有效:例如:Web服务器可能有几个相同的进程为不同的客户端提供服务。其他平台也支持线程,因为产生不同进程的成本比复制当前进程的成本要大得多,这可能比生成新线程的成本要大一些。这是不幸的,因为共享 - 所有线程都是错误的磁铁。

答案 1 :(得分:10)

请记住fork很早就发明在Unix(以及之前或许之前)的机器上,这些机器现在看起来非常小(例如64K字节的内存)。

与通过最基本的可能行动提供基本机制而不是政策的整体(原始)理念更加同步。

fork只是创建一个新流程,最简单的思维方式是克隆当前流程。所以fork语义是非常自然的,它是最简单的机制。

其他系统调用(execve)负责加载新的可执行文件等。

将它们分开(并提供pipedup2系统调用)可以提供很大的灵活性。

在当前系统上,fork非常有效地实现(通过写分页技术的延迟复制)。众所周知,fork机制使Unix进程创建速度非常快(例如,比在Windows或VAX / VMS上更快,系统调用创建的进程更类似于您的建议)。

还有vfork系统调用,我不打算使用它。

posix_spawn API比forkexecve复杂得多,因此说明fork更简单......

答案 2 :(得分:5)

“fork()”是一项出色的创新,通过单一API解决了一大类问题。它是在多处理不常见的时候发明的(并且在你和我今天使用的那种多处理之前大约有二十年)。

答案 3 :(得分:2)

看看spawn和朋友们。

答案 4 :(得分:2)

fork通过复制当前进程创建新进程时,它会执行写时复制。这意味着新进程的内存将与父进程共享,直到更改为止。更改内存后,将复制内存以确保每个进程都有自己的内存有效副本。在execve之后立即执行fork时,没有内存副本,因为新进程只会加载一个新的可执行文件,从而加载一个新的内存空间。

关于为什么要这样做的问题,我不确定,但它似乎是Unix方式的一部分 - 做一件好事。该操作分为两个功能,而不是创建一个创建新进程并加载新可执行文件的函数。这为开发人员提供了最大的灵活虽然我还没有单独使用这两种功能......

答案 5 :(得分:2)

正如其他人所说,fork实现得非常快,所以这不是问题。但为什么不像create_process()这样的函数呢?答案是:简化灵活性。 unix中的所有系统调用都被编程为只做一件事。像create_process这样的函数会做两件事:创建一个进程并将二进制文件加载到该进程中。

每当您尝试并行化时,您都可以使用线程 - 或使用fork()打开的进程。在大多数情况下,您可以通过n打开fork()进程,然后使用IPC机制在这些进程之间进行通信和同步。一些IPC坚持在全球空间中使用变量。

管道示例:

  • 创建管道
  • fork一个继承管道句柄的子项
  • 孩子关闭输入端
  • 父级关闭输出端

不可能没有fork() ...

另一个重要的事实是整个Unix API只有一些功能。每个程序员都可以轻松记住使用过的函数。但是请参阅Windows API:数以千计的功能无人能记住。

总结并再次说出来:简化灵活性

答案 6 :(得分:2)

这是一个很好的问题。我不得不在源头挖掘一下,看看究竟发生了什么。

fork()通过复制调用进程来创建一个新进程。

在Linux下,fork()是使用copy-on-write页面实现的,因此它所产生的唯一损失是复制父页面表所需的时间和内存,以及为子项创建唯一的任务结构。

新进程(称为子进程)与调用进程(称为父进程)完全相同。除了:

  • 子项具有自己唯一的进程ID,并且此PID不匹配 任何现有流程组的ID。
  • 子进程的父进程ID与父进程ID相同。
  • 孩子不会继承父母的记忆锁。
  • 进程资源利用率和CPU时间计数器重置为零 在孩子身上。
  • 孩子的未决信号集最初是空的。
  • 孩子不会从其父母那里继承信号量调整。
  • 子进程不会从其父进程继承记录锁。
  • 孩子不会从其父母那里继承计时器。
  • 子进程不继承未完成的异步I / O操作 来自其父级,也不从其父级继承任何异步I / O上下文。

结论:

fork的主要目标是将父进程的任务划分为较小的子任务而不影响父进程的唯一任务结构。这就是fork克隆现有流程的原因。

来源:

http://www.quora.com/Linux-Kernel/After-a-fork-where-exactly-does-the-childs-execution-start http://learnlinuxconcepts.blogspot.in/2014/03/process-management.html

答案 7 :(得分:1)

假设底层实现使用写时复制寻址系统,fork()可以用很少的内存分配来实现。使用该优化不可能实现create_process函数。

答案 8 :(得分:1)

所以,你主要担心的是:fork()会导致不必要的内存复制。

答案是:不,没有内存浪费。简而言之,fork()是在内存资源非常有限的情况下诞生的,所以没有人会想到这样浪费它。

虽然每个进程都有自己的地址空间,但是物理内存页面和进程的虚拟内存页面之间没有一对一的映射。相反,可以将一页物理内存映射到多个虚拟页面(有关详细信息,请搜索CPU TLB)。

因此,当您使用fork()创建新进程时,其虚拟地址空间将映射到相同的物理内存页面。无需内存副本。这也意味着没有重复使用的库,因为它们的代码段标记为只读。

实际的内存复制仅在父进程或子进程修改某些内存页时发生。在这种情况下,新的物理内存页面被分配并映射到修改页面的进程的虚拟地址空间。

答案 9 :(得分:1)

其他答案很好地解释了为什么fork比看起来更快,以及它最初是如何存在的。但是,保留fork + exec组合也是一个很好的例子,而且它提供了灵活性。

通常,在产生子进程时,在执行子进程之前需要采取预备步骤。例如:您可以使用pipe(读者和作者)创建一对管道,然后将子进程的stdoutstderr重定向到编写器,或者使用读者作为进程的stdin - 或任何其他文件描述符。或者,您可能想要设置环境变量(但仅限于子项)。或者使用setrlimit设置资源限制以限制子项可以使用的资源量(不限制父项)。或者使用setuid / seteuid更改用户(不更改父级)。等等。

当然,您可以使用假设的create_process函数完成所有这些操作。但这需要覆盖很多东西!为什么不提供运行fork的灵活性,做任何你想要设置孩子的事情,然后运行exec

此外,有时候您根本不需要儿童流程。如果您当前的程序(或脚本)仅用于执行某些设置步骤,并且它将要执行的最后一件事是运行新进程,那么为什么还有两个进程呢?您可以使用exec来替换当前进程,释放自己的内存和PID。

分叉还允许关于只读数据集的一些有用的行为。例如,您可以拥有一个父进程来收集和索引大量数据,然后派遣子工作者根据该数据执行遍历和计算。父母不需要将其保存在任何地方,孩子们不需要阅读它,并且您不需要使用共享内存进行任何复杂的工作。 (例如:某些数据库使用此方法让子进程将内存数据库转储到磁盘,而不会阻止父进程。)

以上还包括读取配置,数据库和/或一组代码文件的任何程序,然后继续分离子进程以处理请求并更好地利用多核CPU。这包括Web服务器,还包括Web(或其他)应用程序本身,特别是如果这些应用程序花费大量的启动时间来阅读和/或编译更高级别的代码。

分叉也可以是一种管理内存和避免碎片的有用方法,特别是对于使用自动内存管理(垃圾收集)并且无法直接控制其内存布局的高级语言。如果您的进程短暂需要大量内存用于特定操作,您可以分叉并执行该操作,然后退出,释放您刚刚分配的所有内存。相比之下,如果您在父级中执行了操作,则可能会在进程持续时间内持续存在大量内存碎片 - 对于长时间运行的进程而言并不是很好。

最后:一旦你接受forkexec两者都有自己的用途,彼此独立,那么问题就变成了 - 为什么要创建一个结合两者的单独函数呢?有人说过,Unix的理念是拥有自己的工具,做一件事,做得好#34;通过将forkexec作为单独的构建块 - 并使每个构建块尽可能快速有效 - 它们允许比单个create_process函数更灵活。

答案 10 :(得分:0)

就分页/虚拟内存而言,有些技术中fork()并不总是复制进程的整个地址空间。在写入时有一个副本,其中分叉进程获得与其父进程相同的地址空间,然后只复制一部分更改的空间(通过任一进程)。

答案 11 :(得分:0)

使用fork的主要原因是执行速度。

如果您按照建议使用一组参数启动了流程的新副本,则新流程需要解析这些参数并重复父流程已完成的大部分处理。使用“fork()”,父进程堆栈的完整副本立即可供子进程使用,并按原样进行解析和格式化。

此外,在大多数情况下,程序将是“.so”或“.dll”,因此可执行指令不会仅复制到堆栈,并且将复制堆存储。

答案 12 :(得分:0)

您可以认为这有点像在Windows中生成一个线程,除了进程不共享除文件句柄,共享内存和其他可显式继承的内容之外的资源。因此,如果您要执行新任务,则可以在克隆处理新分配时分叉并在原始作业上继续执行一个进程。

如果要进行并行计算,您的进程可以在循环上方将自身拆分为多个克隆。每个克隆都执行计算的子集,而父级则等待它们完成。操作系统确保它们可以并行运行。在Windows中你会例如需要使用OpenMP来获得相同的可表达性。

如果您需要从文件中读取或写入但无法等待,您可以在继续执行原始任务时进行分叉,克隆会执行i / o操作。在Windows上,您可能会考虑在Unix中使用简单的fork来生成线程或使用重叠的i / o。特别是,进程不具有与线程相同的可读性问题。在32位系统上尤其如此。仅仅分叉比处理重叠i / o的复杂性要容易得多。虽然进程有自己的内存空间,但线程仍然存在,因此对于应该考虑将多少线程放入32位进程是有限制的。使用fork创建32位服务器应用程序非常简单,而使用线程制作32位服务器应用程序可能是一场噩梦。因此,如果您使用32位Windows进行编程,则必须使用其他解决方案,例如重叠I / O,这是一个可以使用的PITA。

由于进程不像线程那样共享全局资源(例如malloc中的全局锁定),因此可扩展性更高。虽然线程经常会相互阻塞,但进程独立运行。

在Unix上,因为fork为你的进程创建了一个copy-on-write克隆,它不比在Windows中生成一个新线程更重要。

如果你处理的是解释性语言,那里通常有一个全局解释器锁(Python,Ruby,PHP ......),那么一个能够分叉的操作系统是不可或缺的。否则,您利用多个处理器的能力就会受到限制。

另一件事是这里存在安全问题。进程不会共享内存空间,也不会搞砸彼此的内部细节。这导致更高的稳定性。如果您的服务器使用线程,则一个线程中的崩溃将占用整个服务器应用程序。通过分支崩溃只会取消分叉克隆。这也使错误处理更加简化。让分叉克隆中止通常就足够了,因为它对原始应用程序没有任何影响。

还存在安全问题。如果分叉进程注入恶意代码,则无法进一步影响父进程。现代网络浏览器利用这一点,例如保护一个标签与另一个标签。如果你有一个fork系统调用,所有这些都更方便编程。

答案 13 :(得分:0)

从历史上看,Unix是在非常小的系统上运行的,不允许在RAM中运行多个进程(它们都在同一地址空间中运行,没有MMU)。 fork只是将当前进程换出到磁盘(或其他辅助存储),而不必费心在另一个进程中换出。您可以继续运行内存中的副本,也可以使用exec加载并继续使用其他可执行文件。

人们习惯于在调用exec之前建立一个新的工作环境(打开文件描述符,管道和内容),所以fork陷入困境。