浅析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\n", ++num, getpid());  
   } else {  
      printf("This is father, his num is: %d, his pid is: %d\n", 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\n", ++num, getpid());  
   } else {  
        printf("This is father, his num is: %d, his pid is: %d\n", 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可以减少不必要的开销。

 


 

posted @ 2013-06-28 18:55  爱生活,爱编程  阅读(1029)  评论(0编辑  收藏  举报