fork()在执行exec()之前有什么用处?

6
在*nix系统中,通过使用fork()系统调用来创建进程。例如,init进程创建另一个进程。首先它复制自身并创建一个具有init上下文的进程。只有在调用exec()时,这个子进程才会变成一个新进程。那么为什么需要中间步骤(使用与父进程相同的上下文创建子进程)呢?难道这不是浪费时间和资源吗,因为我们正在创建一个上下文(消耗时间和浪费内存),然后再进行覆盖吗?
为什么不能实现为分配空闲内存区域,然后调用exec()?这样可以节省时间和资源,对吧?
4个回答

4
中间步骤使您能够在子进程中设置共享资源,而外部程序不知道。典型的例子是构建管道:
// read output of "ls"
// (error checking omitted for brevity)
int pipe_fd[2];
pipe(&pipe_fd);
if (fork() == 0) {       // child:
    close(pipe_fd[0]);   // we don't want to read from the pipe
    dup2(pipe_fd[1], 1); // redirect stdout to the write end of the pipe
    execlp("ls", "ls", (char *) NULL);
    _exit(127);          // in case exec fails
}
// parent:
close(pipe_fd[1]);
fp = fdopen(pipe_fd[0], "r");
while (!feof(fp)) {
    char line[256];
    fgets(line, sizeof line, fp);
    ...
}

请注意,将标准输出重定向到管道是在子进程中的forkexec之间完成的。当然,对于这种简单情况,可能会有一个生成API,只需给出适当的参数即可自动完成此操作。但是,fork()设计使得可以对子进程中的每个进程资源进行任意操作 - 可以关闭不需要的文件描述符,修改每个进程的限制,降低权限,操作信号掩码等等。没有fork(),用于生成进程的API将变得非常庞大或者不太有用。事实上,竞争操作系统的进程生成调用通常处于两者之间。
关于内存的浪费,可以使用写时复制技术避免。fork()不为子进程分配新的内存,而是将子进程指向父进程的内存,并在需要修改某一页时才复制该页。这使得fork()不仅高效利用内存,而且速度快,因为它只需要复制“目录”。

2
这是一个老问题。许多人都问过“为什么要先使用fork()?”通常他们会建议一种既可以从头创建新进程又可以在其中运行程序的操作,这个操作被称为类似于spawn()的东西。
他们总是说:“那样会更快,不是吗?”
实际上,除了Unix家族以外的所有系统都采用“spawn”的方式。只有Unix基于fork()exec()
但有趣的是,Unix始终比其他功能完备的系统快得多。它一直处理着更多的用户和负载。
而且Unix随着时间的推移变得更加快速。Fork()现在不再真正复制地址空间,而是使用一种称为写时复制的技术共享它。(一种非常古老的fork优化称为vfork()仍然存在。)
库拉饮料

0

我不确定在内核中 init 进程的分叉工作原理,但是回答你为什么需要调用 fork 然后再调用 exec 的问题很简单,因为一旦你执行了 exec,就无法回头了。

如果你查看 这里 的文档,它基本上需要生成一个新进程(fork 调用),以便父进程恢复控制并等待其完成或像守护程序一样保持静默。


0
只有在调用exec()时,这个子进程才会变成一个新进程。
实际上不是这样的。在fork之后,你已经有了一个新的进程,甚至与其父进程没有太大区别。有些情况下,在fork之后不需要执行exec。
那么为什么需要创建具有与父进程相同上下文的子进程这个中间步骤呢?
其中一个原因是,这是创建整个进程架构的有效方式。克隆通常比从头开始创建要简单得多。
这难道不是一种浪费时间和资源的行为吗?因为我们正在创建一个上下文(消耗时间和浪费内存),然后又重写它?

大多数资源都是虚拟的,因为使用了写时复制机制,所以这并不是浪费时间和资源。此外,声称创建的上下文被覆盖是不正确的。事实上,一开始什么也没有被写入,所以也就不存在重写的问题。这就是COW的全部意义。只有进程地址空间(代码、堆栈)被替换,而不是被覆盖。许多进程上下文部分或全部被保留,包括环境、文件描述符、优先级、忽略的信号、当前和根目录、限制、各种掩码、处理器绑定、特权和其他一些与进程地址空间无关的东西。


克隆可能比从头开始创建要简单,但如果我们分叉的目的是运行不同的程序,那么在分叉后我们必须执行exec。这个exec将吹掉所有克隆所花费的努力,所以OP提出的问题是,我们为什么要在第一次克隆时费心去克隆呢? - user4815162342
@user4815162342 请仔细阅读我的回复。唯一使用exec替换的是进程地址空间,因为它是写时复制,所以几乎不需要任何努力来克隆。真正重要的内核侧进程上下文会在exec中被保留。没有浪费任何努力。 - jlliagre
好的,我忽略了你的最后一段。我仍然对“几乎没有任何努力”的部分感到好奇。在同一操作系统上比较分叉和无分叉进程创建将是有趣的。 - user4815162342
我相信fork系统调用总是比posix_spawn的exec或者更快。此外,posix_spawn很可能通过共享大部分或全部exec代码来实现,并且可能使用fork代码的一部分来避免维护两个执行相同操作的代码路径。只有在虚拟内存资源本身受限的情况下,即在交换区太小的系统上,posix_spawn确实会产生差异。 - jlliagre
流行的Unix系统实现了posix_spawn以fork+exec为基础,因此对两者进行比较是没有意义的。但它不必一定采用这种方式实现。vfork的存在表明,至少在历史上,fork有时被认为是有问题的。 - user4815162342
开发者确实不必以那种方式实现,但是重新实现可能提供的性能增益(如果有的话)可能不值得付出努力和风险。没有太多意义修复没有问题且正常工作的事情。顺便说一下,vfork通常不是一个全新的实现,而只是一个系统调用入口,它与fork共享大部分相同的底层内核代码。 - jlliagre

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