您的位置:首页 > 其它

【进程管理】系统调用fork(),vfork()与clone()

2014-05-15 15:59 615 查看
clone()主要是用来创建一个线程,包括用户线程和内核线程;创建用户线程时,可以给定子线程用户空间堆栈位置,它也可以用来创建进程,有选择性的复制父进程的资源;fork(),则是全面的复制;vfork()是为了提高创建时的效率,减少系统开销;这三个系统调用都调用do_fork(),只不过调用的参数不一样,下面主要来讲解do_fork();

(1)第一个参数就是clone_flags有两部分组成,第一部分最低的字节为信号类型,用以规定子进程去世时应该向父进程发出信号;其中fork()和vfork()这个信号就是SIGCHLD,而对clone()则该位段应该由调用者决定;第二部分是一些资源和特性的标志位,如eixt所屏蔽的信号GSIGNAL,VM共享的CLONE_VM,fs
info共享的CLONE_FS,以及CLONES_SIGHAND,pid共享的CLONE_PID,tracing的CLONE_PID,CLONE_VFORK子进程mm_release时需wake_up父进程,同一个线程群CLONE_THREAD,CLONE_SIGNAL(CLONE_SIGHAND | CLONE_THREAD),对于fork()这一部分全为0,表示有关的资源都要复制而不是通过指针共享的;而对vfork(),则为CLONE_VFORK
| CLONE_VM,表示父子进程共用虚存空间,而当子进程mm_release时需wake_up父进程;对于clone(),参数由调用者决定,当CLONE_PID为1,表示,父子进程(线程)共用同一个进程号,因此子进程虽然有自己的task_struct数据结构,使用自己的task_struct,但使用父进程的pid,但是只有0号线程才能使用的clone();

(2)然后使用alloc_task_struct()为子进程p分配两个连续的物理页面,低端用作子进程的task_struct,高端用于系统空间堆栈;*p
= *current为整个两个连续物理页面的数据结构复制,而p=current只是指向;

(3)一个用户常常有许多个进程,因此task_struct中含有个user,指向user_struct(每一个用户只有一个),因此有关用户的信息可以通过同一个user来共享;在user_struct中,结构中有个计数器count,对属于该用户的进程数量计数;然而内核线程并不属于任何一个用户,所以user指针为0;task_struct中rilm[]对每一个进程占用的各种资源数量作出限制,其中rilm[RLIMIT_NPROC]规定了一个用户所述进程的个数多少,用户进程超过这个限制,就不允许fork()了,而对于内核线程有两个限制nr_threads和max_threads;exec_domian()代表一个进程所属OS的执行域,pers_low表示某种域的代码,handler用于调用门实现的系统调用,moudle指针指向某个moudle的数据结构,这是与动态安装模块的机制有密切联系,比如一个solaris执行域的进程就可能要用到专门为solaris设置的一些模块,只要有这样一个进程在运行,为solaris所需的模块就不能拆除,因此需通过get_exec_domain()递增具体模块的数据结构中计数器;linux_binfmt指针就是用来对有关模块的使用计数器进行操作;get_pid()使用必须独占,因访问不了而独占,故先把p进程的状态设置成TASK_UNINTERRUPTINLE;copy_flags()来拷贝clone_flags到p->flags;

(4)在get_pid()中,若指定CLONE_PID就直接返回当前进程current的pid,先跳过各种守护线程的pid号,然后通过操作获取一个新的pid;

(5)在do_fork()中,初始化等待队列头p->wait_chldexit,子线程的等待信号处理队列p->sigpending,然后对task_struct的各种计时变量进行初始化;设置p->start_time表示进程创建的时间,为以时钟中断为单位的从系统初始化开始至此时的时间jiffles;至此对task_struct数据结构的复制和
初始化就基本完成了;

(6)当clone_flags中CLONE_FILES为0,
才复制已打开文件的控制结构,否则只是共享父进程已打开的文件;当一个进程有已打开的文件时,task_struct中指针files指向files_struct,否则为0,一般不为0,指向tty相联系的stdin,stdout,stderr;当CLONE_FILES为1时,表示是共享,只需递增files的计数,此时父子进程对文件的访问相互牵制,否则就要正真的分配并复制了,主要是对close_on_exec,open_fds和fd要执行这三组信息,如何复制,取决于已打开的文件数量;

(7)当clone_flags中CLONE_FS为0,才复制文件系统有关的数据,否则也只是共享;在fs_struct中,记录着进程的根目录root,当前工作目录pwd,一个用于文件操作权限管理的umask,还有一个计数器;真正的处理的操作在copy_fs中,复制时,仅仅复制fs_struct,而不复制更深层次的数据结构(还是使用共享的,因此需要通过mntget(),dget()来递增计数);

(8)当clone_flags中CLONE_SIGHAND为0,才复制父进程对信号的处理;信号基本上是一个进程间通信的手段,信号之于进程(设置各种信号的处理程序)好像中断之于一个处理器(为各个中断源设置相应的中断服务程序)一样;在signal_count中的action[]记录了进程对各种信号的反映和处理,复制时是通过sig->action,和sig->count;

(9)然后就是用户空间的继承,即mm_struct,它指向一个代表着进程的用户空间的mm_struct;由于内核线程不属于用户空间,故内核线程的task_struct中mm_struct指针为NULL;在copy_mm()中,如果CLONE_VM为0时才真正的复制,首先是对mm_struct本身的的复制,并且还要对mm_struct更深层次的数据结构进行复制(即vm_area_struct和页面映射表),主要在dup_map()中完成,其中对文件映射部分只是增加计数,如果VM_DENYWRITE为
1的话,还要递减该计数,通过copy_page_range()来逐层来处理页面目录项和页面表项,在对页面表项的循环中,若表项的内容全为0,则pte_none()返回1,说面该页面表的映射尚未建立,不需做任何事,表项的最低位_PAGE_PRESENT为0时,说明已建立映射,但该页面目前不在内存中,已经被调换到交换设备上,此时sawp_duplicate()增加它的共享计数,然后转到cont_copy_pte_range处将次表项复制到子进程的页面表中;若映射已建立,但是屋面页面不有效即VALID_PAGE返回0(这些地址是某些外设接口的总线地址,并不是内存页面,不属于页面换入换出的管辖范围,实际上也不小号动态分配的内存页面,跳转到cont_copy_pte_range将它复制);若复制父进程可写的页面,并未之建立映射,这时采用"copy
on write"技术,先通过复制页面表项暂时共享这个页面,到子进程(或父进程)真的要写这个页面时,再来分配新页面和复制,先将该页表项设置成写保护,当要写时,引发一次页面异常,此时页面异常处理程序会另行分配一个物理页面,并把内容真正的复制到新的物理页面中,让父子进程拥有自己的物理页面,然后将这两个页面改成可写,因此在linux中由于写时复制技术复制一个进程的速度非常快,否则fork一个进程时就要每一个物理页面都要复制了;对于只读页面,这种页面本身就不需要复制,共享物理页面;

(10)其实上述的copy_page_range()一个页面都没有正真的复制,这就是fork()快速的复制的原因;

(11)在copy_mm()->copy_sgments()处理的是进程可能具有的局部描述表LDT;当使用fork()时,由于clone_flags为SIGCHLD,copy_files(),copy_fs(),copy_sighand(),以及copy_mm()都是全部正真的复制的,而使用vfork()时,由于clone_flags为SIGCHLD
| VFORK | CLONE_VM,故只copy_files(),copy_fs(),copy_sighand()真正的复制了,对于copy_mm()只是通过指针共享了其父进程的mm_struct,并没有自己的副本,也就是说vfork()复制的是整个线程,共享了父进程的存储空间,包括用户空间堆栈;clone()根据调用者自己设置参数;

(12)在copy_thread()只是复制父进程的系统空间堆栈(堆栈的内容说明了父进程从通过系统调用进入系统空间开始到进入copy_thread()的来历),而子进程要按照相同的路线返回,所以要把它复制给子进程,但是如果父子进程的系统空间堆栈是完全相同的,那返回也就无需区分谁了,因此还是需要做调整的;先得到子进程的pt_regs,先复制,然后调整(将eax变为0,当子进程接受调度而恢复运行时,这个就是返回值;设置esp,决定了用户空间的堆栈位置,而在clone()时esp是调用设置的,而在fork()和vfork()中还是指向父进程原来的用户空间的堆栈);thread记录着进程切换时(系统空间)的堆栈指针,取指令地址等关键信息,在复制时,该项也被原封不动的复制过来了,但是子进程有自己的系统空间堆栈,将p->thread.esp设置成子进程系统空间堆栈中的pt_regs结构的起始地址,就好像这个进程曾经运行过一样,进入内核以后返回用户空间时被切换一样;p->thread.esp0指向系统空间堆栈的指针的顶端,当一个进程被调度时,内核会将这个变量写入到TSS的esp0字段,表示当这个进程进入0级运行时其堆栈的位置;p->thread.eip设置成ret_from_fork,指表示进程下一次切换进入运行点时的切入点,类似于函数调用或中断返回的地址;保存当前段的寄存器thread.fs;

(13)在do_fork()中,task_struct中thread_group是线程组的意思,counter为进程的运行时间分配额,self_exec_id执行域,swappable换入换出;通过SET_LINK(p)将子进程链入到内核的进程队列,又通过hsah_pid()将其链入到按pid计算的杂凑队列,最后通过wake_up_process将其挂入可执行的进程队列中等待调度;

(14)新进程已经创建好了,并且挂入到可运行进程的队列接受调度了;父子进程在用户空间返回相同的地址,然后会因用户程序的不同而分开(父进程先调度可能性大于子进程);

(15)当CLONE_VFORK为1时,一定要保证子进程先运行,一直到子进程通过系统调用execve()执行一个新的可执行程序,或通过exit()退出;当CLONE_VM为1时,为了防止父子进程对用户空间(尤其是堆栈区)的非法写,所以在do_fork()就把父进程扣留了,对一个局部信号量(初值为0)做一次down(),它也就睡眠等待了;在子进程execve()时,会有相应的up(),来唤醒父进程;
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: