超越 fork() + exec():Linux 内核进程创建的未来
- #Linux内核
- #进程创建
- #posix_spawn
- #Hacker News
- #lwn.net
自 Unix 诞生之初,两个核心的面向进程的系统调用一直是 fork()(创建父进程副本的子进程)和 exec()(用新程序替换当前进程)。在 Linux 内核中,这些系统调用更广为人知的名字是 clone() 和 execve(),但核心功能保持不变。虽然这种进程创建模型有其优雅之处,但也存在不足。Li Chen 最近提出的在内核中添加“spawn templates”的提案,虽然其当前形式不会被接受,但可能为未来新的进程创建原语指明方向。
fork() 是一个相对昂贵的系统调用;它必须为子进程复制整个进程状态(包括内存)。多年来虽然有很多优化,但 fork 本质上仍是一项代价高昂的操作。更糟糕的是,fork() 调用之后通常紧跟着 exec(),而 exec() 会丢弃刚刚为子进程精心复制的所有内存。多年来人们尝试过优化(如 vfork()),但该模式仍然比本可以实现的更昂贵。
Spawn 模板
Chen 的补丁集采用了一种有趣的方法来优化 fork() 和 exec() 模式。它专注于重复启动同一可执行文件的应用程序;例如,一个程序需要反复运行 Git 来获取仓库内容信息。在这种情况下,程序可以建立一个模板来加速这些调用,将设置成本分摊到多次操作中。该模板通过 spawn_template_create() 系统调用创建:
struct spawn_template_create_args {
__aligned_u64 flags;
__s32 execfd;
__u32 exec_flags;
__aligned_u64 filename;
/* Some fields elided */
};
int spawn_template_create(struct spawn_template_create_args *args, size_t args_size);
这个调用会返回一个代表可执行文件模板的文件描述符,可以通过文件描述符(execfd)或绝对路径(filename)指定,但不能同时指定。创建模板时,内核会打开指定文件并缓存一系列信息,使进程将来能更快地运行该文件。
应用程序可能多次运行同一可执行文件,但每次调用在多个方面有所不同。特定调用的详细信息必须放入以下结构的实例中:
struct spawn_template_spawn_args {
__aligned_u64 flags;
__aligned_u64 pidfd;
__aligned_u64 argv;
__aligned_u64 envp;
__aligned_u64 actions;
__aligned_u64 actions_len;
__aligned_u64 reserved[4];
};
argv 字段是指向传递给程序的参数列表的指针,envp 指向环境变量。文件描述符和信号处理的更改则通过 actions 传递,它指向一个结构体数组:
struct spawn_template_action {
__u32 type;
__u32 flags;
__s32 fd;
__s32 newfd;
__aligned_u64 arg;
};
例如,如果要在子进程中关闭文件描述符 4,相关的 spawn_template_action 结构体应将 type 设置为 SPAWN_TEMPLATE_ACTION_CLOSE,fd 设置为 4。还有其他用于复制文件描述符、打开文件、更改工作目录和更改信号处理的操作。
填充好 spawn_template_spawn_args 结构体后,可以通过以下调用运行新进程:
int spawn_template_spawn(int template_fd, struct spawn_template_spawn_args *args, int args_size);
内部实现上,该系统调用遵循接近常规 fork()/exec() 的路径。Chen 特别指出,执行新文件时应用的所有常规检查仍然保留。但模板中缓存的信息使整个过程比以前更快。究竟快多少?封面信中提供的基准测试结果显示大约提升了 2%,这看起来不多,但对于符合预期模式的应用来说可能会产生影响。
走向 posix_spawn()
Mateusz Guzik 发布了最详细的审查,他说:“这个问题对我来说非常重要,我已经断断续续思考了很长时间。整个 fork + exec 模式很糟糕,需要被淘汰。”他指出,补丁集的焦点有点奇怪,因为它没有触及问题中的 fork() 部分。他说,大部分开销都在那里,因此优化工作应致力于将其剔除。与其复制当前进程,“创建干净进程才是正确方向”。
Christian Brauner 对这一目标表示赞同,他说:“为 exec 构建一个 builder API 的想法并非完全疯狂。”不过,他的建议是,新的 API 应构建在现有的 pidfd 抽象之上。在不深入细节的情况下,他说正确的方法是给 pidfd_open() 添加一个选项来创建一个空进程。然后通过一系列对新系统调用 pidfd_config() 的调用来按需配置这个新进程,设置其环境、要执行的映像等。因此 pidfd_config() 将类似于 fsconfig()。
Brauner 说,新接口的一个重要目标是能够支持在用户空间实现 posix_spawn()。posix_spawn() 非常适合替代 fork()/exec() 模式;开发者可能会欢迎一个不像当前实现那样在幕后隐藏 fork() 和 exec() 的原生实现。Chen 同意 Brauner 大致勾勒的 API 似乎更好,并表示未来的工作将朝着这个方向进行。因此,Linux 内核中不会有 spawn templates,但如果 Chen 未来的工作得以实现,Linux 最终可能会获得一个真正的 posix_spawn() 实现。
评论