时间浪费execv()和fork()

时间:2016-10-05 15:00:20

标签: linux unix operating-system multiprocessing 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()将当前地址空间替换为新程序的地址空间。考虑到这一点,使用这种组合效率是否非常低效?我们正在复制进程的整个地址空间,然后立即覆盖它。

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

6 个答案:

答案 0 :(得分:45)

  

使用这个组合(而不是其他一些解决方案)所带来的好处是什么,即使我们有浪费,人们仍然会使用它?

你必须以某种方式创建一个新进程。用户空间程序很少有方法可以实现这一点。 POSIX曾经有vfork() alognside fork(),有些系统可能有自己的机制,例如特定于Linux的clone(),但自2008年以来,POSIX仅指定fork()posix_spawn()家庭。 fork + exec路线更为传统,易于理解且缺点较少(见下文)。 posix_spawn系列设计为特殊用途替代品,用于在fork()出现问题的环境中使用;你可以在&#34;理由&#34;中找到详细信息。 its specification的一部分。

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

  

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

(强调补充)

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

答案 1 :(得分:24)

另一个答案说明:

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

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

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

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

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

这种情况非常痛苦,不仅仅是针对UNIX,硬件的页面错误处理也很快变得“可重放”。

答案 2 :(得分:22)

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

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

立即
几乎立即通过在子进程中调用fork()来跟随对exec()的调用,该进程用新程序替换子进程的内存。例如,这就是shell通常所做的事情。在这种情况下,复制父地址空间所花费的时间在很大程度上被浪费了,因为子进程在调用exec()之前将使用很少的内存。

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

答案 3 :(得分:2)

事实证明,当进程有几千兆字节的可写RAM时,所有这些COW页面错误都不便宜。即使孩子早已打电话exec(),他们也会犯错一次。因为fork()的孩子不再被允许分配内存,即使是单线程的情况(你可以感谢Apple的那个),现在安排调用vfork()/exec()并不困难。

vfork()/exec()模型的真正优势是你可以使用任意当前目录,任意环境变量和任意fs句柄(不仅仅是stdin/stdout/stderr),一个任意信号掩码来设置子项,和一些任意共享内存(使用共享内存系统调用),没有二十个参数CreateProcess() API,每隔几年就会获得更多的参数。

事实证明,“我泄漏的句柄被另一个线程打开了”从线程早期开始的失误可以通过/proc在用户空间中解决,而无需在进程范围内锁定。如果没有新的操作系统版本,那么同样不会出现在巨型CreateProcess()模型中,并且说服所有人调用新的API。

所以你有它。设计事故最终比直接设计的解决方案好得多。

答案 4 :(得分:1)

exec()等创建的进程将从父进程(包括stdin,stdout,stderr)继承其文件句柄。如果父级在调用fork()之后但在调用exec()之前更改了这些,那么它可以控制子级的标准流。

答案 5 :(得分:1)

它并不昂贵(相对于直接生成一个进程),尤其是对于像在Linux中发现的写时复制fork来说,它的优点是:

  1. 当您真的只想分派当前流程的副本时(我发现这对于测试非常有用)
  2. 当您需要在加载新的可执行文件之前执行某些操作时 (重定向文件描述符,使用信号掩码/处置,uid等)

POSIX现在具有posix_spawn,可以有效地使您合并fork / and-exec(可能比fork + exec更有效;如果效率更高,通常会通过一些便宜但不那么健壮的fork(clone / vfork)和exec来实现,但是它实现#2的方式是通过大量相对混乱的选项,这些选项永远无法做到那么完整和强大整洁,就像允许您在加载新过程映像之前运行任意代码一样。