fork()如何知道何时返回0?

时间:2016-04-15 07:00:10

标签: c unix process fork internals

采用以下示例:

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的寄存器中。每次函数想要返回一个值时,它都会使用一个特定的处理器寄存器。因此,如果我能够手动更改进程块中的返回值,我能够更改返回给函数的值吗? / p>

所以我认为fork()的工作方式是正确的吗?

5 个答案:

答案 0 :(得分:53)

它的工作原理在很大程度上是无关紧要的 - 作为在某个级别工作的开发人员(即编写UNIX API),你真的只需知道 它的工作原理。

尽管如此,并且认识到好奇心或需要在某种程度上理解这一点通常是一个很好的特性,但可以通过多种方式完成。

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

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

你可以看到fork单个实例只能返回一个值(根据任何其他C函数),但实际上有多个实例在运行,这就是为什么它& #39; s表示在文档中返回多个值。

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()的末尾,以便返回正确的值,具体取决于它是父进程还是子进程。

答案 1 :(得分:29)

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

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

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

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

答案 2 :(得分:10)

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

然后,当内核分叉进程时(fork()是内核的一个条目),并在 parent 进程中创建几乎所有内容的副本到 child < / em>进程,它能够修改所需的一切。其中一个是修改相应的结构,为子节点返回0,父节点的pid从当前调用fork返回。

注意:nether说&#34; fork返回两次&#34;,函数调用只返回一次。

想想一个克隆机:你一个人进入,但两个人退出,一个是你,另一个是你的克隆(非常不同);克隆机器时可以设置与克隆不同的名称。

答案 3 :(得分:8)

fork系统调用创建一个新进程并从父进程复制大量状态。像文件描述符表这样的东西被复制,内存映射和它们的内容等等。该状态在内核中。

内核跟踪每个进程的一个事项是这个进程需要在系统调用,陷阱,中断或上下文切换返回时恢复的寄存器值(大多数上下文切换发生在系统调用或中断上) 。这些寄存器保存在系统调用/陷阱/中断中,然后在返回用户空间时恢复。系统调用通过写入该状态返回值。叉子是做什么的。父fork获取一个值,子进程获取另一个值。

由于分叉进程与父进程不同,因此内核可以对其执行任何操作。在寄存器中给它任何值,给它任何内存映射。要实际确保除了返回值之外的几乎所有内容都与父进程中的相同,需要更多的努力。

答案 4 :(得分:2)

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

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