别再折腾 fork 了:在 Linux 上更快地创建大进程

💡 原文英文,约900词,阅读约需4分钟。
📝

内容提要

文章讨论 Linux 创建大进程时 fork() 因复制页表而变慢的问题。建议:启动新程序时用 posix_spawn() 替代 fork(),必要时加 POSIX_SPAWN_USEVFORK 标志,以避免复制开销和 vfork() 在多线程下的死锁;若需多进程并行处理大任务,则用进程池预先创建进程,但进程池不适合快速启动外部程序。

🔎

延伸解读

为什么 fork 会变慢

文章指出,即使 fork 采用写时复制(COW),仍需复制页表等数据。当父进程因收集信息而内存膨胀时,复制开销变得不可忽视,导致启动新程序越来越慢。这解释了在模糊测试等场景中,fork 成为性能瓶颈的原因。

进程池的适用与局限

进程池在程序初始化时预先创建进程,避免后续 fork 大进程的开销,适合多进程并行处理大任务。但文章强调,进程池不适合快速启动外部程序,因为一旦子进程调用 exec 替换为新任务,就无法回收再利用,且池大小固定可能限制并发。

vfork 的风险与 posix_spawn 的折中

vfork 不复制父进程地址空间,但多线程下可能因动态链接器锁导致死锁。posix_spawn 在 Linux 上默认用 fork+exec 实现,但可加 POSIX_SPAWN_USEVFORK 标志,在安全时使用 vfork 避免复制页表,同时由系统规避死锁。这是文章推荐的替代方案。

❓

Q&A

为什么在 Linux 上 fork() 创建大进程时会变慢?

因为 fork() 即使使用写时复制(COW)语义,仍然需要复制父进程的页表。页表记录了内存地址是否被修改,当进程内存膨胀时,复制页表的开销会变得非常显著,成为性能瓶颈。

启动新程序时,用什么替代 fork() 可以避免复制页表的开销?

可以使用 posix_spawn() 替代 fork()。在 Linux 上,posix_spawn() 底层通常由 fork() 和 exec() 实现,但若安全则会使用 vfork()。通过添加 POSIX_SPAWN_USEVFORK 标志,可以避免复制页表的开销,同时 Linux 会保护你免受 vfork() 可能引发的死锁问题。

vfork() 在多线程程序中可能导致什么问题?

vfork() 可能导致死锁。当子进程调用外部函数(如 exec())时,动态链接器 ld.so.1 可能需要解析符号并获取互斥锁,而该锁可能已被父进程中的其他线程持有。由于父进程在子进程调用 exec() 或退出前一直挂起,双方会互相等待,造成死锁。

进程池适合用来快速启动外部程序吗?

不适合。进程池适合多任务并行处理,尤其是多进程程序的初始化。但一旦进程被 exec() 替换为全新任务,就无法再回收进池中,导致进程永久丢失。因此进程池不应被用来快速启动其他进程。

如果需要同时运行多个进程处理大任务,应该采用什么方案?

应该使用进程池(Pool)。在程序初始化时预先创建若干空闲进程,之后将任务分配给这些进程执行,执行完毕后回收再利用。这样可以避免在进程内存膨胀后频繁 fork 带来的页表复制开销。

posix_spawn() 在 Linux 上是如何实现的?

在 Linux 上,posix_spawn() 通常由 fork() 和 exec() 实现。如果安全,它会使用 vfork() 代替 fork()。通过 POSIX_SPAWN_USEVFORK 标志,可以避免复制页表的开销,同时 Linux 会防止 vfork() 在多线程下可能引发的死锁。

🏷️

标签

➡️

继续阅读