为什么fork()的工作方式

所以,我用fork() ,我知道它是做什么的。 作为初学者,我非常害怕(而且我还是不完全明白)。 您可以在网上findfork()的一般描述,它复制当前进程并分配不同的PID,父PID,并且进程将具有不同的地址空间。 所有这一切都是好的,但是,考虑到这个function描述,初学者会想:“为什么这个function如此重要?为什么我要复制我的过程?”。 所以我想知道,最终我发现你可以通过execve()系列来调用当前进程中的其他进程。

我还不明白的是为什么你要这样做呢? 最合乎逻辑的是有一个你可以调用的函数

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

这将产生一个新的进程,并开始在main()的开始运行它,并返回新的PID。

令我困扰的是fork / execve解决scheme正在做一些潜在的不必要的工作。 如果我的过程使用大量的内存呢? 内核是否复制我的页面表等等。 我相信它没有真正分配真正的内存,除非我已经触及它。 另外,如果我有线程会发生什么? 在我看来,这太乱了。

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

这个线程谈到了同样的问题,但是我发现它不太令人满意:

谢谢。

这是由于历史原因。 正如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()的优化版本出现了: vfork()和孩子。 在那些vfork()的实现中,父vfork()将被挂起,直到子exec*() 'ed或_exit() '被编辑,从而放弃父进程的内存。 之后, fork()被优化,以便在写入时进行复制,只有在开始父和子之间的差异时才复制内存页面。 vfork()后来又看到了对MMU系统(例如:如果你有一个ADSL路由器,它可能在一个MMU MIPS CPU上运行Linux)对端口的重新兴趣,它不能进行COW优化,而且不能支持fork() 'ed进程有效。

fork()其他低效率的来源是它最初复制了父代的地址空间(和页表),这可能会导致从大型程序运行较短的程序相对较慢,或者可能会使操作系统拒绝fork()没有足够的内存(要解决这个问题,你可以增加交换空间,或者改变你的操作系统的内存过量使用设置)。 作为一个轶事,Java 7使用vfork()/posix_spawn()来避免这些问题。

另一方面, fork()使创建一个相同进程的几个实例非常高效:例如:一个web服务器可能有几个相同的进程为不同的客户端服务。 其他平台更偏爱线程,因为产生一个不同进程的成本比重复当前进程的成本要大得多,这个成本可能比产生一个新线程的成本稍大一些。 这是不幸的,因为共享的一切线程是错误的磁铁。

请记住, fork是很早就在Unix(也许是之前)发明的,当今似乎非常小(例如64K字节的内存)的机器上。

而且,通过最基本的可能行动,提供基本机制而不是政策的整体(原始)理念更为相位。

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

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

分离它们(并提供pipedup2系统调用)提供了很大的灵活性。

而在目前的系统中, fork是非常有效地实现的(在写分页技术上通过懒惰的拷贝)。 众所周知fork机制使得Unix进程的创建速度非常快(例如,比Windows或VAX / VMS更快,其中系统调用的创建过程与您所提议的更类似)。

也有vfork系统调用,我不打扰使用。

而posix_spawn API比forkexecve复杂得多,所以说fork更简单…

“fork()”是一个杰出的创新,它用一个API解决了一整类问题。 它是在多处理不常见的时候发明的(在我们今天使用的那种多处理之前,大概有二十年)。

看看spawn和朋友。

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

至于为什么这样做的问题,我不知道,但它似乎是Unix方式的一部分 – 做一件好事。 而不是创建一个新的进程,并加载一个新的可执行文件的功能,操作分为两个功能。 这给了开发者最大的灵活性。 虽然我自己还没有使用任何一个功能呢…

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

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

管道示例:

  • 创建管道
  • 分叉继承管柄的小孩
  • 孩子关闭输入端
  • 父母关闭输出端

不可能没有fork()

另一个重要的事实是整个Unix API只有一些功能。 每个程序员都可以很容易地记住使用的函数。 但看到Windows API:超过成千上万的功能,谁也不会记得。

所以总结和再说一遍:简单的灵活性

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

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

答案是:不,没有记忆浪费。 总之,fork()是在记忆资源非常有限的情况下诞生的,所以没有人会想这样浪费它。

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

所以当你用fork()创建新的进程时,它们的虚拟地址空间被映射到相同的物理内存页面。 没有内存复制是必需的。 这也意味着没有重复使用的库,因为它们的代码段标记为只读。

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

这是一个很好的问题。 我不得不对源代码进行深入的研究,看看到底发生了什么。

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

在Linux下,fork()是使用写时复制页面实现的,所以它唯一的代价是复制父页表所需的时间和内存,并为子项创建一个唯一的任务结构。

这个称为孩子的新过程与调用过程(称为父辈)完全相同。 除了:

  • 孩子有自己独特的进程ID,这个PID不匹配任何现有进程组的ID。
  • 孩子的父进程ID与父进程ID相同。
  • 孩子不会继承父母的记忆锁。
  • 子进程资源利用率和CPU时间计数器重置为零。
  • 孩子的一组未决信号最初是空的。
  • 孩子不会继承父母的信号量调整。
  • 孩子不从父母继承记录锁。
  • 孩子不从其父母继承定时器。
  • 孩子不从其父母继承未完成的异步I / O操作,也不从其父母继承任何异步I / O上下文。

结论:

分叉的主要目的是将父母过程的任务分成较小的子任务,而不影响父母的独特任务结构。 这就是为什么叉克隆现有的过程。

来源:

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

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

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

如果按照你的建议,你用一组参数启动了一个新的进程副本,新进程将需要分析这些参数,并重复父进程完成的大部分处理。 使用“fork()”,父进程堆栈的完整副本立即可用于孩子,所有事物都应该被解析和格式化。

在大多数情况下,程序将是“.so”或“.dll”,所以可执行指令不会被复制,只有堆栈和堆存储将被复制。

你可以这样想,就像在Windows中产生一个线程,除了进程不共享资源,除了文件句柄,共享内存和其他明确可继承的东西外。 所以,如果你有一个新的任务要做,那么你可以fork和一个进程继续其原来的工作,而克隆负责新的任务。

如果你想做并行计算,你的进程可以将它自己分裂成多个在循环之上的克隆。 每个克隆都执行计算的一个子集,而父节点则等待它们完成。 操作系统确保它们可以并行运行。 在Windows中,您需要使用OpenMP来获得相同的表达式。

如果你需要阅读或写文件,但不能等待,你可以分叉和克隆执行I / O,而你继续你的原始任务。 在Windows上,你可能会考虑产生线程或者在很多情况下使用重叠的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功能的操作系统是必不可少的。 否则,您利用多个处理器的能力将受到更多的限制。

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

还有一个安全问题。 如果分叉进程注入了恶意代码,则不能进一步影响父进程。 现代的网络浏览器利用这个来保护一个标签。 如果你有一个fork系统调用,所有这些对编程来说更加方便。

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

通常,当产生一个孩子的过程时,在执行孩子之前要做好准备。 例如:你可以使用pipe (读写器)创建一对管道,然后将子进程的stdoutstderr重定向到writer,或者使用reader作为进程的stdin或其他文件描述符。 或者,您可能需要设置环境变量(但仅限于子项)。 或者用setrlimit设置资源限制来限制孩子可以使用的资源量(不限制父母)。 或者用setuid / seteuid改变用户(不改变父母)。 等等

当然,你可以用一个假设的create_process函数来完成所有这些。 但是,这是很多东西来涵盖! 为什么不提供运行fork的灵活性,做任何你想要设置孩子,然后运行exec

另外,有时你根本不需要孩子的过程。 如果您当前的程序(或脚本)仅用于执行某些设置步骤,而最后要做的就是运行新的程序,那么为什么要有两个程序呢? 您可以使用exec来替换当前进程,释放自己的内存和PID。

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

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

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

最后,一旦你接受forkexec都有自己的用法,相互独立,问题就变成了 – 为什么还要创建一个独立的函数来结合这两个函数呢? 有人说,Unix的哲学就是要有自己的工具“做一件事,做得好”。 通过将forkexec作为独立的构建块(通过使每个构建块尽可能快速高效),它们比单个create_process函数具有更多的灵活性。