理解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的实现分为以下两步
- 复制进程资源
- 执行该进程
复制进程的资源包括以下几步:
- 进程pcb
- 程序体,即代码段数据段等
- 用户栈
- 内核栈
- 虚拟内存池
- 页表
进行进程的话就比较简单了,只需要将其加入到就绪队列即可,接下来就等待cpu的调度了
将父进程的pcb、虚拟地址位图拷贝给子进程
fork() ,pthread_creat(), vfork()的系统调用分别是sys_fork(),sys_clone(), sys_vfork(),它们的底层都用的是do_fork(),只是传的参数,和标志不同。
对fork源代码分析
- 定义PCB指针struct task_struct *p;
- 分配PID,cat /proc/sys/kernel/pid_max命令可以查看一个系统支持的最大进程数,进程数的范围0~32768,理论值。
- 调用copy_process方法,创建子进程的task_struct.
do_fork
1 /** 2 * 负责处理clone,fork,vfork系统调用。 3 * clone_flags-与clone的flag参数相同 4 * stack_start-与clone的child_stack相同 5 * regs-指向通用寄存器的值。是在从用户态切换到内核态时被保存到内核态堆栈中的。 6 * stack_size-未使用,总是为0 7 * parent_tidptr,child_tidptr-clone中对应参数ptid,ctid相同 8 */ 9 long do_fork(unsigned long clone_flags, 10 unsigned long stack_start, 11 struct pt_regs *regs, 12 unsigned long stack_size, 13 int __user *parent_tidptr, 14 int __user *child_tidptr) 15 { 16 struct task_struct *p; 17 int trace = 0; 18 /** 19 * 申请PID,通过查找pidmap_array位图,为子进程分配新的pid参数. 20 */ 21 long pid = alloc_pidmap(); 22 23 if (pid < 0) 24 return -EAGAIN; 25 /** 26 * 如果父进程正在被跟踪,就检查debugger程序是否想跟踪子进程.并且子进程不是内核进程(CLONE_UNTRACED未设置) 27 * 那么就设置CLONE_PTRACE标志. 28 */ 29 if (unlikely(current->ptrace)) { 30 trace = fork_traceflag (clone_flags); 31 if (trace) 32 clone_flags |= CLONE_PTRACE; 33 } 34 //之前是对参数的检查 35 /** 36 * copy_process复制进程描述符.如果所有必须的资源都是可用的,该函数返回刚创建的task_struct描述符的地址. 37 * 这是创建进程的关键步骤. 38 */ 39 p = copy_process(clone_flags, stack_start, regs, stack_size, parent_tidptr, child_tidptr, pid)
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);
在p=dup_task_struct(current)之前都是对进程的一些判断(检查标志位合法性)和安全性检查
1、分配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 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来决定子进程在执行时父进程是阻塞还是运行,若没有设置该标志,则父子进程同时运行,设置了该标志,则父进程挂起,直到子进程结束为止