date: 2014-10-22 17:40linux
前文已大体讲过fork、vfork与clone的区别,先来看看它们的原型:数组
pid_t fork(void); pid_t vfork(void); int clone(int (*fn)(void *), void *child_stack, int flags, void* arg, ...);
fork是全面的复制。并发
vfork建立一个线程,但主要用做建立进程的中间步骤。既然新的进程最终要调用execve执行新的目标程序,并与父进程分道扬镳,那么复制父进程的资源彻底是多余的,因而vfork便应运而生,其建立一个线程并经过指针拷贝共享父进程的资源(这些资源会在execve中被替换)。 vfork与fork的另外一个不一样是,vfork会保证子进程先运行,在子进程执行execve或者exit以后,父进程才可能被调度运行。函数
clone能够用来建立一个线程,并能够指定子线程的用户空间堆栈的起始位置(child_stack参数)以及自线程的入口(参数fn)。同时clone也能够用来建立一个进程,有选择性的复制父进程的资源(由参数flag来指定)。atom
这几个系统调用在内核中的实现以下(2.4.0内核)线程
<arch/kernel/process.c> asmlinkage int sys_fork(struct pt_regs regs) { return do_fork(SIGCHLD, regs.esp, ®s, 0); } asmlinkage int sys_clone(struct pt_regs regs) { unsigned long clone_flags; unsigned long newsp; clone_flags = regs.ebx; newsp = regs.ecx; if (!newsp) newsp = regs.esp; return do_fork(clone_flags, newsp, ®s, 0); } asmlinkage int sys_vfork(struct pt_regs regs) { return do_fork(CLONE_VFORK | CLONE_VM | SIGCHLD, regs.esp, ®s, 0); }
可见这几个系统调用最终都是以不一样的参数调用do_fork。这三个函数参数中的pt_regs结构是系统调用陷入内核空间后,对用户空间中CPU各通用寄存器的备份。对sys_clone来讲,regs.ebx保存的是clone的参数flag(flag是第三个参数,不是应该保存在edx中吗?),regs.ecx保存的就是clone的参数child_task。指针
do_fork函数的原型为:code
int do_fork(unsigned long clone_flags, unsigned long stack_start, struct pt_regs *regs, unsigned long stack_size);
第一个参数clone_flags包含两部分,第一部分为其最低的字节(8bit)表示信号类型,表示子进程去世时内核应该给其父进程发什么信号。fork和vfork设置的信号为SIGCHILD,而clone则由调用者决定发什么信号。blog
第二部分是一些表示资源或者特性的标志位,这些标志位定义在<include/linux/sched.h>文件中:进程
/* * cloning flags: */ #define CSIGNAL 0x000000ff /* signal mask to be sent at exit */ #define CLONE_VM 0x00000100 /* set if VM shared between processes */ #define CLONE_FS 0x00000200 /* set if fs info shared between processes */ #define CLONE_FILES 0x00000400 /* set if open files shared between processes */ #define CLONE_SIGHAND 0x00000800 /* set if signal handlers and blocked signals shared */ #define CLONE_PID 0x00001000 /* set if pid shared */ #define CLONE_PTRACE 0x00002000 /* set if we want to let tracing continue on the child too */ #define CLONE_VFORK 0x00004000 /* set if the parent wants the child to wake it up on mm_release */ #define CLONE_PARENT 0x00008000 /* set if we want to have the same parent as the cloner */ #define CLONE_THREAD 0x00010000 /* Same thread group? */ #define CLONE_SIGNAL (CLONE_SIGHAND | CLONE_THREAD)
这里的标志位若是为1则表示对应的资源经过指针拷贝(“浅拷贝”)的方式与父进程共享,反之若是为0则要进行深度拷贝了。其中CLONE_PID有特殊的用途,它表示父子进程共用同一进程号PID,也就是说子进程虽然有其本身的task_struct结构,却使用父进程的PID,可是只有0号进程(0号进程不是init进程,init进程的进程号为1,0号进程空转进程即idle进程)被容许如此调用clone。
对于fork,这部分全为0,表示全部的资源都要复制(“深拷贝”);而对vfork,其设置了CLONE_VFORK和CLONE_VM标志,表示子进程(线程)共用父进程(用户空间的)虚存空间,而且当子进程释放其虚存区间时,要通知父进程;至于clone,这一部分也由调用者设定。
下面分析下do_fork的流程,若是读者理解浅拷贝和深拷贝,那么复制的过程很好理解。
下面对流程中的重点函数进行详细介绍。
通常来说,将上一个进程的PID加1便可获得新进程的PID,但系统中规定PID不能≥PID_MAX(PID_MAX 定义为0x8000,可见最大的PID号为0x7fff即32767)。若是PID≥PID_MAX,则要回过头去“捡漏”:某些进程已经去世了,以前分配给他们的PID能够从新利用了。注意捡漏要从PID为300开始扫描,由于300之内(0~299)的PID保留给系统进程使用(包括内核线程和各“守护神”进程)。若是扫描只为找到一个可用的PID,显然不划算,最好是能找到一个可用PID的区间(而不只仅是找到第一个可用的PID)。这样下一次再建立进程时,直接从可用PID区间中取下一个PID便可。
task_struct结构有一个files成员,其为struct files_struct类型的指针,指向进程打开的文件的信息,copy_files函数完成该指针的深度拷贝。files_struct结构体的定义以下:
/* * Open file table structure */ struct files_struct { atomic_t count; rwlock_t file_lock; int max_fds; int max_fdset; int next_fd; struct file ** fd; /* current fd array */ fd_set *close_on_exec; fd_set *open_fds; fd_set close_on_exec_init; fd_set open_fds_init; struct file * fd_array[NR_OPEN_DEFAULT]; };
files_struct结构主要有三个部件:其一是个位图(指针)close_on_exec,表示在进程执行exce时须要close的文件描述符,初始指向结构体中另外一个成员close_on_exec_init的地址;其二也是一个位图(指针)open_fds,初始指向结构体中另外一成员open_fds_init的地址;其三为指向文件表项(struct file)指针数组的指针fd,指向进程打开的文件表项数组,初始指向结构体中另外一成员fd_array的地址。因为位图(fd_set)可以表示最大文件描述符为1024(fd_set共有1024个bit),而fd_array数组的大小固定为NR_OPEN_DEFAULT,随着程序的运行,进程打开的文件数可能超过NR_OPEN_DEFAULT,或者fd_set(1024个bit)已经没法容纳最大的文件描述符,这个时候就须要对这个三个部件进行扩展,而这三个指针则指向结构体外的某个地址。
若有须要(clone_flag中对应的标志位为0,下同),copy_files将执行“深度拷贝”,这里“深度拷贝”主要是这三个部件的拷贝,在拷贝时要判断父进程的三个部件是否已进行过扩展,若是是则子进程的三个部件也要进行扩展。
注意,这里所谓的“深度拷贝”只是中间层次的“深度拷贝”。以fd为例,fd为指向指针数组的指针,拷贝以后子进程有本身的一块存储空间,用来存放全部的文件表项(struct file)的指针,可是并未执行更深层次的拷贝,并无对每一个文件表项指针所指向的文件表项(struct file)进行拷贝。父子进程每一个相同的文件描述符共享同一个文件表项 ,以下图所示:
可见,fork以后,当子进程经过lseek移动某个文件的“当前文件偏移量”时,因为这个偏移量记录在文件表项file中,父进程中对应文件的“当前文件偏移量”也会被更新,这就是在父子进程中同时往标准输出中打印信息时不会重叠的缘由。罕见的,《情景分析》关于这一点的描述有误。
task_struct结构中有一sig成员,其为struct signal_struct类型,后者的定义以下:
struct signal_struct { atomic_t count; struct k_sigaction action[_NSIG]; spinlock_t siglock; };
其核心成员action为一数组,指定了各个信号的处理方式。_NSIG即为信号的最大值64。copy_sighand所执行的深度拷贝就是将action数组拷贝一份到子进程。
task_struct结构中有一个指针mm,相信你们已经很熟悉了,其为mm_struct结构类型,表明了进程的用户空间。mm_struct中用两个重要的成员,其一为vm_area_struct类型的指针mmap,表明着用户空间的全部“虚存空间”;其二为pgd,领衔着“虚存区间”的页面映射。相应的,copy_mm所执行的深度拷贝也包括两部分:“虚存区间”的拷贝和页面表的拷贝。
注意,在拷贝页表时,只是为子进程的页表分配了存储空间,页面表项(pte_t)的内容与父进程一致。这意味着针对每个页面表项,子进程并无像咱们想象的那样,从新申请一个page,从父进程那里拷贝一个page的内容,而后将新page的地址和保护属性写入页面表项。取而代之的作法是:子进程的页面表项仍然指向父进程中的page,只是将这个page的保护属性设置成“只读”,并将保护属性同时设置进父、子进程的页面表项中。这样的话,当无论父进程仍是子进程企图写入这个页面时,都会触发异常页面异常,这种状况下,页面异常处理程序会从新分配一个page,并把内容复制过来,并更新父子进程的页面表项(将保护属性设置成可写),让父子进程拥有各自的物理page。这就是copy_on_write技术。linux之因此能快速的复制出一个进程,彻底归功于copy on write技术(不然的话,在fork时,就得老老实实的分配一个一个的物理页面,并将页面的内容拷贝过来)。
咱们以前讲过进程的系统空间为两个页面即8K的空间,底端为task_struct结构,task_struct结构之上为系统空间堆栈。其实,在系统空间堆栈之上即8K空间的顶端,存放的是pt_regs结构,以下图所示:
pt_regs结构保存着进入内核前夕CPU各个寄存器的内容,这但是系统调用返回到用户空间的重要“现场”,对于刚刚出生的子进程,这些信息只能从父进程拷贝而来,也正因如此,父子进程才能够返回到用户空间的同一个地方。
copy_thread的代码比较有趣,咱们来看看:
<arch/kernel/process.c> 529 int copy_thread(int nr, unsigned long clone_flags, unsigned long esp, 530 unsigned long unused, 531 struct task_struct * p, struct pt_regs * regs) 532 { 533 struct pt_regs * childregs; 534 535 childregs = ((struct pt_regs *) (THREAD_SIZE + (unsigned long) p)) - 1; 536 struct_cpy(childregs, regs); 537 childregs->eax = 0; 538 childregs->esp = esp; 539 540 p->thread.esp = (unsigned long) childregs; 541 p->thread.esp0 = (unsigned long) (childregs+1); 542 543 p->thread.eip = (unsigned long) ret_from_fork; 544 545 savesegment(fs,p->thread.fs); 546 savesegment(gs,p->thread.gs); 547 548 unlazy_fpu(current); 549 struct_cpy(&p->thread.i387, ¤t->thread.i387); 550 551 return 0; 552 }
参数中的p指向新建立的子进程;而regs则来自父进程;esp指向进程用户空间的堆栈的栈顶,对fork与vfork而言,esp来自regs.esp即父进程的用户空间地址的栈顶,对clone而言,栈顶esp可有调用者指定。
首先,要肯定在进程的系统空间中pt_regs结构的起始地址,这是535行的做用。逐步拆解以下:
(THREAD_SIZE + (unsigned long) p) 定位到子进程系统空间(8K)的上边界;
((struct pt_regs *) (THREAD_SIZE + (unsigned long) p)) 将系统空间的上边界强转成struct pt_regs类型的指针;
((struct pt_regs *) (THREAD_SIZE + (unsigned long) p)) – 1,在struct pt_regs类型的指针类型的指针上执行减1操做,指针后移sizeof(struct pt_regs)个存储单元,因而定位到pt_regs结构的起始地址。
其次,子进程的pt_regs拷贝自父进程(第536行),但也要进行些“修补”。第537行修改子进程在用户空间的返回值为0;第538行修改进程用户空间的栈顶esp。
另外,task_struct结构中有一个thread成员,其为struct thread_struct类型,里面存放着进程在切换时系统空间堆栈的栈顶esp、下一条指令eip(进程再次被切换运行时,将从这里开始运行)等关键信息。在复制task_struct结构时,这些内容原封不动从父进程拷贝过来,如今子进程有本身的系统空间堆栈了,因此要适当的加以调整。第540行将p->thread.esp设置成pt_regs结构的起始地址(注意堆栈是向下扩展的),从调度器的角度来看,就好像这个子进程之前曾经进入内核运行过,而在内核中的任务处理完毕(所以进程系统空间堆栈恢复平衡,变成“空”堆栈)准备返回用户空间时被切换了;而p->thread.esp0则应指向系统空间堆栈的顶端,表示这个进程进入0级(内核空间)运行时,其堆栈的位置。第543行,p->thread.eip被赋值为ret_from_fork,当子进程调度运行时(确定先从系统空间运行),将从ret_from_fork处开始运行。
前文提到,vfork会保证子进程先运行,在子进程执行execve或者exit以后,父进程才可能被调度运行。为了实现这个需求,task_struct结构中有一个信号量(struct semaphore)类型的指针vfork_sem。在do_fork函数开始位置,初始化了一个局部信号量sem,并将sem的地址赋给父进程的task_struct.vfork_sem(流程图中黄色标注的部分)。信号量的初始计数为0,意味着此时对信号量执行down操做,进程将被阻塞。do_fork在完成子进程的“复制”之后,调用wake_up_process唤醒子进程,而本身则调用down(&sem)进入睡眠。
在子进程调用exec或者exit时,都会调用mm_release,在该函数中,up(tsk->p_opptr->vfork_sem)增长了父进程vfork_sem的计数,因而父进程被唤醒。
void mm_release(void) { struct task_struct *tsk = current; /* notify parent sleeping on vfork() */ if (tsk->flags & PF_VFORK) { tsk->flags &= ~PF_VFORK; up(tsk->p_opptr->vfork_sem); } }
仔细想来,vfork保证子进程先运行,不见得是内核“精心”为linux用户实现的特性,倒像是内核的无奈之举。咱们知道vfork系统调用,父子进程共享进程用户空间,子进程对用户空间的修改会反过来影响父进程,反之亦然。若是说父子进程各自修改数据区的数据是危险的话,那么修改堆栈那就是致命的了。而恰巧子进程紧接着就会调用exec(或者exit),而函数调用涉及到传参,涉及到设置返回地址这些都会修改堆栈。因此决不能让两个进程都回到用户空间并发的运行,必须扣留其中的一个,而只让一个返回到用户空间去运行,直到两个进程再也不共享用户空间(子进程执行exec)或者其中之一消亡(必然是先返回用户空间的子进程)为止。
即便如此,也仍是有危险,子进程绝对不能从调用vfork的那个函数(caller)中返回(好比vfork以后子进程调用return),由于函数返回时,栈会被恢复平衡,以前用来存放caller的函数参数和函数返回地址的内存单元被回收,以后,这些内存单元可能被用来存放其余的内容。等到子进程去世父进程开始运行时,虽然父进程虽然有本身的esp(保存在pt_regs中),但栈中保存的caller的返回地址被破坏了(esp之上的内容都被破坏),这是很危险的。因此vfork其实是创建在子进程在建立后当即就会调用execve的前提之上的。