fork()一次调用两次返回
2015-09-27 00:00
661 查看
摘要: 本文通过源码解释了fork实现一次调用两次返回的细节,关键在于理解进程调度的概念。
fork是实现进程的关键函数之一,很多书上这样描述:fork函数调用一次,返回两次:父进程调用一次fork,子进程和父进程各返回一次,其中子进程返回0,父进程返回子进程pid(非0)。可以以此来判断fork返回后当前是在子进程里还是在父进程里。
以下是有关fork的部分源码,版本kernel-2.4.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:
arch/i386/kernel/process.c:
arch/i386/kernel/entry.S:
由上面的代码可以知道,在父进程的执行流中,fork首先使子进程的task_struct成为父进程的task_struct的一份拷贝,然后初始化子进程task_struct中与父进程不同的结构成员,其中thread.eip为ret_from_fork,初始化thread.eax为0,等等。初始化完毕后,将子进程加入进程队列等待调度,最后do_fork返回pid。
当子进程被调度时,上下文从子进程的TSS中恢复:
子进程的PC被恢复,这是一个跳转到ret_from_fork标号的指令,一系列操作后又跳转到ret_from_sys_call,通过ret_from_sys_call,子进程便返回了,返回值在eax中,值为0,之后的代码和父进程一致。
以上这就是fork一次,父进程返回child_pid、子进程返回0的实现细节。
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的实现细节。
相关文章推荐
- Linux socket 初步
- linux lsof详解
- 10 篇对初学者和专家都有用的 Linux 命令教程
- Linux 与 Windows 对UNICODE 的处理方式
- Ubuntu12.04下QQ完美走起啊!走起啊!有木有啊!
- 解決Linux下Android开发真机调试设备不被识别问题
- 运维入门
- 运维提升
- Linux 自检和 SystemTap
- Ubuntu Linux使用体验
- c语言实现hashmap(转载)
- Linux 信号signal处理机制
- linux下mysql添加用户
- Scientific Linux 5.5 图形安装教程
- 基于 Linux 集群环境上 GPFS 的问题诊断
- 谁是桌面王者?Win PK Linux三大镇山之宝
- vivi下重新调整分区
- Linux VS Unix:Linux欲一统天下 Unix不死
- linux下设定环境变量