execv()和fork()的时间浪费
我目前正在学习有关fork()
和execv()
,我有一个关于组合效率的问题。
我被显示了以下标准代码:
pid = fork(); if(pid < 0){ //handle fork error } else if (pid == 0){ execv("son_prog", argv_son); //do father code
我知道fork()
克隆整个进程(复制整个堆等), execv()
用新程序replace当前的地址空间。 考虑到这一点,使用这种组合不是很低效吗? 我们正在复制一个进程的整个地址空间,然后立即覆盖它。
所以我的问题是:
即使我们有浪费,使用这个组合(而不是其他一些解决scheme)可以使人们仍然使用这个组合的优点是什么?
使用这个组合(而不是其他一些解决scheme)使得人们仍然在使用这个组合(即使我们有浪费)而获得的优势是什么?
你不得不创造一个新的过程。 用户空间程序有很less的方法来完成这一点。 POSIX过去有vfork()
和fork()
,有些系统可能有自己的机制,比如Linux特有的clone()
,但是从2008年开始,POSIX只指定fork()
和posix_spawn()
系列。 fork
+ exec
路线比较传统,很好理解,缺点很less(见下文)。 posix_spawn
系列被devise为一个特殊用途的替代品,用于在fork()
; 您可以在其规范的“基本原理”部分find详细信息。
Linux vfork()
手册页摘录如下:
在Linux下,
fork
(2)是使用写时复制页面实现的,所以fork
(2)唯一的惩罚就是复制父页表所需的时间和内存,并且创build一个独特的任务结构 。 然而,在过去的糟糕时代,fork
(2)通常会不必要地复制调用者的数据空间,因为通常在exec
(3)之后立即exec
。 因此,为了提高效率,BSD引入了vfork
()系统调用,它没有完全复制父进程的地址空间,而是借用了父进程的内存和线程,直到调用execve
(2)或者退出。 父母的过程在孩子使用其资源时被暂停。 使用vfork
()很棘手:例如,不修改父进程中的数据取决于知道哪些variables保存在寄存器中。
(强调添加)
因此,对于现代系统(不限于Linux)而言,您对浪费的担忧并不是很有根据的,但历史上的确是一个问题,而且确实有机制来避免浪费。 现在这些机制大部分已经过时了。
另一个答案是:
然而,在过去的糟糕时代,fork(2)通常会不必要地复制调用者的数据空间,因为通常在执行exec(3)之后立即执行。
显然,一个人的坏日子比别人记得的年轻很多。
原始的UNIX系统没有运行多个进程的内存,而且没有用于在同一个逻辑地址空间保持物理内存中的多个进程准备运行的MMU:它们将进程换出到磁盘上正在运行。
fork系统调用与将当前进程换出到磁盘几乎完全相同,除了返回值和在另一个进程中不交换剩余内存副本。 既然你必须换掉父进程来运行subprocess,那么fork + exec不会产生任何开销。
确实有一段时间fork + exec很笨拙:当MMU提供了逻辑和物理地址空间之间的映射时,页面错误没有保留足够的信息,以至于写入时拷贝和其他一些虚拟内存/需求分页scheme是可行的。
这种情况已经足够痛苦,不仅仅是UNIX,硬件的页面error handling被调整为“可重放”的速度非常快。
不再。 只有当两个进程(父/子)中的一个尝试写入共享数据时,才会复制COW
(复制写入)。
以往:
fork()
系统调用复制调用进程(父进程fork()
的地址空间以创build新进程(subprocess)。 将父节点地址空间复制到子节点是fork()
操作中最昂贵的部分。
现在:
对fork()
调用通常会立即在subprocess中调用exec()
,用新程序replacesubprocess。 例如,这就是shell通常所做的事情。 在这种情况下,复制父进程的地址空间的时间很大程度上是浪费的,因为subprocess在调用exec()
之前将只占用很less的内存。
由于这个原因,Unix的后续版本利用虚拟内存硬件来允许父母和孩子共享映射到它们各自地址空间的内存,直到其中一个进程实际修改它为止。 这种技术被称为写时复制(copy-on-write) 。 为此,在fork()
,内核会将地址空间映射从父级复制到子级而不是映射页面的内容,同时将只读页面标记为now-shared页面。 当两个进程中的一个尝试写入其中一个共享页面时,该进程会导致页面错误。 此时,Unix内核意识到该页面实际上是一个“虚拟”或“写入时复制”副本,因此它为错误进程创build了一个新的,专用的可写副本。 这样,单个页面的内容在实际写入之前并不实际复制。 这个优化使一个fork()
后面跟着一个exec()
子成本便宜得多:在调用exec()
之前,孩子可能只需要复制一个页面(它的栈的当前页面exec()
。
事实certificate,当进程拥有几千兆字节的可写RAM时,所有这些COW页面错误都不是很便宜。 即使孩子早就称为exec(),他们都会犯一次错误。 因为fork()的子程序不再允许为单线程的情况分配内存(你可以感谢那个苹果),现在安排调用vfork()/ exec()并不是那么困难。
vfork()/ exec()模型的真正优点是你可以用任意的当前目录,任意的环境variables,任意的fs句柄(不只是stdin / stdout / stderr),任意的信号掩码,一些任意的共享内存(使用共享内存系统调用),而没有二十个参数的CreateProcess()API,每隔几年就会有更多的参数。
事实certificate,“哎呀我泄漏处理被打开的另一个线程”从线程早期失意是可修复的用户空间没有进程范围locking感谢/ proc。 没有新的操作系统版本的巨大的CreateProcess()模型也不会出现这种情况,并说服每个人调用新的API。
所以你有它。 devise事故比直接devise的解决scheme要好得多。
由exec()等创build的进程将从父进程(包括stdin,stdout,stderr)inheritance它的文件句柄。 如果父进程在调用fork()之后但在调用exec()之前更改这些,则可以控制subprocess的标准stream。