为什么在创建子进程后立即调用exec()或exit()时使用vfork()?


11

操作系统概念和APUE说

通过vfork(),父进程被挂起,子进程使用父进程的地址空间。由于vfork()不使用写时复制,因此,如果子进程更改了父级地址空间的任何页面,则父级恢复后更改后的页面将对父级可见。因此,必须谨慎使用vfork(),以确保子进程不会修改父进程的地址空间。

vfork()旨在在子进程创建后立即调用exec()或exit()时使用。

我如何理解最后一句话?

当通过vfork()调用创建的子进程时exec(),是否exec()通过加载新程序来修改父进程的地址空间?

当通过vfork()调用创建子进程时,终止子进程时exit()是否 exit()不修改父进程的地址空间?

我偏爱Linux。

谢谢。

Answers:


15

当通过vfork()调用创建的子进程时exec(),是否exec()通过加载新程序来修改父进程的地址空间?

不,exec()为新程序提供新的地址空间;它不会修改父地址空间。例如exec参见POSIX中功能的讨论Linux execve()联机帮助页

当由vfork()创建的子进程调用exit()时,exit()在终止子进程时是否不会修改父进程的地址空间?

Plain exit()可能会运行由运行程序(包括其库)安装的出口挂钩。vfork()更严格;因此,在Linux上,它要求的使用_exit()调用C库的清理功能。

vfork()事实证明,很难做到正确;在POSIX标准的当前版本中已将其删除,posix_spawn()应改为使用。

但是,除非你真的知道你在做什么,你应该没有任何使用vfork()posix_spawn(); 坚持好老fork()和好exec()

上面链接的Linux手册页提供了更多上下文:

但是,在过去的糟糕日子里fork(2) ,通常需要不必要地制作呼叫者数据空间的完整副本,因为通常在此之后立即exec(3)完成。因此,为了提高效率,BSD引入了vfork() 系统调用,该系统调用没有完全复制父进程的地址空间,而是借用了父进程的内存和控制线程,直到execve(2)发生了对调用或退出的调用。当孩子使用其资源时,父进程被暂停。的使用 vfork()非常棘手:例如,在父进程中不修改数据取决于知道哪些变量保存在寄存器中。


谢谢。“ exec()为新程序提供新的地址空间;” exec()的正常行为是将程序加载到进程的地址空间吗?我没有在两个链接中找到正常或特别为vfork()创建新地址空间的地方。
蒂姆(Tim)

1
有趣的是vfork()现在正在赢得其他所有方面的胜利。当您有1 GB的可写内存时,它的速度比fork()快得多。
约书亚

2
请不要告诉人们使用posix_spawn。要使用正确的代码编写代码posix_spawn要比使用普通的代码难得多fork,而且如果尝试这样做,可能会遇到没有文件操作或属性来在fork和之间执行所需操作的障碍exec。而且不能保证它具有类似vfork的效率,因此它甚至不能解决人们希望它解决的问题。
zwol

1
@zwol:这真是个坏建议。虽然posix_spawn可能缺少您想要的功能(您可以通过中间辅助程序(使用C或inline-on-cmdline Shell脚本编写)解决此问题),但任何尝试实现所需功能的尝试都会vfork调用危险的未定义行为。的规范vfork不允许调用随机函数设置子项在之前继承的状态execve,并且尝试这样做可能会破坏父项的状态。
R .. GitHub停止帮助ICE,

1
@Joshua:的现代实现与大多数情况下的posix_spawn表现大致相同vfork。存在差异的情况通常恰恰vfork是高度不安全的情况:安装了posix_spawn必须禁止在exec之前在子级中运行的信号处理程序的情况。
R .. GitHub停止帮助ICE,

4

当您调用时vfork(),将创建一个新进程,并且该新进程借用父进程的进程映像(堆栈除外)。子进程被赋予了自己的新堆栈星,但是不允许return从调用的函数vfork()

子进程运行时,父进程被阻塞,因为子进程借用了父进程的地址空间。

无论您做什么,仅访问堆栈的所有内容都只会修改孩子的私有堆栈。但是,如果您修改全局数据,则这将修改公共数据,从而也影响父级。

修改全局数据的事物包括:

  • 调用malloc()或free()

  • 使用stdio

  • 修改信号设置

  • 修改不是在调用函数本地的变量vfork()

  • ...

一旦调用_exit()(重要,从不调用exit()),子级将终止,并将控制权交还给父级。

如果您从exec*()系列中调用任何函数,则会使用新的程序代码,新的数据以及来自父级的堆栈的一部分来创建新的地址空间(请参见下文)。一旦准备就绪,子代便不再从子代借用地址空间,而是使用自己的地址空间。

该控件被交还给父级,因为它的地址空间不再被另一个进程使用。

重要说明:在Linux上,没有实际的vfork()实现。Linux而是vfork()基于fork()SunOS-4.0在1988年提出的“ 写时复制” 概念实现的。为了使用户相信他们使用vfork(),Linux只是设置共享数据并挂起父级,而子级没有调用_exit()或其中一个exec*()功能。

因此,Linux不能受益于Real vfork()不需要为内核中的子代建立地址空间描述的事实。这导致的vfork()速度不比快fork()。在实现实数的系统上vfork(),它的速度通常比fork()使用vfork()- ksh93,最新的Bourne Shellcsh

永远不要exit()vfork()ed孩子exit()那里打电话的原因是,如果从打电话之前开始就没有数据丢失,则刷新stdio vfork()。这可能会导致奇怪的结果。

BTW:posix_spawn()是在之上实现的vfork(),因此vfork()不会从OS中删除。已经提到Linux不vfork()用于posix_spawn()

对于堆栈,几乎没有文档,这是Solaris手册页所说的内容:

 The vfork() and vforkx() functions can normally be used  the
 same  way  as  fork() and forkx(), respectively. The calling
 procedure, however, should not return while running  in  the
 child's  context,  since the eventual return from vfork() or
 vforkx() in the parent would be to a  stack  frame  that  no
 longer  exists. 

因此,实现可以做任何喜欢的事情。Solaris实现使用共享内存作为函数调用的堆栈帧vfork()。没有实现可从父级授予对堆栈较旧部分的访问权限。


4
GNU C库和musl C库都不在posix_spawn()Linux之上实现vfork()。他们俩都在之上实现了它__clone()
JdeBP '18年

1
@JdeBP:你知道vfork()打来电话clone()对不对?从字面上看,它是内核中的单线。
约书亚州

1
“重要:在Linux上,没有真正的vfork()实现。” <-这是不正确的,并且至少十年来一直不正确。如果您的Shell基准测试未观察到Linux vforkforkLinux 之间的任何性能差异,则说明存在错误。
zwol

1
答案的后半部分以“重要:在Linux上,没有真正的vfork()实现”开头,这在很大程度上或完全是错误的。
R .. GitHub停止帮助ICE,

1
请不要在未经验证的情况下提出索赔。当前的Bourne Shell可以在有无vfork支持的情况下进行编译,因此,即使您认为Linux的调试功能无法提供可靠的结果,您也可以在外壳中与vfork配合使用vfork,并比较配置调用的执行时间。我使用800个测试的配置脚本。在Solaris上,与fork情况相比,使用vfork的Bourne Shell总共需要减少30%的系统cpu时间。在Linux上,相同的测试可使系统cpu时间减少不到10%。在Solaris上不是3x,因为其中包含许多编译器调用。
schily
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.