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

由于生活的复杂,这是一个并行的世界,在同一时刻,会发生不少奇妙的事情,北方下雪,南方下雨,这里在吃饭,那边在睡觉,有人在学习,有人在运动,因此这时一个多彩多姿的世界,天天都发生着不少事情,因此要想很好的表现这个世界,协调完成一件事儿,就得用到多进程或者多线程。因此进程是程序猿必定会接触到的一个东西,他能使咱们的程序效率提升,高效的完成多任务,并行执行。下面主要看看产生进程或线程的三个函数。
       fork,vfork,clone都是linux的系统调用,这三个函数分别调用了sys_fork、sys_vfork、sys_clone,最终都调用了do_fork函数,差异在于参数的传递和一些基本的准备工做不一样,主要用来linux建立新的子进程或线程(vfork创造出来的是线程)。html

       进程的四要素:linux

       (1)有一段程序供其执行(不必定是一个进程所专有的),就像一场戏必须有本身的剧本。
       (2)有本身的专用系统堆栈空间(私有财产)
       (3)有进程控制块(task_struct)(“有身份证,PID”)
       (4)有独立的存储空间。
          缺乏第四条的称为线程,若是彻底没有用户空间称为内核线程共享用户空间的称为用户线程。
1、fork()多线程

           fork()函数调用成功:返回两个值; 父进程:返回子进程的PID;子进程:返回0;
                                    失败:返回-1;函数

          fork 创造的子进程复制了父亲进程的资源(写时复制技术),包括内存的内容task_struct内容(2个进程的pid不一样)。这里是资源的复制不是指针的复制。学习

          说到fork(),就不得不说一个技术:(Copy-On-Write)写时复制技术。ui

          盗用一张图,感受描述的确实挺到位:spa

 

 

          咱们都知道fork建立进程的时候,并无真正的copy内存(听着好像矛盾了,资源的赋值为何有没有真正的赋值呢?),由于咱们知道,对于fork来说,有一个很讨厌的东西叫exec系列的系统调用,它会勾引子进程另起炉灶。若是建立子进程就要内存拷贝的的话,一执行exec,辛辛苦苦拷贝的内存又被彻底放弃了。因为fork()后会产生一个和父进程彻底相同的子进程,但子进程在此后多会exec系统调用,出于效率考虑,linux中引入了“写时复制技术-Copy-On-Write”。.net

            换言之,在fork()以后exec以前两个进程用的是相同的物理空间(内存区),先把页表映射关系创建起来,并不真正将内存拷贝。子进程的代码段、数据段、堆栈都是指向父进程的物理空间,也就是说,二者的虚拟空间不一样,但其对应的物理空间是同一个。当父进程中有更改相应段的行为发生时,如进程写访问,再为子进程相应的段分配物理空间,若是不是由于exec,内核会给子进程的数据段、堆栈段分配相应的物理空间(至此二者有各自的进程空间,互不影响),而代码段继续共享父进程的物理空间(二者的代码彻底相同)。而若是是由于exec,因为二者执行的代码不一样,子进程的代码段也会分配单独的物理空间。fork时子进程得到父进程数据空间、堆和栈的复制因此变量的地址(固然是虚拟地址)是同样的。线程

         具体过程是这样的:
         fork子进程彻底复制父进程的栈空间,也复制了页表,但没有复制物理页面,因此这时虚拟地址相同,物理地址也相同,可是会把父子共享的页面标记为“只读”,若是父子进程一直对这个页面是同一个页面,直到其中任何一个进程要对共享的页面“写操做”,这时内核会复制一个物理页面给这个进程使用,同时修改页表。而把原来的只读页面标记为“可写”,留给另一个进程使用。这就是所谓的“写时复制”。
      在理解上:能够认为fork后,这两个相同的虚拟地址指向的是不一样的物理地址,这样方便理解父进程之间的独立性。
      但实际上,linux为了提升fork的效率,采用了copy-on-write技术,fork后,这两个虚拟地址实际上指向相同的物理地址。(内存页),只有任何一个进程试图修改这个虚拟地址里的内容前,两个虚拟地址才会指向不一样的物理地址。新的物理地址的内容从源物理地址中复制获得。
问题:fork采用了这种写时复制的机制,那么fork出来子进程后,理论上子进程和父进程那个先调度呢(理论效率分析,我的以为有必定的道理)?
       fork以后内核通常会经过将子进程放在队列的前面,以让子进程先执行,由于不少状况下子进程要立刻执行exec,会清空栈、堆,这些和父进程共享的空间,加载新的代码段。。这就避免了父进程“写时复制”拷贝共享页面的机会。若是父进程先调度极可能写共享页面,而子进程什么也没作,会产生“写时复制”的无用功。因此,通常子进程先调度。避免因无心义的复制而形成效率的降低。
下面来看一个例子:
#include"stdio.h"
int main()
{
int count = 1;
int child;
if(0== fork()) //子进程成功返回0;
{ //开始建立子进程
printf("This is son, his count is: %d. and his pid is: %d\n", ++count, getpid());//子进程的内容
}
else
{
printf("This is father, his count is: %d, his pid is: %d\n", count, getpid());
}
}
运行结果:设计

 

         从结果能够看出子进程和父进程的PID不一样,内存资源count是值得复制,子进程改变了count的值,而父进程中的count没有被改变。有人认为这样大批量的复制会致使执行效率太低。其实在复制过程当中,子进程复制了父进程的task_struct,系统堆栈空间和页面表,这意味着上面的程序,咱们没有执行count++前,其实子进程和父进程的count指向的是同一块内存。而当子进程改变了父进程的变量时候,会经过copy_on_write的手段为所涉及的页面创建一个新的副本。因此当咱们执行++count后,这时候子进程才新建了一个页面复制原来页面的内容,基本资源的复制是必须的,并且是高效的。总体看上去就像是父进程的独立存储空间也复制了一遍。这将和下面的vfork有必定的区别。
其次,咱们看到子进程和父进程直接没有互相干扰,明显2者资源都独立了。咱们看下面程序
#include"stdio.h"
int main() {
int count = 1;
int child;
int i;
if(!(child = fork()))
{

for(i = 0; i <20; i++)
{
printf("This is son, his count is: %d. and his pid is: %d\n", i, getpid());
}
}
else
{
for(i=0;i<20;i++)
printf("This is father, his count is: %d, his pid is: %d\n", count, getpid());
}
}
运行结果:

 

 

从运行的结果能够看出父子2个进程是同步运行的,其实不分前后。
2、vfork() 

    vfork是一个过期的应用,vfork也是建立一个子进程,可是子进程共享父进程的空间。在vfork建立子进程以后,父进程阻塞,直到子进程执行了exec()或者exit()。vfork最初是由于fork没有实现COW机制,而不少状况下fork以后会紧接着exec,而exec的执行至关于以前fork复制的空间所有变成了无用功,因此设计了vfork。而如今fork使用了COW机制,惟一的代价仅仅是复制父进程页表的代价,因此vfork不该该出如今新的代码之中。      

     vfork建立出来的不是真正意义上的进程,而是一个线程,由于它缺乏常常要素(4),独立的内存资源,看下面的程序:

#include "stdio.h"
int main() {
int count = 1;
int child;
printf("Before create son, the father's count is:%d\n", count);
if(!(child = vfork()))
{
printf("This is son, his pid is: %d and the count is: %d\n", getpid(), ++count);
exit(1);
}
else
{
printf("After son, This is father, his pid is: %d and the count is: %d, and the child is: %d\n", getpid(), count, child);
}
}
运行结果:

 

      从运行结果能够看到vfork建立出的子进程(线程)共享了父进程的count变量,这一次是指针复制,2者的指针指向了同一个内存,因此子进程修改了count变量,父进程的 count变量一样受到了影响。

    另外由vfork建立的子进程要先于父进程执行,子进程执行时,父进程处于挂起状态,子进程执行完,唤醒父进程。除非子进程exit或者execve才会唤起父进程,看下面程序:

#include "stdio.h"
int main()
{
int count = 1;
int child;
printf("Before create son, the father's count is:%d\n", count);
if(!(child = vfork()))
{
int i;
for(i = 0; i < 100; i++)
{
printf("This is son, The i is: %d\n", i);
count++;
if(i == 20)
{
printf("This is son, his pid is: %d and the count is: %d\n", getpid(), ++count);
exit(1);
}
}


}
else
{
printf("After son, This is father, his pid is: %d and the count is: %d, and the child is: %d\n", getpid(), count, child);
}
}
运行结果:

 

从运行的结果能够看到父进程老是等子进程执行完毕后才开始继续执行。
3.clone

Linux上建立线程通常使用的是pthread库 实际上linux也给咱们提供了建立线程的系统调用,就是clone

       clone是Linux为建立线程设计的(虽然也能够用clone建立进程)。因此能够说clone是fork的升级版本,不只能够建立进程或者线程,还能够指定建立新的命名空间(namespace)、有选择的继承父进程的内存、甚至能够将建立出来的进程变成父进程的兄弟进程等等。

        clone函数功能强大,带了众多参数,它提供了一个很是灵活自由的常见进程的方法。所以由他建立的进程要比前面2种方法要复杂。clone可让你有选择性的继承父进程的资源,你能够选择像vfork同样和父进程共享一个虚存空间,从而使创造的是线程,你也能够不和父进程共享,你甚至能够选择创造出来的进程和父进程再也不是父子关系,而是兄弟关系。先有必要说下这个函数的结构:

      int clone(int (*fn)(void *), void *child_stack, int flags, void *arg);
     fn为函数指针,此指针指向一个函数体,即想要建立进程的静态程序(咱们知道进程的4要素,这个就是指向程序的指针,就是所谓的“剧本", );
     child_stack为给子进程分配系统堆栈的指针(在linux下系统堆栈空间是2页面,就是8K的内存,其中在这块内存中,低地址上放入了值,这个值就是进程控制块task_struct的值);
     arg就是传给子进程的参数通常为(0);
     flags为要复制资源的标志,描述你须要从父进程继承那些资源(是资源复制仍是共享,在这里设置参数:
下面是flags能够取的值
  标志                    含义
  CLONE_PARENT   建立的子进程的父进程是调用者的父进程,新进程与建立它的进程成了“兄弟”而不是“父子”
  CLONE_FS           子进程与父进程共享相同的文件系统,包括root、当前目录、umask
  CLONE_FILES      子进程与父进程共享相同的文件描述符(file descriptor)表
  CLONE_NEWNS   在新的namespace启动子进程,namespace描述了进程的文件hierarchy
  CLONE_SIGHAND   子进程与父进程共享相同的信号处理(signal handler)表
  CLONE_PTRACE   若父进程被trace,子进程也被trace
  CLONE_VFORK     父进程被挂起,直至子进程释放虚拟内存资源
  CLONE_VM           子进程与父进程运行于相同的内存空间
  CLONE_PID          子进程在建立时PID与父进程一致
  CLONE_THREAD    Linux 2.4中增长以支持POSIX线程标准,子进程与父进程共享相同的线程群
下面的例子是建立一个线程(子进程共享了父进程虚存空间,没有本身独立的虚存空间不能称其为进程)。父进程被挂起当子线程释放虚存资源后再继续执行。

#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <sched.h>
#define FIBER_STACK 8192
int a;
void * stack;
int do_something(){
a=10;
printf("This is son, the pid is:%d, the a is: %d\n", getpid(), a);
free(stack);
exit(1);
}
int main() {
void * stack;
a = 1;
stack = malloc(FIBER_STACK);//为子进程申请系统堆栈
if(!stack) {
printf("The stack failed\n");
exit(0);
}
printf("creating son thread!!!\n");
clone(&do_something, (char *)stack + FIBER_STACK, CLONE_VM|CLONE_VFORK, 0);//建立子线程
printf("This is father, my pid is: %d, the a is: %d\n", getpid(), a);
exit(1);
}
运行的结果:

son的PID:10692;

father的PID:10691;

parent和son中的a都为10;因此证实他们公用了一份变量a,是指针的复制,而不是值的复制。

问题:clone和fork的区别:

     (1) clone和fork的调用方式很不相同,clone调用须要传入一个函数,该函数在子进程中执行

      (2)clone和fork最大不一样在于clone再也不复制父进程的栈空间,而是本身建立一个新的。 (void *child_stack,)也就是第二个参数,须要分配栈指针的空间大小,因此它再也不是继承或者复制,而是全新的创造。

 

 

博客资料参考:

http://blog.csdn.net/xy010902100449/article/details/44851453

http://www.cnblogs.com/blankqdb/archive/2012/08/23/2652386.html

http://blog.chinaunix.net/uid-24774106-id-3361500.html
http://www.linuxidc.com/Linux/2015-03/114888.htm
http://igaozh.iteye.com/blog/1677969

http://blog.chinaunix.net/uid-24410388-id-195503.html

http://blog.chinaunix.net/uid-18921523-id-265538.html

http://blog.csdn.net/wdjhzw/article/details/25614969

感谢各位博主的分享!
---------------------
做者:尘虚緣_KY
来源:CSDN
原文:https://blog.csdn.net/gogokongyin/article/details/51178257
版权声明:本文为博主原创文章,转载请附上博文连接!

感谢各位博主的分享!

相关文章
相关标签/搜索