您的位置:首页 > 运维架构 > Linux

fork()一次调用两次返回

2015-09-27 00:00 661 查看
摘要: 本文通过源码解释了fork实现一次调用两次返回的细节,关键在于理解进程调度的概念。

fork是实现进程的关键函数之一,很多书上这样描述:fork函数调用一次,返回两次:父进程调用一次fork,子进程和父进程各返回一次,其中子进程返回0,父进程返回子进程pid(非0)。可以以此来判断fork返回后当前是在子进程里还是在父进程里。

以下是有关fork的部分源码,版本kernel-2.4.0。

#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>
int main(int argc, char argv[], char envp[])
{
if ( 0 == fork() )
puts("皇家SCI");
else puts("圣光电");
exit(0);
}

如上,子进程打印“皇家SCI”,父进程打印“圣光电”。

刚开始学fork时对上面代码很费解,对“一次调用,两次返回”的描述很不理解。直到最近看到操作系统,结合kernel源码后才稍微深入理解一些。实际上,“一次调用,两次返回”的描述很不完善,很容易误解成“父进程一次调用->返回了两次”。准确的说法,应该是“父进程调用一次->父进程返回一次,子进程返回一次”,在各自的进程中,其实都是像普通函数那样返回而已。

进程调度负责分配好各个进程的CPU运行时间,还负责保存和恢复各个进程的上下文等工作。Linux中表示进程PCB的数据结构是叫task_struct的家伙,是一个很庞大的结构体,task_struct的定义在include/linux/sched.h中。进程被调度时,操作系统要从进程的task_struct中恢复进程上下文。下面就来看看kenel关于fork的细节。

fork是通过调用do_fork来实现的,以下便是do_fork以及相关函数的源码:

kernel/fork.c:

int do_fork(unsigned long clone_flags, unsigned long stack_start, struct pt_regs *regs, unsigned long stack_size)
/*clone_flags是一个掩码,用来告诉do_fork父进程的哪些flags是要clone给子进程的。regs、stack_start保存了父进程的寄存器、堆栈等信息*/
{
int retval = -ENOMEM;   //父进程fork的返回值
struct task_struct *p;  //子进程的task_struct
...
p = allco_task_struct();  //分配内存建立子进程的task_struct
...
*p = *current;   //复制当前进程的task_struct的内容到子进程task_struct中
...
p->pid = get_pid(clone_flags);   //为子进程分配新的PID
...
retval = copy_thread(0, clone_flags, stack_start, stack_size, p, regs);   //初始化子进程的TSS(Task State Segment)等项,后面有叙述
...
retval = p->pid;   //将子进程PID作为父进程fork正常返回的值
...
write_lock_irq(&tasklist_lock);   //加锁,避免被进程调度打断导致混乱。以下代码块是进程分裂的关键环节
if (clone_flags & CLONE_THREAD) {
p->tgid = current->tgid;
list_add(&p->thread_group, ¤t->thread_group);
}
SET_LINKS(p);   //把子进程加入到进程链表
hash_pid(p);   //把子进程放到pid散列表
nr_threads++;
write_unlock_irq(&tasklist_lock);   //解锁
...
wake_up_process(p);		/* do this last ,子进程加入运行队列,使其获得运行机会*/
...
return retval;    //父进程fork返回
...
}

arch/i386/kernel/process.c:

int copy_thread(...)
{
...
childregs->eax = 0;      //子进程TSS的eax寄存器,初始化为0,进程被调度eax从这里恢复。在过程调用时,eax是函数的返回值。
...
p->thread.eip = (unsigned long) ret_from_fork;     //子进程TSS的eip寄存器,也就是PC,子进程被调度后PC值从eip恢复!
...
}

arch/i386/kernel/entry.S:

ENTRY(ret_from_fork)
...
jmp        ret_from_sys_call         #从系统调用中返回,可以理解成从fork中返回,之后的东西便和父进程相同了

由上面的代码可以知道,在父进程的执行流中,fork首先使子进程的task_struct成为父进程的task_struct的一份拷贝,然后初始化子进程task_struct中与父进程不同的结构成员,其中thread.eip为ret_from_fork,初始化thread.eax为0,等等。初始化完毕后,将子进程加入进程队列等待调度,最后do_fork返回pid。

当子进程被调度时,上下文从子进程的TSS中恢复:

eax <- childregs.eax   (0)
PC <- hread.eip    (ret_from_fork)

子进程的PC被恢复,这是一个跳转到ret_from_fork标号的指令,一系列操作后又跳转到ret_from_sys_call,通过ret_from_sys_call,子进程便返回了,返回值在eax中,值为0,之后的代码和父进程一致。

以上这就是fork一次,父进程返回child_pid、子进程返回0的实现细节。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签:  fork Linux 进程