【问题标题】:How does fork() know when to return 0?fork() 怎么知道什么时候返回 0?
【发布时间】:2016-08-07 00:28:54
【问题描述】:

举个例子:

int main(void)
{
     pid_t  pid;

     pid = fork();
     if (pid == 0) 
          ChildProcess();
     else 
          ParentProcess();
}

如果我错了,请纠正我,一旦 fork() 执行,就会创建一个子进程。现在通过这个answer fork() 返回两次。一次用于父进程,一次用于子进程。

这意味着在 fork 调用期间而不是在它结束之后存在两个独立的进程。

现在我不明白它如何理解如何为子进程返回 0 以及为父进程返回正确的 PID。

这真是令人困惑的地方。这个answer表示fork()通过复制进程的上下文信息并手动将返回值设置为0来工作。

首先我是否正确地说任何函数的返回都放在一个寄存器中? 由于在单处理器环境中,一个进程只能调用一个只返回一个值的子例程(如果我在这里错了,请纠正我)。

假设我在例程中调用函数 foo() 并且该函数返回一个值,该值将存储在 BAR 寄存器中。每次函数想要返回一个值时,它都会使用特定的处理器寄存器。因此,如果我能够手动更改进程块中的返回值,我就可以更改返回给函数的值,对吗?

那么我认为 fork() 的工作原理是否正确?

【问题讨论】:

  • 没有“它”可以一次返回两个值。当fork 被调用时,操作系统接管。它创建了进程的两个副本,除了一个内存位置的内容外,所有方面都相同。该内存位置恰好是最终从fork 返回的位置。
  • '在 fork 调用期间出现了两个独立的进程' - 不,一个已经存在。在 fork 调用期间会出现第二个进程。
  • @n.m.虽然这甚至没有必要。应用程序或库代码可以将getpid 的结果存储在一个变量中。然后当操作系统返回时,它可以再次调用getpid。在父级中,两者将匹配。在孩子身上,他们不会。你所需要的只是一些东西,任何东西,你可以测试它们两者不同,并且 PID 合格。
  • @DavidSchwartz 但是父母如何获得孩子的 PID?
  • @SebastianRedl 随心所欲。一旦每个进程都知道哪个是哪个,就没有进一步的问题了。例如,系统调用可以将子进程的 PID 返回给两个进程,而子进程可以在父进程返回时忽略它。您需要解决的唯一问题是每个进程如何知道它是哪一个,之后,可以通过向两个进程提供相同的信息并让“错误”进程忽略它来解决其他任何问题。

标签: c unix process fork internals


【解决方案1】:

它是如何工作的在很大程度上是无关紧要的 - 作为一个在特定级别工作的开发人员(即,对 UNIX API 进行编码),你真的只需要知道它有效。

话虽如此,但并认识到好奇心或需要深入了解通常是一个很好的特质,有很多方法可以做到这一点。

首先,你关于一个函数只能返回一个值的论点是正确的,但你需要记住,在进程拆分之后,实际上有 两个 函数的实例运行,每个进程一个。它们大多相互独立,可以遵循不同的代码路径。下图可能有助于理解这一点:

Process 314159 | Process 271828
-------------- | --------------
runs for a bit |
calls fork     |
               | comes into existence
returns 271828 | returns 0

您希望在那里看到fork单个 实例只能返回一个值(与任何其他 C 函数一样),但实际上有多个实例正在运行,这就是为什么它被称为在文档中返回多个值。


这是关于它如何工作的一种可能性。

fork() 函数开始运行时,它会存储当前进程 ID (PID)。

然后,当返回时,如果 PID 与存储的 PID 相同,则它是父级。否则就是孩子。伪代码如下:

def fork():
    saved_pid = getpid()

    # Magic here, returns PID of other process or -1 on failure.

    other_pid = split_proc_into_two();

    if other_pid == -1:        # fork failed -> return -1
        return -1

    if saved_pid == getpid():  # pid same, parent -> return child PID
        return other_pid

    return 0                   # pid changed, child, return zero

请注意,split_proc_into_two() 调用中有很多神奇之处,而且几乎可以肯定它根本不会以这种方式工作(a)。只是为了说明它周围的概念,基本上是:

  • 获取拆分前的原始 PID,两个进程在拆分后将保持相同。
  • 进行拆分。
  • 得到拆分后的当前PID,两个进程中会不同

您可能还想看看this answer,它解释了fork/exec 的理念。


(a) 这几乎肯定比我解释的要复杂。例如,在 MINIX 中,对fork 的调用最终在内核中运行,内核可以访问整个进程树。

它只是将父进程结构复制到子进程的空闲槽中,如下所示:

sptr = (char *) proc_addr (k1); // parent pointer
chld = (char *) proc_addr (k2); // child pointer
dptr = chld;
bytes = sizeof (struct proc);   // bytes to copy
while (bytes--)                 // copy the structure
    *dptr++ = *sptr++;

然后对子结构进行轻微修改以确保它适合,包括以下行:

chld->p_reg[RET_REG] = 0;       // make sure child receives zero

因此,基本上与我提出的方案相同,但使用数据修改而不是代码路径选择来决定返回给调用者的内容 - 换句话说,您会看到类似的内容:

return rpc->p_reg[RET_REG];

fork() 的末尾,以便根据它是父进程还是子进程返回正确的值。

【讨论】:

  • fork() 在父级中返回子级的PID,在子级中返回0,所以它返回两个值,一个在父级(PID)中和一个孩子(0)(成功...)
  • @ralfhtp,这完全取决于您对“它”的定义。 fork 的单个实例仅返回 一个 值,只是现在有 两个 副本,子进程和父进程各一个。
  • @Machina333,是的,就标准描述的C“虚拟设备”环境而言,一个函数只能返回一个值。分叉在标准范围之外运行,因此该标准仅适用于单个实例。
  • 没有足够的 JQuery。 “它是如何工作的”也许对你来说是无关紧要的。但问题很明确,OP 准确地问“如何”。
  • @paxdiablo:不,fork 只返回一个值。只是在调用fork之后,有两个独立的进程在运行,fork在每个进程中返回不同的值。这与运行一个程序的两个实例并没有什么不同,并且程序中的某些函数调用根据每个实例的状态返回不同的值。
【解决方案2】:

fork 系统调用创建一个新进程并从父进程复制大量状态。复制文件描述符表、内存映射及其内容等内容。该状态位于内核内部。

内核为每个进程跟踪的一件事是该进程在从系统调用、陷阱、中断或上下文切换返回时需要恢复的寄存器值(大多数上下文切换发生在系统调用或中断上) .这些寄存器保存在系统调用/陷阱/中断中,然后在返回用户态时恢复。系统调用通过写入该状态来返回值。这就是 fork 的作用。父 fork 获取一个值,子进程获取不同的值。

由于分叉的进程与父进程不同,内核可以对它做任何事情。给它寄存器中的任何值,给它任何内存映射。要真正确保除了返回值之外的几乎所有内容都与父进程中的相同需要更多的努力。

【讨论】:

    【解决方案3】:

    在 Linux 中,fork() 发生在内核中;实际位置是_do_fork here。简化后,fork() 系统调用可能类似于

    pid_t sys_fork() {
        pid_t child = create_child_copy();
        wait_for_child_to_start();
        return child;
    }
    

    所以在内核中,fork() 真正返回一次,进入父进程。然而,内核也会创建子进程作为父进程的副本;但不是从普通函数返回,而是为子进程新创建的线程综合创建一个新的内核堆栈;然后上下文切换到该线程(和进程);当新创建的进程从上下文切换函数返回时,它会使子进程的线程最终返回到用户态,从fork()返回值为0。


    基本上fork() 在用户空间中只是一个瘦包装器,返回内核放入其堆栈/返回寄存器的值。内核设置新的子进程,以便它通过这种机制从它的唯一线程返回 0;并且子 pid 在父系统调用中返回,就像来自任何系统调用(例如 read(2))的任何其他返回值一样。

    【讨论】:

    • “为子进程综合创建一个新堆栈”属于“复制整个父进程的地址空间”的类别 - 这部分没有什么不同。
    • @ClickRick 不,它绝对不会。这是关于内核堆栈的。
    • @ClickRick 子进程线程的内核栈将在另一个地址,无法复制。
    【解决方案4】:

    您首先需要了解多任务处理的工作原理。了解所有细节并没有什么用处,但每个进程都运行在某种由内核控制的虚拟机中:一个进程有自己的内存、处理器和寄存器等。这些虚拟对象映射到真实对象(魔法在内核中),随着时间的推移,有一些机器可以将虚拟上下文(进程)交换到物理机器。

    然后,当内核 fork 一个进程(fork() 是内核的入口),并创建 parent 进程中几乎所有内容的副本到 child 进程,它能够修改所需的一切。其中之一是修改相应的结构以从当前调用 fork 中返回 0 为子级和父级中子级的 pid。

    注意:不要说“fork 返回两次”,函数调用只返回一次。

    想象一下克隆机:你一个人进去,但是两个人出去,一个是你,另一个是你的克隆(略有不同);克隆机器时,可以为克隆设置与您不同的名称。

    【讨论】:

    • is an entry to the kernel 是指syscall 吗?
    • 就是这样(在 Unix 系列中)。
    • 我只是对措辞感到好奇 :)
    【解决方案5】:

    对于每个正在运行的进程,内核都有一个寄存器表,以便在进行上下文切换时加载回来。 fork() 是系统调用;一个特殊的调用,当进行时,进程会获得上下文切换,并且执行调用的内核代码在不同的(内核)线程中运行。

    系统调用返回的值放在一个特殊寄存器(x86 中的 EAX)中,您的应用程序会在调用后读取该寄存器。当调用fork() 时,内核会复制进程,并在每个进程描述符的每个寄存器表中写入适当的值:0 和 pid。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2021-05-04
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2021-08-02
      • 1970-01-01
      • 1970-01-01
      • 2010-11-29
      相关资源
      最近更新 更多