fork,vfork,clone

fork

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

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

  但实际上,linux为了提高fork的效率,采用了copy-on-write技术,fork后,这两个虚拟地址实际上指向相同的物理地址。(内存页),只有任何一个进程试图修改这个虚拟地址里的内容前(而当子进程改变了父进程的变量时候,会通过copy_on_write的手段为所涉及的页面建立一个新的副本),两个虚拟地址才会指向不同的物理地址。新的物理地址的内容从源物理地址中复制得到。
  问题:fork采用了这种写时复制的机制,那么fork出来子进程后,理论上子进程和父进程那个先调度呢?
       fork之后内核一般会通过将子进程放在队列的前面,以让子进程先执行,因为很多情况下子进程要马上执行exec,会清空栈、堆,这些和父进程共享的空间,加载新的代码段。。这就避免了父进程“写时复制”拷贝共享页面的机会。如果父进程先调度很可能写共享页面,而子进程什么也没做,会产生“写时复制”的无用功。所以,一般子进程先调度。避免因无意义的复制而造成效率的下降。而事实上同步执行不分先后

  fork调用的一个奇妙之处就是它仅仅被调用一次,却能能够返回两次,它可能有三种不同的返回值。如果创建成功一个子进程,对于父进程来说是返回子进程的ID.而对于子进程来说就是返回0.而返回-1代表创建子进程失败.

fork,vfork,clone

如fork执行成功。fork返回新创建子进程的进程ID。在子进程中,fork返回0,所以可以通过返回值来判断当前是子进程还是父进程

如fork执行失败。1)当前的进程数已经达到了系统规定的上限,这时errno的值被设置为EAGAIN。

        2)系统内存不足,这时errno的值被设置为ENOMEM。

父进程和子进程哪个进程先执行要看系统的进程调度策略

#include <sys/types.h>
#include <unistd.h>
#include <stdio.h>
#include <signal.h>
#include <errno.h>

int main(void)
{
    pid_t pid ;
    signal(SIGCHLD,SIG_IGN);
    printf("before fork pid:%d\n",getpid());
    int abc = 10;      
    pid = fork();          
    if(pid == -1)   //错误返回
    {
        perror("tile");
        return -1;
    }
    if(pid > 0)     //父进程空间
    {
        abc++;
        printf("parent:pid:%d \n",getpid());
        printf("abc:%d \n",abc);
        sleep(20);
    }
    else if(pid == 0)
    {   //子进程空间
        abc++;
        printf("child:%d,parent: %d\n",getpid(),getppid());
        printf("abc:%d",abc);
    }
    printf("fork after...\n");
}

总结:

1)fork系统调用之后,父进程和子进程交替执行,并且它们处于不同空间中。

int main()
{
   int pid;
   int num=1;
   pid=fork();
   if(pid>0)
  {
       num++;
       printf("in parent:num:%d addr:%x\n",num,&num);
   }
   else if(pid==0)
  {
       printf("in child:num:%d addr:%x\n",num,&num);
   }
}

答案:父子进程中输出的num不同,num地址相同 
虚拟地址空间,num地址的值相同,但是其实真实的物理地址却不一样。

如果安装两个进程各处在独自的虚拟进程地址空间分析的话,这个题很容易会选择num地址不相同,但是Linux中资源分配都是虚拟机制,也就是说,他们还是共用一个虚拟的地址,但是映射到物理内存就可能不一样

 

  2)fork()函数的一次调用返回2次返回,这个有点抽象难理解,此时二个进程处于独立的空间,它们各自执行者自己的东西,不产生冲突,所以返回2次一次pid ==0,一次pid大于0.而至于是先子进程还是父进程先执行,这没有确切的规定,是随机的.

  3)将fork()返回值大于零设置为父进程,这是因为子进程获得父进程的pid(getppid())相对容易,而父进程获子进程pid叫难,所以在在fork()系统调用中将子进程的pid字节有它自己返回给父进程.

  4)forl()的子执行过程在fork()之后并不是从头开始,因为在fork()之前,父进程已经为子进程搭建好了运行环境了.所以从fork()下一条语句开始。

 

vfork 

  vfork也是创建一个子进程,但是子进程共享父进程的空间。在vfork创建子进程之后,父进程阻塞,直到子进程执行了exec()或者exit()。vfork最初是因为fork没有实现COW机制,而很多情况下fork之后会紧接着exec,而exec的执行相当于之前fork复制的空间全部变成了无用功,所以设计了vfork。而现在fork使用了COW机制,唯一的代价仅仅是复制父进程页表的代价,所以vfork不应该出现在新的代码之中。      

  1. vfork创建出来的不是真正意义上的进程,而是一个线程,因为它缺少经常要素有独立的存储空间
  2. 由vfork创建的子进程要先于父进程执行,子进程执行时,父进程处于挂起状态,子进程执行完,唤醒父进程。除非子进程exit或者execve才会唤起父进程
  3. 用vfork()创建的子进程必须显示调用exit()来结束,否则子进程将不能结束,而fork()则不存在这个情况。
  4. vfork的好处是在子进程被创建后往往仅仅是为了调用exec执行另一个程序,因为它就不会对父进程的地址空间有任何引用,所以对地址空间的复制是多余的 ,因此通过vfork共享内存可以减少不必要的开销。

clone

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

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

int clone(int (*fn)(void *), void *child_stack, int flags, void *arg);
  1. fn为函数指针,此指针指向一个函数体,即想要创建进程的静态程序(我们知道进程的4要素,这个就是指向程序的指针,就是所谓的“剧本", );
  2. child_stack为给子进程分配系统堆栈的指针(在linux下系统堆栈空间是2页面,就是8K的内存,其中在这块内存中,低地址上放入了值,这个值就是进程控制块task_struct的值);
  3. arg就是传给子进程的参数一般为(0);
  4. 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线程标准,子进程与父进程共享相同的线程群

区别:

  1. fork不对父子进程的执行次序进行任何限制,fork返回后,子进程和父进程都从调用fork函数的下一条语句开始行,但父子进程运行顺序是不定的,它取决于内核的调度算法
  2. vfork调用中,子进程先运行,父进程挂起,直到子进程调用了exec或exit之后,父子进程的执行次序才不再有限制
  3. clone中由标志CLONE_VFORK来决定子进程在执行时父进程是阻塞还是运行,若没有设置该标志,则父子进程同时运行,设置了该标志,则父进程挂起,直到子进程结束为止。
  4.  clone和fork的调用方式很不相同,clone调用需要传入一个函数,该函数在子进程中执行。

  5. clone和fork最大不同在于clone不再复制父进程的栈空间,而是自己创建一个新的。 (void *child_stack,)也就是第二个参数,需要分配栈指针的空间大小,所以它不再是继承或者复制,而是全新的创造。

转自:https://www.cnblogs.com/tianzeng/p/9309759.html