【问题标题】:What is the use of fork() - ing before exec()?在 exec() 之前使用 fork() - ing 有什么用?
【发布时间】:2013-04-04 17:18:01
【问题描述】:

在 *nix 系统中,进程是通过使用 fork() 系统调用创建的。考虑例如,init 进程创建另一个进程。首先它分叉自己并创建一个具有类似 init 上下文的进程。只有在调用 exec() 时,这个子进程才会变成一个新进程。那么为什么需要中间步骤(创建一个与父级具有相同上下文的子级)?这不是浪费时间和资源吗,因为我们正在创建一个上下文(消耗时间和内存)然后重写它?

为什么这不是分配一个空的内存区域然后调用exec()?这会节省时间和资源,对吧?

【问题讨论】:

    标签: c unix process


    【解决方案1】:

    中间步骤使您能够在子进程中设置共享资源,而外部程序不知道它。典型的例子是构建管道:

    // 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 之间。当然,对于这种简单的情况,可能有一个spawning API,只要给定适当的参数,它就会自动执行此操作。但是fork() 设计允许对子进程中的每个进程资源进行任意操作——可以关闭不需要的文件描述符、修改每个进程的限制、删除特权、操纵信号掩码等等。如果没有fork(),用于生成进程的 API 最终会变得非常臃肿或不是很有用。事实上,竞争操作系统的进程生成调用通常介于两者之间。

    至于内存浪费,使用copy on write技术可以避免。 fork() 不会为子进程分配新内存,而是将子进程指向父进程的内存,并指示仅当页面被写入时才复制页面。这使得fork() 不仅内存效率高,而且速度快,因为它只需要复制一个“目录”。

    【讨论】:

      【解决方案2】:

      这是一个古老的抱怨。许多人问为什么要先fork(),他们通常会建议一个操作,既可以从头开始创建一个新进程,又可以在其中运行一个程序。这个操作叫做spawn().

      他们总是说,那不是更快吗?

      事实上,除了 Unix 家族之外的所有系统确实走的是“衍生”的道路。只有Unix基于fork()exec().

      但有趣的是,Unix 总是比其他全功能系统快得多。它总是能处理更多的用户和负载。

      多年来,Unix 变得更快了。 Fork() 不再真正复制地址空间,它只是使用一种称为 copy-on-write. 的技术共享它(一个非常古老的 fork 优化称为 vfork() 也仍然存在。)

      Kool-Aid.

      【讨论】:

        【解决方案3】:

        我不确切知道 init 进程在分叉方面如何在内核上工作,但要回答您为什么需要先调用 fork 然后 的问题>exec 只是因为一旦你exec 就没有回头路了。

        如果您查看文档here,它本质上需要生成一个新进程(fork 调用),以便父进程恢复控制并等待 让它完成或像守护进程一样坐着。

        【讨论】:

          【解决方案4】:

          只有在调用 exec() 时,这个子进程才会变成一个新的 过程。

          不是真的。在一个分叉之后,你已经有了新的进程,甚至和它的父进程没有太大的不同。在某些情况下,没有 exec 需要遵循分叉。

          那么为什么中间步骤(创建一个具有相同 需要上下文作为父级)?

          一个原因是因为它是创建整个 shebang 的有效方式。克隆通常没有从头开始创建复杂。

          这不是浪费时间和资源吗,因为我们正在创建一个 上下文(消耗时间和浪费内存)然后重写它?

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

          【讨论】:

          • 克隆可能不如从头创建复杂,但如果我们为了运行不同的程序而分叉,我们必须在分叉后exec。这个exec 将破坏克隆的所有努力,所以 OP 提出的问题是,为什么我们首先要费心克隆?
          • @user4815162342 仔细阅读我的回复。唯一用 exec 代替的东西,即进程地址空间,几乎不需要克隆,因为它是写时复制。真正重要的内核端进程上下文由 exec 保存。没有任何努力。
          • 好点,我跳过了你的最后一段。我仍然想知道“几乎不费吹灰之力”的部分。比较在同一操作系统上创建分叉和无分叉的进程会很有趣。
          • 我相信 fork 系统调用总是比 posix_spawn 的 exec 快得多。此外,posix_spawn 很可能通过共享大部分或全部 exec 代码来实现,并且可能使用了太多的 fork 代码以避免需要维护两个代码路径来做同样的事情。 posix_spawn 真正发挥作用的唯一一点是虚拟内存资源本身有限的地方,即在交换区域太少的系统上。
          • 流行的 Unix 系统实现了posix_spawnin terms of fork+exec,因此两者的比较是没有用的。但是have to 并不是这样实现的。 vfork 的存在本身就证明,至少在历史上,fork 偶尔被认为是有问题的。
          猜你喜欢
          • 1970-01-01
          • 2011-02-06
          • 2013-09-22
          • 2011-07-22
          • 2018-06-10
          • 2020-04-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          相关资源
          最近更新 更多