11

由于父进程正在使用大量内存,因此在某些内核过度使用策略配置下可能fork会失败。即使子进程可能只是像 ls 这样的低内存消耗程序。errnoENOMEMexec

为了澄清问题,当 /proc/sys/vm/overcommit_memory 配置为 2 时,(虚拟)内存的分配限制为SWAP + MEMORY * ration(default to 50%). 当一个进程分叉时,由于COW,虚拟内存不会被复制。但是内核仍然需要分配虚拟内存空间。打个比方,fork 就像 malloc(virtual memory space size),不会分配物理内存,写入共享内存会导致复制虚拟内存,分配物理内存。当 overcommit_memory 配置为 2 时,fork 可能会因为虚拟内存空间分配而失败。

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

  1. 如果子进程调用execfork

  2. 如果子进程没有调用exec并且不会使用父进程中的任何全局或静态变量。例如,子进程只是做一些记录然后退出。

4

5 回答 5

8

不,这是不可能的。您可能对我不推荐的vfork(2)感兴趣。还要查看mmap(2)及其MAP_NORESERVE标志。但是内核使用了写时复制技术,因此您实际上不会使 RAM 消耗翻倍。

我的建议是要有足够的交换空间,以免被这样的问题所担心。因此,将您的计算机设置为比最大的运行进程拥有更多的可用交换空间。您总是可以创建一些临时交换文件(例如使用dd if=/dev/zero of=/var/tmp/swapfile bs=1M count=32768then mkswap /var/tmp/swapfile),然后将其添加为临时交换区域 ( ) 并在不再需要时swapon /var/tmp/swapfile将其删除 ( swapoff /var/tmp/swapfileand )。rm /var/tmp/swapfile

您可能不想像往常一样在tmpfs文件系统上进行交换/tmp/,因为tmpfs文件系统由交换空间备份!

我不喜欢内存过度使用并禁用它(通过proc(5))。YMMV。

于 2015-07-24T07:19:15.940 回答
8

正如 Basile Starynkevitch回答的那样,这是不可能的。

然而,有一个非常简单和常用的解决方案,它不依赖于 Linux 特定的行为或内存过度使用控制:使用早期分叉的从属进程执行 fork 和 exec。

让大型父进程创建一个 unix 域套接字并尽早派生一个从属进程,关闭从属进程中的所有其他描述符(重新打开STDIN_FILENOSTDOUT_FILENOSTDERR_FILENOto /dev/null)。我更喜欢数据报套接字,因为它的简单性和保证性,尽管流套接字也可以工作。

在极少数情况下,让从属进程执行单独的专用小型帮助程序很有用。在大多数情况下,这不是必需的,并且使安全设计更容易。(在 Linux 中,您可以在使用 Unix 域套接字传递数据时包含SCM_CREDENTIALS/proc/PID/exe辅助消息,并使用其中的进程 ID 来验证对等方正在使用伪文件的身份/可执行文件。)

在任何情况下,从属进程都会阻塞从套接字读取。当另一端关闭socket时,read/receive会返回0,slave进程退出。

从进程接收到的每个数据报都描述了一个要执行的命令。(使用数据报允许使用 C 字符串,用 NUL 字符分隔,没有任何转义等;使用 Unix 流套接字通常需要您以某种方式分隔“命令”,这反过来意味着转义命令组件字符串中的分隔符。)

从进程创建一个或多个管道,并派生一个子进程。这个子进程关闭原始的 Unix 套接字,用各自的管道端替换标准流(关闭另一端),并执行所需的命令。我个人更喜欢在 Linux 中使用额外的 close-on-exec 套接字来检测成功执行;在错误情况下,errno 代码被写入套接字,以便从属父节点也可以可靠地检测到故障和确切原因。如果成功,slave-parent 关闭不必要的管道端,回复原始进程关于成功,另一个管道作为SCM_RIGHTS辅助数据结束。发送消息后,它关闭其余的管道端,并等待新消息。

在原始流程方面,上述流程是顺序的;一次只能执行一个线程开始执行外部进程。(您只需使用互斥锁序列化访问。)几个可以同时运行;只有对从属助手的请求和响应才被序列化。

如果这是一个问题——在典型情况下不应该——例如,您可以通过在每条消息前加上一个 ID 号(由父进程分配,单调递增)来多路复用连接。在这种情况下,您可能会在父端使用专用线程来管理与从属端的通信,因为您当然不能同时从同一个套接字读取多个线程,并期望得到确定的结果。

对该方案的进一步改进包括为执行的进程使用专用进程组、对其设置限制(通过设置从属进程的限制)以及通过使用特权从属进程作为专用用户和组执行命令。

特权从属情况是让父级为其执行单独的辅助进程最有用的地方。在 Linux 中,双方都可以SCM_CREDENTIALS通过 Unix 域套接字使用辅助消息来验证对等方的身份(PID,以及 ID,可执行文件),这使得实现强大的安全性变得相当简单。(但请注意,/proc/PID/exe必须多次检查,以捕捉恶意程序发送消息的攻击,快速执行适当的程序但使用导致它很快退出的命令行参数,使其偶尔看起来像正确的可执行文件发出了请求,而描述符的副本——以及整个通信通道——被恶意用户控制。)

总之,可以解决最初的问题,尽管对提出的问题的回答是否定的。如果执行是安全敏感的,例如更改权限(用户帐户)或功能(在 Linux 中),那么设计必须谨慎考虑过,但在正常情况下,实现非常简单。

如有必要,我很乐意详细说明。

于 2015-07-31T00:29:41.397 回答
5

我不知道有什么方法可以做(2),但是对于(1),您可以尝试使用vforkwhich 将分叉一个新进程而不复制父进程的页表。但是出于多种原因,通常不建议这样做,包括因为它会导致父级阻塞,直到子级执行execve或终止。

于 2015-07-24T07:22:35.363 回答
2

这在 Linux 上是可能的。使用不带标志和带标志的clone系统调用。父进程和子进程将使用相同的映射,就像线程一样;没有 COW 或页表复制。CLONE_THREADCLONE_VM

于 2019-03-30T07:39:04.533 回答
0
madvise(addr, size, MADV_DONTFORK)

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

于 2019-07-09T11:46:44.420 回答