POSIX C 中 fork() 的更轻量级替代品?

Posted

技术标签:

【中文标题】POSIX C 中 fork() 的更轻量级替代品?【英文标题】:Lighter weight alternatives to fork() in POSIX C? 【发布时间】:2015-10-20 21:15:41 【问题描述】:

在我一直在阅读的手册页中,似乎 popen、system 等倾向于调用 fork()。反过来, fork() 复制进程的整个内存状态。这似乎真的很重,尤其是在许多情况下,调用 fork() 的子进程使用很少(如果有的话)为父进程分配的内存。

所以,我的问题是,我可以在不复制父进程的整个内存状态的情况下获得类似 fork() 的行为吗?还是我遗漏了一些东西,例如 fork() 并不像看起来那么重(例如,可能会优化调用以避免不必要的内存重复)?

【问题讨论】:

你已经知道了,不是吗?有写时复制。 你应该看看什么是 COW (copy-on-write) ***.com/questions/13813636/… Is it possible to fork a process without inherit virtual memory space of parent process?的可能重复 【参考方案1】:

fork(2) 和所有syscalls 一样,从用户空间应用程序的角度来看,是一个原始操作(但一些C 库使用clone(2))。从用户模式切换到内核模式主要是一条机器指令SYSCALLSYSENTER,然后(最新版本的)Linux 内核正在做相当重要的处理。

实际上它非常有效(例如,不到一毫秒,有时甚至不到十分之一),因为内核广泛使用惰性copy-on-write 技术在父进程和子进程之间共享页面。实际复制将在稍后在page faults 覆盖共享页面时发生。

而forking 有一个巨大的优势,因为其他一些程序的启动被委托给execve(2):概念上很简单:父进程和子进程之间的唯一区别是结果fork

顺便说一句,在 Linux 等 POSIX 系统上,fork(2) 或合适的 clone(2) 等效项是创建进程的唯一方法(您通常应该忽略一些奇怪的异常:内核正在创建一些进程,例如 /sbin/init 等...),因为 vfork(2) 已过时。

【讨论】:

太完美了,谢谢!我想它确实比我想象的更有效。 请注意,例如,这是正确的。当前的 Linux 以及许多其他现代内核,但谈论 内核 有点误导。那时引入vfork() 是有原因的(甚至在 POSIX 中也提到过),虽然它是一团糟,当然...... @FelixPalmen - 咬你的舌头! vfork() 很讨厌。 ;) 见这里:ewontfix.com/7 @AndrewHenle 我没有另外说。只是说 “内核” 并不是一个准确的描述,并且存在(或更好:曾经)系统的 fork() 开销 是一个问题。 在我的系统上,fork 需要 56 微秒的子节点和 37 微秒的父节点【参考方案2】:

问题在于,要运行标准链接的可执行文件的 main 函数,您需要调用execve,而 exec 会替换整个进程映像,因此您需要一个新的地址空间,这就是 fork 的用途.

您可以通过让您的 calee 在共享库中公开其 main 功能(但不能将其称为 main)来解决此问题,然后您可以使用 main 功能加载该函数而无需分叉(前提是没有符号冲突)。

这将是system 的更有效替代方案(基本上具有函数调用的效率)。 现在popen 涉及管道,要使用管道,您需要将管道末端置于不同的可调度单元中。使用相同地址空间的线程可以在这里用作单独进程的更轻量级替代方案。

【讨论】:

作为旁注,我认为拥有一个允许像在管道中连接 C/C++ 模块但没有分叉开销的系统会非常好。本质上,您需要确保没有符号冲突和文件描述符抽象,以便纯粹的进程内管道不需要联系内核。实际上,我一直在努力将类似的东西放在一起。【参考方案3】:

正如你提到的fork() 有点疯狂的系统调用,由于历史原因一直存在。有一篇很棒的文章介绍了它的缺陷here,还有this post 介绍了一些细节和潜在的解决方法。

尽管在 Linux 上 fork() 已针对内存使用写时复制进行了优化,但它仍然不是“免费”的,因为:

    它仍然需要做一些与内存相关的管理(新页表等) 如果您使用的是 RAII(例如在 C++ 或可能是 Rust 中),那么所有被复制的对象都将被清理两次。这甚至可能导致逻辑错误(例如两次删除临时文件)。 父进程可能会继续运行,可能会修改其大量内存,然后复制它。

替代方案似乎是:

vfork() clone() posix_spawn()

vfork() 是为执行fork() 然后execve() 运行程序的常见用例而创建的。 execve() 将当前进程的所有内存替换为一组新的内存,因此如果您要删除父进程的内存,则没有必要复制它。

所以vfork() 不会那样做。相反,它与父进程在相同的内存空间中运行并暂停它,直到它到达execve()vfork() 的 Linux 手册页说除了 vfork()execve() 之外的任何事情都是未定义的行为。

posix_spawn() 基本上是对vfork()execve() 的一个很好的包装。

clone() 类似于fork(),但允许您准确指定复制的内容(文件描述符、内存等)。它有很多选项,包括一个 (CLONE_VM),它允许子进程在与父进程相同的地址空间中运行,这非常疯狂!我想这是创建新进程的最轻量级的方法,因为它根本不涉及任何内存复制!

但在实践中,我认为在大多数情况下,您应该:

使用线程,或 使用posix_spawn()

(注意,我现在只是在研究这个;我不是专家,所以我可能会弄错一些东西。)

【讨论】:

您有点需要考虑提倡删除论文的来源。微软。当然也不是没有对此事有任何特殊兴趣。 没错,但是我已经阅读了这篇论文,它似乎是一个公平的技术评估。他们远不是唯一一个说fork() 有缺陷的人。 是的,同意,但让我印象深刻的是当前论点中的一个缺陷是作者无法指出一个本身没有缺陷的替代品。论点基本上是 1)“复制父环境进行初始化 - 不好”,2)“其他一些替代方法 - 好,但我们不能指出任何具有共识的替代方法,并且子节点的初始化没有其他缺点。”所以我的印象是,fork() 可以改进一些东西,并避免一些它强制的锁定,但目前没有一种万能的替代品。 其实我同意。在理想世界中,您可以通过指定函数及其参数来启动新进程。新进程将使用与当前进程相同的二进制文件(因此您不必弄乱路径和不可靠的argv[0]),而不是复制父进程的堆/堆栈(因此速度很快,没有逻辑错误)并且只有复制其参数的内存(因此您不必通过命令行参数传递所有内容)。不过,由于各种原因,最后一点会很棘手。我怀疑有人在做类似的事情,因为fork() 的作品和人们盲目地喜欢它。

以上是关于POSIX C 中 fork() 的更轻量级替代品?的主要内容,如果未能解决你的问题,请参考以下文章

POSIX 消息队列的替代方案

pcntl_fork 进程

C++ Fork Join 并行阻塞

PayPal Adaptive Payments 和 Stripe 的更便宜的替代品

Flutter插件包选择 ( 查看文档是否全面 | 查看插件包的更新版本次数 | 查看使用示例 | 查看 GitHub 项目的 Star Fork Issues )

支持参数的 EXEC() 的 C 替代方案?