时间浪费execv()和fork()

Dea*_*orf 43 unix linux operating-system fork multiprocessing

目前我正在学习fork(),并execv()和我有关于组合的效率问题.

我看到了以下标准代码:

pid = fork();
if(pid < 0){
    //handle fork error
}
else if (pid == 0){
    execv("son_prog", argv_son);
//do father code
Run Code Online (Sandbox Code Playgroud)

我知道fork()克隆整个过程(复制整个堆等)并execv()用新程序替换当前地址空间.考虑到这一点,使用这种组合不是非常低效吗?我们正在复制进程的整个地址空间,然后立即覆盖它.

所以我的问题是:
即使我们有浪费,使用这个组合(而不是其他解决方案)使人们仍然使用它的优势是什么?

Joh*_*ger 45

使用这个组合(而不是其他一些解决方案)可以获得的优势是什么,即使我们有浪费,人们仍然会使用它?

你必须以某种方式创建一个新的进程.用户空间程序很少有方法可以实现这一点.POSIX曾经有过vfork()alognside fork(),有些系统可能有自己的机制,比如特定于Linux clone(),但自2008年以来,POSIX只指定fork()posix_spawn()系列.该fork+ exec路线是比较传统的,是很好理解的,并且有一些缺点(见下文).该posix_spawn家庭被设计为一种特殊目的替代品,用于存在困难的环境fork(); 您可以在其规范的"基本原理"部分找到详细信息.

这篇来自Linux手册页的摘录vfork()可能很有启发性:

在Linux下,fork(2)是使用copy-on-write页面实现的,因此fork(2)引起的唯一损失是复制父页面表所需的时间和内存,以及为子项创建唯一的任务结构.然而,在过去的糟糕时期,fork(2)需要制作一个完整的呼叫者数据空间副本,通常是不必要的,因为通常紧接着exec(3)完成.因此,为了提高效率,BSD引入了vfork()系统调用,该调用没有完全复制父进程的地址空间,而是借用父进程的内存和控制线程,直到调用execve(2)或退出.当孩子使用其资源时,父进程被暂停.vfork()的使用很棘手:例如,不修改父进程中的数据取决于知道寄存器中保存了哪些变量.

(重点补充)

因此,您对废物的关注对于现代系统(不限于Linux)来说并不是很有根据,但它在历史上确实是一个问题,并且确实存在旨在避免它的机制.目前,大多数这些机制已经过时.

  • POSIX现在也有`posix_spawn`. (4认同)
  • 谢谢,@ hvd,我显然忽略了posix_spawn.我已经更新了答案来解释它. (2认同)
  • @immibis:是的,但是那不是vfork()吗? (2认同)

小智 24

另一个答案是:

然而,在过去的糟糕时期,fork(2)需要制作调用者数据空间的完整副本,这通常是不必要的,因为通常紧接着就会执行exec(3).

显然,一个人过去的坏日子比其他人记得要年轻得多.

原始的UNIX系统没有用于运行多个进程的内存,并且它们没有用于将物理内存中的多个进程保持在同一逻辑地址空间中运行的MMU:它们将进程交换到磁盘而不是目前正在运行

fork系统调用与将当前进程交换到磁盘几乎完全相同,除了返回值和通过交换另一个进程替换剩余的内存中副本.因为你必须换掉父进程才能运行子进程,所以fork + exec不会产生任何开销.

确实有一段时间fork + exec很尴尬:当有MMU提供逻辑和物理地址空间之间的映射但是页面错误没有保留足够的信息,即写时复制和其他一些虚拟 - 存储/请求 - 寻呼方案是可行的.

这种情况非常痛苦,不仅仅是对于UNIX,硬件的页面错误处理被调整为非常快速地"重放".

  • 这个答案中的信息实际上并没有得到它应该得到的赞赏.当然,它几乎更适合作为对另一个答案的评论或补充,或者编辑为完整答案,复制其他答案参考中的一些信息 - 因为在目前的形式中,这个答案更多的是对一个答案的澄清/补充其他答案.不过,来自我的+1:我很高兴能够学到这些历史信息. (6认同)

Ton*_*ous 23

不再是.有一种称为COW(写入时复制)的东西,只有当两个进程中的一个(父/子)尝试写入共享数据时,才会复制它.

在过去的:
fork()系统调用复制调用进程(父)的地址空间中创建一个新的进程(孩子).将父母的地址空间复制到孩子身上是操作中最昂贵的部分fork().

现在:
呼叫到fork()经常几乎立即将呼叫跟随exec()子进程,这与一个新的程序取代了孩子的记忆.例如,这就是shell通常所做的事情.在这种情况下,复制父地址空间所花费的时间在很大程度上被浪费了,因为子进程在调用之前将使用很少的内存exec().

出于这个原因,Unix的更高版本利用虚拟内存硬件来允许父和子共享映射到其各自地址空间的内存,直到其中一个进程实际修改它.这种技术称为写时复制.为此,在fork()内核上将地址空间映射从父级复制到子级而不是映射页面的内容,同时将现在共享的页面标记为只读.当其中一个进程尝试写入其中一个共享页面时,该进程会发生页面错误.此时,Unix内核意识到该页面实际上是一个"虚拟"或"写时复制"副本,因此它为错误进程创建了一个新的,私有的,可写的页面副本.这样,在实际写入之前,实际上不会复制各个页面的内容.这种优化使得fork()后面exec()的孩子便宜得多:孩子可能只需要在调用之前复制一个页面(其堆栈的当前页面)exec().

  • @DeanLeitersdorf一个进程内存中只有一个页面.它有更多,虽然您可以复制包含存储在fork的返回值中的变量的页面,但是通过让子进程先运行来保存许多不需要的复制操作,这样如果它调用execv,则保存所有这些副本操作. (4认同)
  • @Dean:不,没有什么被忽略 - 但是`fork()`的返回值可能只涉及单个页面(如果有的话;取决于ABI,它可能永远不会离开寄存器).与Emacs等长期流程的地址空间相比,这个数字很小. (4认同)
  • 是的,它要么是一页堆栈(一旦孩子开始*不做任何事情就会立即取出共享)或者什么都没有,因为返回值只是在eax或者其他什么:) (4认同)