zoukankan      html  css  js  c++  java
  • 浅析linux中的fork、vfork和clone

         各种大神的混合,做个笔记。

                    http://blog.sina.com.cn/s/blog_7598036901019fcg.html

                              http://blog.csdn.net/kennyrose/article/details/7532912

                               http://blog.sina.com.cn/s/blog_92554f0501013pl3.html

                              http://www.cnblogs.com/peteryj/archive/2007/08/05/1944905.html

    进程的四大要素:

    Linux进程所需具备的四要素:
         (1)程序代码。代码不一定是进程专有,可以与其它进程共用。
         (2)系统堆栈空间,这是进程专用的。
         (3)在内核中维护相应的进程控制块。只有这样,该进程才能成为内核调度的基本单位,接受调度。并且,该结构也记录了进程所占用的各项资源。
         (4)有独立的存储空间,表明进程拥有专有的用户空间。

           以上四条,缺一不可。如果缺少第四条,那么就称其为“线程”。如果完全没有用户空间,称其为“内核线程”;如果是共享用户空间,则称其为“用户线程”。


    do_fork函数

     

      Linux的用户进程不能直接被创建出来,因为不存在这样的API。它只能从某个进程中复制出来,再通过exec这样的API来切换到实际想要运行的程序文件

          复制的API包括三种:fork、clone、vfork。

          这三个API的内部实际都是调用一个内核内部函数do_fork,只是填写的参数不同而已

     

    do_fork的实现源码在kernel/fork.c文件中,其主要的作用就是复制原来的进程成为另一个新的进程,它完成了整个进程的创建过程。do_fork()的实现主要由以下5个步骤:
    (1)首先调用copy_process()函数,copy_process)函数实现了进程的大部分拷贝工作。 对传入的clone_flag进行检查, 为新进程创建一个内核栈、thread_info结构和task_struct;其值域当前进程的值完全相同(父子进程的描述符此时也相同);根据clone的参数标志,拷贝或共享打开的文件、文件系统信息、信号处理函数、进程地址空间和命名空间;为新进程获取一个有效的PID,调用pid = alloc_pidmap();紧接着使用alloc_pidmap函数为这个新进程分配一个pid。由于系统内的pid是循环使用的,所以采用位图方式来管理,用每一位(bit)来标示该位所对应的pid是否被使用。分配完毕后,判断pid是否分配成功。
    (2)init_completion(&vfork);
    (3)检查子进程是否设置了CLONE_STOPPED标志。
    (4)检查CLONE_VFORK标志被设置
    (5)返回pid


    clone函数

                     int clone(int (*fn)(void *), void *child_stack, int flags, void *arg);

          这里fn是函数指针,我们知道进程的4要素,这个就是指向程序的指针,就是所谓的“剧本", child_stack明显是为子进程分配系统堆栈空间(在linux下系统堆栈空间是2页面,就是8K的内存,其中在这块内存中,低地址上放入了值,这个值就是进程控制块task_struct的值),flags就是标志用来描述你需要从父进程继承那些资源, arg就是传给子进程的参数)。下面是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线程标准,子进程与父进程共享相同的线程群

    内核线程是由kernel_thread(  )函数在内核态下创建的,这个函数所包含的代码大部分是内联式汇编语言,但在某种程度上等价于下面的代码:

     

    int kernel_thread(int (*fn)(void *), void * arg,

                      unsigned long flags)

    {

        pid_t p;

        p = clone( 0, flags | CLONE_VM );

        if ( p )       

            return p;

        else {         

            fn(arg);

            exit(  );

       }

    }


    fork函数

           由fork创建的新进程被称为子进程(child process)。该函数被调用一次,但返回两次。两次返回的区别是子进程的返回值是0,而父进程的返回值则是新进程(子进程)的进程 id。将子进程id返回给父进程的理由是:因为一个进程的子进程可以多于一个,没有一个函数使一个进程可以获得其所有子进程的进程id。 对子进程来说,之所以fork返回0给它,是因为它随时可以调用getpid()来获取自己的pid;也可以调用getppid()来获取父进程的id。

    fork创建一个进程时,子进程只是完全复制父进程的资源,复制出来的子进程有自己的task_struct结构和pid,但却复制父进程其它所有的资源例如,要是父进程打开了五个文件,那么子进程也有五个打开的文件,而且这些文件的当前读写指针也停在相同的地方。所以,这一步所做的是复制。这样得到的子进程独立于父进程, 具有良好的并发性,但是二者之间的通讯需要通过专门的通讯机制,如:pipe,共享内存等机制, 另外通过fork创建子进程,需要将上面描述的每种资源都复制一个副本。但由于现在Linux中是采取了copy-on-write(COW写时复制)技术,为了降低开销,fork最初并不会真的产生两个不同的拷贝,因为在那个时候,大量的数据其实完全是一样的。写时复制是在推迟真正的数据拷贝。若后来确实发生了写入,那意味着parent和child的数据不一致了,于是产生复制动作,每个进程拿到属于自己的那一份,这样就可以降低系统调用的开销。

    指令指针也完全相同,子进程拥有父进程当前运行到的位置(两进程的程序计数器pc值相同,也就是说,子进程是从fork返回处开始执行的),但是父子进程谁先被调用就得看操作系统的调度程序了。

    范例1:

    int main(){ 
       int num = 1;  
       int child;  
       
       if(!(child = fork())) {   
         printf("This is son, his num is: %d. and his pid is: %d
    ", ++num, getpid());  
       } else {  
          printf("This is father, his num is: %d, his pid is: %d
    ", num, getpid());  
       }  
    } 

    执行结果为:

     

           This is son, his num is: 2. and his pid is: 2139

           This is father, his num is: 1, his pid is: 2138

    从代码里面可以看出2者的pid不同,子进程改变了num的值,而父进程中的num没有改变。

    总结:优点是子进程的执行独立于父进程,具有良好的并发性。缺点是两者的通信需要专门的通信机制,如pipe、fifo和system V等。有人认为这样大批量的复制会导致执行效率过低。其实在复制过程中,子进程复制了父进程的task_struct,系统堆栈空间和页面表,在子进程运行前,两者指向同一页面。而当子进程改变了父进程的变量时候,会通过copy_on_write的手段为所涉及的页面建立一个新的副本。因此fork效率并不低。

    范例2:

     

    #include <unistd.h>   
    #include <stdio.h>   
    int main(void)   
    {   
       int i=0;   
       for(i=0;i<3;i++){   
           pid_t fpid=fork();   
           if(fpid==0)   
               printf("son/n");   
           else   
               printf("father/n");   
       }   
       return 0;   
       
    } 


    这里就不做详细解释了,只做一个大概的分析。
        for        i=0         1           2
                  father     father     father
                                            son
                                son       father
                                            son
                   son       father     father
                                            son
                                son       father
                                            son
        其中每一行分别代表一个进程的运行打印结果。
        总结一下规律,对于这种N次循环的情况,执行printf函数的次数为2*(1+2+4+……+2N-1)次,创建的子进程数为1+2+4+……+2N-1个。

    范例3:

    #include <unistd.h>   
    #include <stdio.h>   
    int main() {   
        pid_t fpid;//fpid表示fork函数返回的值   
        //printf("fork!");   
        printf("fork!/n");   
        fpid = fork();   
        if (fpid < 0)   
            printf("error in fork!");   
        else if (fpid == 0)   
            printf("I am the child process, my process id is %d/n", getpid());   
        else   
            printf("I am the parent process, my process id is %d/n", getpid());   
        return 0;   
    }

    执行结果如下:
        fork!
        I am the parent process, my process id is 3361
        I am the child process, my process id is 3362 
        如果把语句printf("fork!/n");注释掉,执行printf("fork!");
        则新的程序的执行结果是:
        fork!I am the parent process, my process id is 3298
        fork!I am the child process, my process id is 3299 
        程序的唯一的区别就在于一个/n回车符号,为什么结果会相差这么大呢?
        这就跟printf的缓冲机制有关了,printf某些内容时,操作系统仅仅是把该内容放到了stdout的缓冲队列里了,并没有实际的写到屏幕上。但是,只要看到有/n 则会立即刷新stdout,因此就马上能够打印了。
        运行了printf("fork!")后,“fork!”仅仅被放到了缓冲里,程序运行到fork时缓冲里面的“fork!”  被子进程复制过去了。因此在子进程度stdout缓冲里面就也有了fork! 。所以,你最终看到的会是fork!  被printf了2次!!!!
        而运行printf("fork! /n")后,“fork!”被立即打印到了屏幕上,之后fork到的子进程里的stdout缓冲里不会有fork! 内容。因此你看到的结果会是fork! 被printf了1次!!!!

    范例4:

    int main(int argc, char* argv[])   
    {   
       fork();   
       fork() && fork() || fork();   
       fork();   
       printf("+/n");   
     
    } 

    答案是总共20个进程,除去main进程,还有19个进程。


    vfork函数

    vfork系统调用不同于fork,用vfork创建的子进程与父进程共享地址空间,也就是说子进程完全运行在父进程的地址空间上,如果这时子进程修改了某个变量,这将影响到父进程。

    因此,上面的例子如果改用vfork()的话,那么两次打印a,b的值是相同的,所在地址也是相同的。

    但此处有一点要注意的是用vfork()创建的子进程必须显示调用exit()来结束,否则子进程将不能结束,而fork()则不存在这个情况。

    Vfork也是在父进程中返回子进程的进程号,在子进程中返回0。

    用 vfork创建子进程后,父进程会被阻塞直到子进程调用exec(exec,将一个新的可执行文件载入到地址空间并执行之。)或exit。vfork的好处是在子进程被创建后往往仅仅是为了调用exec执行另一个程序,因为它就不会对父进程的地址空间有任何引用,所以对地址空间的复制是多余的 ,因此通过vfork共享内存可以减少不必要的开销。

    int main() {  
       int num = 1;  
       int child;  
       if(!(child = vfork())) {   
            printf("This is son, his num is: %d. and his pid is: %d
    ", ++num, getpid());  
       } else {  
            printf("This is father, his num is: %d, his pid is: %d
    ", num, getpid());  
       }  
    }

    This is son, his num is: 2. and his pid is:4139
    This is father, his num is: 2, his pid is: 4138

    从运行结果可以看到vfork创建出的子进程(线程)共享了父进程的num变量,这一次是指针复制,2者的指针指向了同一个内存。

    总结:当创建子进程的目的仅仅是为了调用exec()执行另一个程序时,子进程不会对父进程的地址空间又任何引用。因此,此时对地址空间的复制是多余的,通过vfork可以减少不必要的开销。


  • 相关阅读:
    LeetCode Best Time to Buy and Sell Stock
    LeetCode Scramble String
    LeetCode Search in Rotated Sorted Array II
    LeetCode Gas Station
    LeetCode Insertion Sort List
    LeetCode Maximal Rectangle
    Oracle procedure
    浏览器下载代码
    Shell check IP
    KVM- 存储池配置
  • 原文地址:https://www.cnblogs.com/snake-hand/p/3161450.html
Copyright © 2011-2022 走看看