LINUX内核研究----内核角度看fork(),clone(),vfork() 的异同
Linux系统将进程的创建与目标进程的执行分成两步
第一步是从已经存在的进程那里像细胞分裂一样复制出一个子进程。子进程有自己的task_struct结构和系统的堆栈空间,但是和父进程共享其他所有资源。比如说文件描述符,文件的读写指针都停留在一个地方,父子共用。
LINUX为复制提供三个系统调用:
fork():
父进程的所有的资源通过PCB复制给子进程。那么fork底层究竟做了哪些复制?一个函数返回两次是怎么做到的?通过源码的阅读再分析。
clone():
可以将资源有选择的复制给子进程,没有复制的数据结构通过指针的复制共享。一般用来创建线程
vfork():
vfork()跟fork()类似,都是创建一个子进程,这两个函数的的返回值也具有相同的含义。但是vfork()创建的子进程基本上只能做一件事,那就是立即调用_exit()函数或者exec函数族成员,调用任何其它函数(包括exit())、修改任何数据(除了保存vfork()返回值的那个变量)、执行任何其它语句(包括return)都是不应该的。此外,调用vfork()之后,父进程会一直阻塞,直到子进程调用_exit()终止,或者调用exec函数族成员运行其他进程。
vfork()和fork()之间的几点不同导致了以上原因的出现:
1. fork()会复制父进程的页表,而vfork()不会复制,直接让子进程共用父进程的页表;
2. fork()使用了写时复制技术,而vfork()没有,它任何时候都不会复制父进程地址空间。
3. fork()不会阻塞父进程,而vfork()会阻塞。
所以vfork()产生的子进程跟父进程完全共同使用同一个地址空间,甚至共享同一个函数堆栈!也就是子进程中对任何数据变量的修改,不管是局部的还是全局的,都会影响到父进程。而任何一个函数调用都会修改栈空间,这就是为什么vfork()的子进程不能随便调用别的函数。并return语句会释放局部变量,并有pop()栈的行为,回到上级函数执行。exit直接退掉,没有退栈的操作也就是不影响栈的内存。c++ 中,return会调用局部对象的析构函数,exit不会。(注:exit不是系统调用,是glibc对系统调用 _exit()的封装)。exit在调用_exit()时会做许多其他的事情。
这三个系统调用的底层都是通过do_fork()内核函数实现,只不过是通过对do_fork()传递的不同参数来实现不同的功能。
第一个参数clone_flags由两部分组成,其最低的字节为信号类型,用以规定子进程去世时应该向父进程发出的信号SIGCHLD;
第二部分是一些表示资源和特性的标志位,来标识对于父进程的资源是拷贝还是通过指针共享。
fork(),这些标志全为0,表示要全部拷。
vfork(),则是父、子进程共用虚存空间。
clone(),由调用者设定并作为参数传递,一般是用来创建线程
所以,fork()创建进程;
vfork()是创建的线程,它是创建进程的中间步骤,如若创建进程后会直接调用exec(),则可用vfork()来减少不必要的拷贝;
clone()创建线程,可以是内核线程,也可以是用户线程
统调用fork的内核入口是sys_fork()
而sys_fork(),sys_clone(),sys_vfork()都是对do_fork()的封装。
具体的调用过程看图。
复制:只是基本资源的复制,task_struct数据结构、系统堆栈空间。而对父进程代码和全局变量则并不需要复制,而是通过读共享,写时拷贝(即写的时候复制),所以说复制的代价比较低。
基于复制的特点,有利于父子进程通过管道来实现一种简单有效的进程间的通信,shell中的管道机制。
第二步是目标程序的执行:子进程调用execve()函数执行可执行文件的影像开始新的进程,涉及程序的加载过程。程序的加载执行过程在我的另外一篇博客有详细的分析
系统调用execve()的内核入口sys_execve()