fork vfork clone学习

Linux/Unix进程创建相关基本知识

linux中的0号、1号进程

1. 进程0
Linux引导中创建的第一个进程,完成加载系统后,演变为进程调度、交换及存储管理进程(也就是说0号进程自从创建完1号进程后就不会再次去创建其他进程了,之后由1号进程负责新子进程的创建)
Linux中1号进程是由0号进程来创建的,由于在创建进程时,程序一直运行在内核态,而进程运行在用户态,因此创建0号进程涉及到特权级的变化,即从特权级0变到特权级3,Linux是通过模拟中断返回来实现特权级的变化以及创建0号
进程,通过将0号进程的代码段选择子以及程序计数器EIP直接压入内核态堆栈,然后利用iret汇编指令中断返回跳转到0号进程运行。

2. 进程1
init 进程,由0进程创建,完成系统的初始化。是系统中所有其它用户进程的祖先进程。

Linux进程管理

0x1: 进程概念

进程就是处于执行期的程序(目标码存放在某种存储介质上),从广义上讲,它包括

1. 一般的可执行代码(即代码段)
2. 打开的文件
3. 挂起的信号
4. 内核内部数据结构
5. 处理器状态
6. 一个或多个具有内存映射的内存地址空间
7. 一个或多个执行线程(thread of execution)
8. 存放全局变量的数据段
//进程就是正在执行的程序代码的"实时结果",内核需要有效而又透明地管理所有细节

0x2: 创建进程 && 创建新进程

在学习Linux进程创建相关知识的时候,我们需要对Linux下"进程创建"和"新进程创建"这两个概念进行区分,完整地说,Linux下进程创建有如下几个场景

1. 从当前进程复制一份和父进程完全一样的新进程: 准确地说是复制了一份父进程作为新进程
从系统调用的角度来说,和进程创建相关的系统调用只有fork(),进程在调用fork()创建它的时刻开始存活,fork()通过"复制"(Linux下所有进程都是"复制"出来的)一个现有进程来创建一个新的进程,调用fork()的进程称为父进程,新产生的进程称为子进程。在该调用结束时,在返回到这个相同位置上,父进程恢复执行,子进程开始执行。
fork()系统调用从内核返回两次,一次返回到父进程、另一次返回到新产生的子进程
    1) 调用fork()
    or
    2) 调用clone()
/*
就像一个细胞复制了一份和自己相同的新细胞,两个细胞同时运行
*/

2. 运行新代码的新进程创建: 在调用fork的基础上,继续调用exec(),读取并载入新进程代码并继续运行
通常,创建新的进程都是为了立即执行新的、不同的代码,而接着调用exec这组函数就可以创建新的"地址空间",并把新的程序载入其中。在现代Linux内核中,fork()实际上是由clone()系统调用实现的
    1) fork()/clone() + exec()
/*
就像一个细胞复制了一份和自己相同的新细胞,并填充进了新的细胞核,两个细胞同时运行
*/

3. 运行新进程: 直接将当前进程转变为一个包含不同代码的新进程
    1) exec()
/*
就像一个细胞使用新的蛋白质将自己的细胞核改变了,并继续运行
*/

0x3: 进程描述符及任务(task)结构

内核把进程的的列表存放在"任务队列(task list)"(这是一个双向循环链表)中,链表中的每一项都是类型为task_struct称为进程描述符(process descriptor)的结构,该结构中包含了具体进程的所有相关信息,例如

1. 打开的文件
2. 进程的地址空间
3. 挂起的信号
4. 进程的状态
..

关于task_struct数据结构的相关知识,请参阅另一篇文章

http://www.cnblogs.com/LittleHann/p/3865490.html
//搜索:0x1: struct task_struct

0x4: Linux进程创建方法

从程序员的角度来说,Linux下实现进程创建可以通过以下方法

1. 通过系统提供的系统调用
    1) fork()/clone(): 复制一份新进程
    2) exec(): 运行新进程
    3) fork()/clone() + exec(): 复制并运行一个新进程(父进程和子进程运行不同的代码)
2. 通过glibc提供的API函数: exec系列函
    1) exec系列函数: glibc实现对系统调用exec()的一层包装
    2) fork api

流程图:

理解fork

  一个进程,包括代码、数据和分配给进程的资源。fork()函数通过系统调用创建一个与原来进程几乎完全相同的进程,也就是两个进程可以做完全相同的事,但如果初始参数或者传入的变量不同,两个进程也可以做不同的事。
    一个进程调用fork()函数后,系统先给新的进程分配资源,例如存储数据和代码的空间。然后把原来的进程的所有值都复制到新的新进程中,只有少数值与原来的进程的值不同。相当于克隆了一个自己。

实例

 1 #include <unistd.h>  
 2 #include <stdio.h>   
 3 int main ()   
 4 {   
 5     pid_t fpid; //fpid表示fork函数返回的值  
 6     int count=0;  
 7     fpid=fork();   
 8     if (fpid < 0)   
 9         printf("error in fork!");   
10     else if (fpid == 0) {  
11         printf("i am the child process, my process id is %d/n",getpid());   
12         count++;  
13     }  
14     else {  
15         printf("i am the parent process, my process id is %d/n",getpid());   
16         count++;  
17     }  
18     return 0;  
19 }  

为什么两个进程的fpid不同

这与fork函数的特性有关。fork调用的一个奇妙之处就是它仅仅被调用一次,却能够返回两次,它可能有三种不同的返回值:
    1)在父进程中,fork返回新创建子进程的进程ID;
    2)在子进程中,fork返回0;
    3)如果出现错误,fork返回一个负值;

    在fork函数执行完毕后,如果创建新进程成功,则出现两个进程,一个是子进程,一个是父进程。在子进程中,fork函数返回0,在父进程中,fork返回新创建子进程的进程ID。我们可以通过fork返回的值来判断当前进程是子进程还是父进程。

    引用一位网友的话来解释fpid的值为什么在父子进程中不同。“其实就相当于链表,进程形成了链表,父进程的fpid(p 意味point)指向子进程的进程id, 因为子进程没有子进程,所以其fpid为0.

fork出错可能有两种原因:

    1)当前的进程数已经达到了系统规定的上限,这时errno的值被设置为EAGAIN。
    2)系统内存不足,这时errno的值被设置为ENOMEM。
    创建新进程成功后,系统中出现两个基本完全相同的进程,这两个进程执行没有固定的先后顺序,哪个进程先执行要看系统的进程调度策略。
    每个进程都有一个独特(互不相同)的进程标识符(process ID),可以通过getpid()函数获得,还有一个记录父进程pid的变量,可以通过getppid()函数获得变量的值。
    fork执行完毕后,出现两个进程,

fork()整体分析

使用fork创建的进程被称为原父进程(parents process)的子进程(child process)。从用户的角度来看,子进程是父进程的一个精确副本,两个进程只是PID不同,fork系统调用从内核态返回2次,PID分别为

1. 子进程: PID = 0
2. 父进程: PID = 子进程的PID
//程序可以通过检测fork的返回值来判断当前进程是父进程还是子进程
 1 /*
 2 Sys_fork系统调用通过 do_fork()函数实现,通过对do_fork()函数传递不同的clone_flags来实现:
 3 1. fork
 4 2. clone
 5 3. vfork
 6 */
 7 int sys_fork(struct pt_regs *regs)
 8 {
 9     return do_fork(SIGCHLD, regs->sp, regs, 0, NULL, NULL);
10 }

从整体上来看,一次fork调用包括了以下几步

1. 为子进程分配和初始化一个新的task_struct结构
    1) 从父进程中复制: 包括所有从父进程继承而来的特权和限制
        1.1) 进程组和会话信息
        1.2) 信号状态(忽略、捕获、阻塞信号的掩码)
        1.3) kg_nice调度参数
        1.4) 对父进程凭据的引用
        1.5) 对父进程打开文件的引用(即文件句柄表。以及相关引用数据结构,使用这些数据结构可以操作对应的文件)
        1.6) 对父进程限制(resources limitation)的引用
    2) 清零
        2.1) 最近CPU利用率
        2.2) 等待通道
        2.3) 交换和睡眠时间
        2.4) 定时器
        2.5) 跟踪机制
        2.6) 挂起信号的信息
    3) 显式地进行初始化
        3.1) 包括所有进程的链表的入口
        3.2) 父进程的子进程链表的入口以及指向其父进程的返回指针
        3.3) 父进程的进程组链表的入口
        3.4) 散列结构的入口,该结构使得进程可以通过其PID进行查找
        3.5) 指向进程统计结构的指针,该结构位于用户结构中
        3.6) 指向进程信号处理结构的指针,该结构位于用户结构中
        3.7) 该进程的新PID
2. 复制父进程的地址空间
在复制一个进程的映像时,内核通过vm_forkproc()来调用内存管理机制。vm_forkproc()例程的参数是一个指向一个已经初始化过的子进程的task_struct的指针,它的任务是为该子进程分配其执行所需的全部资源。vm_forkproc()调用在子进程中通过另一条直接进入用户态的执行线路返回,而在父进程中沿着正常的执行线路返回(即一次调用、2次返回)
将父进程的上下文复制给子进程,包括
    1) 线程结构
    2) 父进程的register寄存器状态,fork系统调用结束后,父进程进程从同一个代码位置开始继续执行
    2) 虚拟内存资源,只是复制了一份引用,copy on write机制
3. 调度子进程运行(execve)
子进程最终创建完毕之后,就被放入运行队列,这样调度程序就知道这个新进程了

fork源码分析

fork() ,pthread_creat(), vfork()的系统调用分别是sys_fork(),sys_clone(), sys_vfork(),它们的底层都用的是do_fork(),只是传的参数,和标志不同。

对fork源代码分析

  1. 定义PCB指针struct task_struct *p;
  2. 分配PID,cat /proc/sys/kernel/pid_max命令可以查看一个系统支持的最大进程数,进程数的范围0~32768,理论值。
  3. 调用copy_process方法,创建子进程的task_struct.

do_fork

 

  1 /*
  2 1. clone_flags: 指定了子进程结束时,需要向父进程发送的信号,通常这个信号是SIGCHLD,同时clone_flags还指定子进程需要共享父进程的哪些资源
  3 2. stack_start: 子进程用户态堆栈起始地址。通常设置为0,父进程会复制自己的堆栈指针,当子进程对堆栈进行写入时,缺页中断处理程序会设置新的物理页面(即copy on write 写时复制)
  4 3. regs: pt_regs结构,保存了进入内核态时的存储器的值,父进程会将寄存器状态完整的复制给子进程
  5 4. stack_size: 默认为0
  6 5. parent_tidptr: 用户态内存指针,当CLONE_PARENT_SETTID被设置时,内核会把新建立的子进程ID通过parent_tidptr返回
  7 6. child_tidptr: 用户态内存指针,当CLONE_CHILD_SETTID被设置时,内核会把新建立的子进程ID通过child_tidptr返回
  8 */
  9 long do_fork(unsigned long clone_flags, unsigned long stack_start, struct pt_regs *regs, unsigned long stack_size, int __user *parent_tidptr, int __user *child_tidptr)
 10 {
 11     struct task_struct *p;
 12     int trace = 0;
 13     long nr;
 14 
 15     /*
 16      * Do some preliminary argument and permissions checking before we actually start allocating stuff
 17     */
 18     if (clone_flags & CLONE_NEWUSER) 
 19     {
 20         if (clone_flags & CLONE_THREAD)
 21             return -EINVAL;
 22         /* hopefully this check will go away when userns support is
 23          * complete
 24          */
 25         if (!capable(CAP_SYS_ADMIN) || !capable(CAP_SETUID) || !capable(CAP_SETGID))
 26             return -EPERM;
 27     }
 28 
 29     /*
 30     We hope to recycle these flags after 2.6.26
 31     采用向下兼容的模式,2.6.26之后,将CLONE_STOPPED废除
 32     */
 33     if (unlikely(clone_flags & CLONE_STOPPED)) 
 34     {
 35         static int __read_mostly count = 100;
 36 
 37         if (count > 0 && printk_ratelimit()) 
 38         {
 39             char comm[TASK_COMM_LEN];
 40 
 41             count--;
 42             printk(KERN_INFO "fork(): process `%s' used deprecated clone flags 0x%lx
", get_task_comm(comm, current), clone_flags & CLONE_STOPPED);
 43         }
 44     }
 45 
 46     /*
 47     When called from kernel_thread, don't do user tracing stuff.
 48     */
 49     if (likely(user_mode(regs)))
 50         trace = tracehook_prepare_clone(clone_flags);
 51     
 52     /*
 53     Do_fork()函数的核心是copy_process()函数,该函数完成了进程创建的绝大部分工作
 54     分配子进程的task_struct结构,并复制父进程的资源
 55     */
 56     p = copy_process(clone_flags, stack_start, regs, stack_size, child_tidptr, NULL, trace);
 57     /*
 58      * Do this prior waking up the new thread - the thread pointer
 59      * might get invalid after that point, if the thread exits quickly.
 60      */
 61     if (!IS_ERR(p)) 
 62     {
 63         struct completion vfork;
 64 
 65         trace_sched_process_fork(current, p);
 66         /*
 67         /source/include/linux/sched.h
 68         /source/kernel/pid.c
 69         设置pid namespace,不同的namespace中,可以建立相同的pid的进程
 70         */
 71         nr = task_pid_vnr(p);
 72 
 73         if (clone_flags & CLONE_PARENT_SETTID)
 74             put_user(nr, parent_tidptr);
 75 
 76         /*
 77         CLONE_VFORK要求父进程进入子进程,现在初始化一个等待对象
 78         */
 79         if (clone_flags & CLONE_VFORK) 
 80         {
 81             p->vfork_done = &vfork;
 82             init_completion(&vfork);
 83         }
 84 
 85         audit_finish_fork(p);
 86         tracehook_report_clone(regs, clone_flags, nr, p);
 87 
 88         /*
 89          We set PF_STARTING at creation in case tracing wants to use this to distinguish a fully live task from one that hasn't gotten to tracehook_report_clone() yet.  
 90          Now we clear it and set the child going.
 91          */
 92         p->flags &= ~PF_STARTING;
 93 
 94         /*
 95         如果被设置了CLONE_STOPPED标志,则向进程发送SIGSTOP信号
 96         */
 97         if (unlikely(clone_flags & CLONE_STOPPED)) 
 98         {
 99             /*
100             We'll start up with an immediate SIGSTOP.
101             */
102             sigaddset(&p->pending.signal, SIGSTOP);
103             set_tsk_thread_flag(p, TIF_SIGPENDING);
104             __set_task_state(p, TASK_STOPPED);
105         } 
106         else 
107         {
108             //如果没有设置CLONE_STOPPED标志,就把进程加入就绪队列
109             wake_up_new_task(p, clone_flags);
110         }
111 
112         tracehook_report_clone_complete(trace, regs, clone_flags, nr, p);
113 
114         if (clone_flags & CLONE_VFORK) 
115         {
116             freezer_do_not_count();
117             //当前进程进入之前初始化好等待队列
118             wait_for_completion(&vfork);
119             freezer_count();
120             tracehook_report_vfork_done(p, nr);
121         }
122     } else {
123         nr = PTR_ERR(p);
124     }
125     return nr;
126 }

 

do_fork()函数的核心是copy_process()函数,该函数完成了进程创建的绝大部分工作

 

copy_process

完成创建子进程的PCB

 1 /**
 2  * 创建进程描述符以及子进程执行所需要的所有其他数据结构
 3  * 它的参数与do_fork相同。外加子进程的PID。
 4  */
 5 static task_t *copy_process(unsigned long clone_flags,
 6                  unsigned long stack_start,
 7                  struct pt_regs *regs,
 8                  unsigned long stack_size,
 9                  int __user *parent_tidptr,
10                  int __user *child_tidptr,
11                  int pid)
12 {
13     int retval;
14     struct task_struct *p = NULL;
15  
16     /**
17      * 检查clone_flags所传标志的一致性。
18      */
19  
20     /**
21      * 如果CLONE_NEWNS和CLONE_FS标志都被设置,返回错误
22      */
23     if ((clone_flags & (CLONE_NEWNS|CLONE_FS)) == (CLONE_NEWNS|CLONE_FS))
24         return ERR_PTR(-EINVAL);
25  
26     /*
27      * Thread groups must share signals as well, and detached threads
28      * can only be started up within the thread group.
29      */
30     /**
31      * CLONE_THREAD标志被设置,并且CLONE_SIGHAND没有设置。
32      * (同一线程组中的轻量级进程必须共享信号)
33      */
34     if ((clone_flags & CLONE_THREAD) && !(clone_flags & CLONE_SIGHAND))
35         return ERR_PTR(-EINVAL);
36  
37     /*
38      * Shared signal handlers imply shared VM. By way of the above,
39      * thread groups also imply shared VM. Blocking this case allows
40      * for various simplifications in other code.
41      */
42     /**
43      * CLONE_SIGHAND被设置,但是CLONE_VM没有设置。
44      * (共享信号处理程序的轻量级进程也必须共享内存描述符)
45      */
46     if ((clone_flags & CLONE_SIGHAND) && !(clone_flags & CLONE_VM))
47         return ERR_PTR(-EINVAL);
48  
49     /**
50      * 通过调用security_task_create以及稍后调用security_task_alloc执行所有附加的安全检查。
51      * LINUX2.6提供扩展安全性的钩子函数,与传统unix相比,它具有更加强壮的安全模型。
52      */
53     retval = security_task_create(clone_flags);
54     if (retval)
55         goto fork_out;
56  
57     retval = -ENOMEM;
58     /**
59      * 调用dup_task_struct为子进程获取进程描述符。
60      */
61     p = dup_task_struct(current);

copy_process()完成的工作主要是进行必要的检查、初始化、复制必要的数据结构。这里我们重点分析两个函数


1. copy_mm(): 涉及到父子进程的copy on write,以及共享内核虚拟地址的实现
2. copy_thread(): 涉及到父子进程返回的实现(一次调用、2次返回)

//复制父进程的内核态堆栈到子进程
retval = copy_thread(clone_flags, stack_start, stack_size, p, regs);

应用程序通过fork()系统调用进入内核空间,其内核态堆栈上保存着该进程的"进程上下文(寄存器状态)",通过copy_thread将复制父进程的内核态堆栈上的"进程上下文"到子进程中,同时把子进程堆栈上的EAX设置为0。由于父子进程的代码和数据是共享的,所以在返回后将接着执行,所以会发现以下现象

1. 父子进程从同一个代码位置开始继续执行: 因为它们的"进程上下文"相同
2. 父进程调用fork()返回子进程的PID: 父进程是正常调用
3. 子进程返回0,因为内核态的EAX被设置为了0
4. 父子进程不一定同时开始执行,但会有从内核态返回2次,一次是父进程,一次是子进程

分配PCB,继承父进程的PCB中的值,只是将特有的信息改过来。每个进程都有task_thread,thread_info结构体保存的是进程上下文的信息。要修改thread_info *info,子进程的task_struct的成员struct thread_info *info指向自己的struct thread_info,而且struct thread_info结构体的成员struct task_struct *p指向子进程自己的struct task_struct.

 1 /**
 2  * 为子进程获取进程描述符。
 3  */
 4 static struct task_struct *dup_task_struct(struct task_struct *orig)
 5 {
 6     struct task_struct *tsk;
 7     struct thread_info *ti;
 8  
 9     /**
10      * prepare_to_copy中会调用unlazy_fpu。
11      * 它把FPU、MMX和SSE/SSE2寄存器的内容保存到父进程的thread_info结构中。
12      * 稍后,dup_task_struct将把这些值复制到子进程的thread_info中。
13      */
14     prepare_to_copy(orig);
15  
16     /**
17      * alloc_task_struct宏为新进程获取进程描述符,并将描述符保存到tsk局部变量中。
18      */
19     tsk = alloc_task_struct();
20     if (!tsk)
21         return NULL;
22  
23     /**
24      * alloc_thread_info宏获取一块空闲内存区,用来存放新进程的thread_info结构和内核栈。
25      * 这块内存区字段的大小是8KB或者4KB。
26      */
27     ti = alloc_thread_info(tsk);
28     if (!ti) {
29         free_task_struct(tsk);
30         return NULL;
31     }
32  
33     /** 
34      * 将current进程描述符的内容复制到tsk所指向的task_struct结构中,然后把tsk_thread_info置为ti
35      * 将current进程的thread_info内容复制给ti指向的结构中,并将ti_task置为tsk.
36      */
37     *ti = *orig->thread_info;
38     *tsk = *orig;
39     tsk->thread_info = ti;
40     ti->task = tsk;

2、其中copy_files()复制父进程打开的文件描述符

 1 /**
 2  * 复制进程文件描述符
 3  */
 4 static int copy_files(unsigned long clone_flags, struct task_struct * tsk)

3、其中copy_mm()复制地址空间,struct mm_struct *mm,*active_mm, mm表示:进程所拥有的内存空间的描述符,对于内核线程的mm为NULL,active_mm表示:进程运行时所使用的进程描述符。

       1、判断是否设置了CLONE_VM标志,如果设置,创建线程,新线程共享父进程的地址空间,将mm_users加1,然后mm=oldmm,把父进程的mm_struct指针赋给子进程的mm_struct.

                      如果没有设置,当前进程分配一个新的内存描述符,mm=allocate_mm(),  将它的地址放在子进程的mm中。再把父进程(*oldmm)的内容拷进(*mm)中

 1 /**
 2  * 当创建一个新的进程时,内核调用copy_mm函数,
 3  * 这个函数通过建立新进程的所有页表和内存描述符来创建进程的地址空间。
 4  * 通常,每个进程都有自己的地址空间,但是轻量级进程共享同一地址空间,即允许它们对同一组页进行寻址。
 5  */
 6 static int copy_mm(unsigned long clone_flags, struct task_struct * tsk)
 7 {
 8     struct mm_struct * mm, *oldmm;
 9     int retval;
10  
11     tsk->min_flt = tsk->maj_flt = 0;
12     tsk->nvcsw = tsk->nivcsw = 0;
13  
14     tsk->mm = NULL;
15     tsk->active_mm = NULL;
16  
17     /*
18      * Are we cloning a kernel thread?
19      *
20      * We need to steal a active VM for that..
21      */
22     oldmm = current->mm;
23     /**
24      * 内核线程??
25      */
26     if (!oldmm)
27         return 0;
28  
29     /**
30      * 指定了CLONE_VM标志,表示创建线程。
31      */
32     if (clone_flags & CLONE_VM) {
33         /**
34          * 新线程共享父进程的地址空间,所以需要将mm_users加一。
35          */
36         atomic_inc(&oldmm->mm_users);
37         mm = oldmm;
38         /*
39          * There are cases where the PTL is held to ensure no
40          * new threads start up in user mode using an mm, which
41          * allows optimizing out ipis; the tlb_gather_mmu code
42          * is an example.
43          */
44         /**
45          * 如果其他CPU持有进程页表自旋锁,就通过spin_unlock_wait保证在释放锁前,缺页处理程序不会结果。
46          * 实际上,这个锁除了保护页表,还必须禁止创建新的轻量级进程。因为它们共享mm描述符
47          */
48         spin_unlock_wait(&oldmm->page_table_lock);
49         /**
50          * 在good_mm中,将父进程的地址空间赋给子进程。
51          * 注意前面对mm的赋值,表示了新线程使用的mm
52          * 完了,就这么简单
53          */
54         goto good_mm;
55     }
56  
57     /**
58      * 没有CLONE_VM标志,就必须创建一个新的地址空间。
59      * 必须要有地址空间,即使此时并没有分配内存。
60      */
61     retval = -ENOMEM;
62     /**
63      * 分配一个新的内存描述符。把它的地址存放在新进程的mm中。
64      */
65     mm = allocate_mm();
66     if (!mm)
67         goto fail_nomem;
68  
69     /* Copy the current MM stuff.. */
70     /**
71      * 并从当前进程复制mm的内容。
72      */
73     memcpy(mm, oldmm, sizeof(*mm));
74     if (!mm_init(mm))
75         goto fail_nomem;
76  
77     /**
78      * 调用依赖于体系结构的init_new_context。
79      * 对于80X86来说,该函数检查当前进程是否有定制的局部描述符表。
80      * 如果有,就复制一份局部描述符表并把它插入tsk的地址空间
81      */
82     if (init_new_context(tsk,mm))
83         goto fail_nocontext;
84  
85     /**
86      * dup_mmap不但复制了线程区和页表,也设置了mm的一些属性.
87      * 它也会改变父进程的私有,可写的页为只读的,以使写时复制机制生效。
88      */
89     retval = dup_mmap(mm, oldmm);

2、dup_mmap(mm, oldmm)

        复制线性区和页表,设置mm的一些属性,改变父进程的私有,可写的页为只读的,以使写时拷贝技术生效。

  1 /**
  2  * 既复制父进程的线性区,也复制它的页表。
  3  */
  4 static inline int dup_mmap(struct mm_struct * mm, struct mm_struct * oldmm)

4、除此之外,我们还要复制父进程的内核栈,copy_thread()

   调用copy_thread,用发出clone系统调用时CPU寄存器的值(它们保存在父进程的内核栈中)
   来初始化子进程的内核栈。不过,copy_thread把eax寄存器对应字段的值(这是fork和clone系统调用在子进程中的返回值)
   强行置为0。子进程描述符的thread.esp字段初始化为子进程内核栈的基地址。ret_from_fork的地址存放在thread.eip中。
   如果父进程使用IO权限位图。则子进程获取该位图的一个拷贝。
   最后,如果CLONE_SETTLS标志被置位,则子进程获取由CLONE系统调用的参数tls指向的用户态数据结构所表示的TLS段。

    这就是为什么父子进程沿着统一位置执行,以及子进程的返回值是0。

 1 int copy_thread(int nr, unsigned long clone_flags, unsigned long esp,
 2     unsigned long unused,
 3     struct task_struct * p, struct pt_regs * regs)

fork流程

fork vfork clone 区别

fork:

fork创建一个进程时,子进程只是完全复制父进程的资源,复制出来的子进程有自己的task_struct结构和pid,但却复制父进程其它所有的资源。例如,要是父进程打开了五个文件,那么子进程也有五个打开的文件,而且这些文件的当前读写指针也停在相同的地方。所以,这一步所做的是复制。这样得到的子进程独立于父进程, 具有良好的并发性,但是二者之间的通讯需要通过专门的通讯机制,如:pipe,共享内存等机制, 另外通过fork创建子进程,需要将上面描述的每种资源都复制一个副本。
这样看来,fork是一个开销十分大的系统调用,这些开销并不是所有的情况下都是必须的,比如某进程fork出一个子进程后,其子进程仅仅是为了调用exec执行另一个可执行文件,那么在fork过程中对于虚存空间的复制将是一个多余的过程。但由于现在Linux中是采取了copy-on-write(COW写时复制)技术,为了降低开销,fork最初并不会真的产生两个不同的拷贝,因为在那个时候,大量的数据其实完全是一样的。写时复制是在推迟真正的数据拷贝。若后来确实发生了写入,那意味着parent和child的数据不一致了,于是产生复制动作,每个进程拿到属于自己的那一份,这样就可以降低系统调用的开销。所以有了写时复制后呢,vfork其实现意义就不大了。
fork()调用执行一次返回两个值,对于父进程,fork函数返回子程序的进程号,而对于子程序,fork函数则返回零,这就是一个函数返回两次的本质。
在fork之后,子进程和父进程都会继续执行fork调用之后的指令。子进程是父进程的副本。它将获得父进程的数据空间,堆和栈的副本,这些都是副本,父子进程并不共享这部分的内存。也就是说,子进程对父进程中的同名变量进行修改并不会影响其在父进程中的值。但是父子进程又共享一些东西,简单说来就是程序的正文段。正文段存放着由cpu执行的机器指令,通常是read-only的。

vfork:

vfork系统调用不同于fork,用vfork创建的子进程与父进程共享地址空间,也就是说子进程完全运行在父进程的地址空间上,如果这时子进程修改了某个变量,这将影响到父进程。
       其次,子进程在vfork()返回后直接运行在父进程的栈空间,并使用父进程的内存和数据。这意味着子进程可能破坏父进程的数据结构或栈,造成失败。为了避免这些问题,需要确保一旦调用vfork(),子进程就不从当前的栈框架中返回,并且如果子进程改变了父进程的数据结构就不能调用exit函数。子进程还必须避免改变全局数据结构或全局变量中的任何信息,因为这些改变都有可能使父进程不能继续。
但此处有一点要注意的是用vfork()创建的子进程必须显示调用exit()来结束,否则子进程将不能结束,而fork()则不存在这个情况。
vfork也是在父进程中返回子进程的进程号,在子进程中返回0。
用vfork创建子进程后,父进程会被阻塞直到子进程调用exec(exec,将一个新的可执行文件载入到地址空间并执行之。)或exit。vfork的好处是在子进程被创建后往往仅仅是为了调用exec执行另一个程序,因为它就不会对父进程的地址空间有任何引用,所以对地址空间的复制是多余的 ,因此通过vfork共享内存可以减少不必要的开销
再次强调:在使用vfork()时,必须在子进程中调用exit()函数调用,否则会出现:__new_exitfn: Assertion `l != ((void *)0)' failed 错误!而且,现在这个函数已经很少使用了!

clone:

    系统调用fork()和vfork()是无参数的,而clone()则带有参数。fork()是全部复制,vfork()是共享内存,而clone()是则可以将父进程资源有选择地复制给子进程,而没有复制的数据结构则通过指针的复制让子进程共享,具体要复制哪些资源给子进程,由参数列表中的clone_flags决决定。
fork不对父子进程的执行次序进行任何限制,fork返回后,子进程和父进程都从调用fork函数的下一条语句开始行,但父子进程运行顺序是不定的,它取决于内核的调度算法;而在vfork调用中,子进程先运行,父进程挂起,直到子进程调用了exec或exit之后,父子进程的执行次序才不再有限制;clone中由标志CLONE_VFORK来决定子进程在执行时父进程是阻塞还是运行,若没有设置该标志,则父子进程同时运行,设置了该标志,则父进程挂起,直到子进程结束为止

原文地址:https://www.cnblogs.com/mysky007/p/12331200.html