fork()如何知道何时返回0?
以下面的例子:
int main(void) { pid_t pid; pid = fork(); if (pid == 0) ChildProcess(); else ParentProcess(); }
所以纠正我,如果我错了,一旦fork()执行一个subprocess创build。 现在通过这个答案 fork()返回两次。 这是一次为父进程,一次为subprocess。
这意味着两个独立的进程在fork调用期间生成,而不是在结束之后生成。
现在我不明白它如何理解如何为subprocess返回0和为父进程正确的PID。
这在哪里变得非常混乱。 这个答案指出fork()通过复制进程的上下文信息并手动将返回值设置为0来工作。
首先我正确地说,返回任何函数都放在一个寄存器中? 由于在单个处理器环境中,一个进程只能调用一个只返回一个值的子例程(如果我在这里错了,请纠正我)。
比方说,我在一个例程中调用一个函数foo(),该函数返回一个值,该值将被存储在一个寄存器中,比如BAR。 每次函数想要返回一个值,它都将使用一个特定的处理器寄存器。 所以,如果我能够手动更改过程块中的返回值,我可以更改返回到该function的值?
所以我正确地认为fork()是如何工作的?
它是如何工作的,在很大程度上是不相关的 – 作为一个开发人员在某个级别上工作(即编码到UNIX API),你只需要知道它的工作原理。
话虽如此,并且认识到好奇心或需要在一定程度上理解是一个很好的特征,但有许多方法可以做到这一点。
首先,一个函数只能返回一个值的争论是正确的,但是您需要记住,在进程拆分之后,实际上有两个函数运行的实例,每个进程中有一个实例。 他们大多是相互独立的,可以遵循不同的代码path。 下图可能有助于理解这一点:
Process 314159 | Process 271828 -------------- | -------------- runs for a bit | calls fork | | comes into existence returns 271828 | returns 0
你可以希望在那里看到fork
的一个实例只能返回一个值(如同任何其他C函数一样),但实际上有多个实例在运行,这就是为什么在文档中返回多个值的原因。
这是一个如何工作的可能性。
当fork()
函数开始运行时,它存储当前进程ID(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,这在两个进程中是不同的。
您可能也想看看这个答案 ,它解释了fork/exec
哲学。
(a)几乎肯定比我解释的更复杂。 例如,在MINIX中,对fork
的调用最终在内核中运行,该内核可以访问整个进程树。
它简单地将父进程结构复制到subprocess的空闲槽中,如下所示:
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
所以,与我所设想的scheme基本相同,但是使用数据修改而不是代码pathselect来决定返回给调用者的内容 – 换句话说,您会看到如下所示的内容:
return rpc->p_reg[RET_REG];
在fork()
的末尾,以便返回正确的值,具体取决于它是父进程还是subprocess。
在Linux中fork()
发生在内核中; 实际的地方是这里的_do_fork
。 简化的fork()
系统调用可能类似于
pid_t sys_fork() { pid_t child = create_child_copy(); wait_for_child_to_start(); return child; }
所以在内核中, fork()
真的返回一次 ,进入父进程。 但是,内核也会将subprocess创build为父进程的副本; 但不是从普通函数返回,而是为新创build的subprocess的线程合成一个新的内核栈 ; 然后上下文切换到该线程(和进程); 因为新创build的进程从上下文切换函数返回,它将使subprocess的线程以fork()
的返回值0返回到用户模式。
基本上,userland中的fork()
只是一个简单的包装器,它返回内核放入其堆栈的值/返回寄存器。 内核设置新的subprocess,以便它通过这个机制从其唯一的线程返回0; 并且父系统调用中返回子pid,因为来自任何系统调用(例如read(2)
任何其他返回值都将是。
你首先需要知道多任务是如何工作的。 理解所有的细节是没有用的,但是每个进程都运行在某种由内核控制的虚拟机上:一个进程有自己的内存,处理器和寄存器等等。这些虚拟对象映射到真实的对象上(神奇的是在内核中),并且有一些机器可以随着时间的推移将虚拟上下文(进程)交换到物理机器上。
然后,当内核派生一个进程( fork()
是内核的入口),并且创build父进程中的几乎所有内容到子进程的副本时,就可以修改所需的所有内容。 其中之一就是修改相应的结构,以便将当前调用返回给子代的子代和父代中的子代的pid。
注意:nether说“fork返回两次”,函数调用只返回一次。
想一想克隆机器:你一个人进入,但两个人退出,一个是你,另一个是你的克隆(非常不同); 而克隆机器可以设置一个不同于你的克隆名称。
fork系统调用创build一个新进程,并从父进程复制大量的状态。 像文件描述符表的东西被复制,内存映射及其内容等等。这个状态在内核里面。
内核为每个进程logging的内容之一是该进程在系统调用,陷阱,中断或上下文切换(大部分上下文切换发生在系统调用或中断)的返回时需要恢复的寄存器的值。 这些寄存器保存在系统调用/陷阱/中断,然后返回到用户区时恢复。 系统通过写入该状态来调用返回值。 叉子是做什么的 父叉获得一个值,subprocess不同。
由于分叉进程与父进程不同,内核可以做任何事情。 在寄存器中给它任何值,给它任何内存映射。 为了确保几乎除了返回值之外的所有内容都与父进程中的相同,需要付出更多的努力。
对于每个正在运行的进程,内核都有一个寄存器表,在进行上下文切换时加载回来。 fork()
是一个系统调用; 一个特殊的调用,当进行时,进程得到上下文切换,执行调用的内核代码在不同的(内核)线程中运行。
系统调用返回的值放置在应用程序在调用之后读取的特殊寄存器(x86中的EAX)中。 当进行fork()
调用时,内核会复制进程,并在每个进程描述符的每个寄存器表中写入相应的值:0和pid。