系统调用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内核源代码情景分析》(上册)

时间: 2024-10-08 01:59:07

系统调用fork()、vfork()以及clone()的相关文章

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

fork,vfork,clone Unix标准的复制进程的系统调用时fork(即分叉),但是Linux,BSD等操作系统并不止实现这一个,确切的说linux实现了三个,fork,vfork,clone(确切说vfork创造出来的是轻量级进程,也叫线程,是共享资源的进程) 系统调用 描述 fork fork创造的子进程是父进程的完整副本,复制了父亲进程的资源,包括内存的内容task_struct内容 vfork vfork创建的子进程与父进程共享数据段,而且由vfork()创建的子进程将先于父进程

fork()、vfork()、clone()的区别

因为生活的复杂,这是一个并行的世界,在同一时刻,会发生很多奇妙的事情,北方下雪,南方下雨,这里在吃饭,那边在睡觉,有人在学习,有人在运动,所以这时一个多彩多姿的世界,每天都发生着很多事情,所以要想很好的表现这个世界,协调完成一件事儿,就得用到多进程或者多线程.所以进程是程序猿一定会接触到的一个东西,他能使我们的程序效率提高,高效的完成多任务,并行执行.下面主要看看产生进程或线程的三个函数. fork,vfork,clone都是linux的系统调用,这三个函数分别调用了sys_fork.sys_v

linux系统调用fork()总结(二)

一,进程复制(或产生) 使用fork函数得到的子进程从父进程的继承了整个进程的地址空间,包括:进程上下文.进程堆栈.内存信息.打开的文件描述符.信号控制设置.进程优先级.进程组号.当前工作目录.根目录.资源限制.控制终端等. 子进程与父进程的区别在于: 1.父进程设置的锁,子进程不继承(因为如果是排它锁,被继承的话,矛盾了) 2.各自的进程ID和父进程ID不同 3.子进程的未决告警被清除: 4.子进程的未决信号集设置为空集. 二,fork系统调用 包含头文件 <sys/types.h> 和 &

系统调用fork笔记

fork函数的原型是这样的: 1 pid_t fork(void); 它实际上是一个系统调用,被包装在unistd.h中 由fork创建的新进程称为子进程,创建子进程的进程叫做父进程.子进程拥有与父进程一模一样的数据,从fork()语句开始分化. 它的返回值类型pid_t是一个内容为int的宏,在sys/types.h中声明.子进程返回0,父进程中返回子进程的pid(可以在子进程中调用getpid()得到,它同样被包装在unistd.h中).出错返回-1.出错原因可能是当前进程数超过限定或内存不

Linux 系统调用 —— fork 内核源码剖析

系统调用流程简述 fork() 函数是系统调用对应的 API,这个系统调用会触发一个int 0x80 的中断: 当用户态进程调用 fork() 时,先将 eax(寄存器) 的值置为 2(即 __NR_fork 系统调用号): ? 执行 int $0x80,cpu 进入内核态: ? 执行 SAVE_ALL,保存所有寄存器到当前进程内核栈中: ? 进入 sys_call,将 eax 的值压栈,根据系统调用号查找 system_call_table ,调用对应的函数: ? 函数返回,执行 RESTOR

进程控制fork vfork

主要函数: fork 用于创建一个新进程 exit 用于终止进程 exec 用于执行一个程序 wait 将父进程挂起,等待子进程结束 getpid 获取当前进程的进程ID nice 改变进程的优先级 --------------------------------- 孤儿进程: 如果一个子进程的父进程先于子进程结束,子进程就成为一个孤儿进程,他由init进程收养,成为init进程的子进程. #include <stdio.h> #include <stdlib.h> #includ

基于Linux的进程分析

什么是进程? 进程是程序执行的实例,是有限状态机的一次迁移过程.进程和程序的区别:动态与静止:多对一. windows10的相关进程 在Linux中用ps-a列出所有运行中/激活进程 在Linux中使用top命令,可以监视不同进程所使用的资源.(包括:PID,优先级,%CPU,%memory等) 进程运行流程: 进程的创建 用户空间内可以通过执行一个程序.或者在程序内调用fork(或exec)系统调用来创建进程,fork调用会导致创建一个子进程,而exec调用则会用新程序代替当前进程上下文.一个

linux 进程创建clone、fork与vfork

目录: 1.clone.fork与vfork介绍 2.fork说明 3.vfork说明 4.clone说明5.fork,vfork,clone的区别 内容: 1.clone.fork与vfork介绍 Linux下的进程与线程相同点是都有进程控制块(PCB,具体的类是task_struct).区别在于一个有独立的进程资源,一个是共享的进程资源.除了内核线程是完全没有用户空间.进程资源包括进程的PCB.线程的系统堆栈.进程的用户空间.进程打开的设备(文件描述符集)等. Linux的用户进程不能直接被

linux线程的实现

http://www.cnblogs.com/zhaoyl/p/3620204.html 首先从OS设计原理上阐明三种线程:内核线程.轻量级进程.用户线程 内核线程 内核线程就是内核的分身,一个分身可以处理一件特定事情.这在处理异步事件如异步IO时特别有用.内核线程的使用是廉价的,唯一使用的资源就是内核栈和上下文切换时保存寄存器的空间.支持多线程的内核叫做多线程内核(Multi-Threads kernel ). 轻量级进程 轻量级线程(LWP)是一种由内核支持的用户线程.它是基于内核线程的高级