针对于神奇而又让人心驰神往的fork
,在研读并且实现一个之后,我有必要讨论下自己的理解。这个fork实在是神奇地很。
fork()
在linux下顾名思义是用来产生一个子进程的。而且事实上,针对于linux 0.26而言,除了fork
之外,还有一个do_fork
函数。虽然名字好像,而前者其实“调用”了后者,但是其实二者有截然的差别~其实在我的观察下,do_fork
只是进行了copy并[设置]成和current的各种进程上下文一模一样而已,并没有真正地进行启动新的进程。而fork
又仅仅只直接调用了do_fork
,而其他什么也没干!那么为什么fork()
会一下子变成两个进程呢?还有两个返回值呢?
其实乍一看好像仅仅在“设置”,而没有进行“调度”,但是其实在暗中已经进行调度了。秘密就在于,fork()
函数虽然只调用了do_fork()
函数,但是用的是0x80系统调用
。也就是说,整个调用链是这样的:fork->(int 0x80中断)sys_fork->do_fork
。由于fork
就调用了一个sys_fork
,而sys_fork
就调用了一个do_fork
,因而后边在不必要的地方,我们只说fork
,而不说sys_fork
了。
而且,虽然只有一层调用关系,但是这两个函数的意义却是截然不同的。fork()
函数是在用户空间调用的,而do_fork()
函数却是在内核空间调用的。而联系用户空间和内核空间的纽带,就是0x80系统调用。因此,在fork()
的asm volatile ("int $0x80;"::....)
函数中,父进程,也就是current
进程所做的事情只有一个:系统调用中断发生,自己切换到了内核态,栈由用户栈变成了内核栈;然后用do_fork
设置了一波子进程的所有东西,(仅仅是设置,并没有调度),然后就原路中断返回,之后继续执行,没有半点和单进程的不同之处啊。
看一段代码:user_main
仅仅是普普通通的用户态fork
调用而已。对于父进程current
而言,其实只是正常的返回了。是的!就是普普通通的正常返回了!返回了设置的子进程的pid
。为何我要强调设置这么多次呢?因为它确确实实只是设置,其他什么也没有干,没有任何的调度!
那为毛fork
竟然有俩返回值呢????
答案就是:因为本来只有一个fork
,在进行父进程设置do_fork
之后,是fork
变成了两个!
啥?
确实没错~C语言仅仅允许一个返回值。返回两个值是根本不可能的。所以,不要听别人“fork返回了两个值!”来忽悠你,那就是在扯淡了。这种言论会让新人越听越懵逼的。
其实在设置do_fork
的时候,开辟了子进程的pcb
块,然后重点的就是,此父进程的eip
会被复制一份。也就是,调度到子进程之后,子进程就会直接在父进程设置好的eip
处苏醒过来。
那么在int $0x80
中断的时候,其实idtframe
中断帧已经由CPU自动压入了下一条指令,也就是把当前的eip+4
变成了中断返回应该恢复的eip
了。那么子进程将不会执行fork,而是跳过了int中断来直接从int的下一条语句执行!!这里一度困扰了我好久啊。更详细的分析见这里:sys_fork。
上边那一段说的是子进程的苏醒。那么我们可以知道,子进程苏醒之后,也是差不多从和父进程一样的地点苏醒的。也就是:又执行了一遍fork的后半部分。这个后半部分,具体指的是“fork的返回部分”。因为fork
中其实只有一条int 0x80
指令,那么子进程还跳过了这个指令,从下一条开始执行,那么这所谓的下一条,自然就是return
语句了。
是了~~现在我们可以看出,子进程直接就是从父进程所执行的fork
跳过了设置函数do_fork
而开始执行的,也就是只进行了一个返回。那么这个返回值就必然是子进程fork
的返回值了。由于整个fork
过程中,eax
寄存器的值并没有改变!因此,只要在一开始系统调用0x80
之前,把eax
强制清空一次就好了~所以子进程因为直接从fork
的返回开始执行,因此直接返回了eax
~~所以这就是返回0的秘密啦!!而刚才也说过了,父进程是一直稳稳地执行并返回,非常正常,所以其实他返回的是do_fork
的值,也就是子进程的pid
咯!
总结一下,因此,fork
返回两个值的真正秘密,其实就是因为子进程是直接从fork
的返回处开始执行,所以其实父子加起来,共计执行了两次fork
。而不是fork
有两个返回值啊。
完~~