可以在不继承父进程虚拟内存空间的情况下分叉一个进程吗?

11

由于父进程使用了大量内存,在某些内核 overcommit 策略的配置下,fork 可能会因为 ENOMEM 的错误码而失败。即使孩子进程只是像 ls 这样占用很少内存的程序。

为了澄清这个问题,当 /proc/sys/vm/overcommit_memory 被配置为 2 时,(虚拟) 内存的分配被限制为 SWAP + MEMORY * ratio(默认为50%)。 当一个进程进行 fork 操作时,虚拟内存并没有被复制,这得益于 COW 技术。但内核仍然需要分配虚拟内存空间。类比一下,fork 就像是 malloc(virtual memory space size),它不会分配物理内存,写入共享内存区域时,将导致虚拟内存和物理内存的复制。当 overcommit_memory 被配置为 2 时,由于分配虚拟内存空间,fork 可能会失败。

在以下情况下,是否有可能在不继承父进程的虚拟内存空间的情况下进行 fork

  1. 如果子进程在 fork 后调用 exec

  2. 如果子进程不调用 exec,也不使用来自父进程的全局或静态变量。例如,子进程只是做一些日志记录然后退出。


我并不是很理解,这不是共享虚拟内存写时复制吗?因此,任何额外的内存实际上都是私有的子进程。如果不共享虚拟内存,难道不会加剧问题吗? - trojanfoe
2
@trojanfoe 当一个进程分叉时,由于COW的存在,虚拟内存并不会被复制。但是内核仍然需要分配虚拟内存空间。类比一下,fork就像是malloc(虚拟内存空间大小),它不会分配物理内存,而写入共享内存将导致虚拟内存和物理内存的复制。当/proc/sys/vm/overcommit_memory为2时,内存分配被限制在SWAP+MEMORY*ratio。因此,fork可能会因为ENOMEM而失败。 - tianyapiaozi
5个回答

9
正如Basile Starynkevitch所回答的,这是不可能的。
然而,有一种非常简单和常见的解决方案,不依赖于Linux特定的行为或内存超额控制:使用一个早期派生的从进程来进行fork和exec。
让大型父进程尽早创建一个Unix域套接字,并fork一个从进程,在从进程中关闭所有其他描述符(重新打开STDIN_FILENO、STDOUT_FILENO和STDERR_FILENO到/dev/null)。我更喜欢数据报套接字,因为它简单且保证可靠,尽管流套接字也可以工作。
在一些罕见的情况下,有用的是让从进程执行一个单独的专用小助手程序。在大多数情况下,这是不必要的,并且使安全设计更加容易。(在Linux中,当使用Unix域套接字传递数据时,您可以包含SCM_CREDENTIALS辅助消息,并在其中使用进程ID来验证对等方正在使用的标识/可执行文件/proc/PID/exe伪文件。)
无论如何,从套接字读取时,从属进程将被阻塞。当另一端关闭套接字时,读/接收将返回0,并且从属进程将退出。
从属进程接收到的每个数据报都描述了要执行的命令。(使用数据报允许使用C字符串,用NUL字符分隔,无需进行任何转义等;使用Unix流套接字通常需要以某种方式定界“命令”,这反过来意味着需要在命令组件字符串中转义定界符。)
从属进程创建一个或多个管道,并fork出一个子进程。该子进程关闭原始的Unix套接字,将标准流替换为相应的管道端(关闭其他端),并执行所需的命令。我个人喜欢在Linux上使用额外的close-on-exec套接字来检测成功执行;在错误情况下,errno代码将写入套接字,以便从属-父进程可以可靠地检测故障和确切原因。如果成功,从属-父进程将关闭不必要的管道端,回复原始进程关于成功的信息,并使用SCM_RIGHTS附加数据作为其他管道端。发送消息后,它关闭其余的管道端,并等待新消息。
在原始进程方面,上述过程是顺序的;每次只能有一个线程开始执行外部进程。(您只需使用互斥锁对访问进行序列化。)可以同时运行多个线程;只有与从属助手的请求和响应是序列化的,如果这是一个问题--在典型情况下不应该是--您可以例如通过在每个消息前缀中添加ID号(由父进程分配,单调递增)来复用连接。在这种情况下,您可能会在父端使用专用线程来管理与从属的通信,因为您肯定不能同时有多个线程从同一套接字读取,并期望确定性结果。改进计划的进一步改进包括使用专用进程组执行进程、通过设置从属进程的限制来设置限制,并使用特权从属将命令作为专用用户和组执行。
特权从属案例是最有用的父进程为其执行单独的辅助程序。在Linux中,双方都可以使用通过Unix域套接字的SCM_CREDENTIALS辅助消息来验证对等方的身份(PID,并且具有ID,可执行文件),从而使实现强大的安全性变得相当简单。(但请注意,必须检查/proc/PID/exe不止一次,以捕获恶意程序发送消息的攻击,该程序快速执行适当的程序,但使用命令行参数导致它很快退出,偶尔看起来像正确的可执行文件发出了请求,而描述符的副本——因此整个通信通道——由邪恶的用户控制。)
总之,原始问题可以解决,尽管所提问的答案是否定的。如果执行对安全性敏感,例如更改特权(用户账户)或能力(在Linux中),则必须仔细考虑设计,但在正常情况下,实现相当简单。
如有必要,我很乐意进一步阐述。

8
不,这是不可能的。你可能会对vfork(2)感兴趣,但我不建议使用。还可以了解mmap(2)及其MAP_NORESERVE标志。但内核使用写时复制技术,因此实际上不会使RAM消耗翻倍。
我的建议是要有足够的交换空间,以免受到这样的问题困扰。因此,设置您的计算机具有比最大运行进程更多的可用交换空间。您可以随时创建一些临时交换文件(例如使用“dd if=/dev/zero of=/var/tmp/swapfile bs=1M count=32768”然后“mkswap /var/tmp/swapfile”),然后将其添加为临时交换区域(“swapon /var/tmp/swapfile”)并在不再需要时将其删除(“swapoff /var/tmp/swapfile”和“rm /var/tmp/swapfile”)。
您可能不想经常在像“/tmp/”这样的tmpfs文件系统上进行交换,因为tmpfs文件系统由交换空间支持!
我不喜欢内存过度提交,所以我通过proc(5)禁用了它。个人经验可能会有所不同。

1
不要使用vfork()。它存在一些严重问题,足以使其基本上无法正常工作。在Linux上,它只会阻塞调用的线程,因此在多线程进程中,将会有两个不同的进程在同一地址空间中运行。如果子进程在运行时接收到信号,它可能会破坏父进程。一些列在https://ewontfix.com/7/上的Linux实现问题已得到解决,例如与setuid进程的竞争条件,但由于另一个进程在同一地址空间中运行而产生的根本问题仍然存在。 - Andrew Henle
页表开销为每2MB的堆连续时4KB,而且vfork()没有问题,这使得/bin/sh和许多重要的东西运行得非常快。我知道并不是每个人都同意Linux的设计;他们可能想考虑使用Windows NT,因为微软分享他们的观点。 - Justine Tunney
交换文件和更多的可用内存并不能帮助解决例如32位应用程序在VSS=4G崩溃的问题。 - Hi-Angel
如果出现32位Linux应用程序(使用VSS = 4G崩溃)需要从源代码重新编译,然后作为64位应用程序进行调试的情况,则需要阅读[ GCC ](https://gcc.gnu.org/)和[ GDB ](https://www.gnu.org/software/gdb/)的文档。 - Basile Starynkevitch

5

我不知道有什么方法可以实现(2),但是对于(1)你可以尝试使用vfork,它会在不复制父进程的页面表的情况下fork一个新进程。但通常情况下不建议这样做,原因有很多,包括它会导致父进程阻塞直到子进程执行一个execve或终止。


1
它会导致父进程阻塞,直到子进程执行execve或终止。但这并不总是正确的 - 在Linux上只有调用的线程被阻塞。这可能更糟糕,因为这意味着两个不同的进程在同一地址空间中同时运行。 - Andrew Henle
您是正确的 @AndrewHenle。请注意,将 fork 与线程组合使用是危险的,最好避免使用。 - Nik Bougalis

2
这在Linux上是可行的。使用CLONE_THREAD标志而不是CLONE_VM标志调用clone系统调用。父进程和子进程将使用相同的映射,就像线程一样;没有COW或页面表复制。"最初的回答"

1
madvise(addr, size, MADV_DONTFORK)

另外,您可以在fork()之后调用munmap()来删除从父进程继承的虚拟地址。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接