系统调用fork()vfork()以及clone()

Posted

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了系统调用fork()vfork()以及clone()相关的知识,希望对你有一定的参考价值。

一、宏观实现

以前介绍过fork()和clone()的区别,下面介绍一下两者在程序接口上的不同:

pid_t fork(void);
int __clone(int(*fn)(void *arg), void * child_stack, int flags, void *args)

系统调用__clone()的主要用途是创建一个线程,这个线程可以是内核线程,也可以是用户线程。创建用户空间线程时,可以给定子线程用户空间堆栈的位置,还可以指定子进程运行的起点。
同时,也可以用__clone()创建进程,有选择的复制父进程的资源。而fork()则是全面的复制。还有一个系统调用vfork(),其作用也是创建一个线程,但主要只是作为创建进程的中间步骤,目的在于提高创建时的效率,减少系统开销,其程序设计接口则与fork相同。这几个系统调用的代码如下:

 1 asmlinkage int sys_fork(struct pt_regs regs)
 2 {
 3     return do_fork(SIGCHLD, regs.esp, &regs, 0);
 4 }
 5 asmlinkage int sys_clone(struct pt_regs regs)
 6 {
 7     unsigned long clone_flags;
 8     unsigned long newsp;
 9 
10     clone_flags = regs.ebx; 
11     newsp = regs.ecx;            //是(0)否(1)使用父进程用户空间堆栈         
12     if(!newsp) newsp = regs.esp;
13     return do_fork(clone_flags, newsp, &regs, 0);
14 }
15 asmlinkage int sys_vfork(struct pt_regs regs)
16 {
17     return do_fork((CLONE_VFORK|CLONE_VM|SIGCHLD), regs.esp, &regs, 0);
18 }

显而易见,三个系统调用的实现都是通过调用do_fork()来完成的,不同的只是对do_fork()的调用参数。以后会介绍do_fork()的代码。

二、有关do_fork()调用

do_fork()函数定义: int do_fork(unsigned long clone_flags, unsigned long stack_start, struct pt_regs *regs, unsigned long stack_size){}

(1) 参数clone_flags由两部分组成,其最低的字节为信号类型,用以规定了子进程去世时应该向父进程发出的信号。我们已经看到,对于fork()和vfork()这个信号就是SIGCHLD,而对__clone()则该位段可由调用者决定。第二部分则是一些表示资源和特性的标志位。

 1 #define CSIGNAL             0x000000ff  /*signal mask to be sent at exit*/
 2 #define CLONE_VM            0x00000100  /*set if VM shared between processes*/
 3 #define CLONE_FS            0x00000200  /*set if fs info shared between processes*/
 4 #define CLONE_FILES         0x00000400  /*set if open files shared between processes*/
 5 #define CLONE_SIGHAND       0x00000800  /*set if signal handlers and blocked signals shared*/
 6 
 7 #define CLONE_PID           0x00001000  /*set if pid shared*/
 8 #define CLONE_PTRACE        0x00002000  /*set if we want to let tracing continue on the child too*/
 9 
10 #define CLONE_VFORK         0x00004000  /*set if the parent wants ths child to wake it up on mm_release*/
11 #define CLONE_PARENT        0x00008000  /*set if we want to have the same parent as the cloner*/
12 #define CLONE_THREAD        0x00010000  /*same thread group?*/
13 #define CLONE_SIGNAL        (CLONE_SIGHAND|CLONE_THREAD)

对于fork(),这一部分全为0,意思是对有关的资源都要复制而不是共享。对vfork() 则为 CLONE_VFOKR|CLONE_VM,表示父子进程共用(用户)虚存空间,并且子进程在释放其虚拟内存时要唤醒父进程。
至于__clone(),则这一部分完全由调用者设定而作为参数传递下来。其中标志位CLONE_PID由特殊的作用,此标志位为1时,父子进程(线程)共同使用一个进程号。但是子进程有自己的task_struct结构。并不是所有进程都可以这样调用。只有0号进程,也就是系统中的原始线程。才允许这样调用。

(2)在do_fork()函数体内存在copy_files(unsigned long clone_flags, struct task_struct *tsk)函数,用于有条件地复制已经打开文件的控制结构.这种复制只有在clone_flags中CLONE_FLAGS标志位为0时才真正进行,否则就只是共享父进程的已打开文件。下面介绍一下copy_files()函数。代码(参考《linux内核源代码情景分析》相关章节)比较长,只做简单介绍。

    因为是当前进程作为父进程在创建子进程,因此相关结构是从当前进程拷贝到即将创建的子进程。我们把当前进程的task_struct结构中的files_struct结构指针作为oldf.

    如果参数CLONE_FILES标志位是1,就只是通过atomic_inc()递增当前进程的files_struct结构中的共享计数,表示这个数据结构现在多了一个用户,就返回了。由于在此之前已通过数据结构复制将当前进程 的整个task_struct结构都复制给了子进程,结构中的指针files自然也就复制到了子进程的task_struct结构中,使得子进程通过这个指针共享当前进程的files_strcut数据结构。

    如果参数CLONE_FILES标志位为0,那就要复制了。首先通过kmem_cache_alloc()为子进程分配一个files_struct数据结构作为newf,然后从oldf把内容复制到newf。

    复制和通过指针共享的区别?区别在于子进程(以及父进程本身)。当复制完成之初,子进程有了一个副本,它的内容与父进程的“正本”在内容上基本是相同的,在这一点几乎与共享没有什么区别。可是,在共享的情况下,两个进程是相互牵制的。如果子进程对某个已经打开的文件调用了一次lseek(),则父进程对这个文件的读写位置也就改变了,因为两个进程共享者对文件的同一个读写上下文。而在复制的情况下就不一样了,由于子进程有自己的就有了对同一个文件读写的另一个上下文,以后就走各自的路,互不干扰了。

(3)另外一个标志CLONE_SIGHAND是表示是否复制父进程对信号的处理。信号基本上是一种进程通信手段。如果一个进程设置了信号处理程序,其task_struct结构中的指针sig就指向一个signal_struct数据结构:

struct signal_struct{

  atomic_t    count;

       struct k_sgiaction action[_NSIG];

       spinlock_t   siglock;

};

其中数组action[]确定了一个进程对各种信号的反应和处理(和中断处理类似),子进程可以通过复制和共享(copy_sighand())把它从父进程继承下来。其中copy_sighand()也是只有在CLONE_SIGHAND为0时才真正进行。否则就共享父进程的sig指针,并将父进程的signal_struct中的共享计数加1。

 (4)对于用户空间的继承。进程的task_struct结构中有个指针mm指向一个代表着进程的用户空间的mm_struct数据结构。由于内核线程并不拥有用户空间所以在内核线程的task_struct结构中该指针为0。对mm_struct的复制也只是在clone_flags中CLONE_VM标志为0时才真正进行。否则就只是通过已经复制的指针共享父进程的用户空间。对mm_struct的复制就不只是局限于这个数据结构的本身了,也包括了对更深层次结构的肤质,例如:vm_area_struct结构和页面映射表等。

(5)前面四条主要介绍了clone_flags结构,继续说do_fork()代码。在创建子进程过程中,调用alloc_task_struct()分配了两个连续页面作为系统堆栈空间,在8K低端空间是task_struct结构,基本上已经复制好了。而在8K高端空间却没有复制。现在由函数copy_thread来完成高端内存的复制。这个函数是非常重要的。着重说一下,源代码如下:

 1 #define savesegment(seg, value) asm volatile("movl %%" #seg ",%0":"=m" (*(int *))&(value))
 2 
 3 int copy_thread(int nr, unsigned long clone_flags, unsigned long esp, unsigned long unused,
 4                 struct task_struct * p, struct pt_regs * regs){
 5     struct pt_regs * childregs;
 6     childregs = ((struct pt_regs *) (THREAD_SIZE + (unsigned long) p)) - 1;
 7     struct_cpy(childregs, regs);
 8     childregs->eax = 0;
 9     childregs->esp = esp;
10     
11     p->thread.esp = (unsigned long)childregs;
12     p->thread.esp0 = (unsigned long)(childregs + 1);
13     
14     p->thread.eip = (unsigned long) ret_from_fork;
15     
16     savesegment(fs, p->thread.fs);
17     savesegment(gs, p->thread.gs);
18     
19     unlazy_fpu(current);
20     struct_cpy(&p->thread.i387, &current->thread.i387);
21     
22     return 0;

(内联汇编:第一个宏用于保存一个segment)

该函数只是复制父进程的系统空间堆栈。堆栈中的内容说明了父进程从通过系统调用进入系统空间开始到进入copy_thread()的来历,子进程将要遵循相同的路线返回,所以要把它复制给子进程。但是如果子进程的系统空间堆栈与父进程的完全相同,那返回以后就无法区分谁是子进程了,所以复制以后还要略作调整。

当一个进程因系统调用或中断进入内核时,其系统空间堆栈的顶部保存着CPU进入内核前夕各个寄存器的内容。并形成一个pt_regs结构,第6行中的p为紫禁城的task_struct指针,指向两个连续物理页面的起始地址;而THREAD_SIZE+(unsigned long)p则指向这两个页面的顶端。将其变换成struct pt_regs*,再从中减1,就指向了子进程系统空间堆栈中的pt_regs结构。

得到了指向子进程系统空间堆栈中pt_reg结构的指针childregs以后,就先将当前进程系统空间堆栈中的pt_regs结构复制过去,再来做少量的调整。a.首先将该结构中的eax置成0.当子进程受调度而运行,从系统调用返回时,这就是返回值。即,子进程的返回值为0。b.其次还要将esp置成这里的参数esp,它决定了进程在用户空间的堆栈位置。(在__clone()调用中这个参数是由调用者决定的,而在fork()和vfork()中,则来自调用do_fork()前夕的regs.esp, 因此实际上并没有改变,还是指向父进程原来在用户空间的堆栈。)

在进程的task_struct结构中有个重要的成分thread,它本身是一个数据结构thread_struct,里面记录着进程在切换时(系统空间)堆栈指针,返回地址等信息。在复制task_struct数据结构的时候,这些信息也原封不动的复制了过来。但由于子进程有自己的系统空间堆栈,所以应该调整。将p->thread.esp设置成子进程系统空间堆栈中pt_regs结构的起始地址。这个子进程就好像父进程一样曾经运行过,p->thread.esp0则应该指向子进程的系统空间堆栈的顶端。当一个进程被调度运行时,内核会将这个变量值写入TSS的esp0字段,表示当这个进程进入0级运行时的堆栈的位置。此外,p->thread.eip的值表示当进程下一次被切换进入运行的切入点,类似于函数调用或中断的返回地址。ret_from_fokr是新建的子进程首次运行的起始地址。

参考:

    毛德操、胡希明《linux内核源代码情景分析》(上册)

以上是关于系统调用fork()vfork()以及clone()的主要内容,如果未能解决你的问题,请参考以下文章

Linux中fork,vfork和clone详解(区别与联系)

fork vfork clone学习

do_fork实现--上

Android NDK ——Linux 创建应用进程之 fork vs vfork 小结

Android NDK ——Linux 创建应用进程之 fork vs vfork 小结

Android NDK ——Linux 创建应用进程之 fork vs vfork 小结