feisky

云计算、虚拟化与Linux技术笔记
  博客园  :: 首页  :: 新随笔  :: 联系 :: 订阅 订阅  :: 管理

fork,你拿什么证明你的写时拷贝(COW)

Posted on 2013-03-05 13:18  feisky  阅读(1034)  评论(0编辑  收藏  举报

 前段时间在学习内核的进程管理方面的东西,看了进程创建和进程调度的代码,想写个大而全的东西,即有内核代码分析,又有一些实验在效果上证明内核的代码。 但是这篇文章很难产,感觉自己还是驾驭不了这个宏大的主题。 好久没写文章了,今天就放弃这个想法,写一个简单的东西。

 
    我们都知道fork创建进程的时候,并没有真正的copy内存,因为我们知道,对于fork来讲,有一个很讨厌的东西叫exec系列的系统调用,它会勾引子进程另起炉灶。如果创建子进程就要内存拷贝的的话,一执行exec,辛辛苦苦拷贝的内存又被完全放弃了。
 
    内核采用的策略是写时拷贝,换言之,先把页表映射关系建立起来,并不真正将内存拷贝。如果进程读访问,什么都不许要做,如果进程写访问,情况就不同了,因为父子进程的内存空间是独立的,不应该互相干扰。所以这时候不能在公用同一块内存了,否则子进程的改动会被父进程觉察到。
 
    下图是linux toolbox里面的一张图,比较好,我就拷贝出来了(如果有侵权通知立删),很好的解释了COW的原理。
 
    下面我们看下,fork一个进程,在kernel/fork.c文件中那些函数调到了。vfork,fork,pthread_create,最终,都会调用do_fork,所不同的就是传递的标志位不同,标志位又控制父子进程,或者父进程和线程他们哪些资源是共用的,哪些资源需要各存一份。
 
    

 

  1. #include<stdio.h>
  2. #include<stdlib.h>
  3. #include<unistd.h>
  4. #include<sys/types.h>
  5. #include<sys/wait.h>
  6. #include<string.h>
  7. int g_var[102400] = {0};
  8. int main()
  9. {
  10.         int l_var[102400] = {0};
  11.         fprintf(stderr,"g_var 's address is %lx\n",(unsigned long)g_var);
  12.         fprintf(stderr,"l_var 's address is %lx\n",(unsigned long)l_var);
  13.         memset(g_var,0,sizeof(g_var));
  14.         memset(l_var,0,sizeof(l_var));
  15.         sleep(15);
  16.         int ret = fork();
  17.         if(ret < 0 )
  18.         {
  19.                 fprintf(stderr,"fork failed ,nothing to do now!\n");
  20.                 return -1;
  21.         }
  22.         if(ret == 0)
  23.         {
  24.                 sleep(10);
  25.                 fprintf(stderr, "I begin to write now\n");
  26.                 fprintf(stderr,"address at %-10lx value(%-6d) will cause page falut\n",
  27.                                (unsigned long)(g_var+2048),g_var[2048]);
  28.                 g_var[2048] = 4;
  29.  
  30.                 sleep(6);
  31.                 fprintf(stderr,"address at %-10lx value(%-6d) will cause page fault\n",
  32.                                 (unsigned long)(g_var+10240),g_var[10240]);
  33.                 g_var[10240] = 8;
  34.                 sleep(4);
  35.                 fprintf(stderr,"address at %-10lx value(%-6d) will cause page falut\n",
  36.                                 (unsigned long)(l_var+2048),l_var[2048]);
  37.                 l_var[2048] = 8;
  38.                 sleep(4);
  39.                 fprintf(stderr,"address at %-10lx value(%-6d) will cause page falut\n",
  40.                                  (unsigned long)(l_var+10240),l_var[10240]);
  41.                 l_var[10240] = 8;
  42.               
  43.         }
  44.         if(ret >0)
  45.         {
  46.                 waitpid(-1,NULL,0);
  47.                 fprintf(stderr,"child process exit, now check the value\n");
  48.                 fprintf(stderr,"g_var[%-6d] = %-4d\ng_var[%-6d] = %-4d\n",
  49.                                 2048,g_var[2048],10240,g_var[10240]);
  50.                 fprintf(stderr,"l_var[%-6d] = %-4d\nl_var[%-6d] = %-4d\n",
  51.                                 2048,l_var[2048],10240,l_var[10240]);
  52.                 return 0;
  53.         }
  54. }


    这里面执行了一个fork系统调用,我们调用下systemtap脚本看下他都调用了kernel/fork.c里面的那些函数:systemtap脚本如下:

  1. probe kernel.function("*@kernel/fork.c")
  2. {
  3.         if(pid() == target())
  4.         { 
  5.                 printf("PID(%d) ,execname(%s) probe point:(%s) \n",pid(),execname(),pp());
  6.         } 
  7. }
  8. probe timer.s(60)
  9. {
  10.         exit();
  11. }

    

  1. root@libin:~/program/systemtap/process# stap fork_call.stp -x 7192
  2. PID(7192) ,execname(fork_cow) probe point:(kernel.function("do_fork@/build/buildd/linux-2.6.32/kernel/fork.c:1364")) 
  3. PID(7192) ,execname(fork_cow) probe point:(kernel.function("copy_process@/build/buildd/linux-2.6.32/kernel/fork.c:978")) 
  4. PID(7192) ,execname(fork_cow) probe point:(kernel.function("dup_task_struct@/build/buildd/linux-2.6.32/kernel/fork.c:221")) 
  5. PID(7192) ,execname(fork_cow) probe point:(kernel.function("account_kernel_stack@/build/buildd/linux-2.6.32/kernel/fork.c:141")) 
  6. PID(7192) ,execname(fork_cow) probe point:(kernel.function("rt_mutex_init_task@/build/buildd/linux-2.6.32/kernel/fork.c:941")) 
  7. PID(7192) ,execname(fork_cow) probe point:(kernel.function("copy_flags@/build/buildd/linux-2.6.32/kernel/fork.c:923")) 
  8. PID(7192) ,execname(fork_cow) probe point:(kernel.function("posix_cpu_timers_init@/build/buildd/linux-2.6.32/kernel/fork.c:960")) 
  9. PID(7192) ,execname(fork_cow) probe point:(kernel.function("copy_files@/build/buildd/linux-2.6.32/kernel/fork.c:747")) 
  10. PID(7192) ,execname(fork_cow) probe point:(kernel.function("copy_fs@/build/buildd/linux-2.6.32/kernel/fork.c:727")) 
  11. PID(7192) ,execname(fork_cow) probe point:(kernel.function("copy_sighand@/build/buildd/linux-2.6.32/kernel/fork.c:799")) 
  12. PID(7192) ,execname(fork_cow) probe point:(kernel.function("copy_signal@/build/buildd/linux-2.6.32/kernel/fork.c:854")) 
  13. PID(7192) ,execname(fork_cow) probe point:(kernel.function("posix_cpu_timers_init_group@/build/buildd/linux-2.6.32/kernel/fork.c:826")) 
  14. PID(7192) ,execname(fork_cow) probe point:(kernel.function("copy_mm@/build/buildd/linux-2.6.32/kernel/fork.c:680")) 
  15. PID(7192) ,execname(fork_cow) probe point:(kernel.function("dup_mm@/build/buildd/linux-2.6.32/kernel/fork.c:624")) 
  16. PID(7192) ,execname(fork_cow) probe point:(kernel.function("mm_init@/build/buildd/linux-2.6.32/kernel/fork.c:448")) 
  17. PID(7192) ,execname(fork_cow) probe point:(kernel.function("mm_alloc_pgd@/build/buildd/linux-2.6.32/kernel/fork.c:403")) 
  18. PID(7192) ,execname(fork_cow) probe point:(kernel.function("mm_init_aio@/build/buildd/linux-2.6.32/kernel/fork.c:440")) 
  19. PID(7192) ,execname(fork_cow) probe point:(kernel.function("mm_init_owner@/build/buildd/linux-2.6.32/kernel/fork.c:951")) 
  20. PID(7192) ,execname(fork_cow) probe point:(kernel.function("dup_mmap@/build/buildd/linux-2.6.32/kernel/fork.c:278")) 
  21. PID(7192) ,execname(fork_cow) probe point:(kernel.function("copy_io@/build/buildd/linux-2.6.32/kernel/fork.c:774")) 
  22. PID(7192) ,execname(fork_cow) probe point:(kernel.function("__cleanup_sighand@/build/buildd/linux-2.6.32/kernel/fork.c:816")) 
  23. PID(7192) ,execname(fork_cow) probe point:(kernel.function("__cleanup_signal@/build/buildd/linux-2.6.32/kernel/fork.c:916")) 
  24. PID(7192) ,execname(fork_cow) probe point:(kernel.function("mm_release@/build/buildd/linux-2.6.32/kernel/fork.c:570")) 
  25. PID(7192) ,execname(fork_cow) probe point:(kernel.function("mmput@/build/buildd/linux-2.6.32/kernel/fork.c:509"))

    fork调用了do_fork这个内核函数,这个函数比较大,主干程序是copy_process,这里有一系列的copy_xxx系列产品,这个系列产品会根据传进来的标志位,来决定那些资源子进程需要copy一份,那些不用拷贝了,直接用父进程的就可以了。 我们关注的copy_mm这个函数,如果用户标志位中的CLONE_VM置了1,得了,和父进程共享一份就成了,不需要费劲在copy一份了:

    
  1. if (clone_flags & CLONE_VM) {
  2.     atomic_inc(&oldmm->mm_users);
  3.     mm = oldmm;
  4.     goto good_mm;
  5.   }
    这个地方语意很怪,正常应该是CLONE_VM是1,我应该copy一份,但是正好相反,CLONE_XX意味值share_XX,意味着,不需要copy。
 
    需要copy内存的话,真正干活的函数是dup_mm,pthread_create函数就不会走到dup_mm函数,因为他不需要copy一份父进程的内存空间,他是共用一份内存空间的。请看下面pthread_create引发的do_fork。
 

  1. root@libin:~/program/C/process_share# ./pthread_cmp &
  2. [3] 7787
  3. root@libin:~/program/C/process_share# thread OUT
  4. thread IN
  5. thread OUT
  6. [2]- Done ./pthread_cmp
  7. [3]+ Done ./pthread_cmp
  1. root@libin:~/program/systemtap/process# stap fork_call.stp -x 7787
  2. PID(7787) ,execname(pthread_cmp) probe point:(kernel.function("do_fork@/build/buildd/linux-2.6.32/kernel/fork.c:1364")) 
  3. PID(7787) ,execname(pthread_cmp) probe point:(kernel.function("copy_process@/build/buildd/linux-2.6.32/kernel/fork.c:978")) 
  4. PID(7787) ,execname(pthread_cmp) probe point:(kernel.function("dup_task_struct@/build/buildd/linux-2.6.32/kernel/fork.c:221")) 
  5. PID(7787) ,execname(pthread_cmp) probe point:(kernel.function("account_kernel_stack@/build/buildd/linux-2.6.32/kernel/fork.c:141")) 
  6. PID(7787) ,execname(pthread_cmp) probe point:(kernel.function("rt_mutex_init_task@/build/buildd/linux-2.6.32/kernel/fork.c:941")) 
  7. PID(7787) ,execname(pthread_cmp) probe point:(kernel.function("copy_flags@/build/buildd/linux-2.6.32/kernel/fork.c:923")) 
  8. PID(7787) ,execname(pthread_cmp) probe point:(kernel.function("posix_cpu_timers_init@/build/buildd/linux-2.6.32/kernel/fork.c:960")) 
  9. PID(7787) ,execname(pthread_cmp) probe point:(kernel.function("copy_files@/build/buildd/linux-2.6.32/kernel/fork.c:747")) 
  10. PID(7787) ,execname(pthread_cmp) probe point:(kernel.function("copy_fs@/build/buildd/linux-2.6.32/kernel/fork.c:727")) 
  11. PID(7787) ,execname(pthread_cmp) probe point:(kernel.function("copy_sighand@/build/buildd/linux-2.6.32/kernel/fork.c:799")) 
  12. PID(7787) ,execname(pthread_cmp) probe point:(kernel.function("copy_signal@/build/buildd/linux-2.6.32/kernel/fork.c:854")) 
  13. PID(7787) ,execname(pthread_cmp) probe point:(kernel.function("copy_mm@/build/buildd/linux-2.6.32/kernel/fork.c:680")) 
  14. PID(7787) ,execname(pthread_cmp) probe point:(kernel.function("copy_io@/build/buildd/linux-2.6.32/kernel/fork.c:774")) 
  15. PID(7787) ,execname(pthread_cmp) probe point:(kernel.function("mm_release@/build/buildd/linux-2.6.32/kernel/fork.c:570")) 
  16. PID(7787) ,execname(pthread_cmp) probe point:(kernel.function("mmput@/build/buildd/linux-2.6.32/kernel/fork.c:509")) 
  17. PID(7787) ,execname(pthread_cmp) probe point:(kernel.function("__cleanup_sighand@/build/buildd/linux-2.6.32/kernel/fork.c:816")) 
  18. PID(7787) ,execname(pthread_cmp) probe point:(kernel.function("mm_release@/build/buildd/linux-2.6.32/kernel/fork.c:570")) 
  19. PID(7787) ,execname(pthread_cmp) probe point:(kernel.function("mmput@/build/buildd/linux-2.6.32/kernel/fork.c:509"))
    dup_mm这里面有两个分支指的注意
   1 mm_init-->mm_alloc_pgd
   2 dup_mmap
 
    这两个分支真正将父进程的页表拷贝了一份,尤其是dup_mmap,沿着copy_page_range-->copy_pud_range---> copy_pmd_range--->copy_pte_range,一路向西,将页表拷贝了一份。
 
    由于fork创建的子进程并没有拷贝整个内存,所以,当子进程修改内存某地址对应的值的时候,会产生缺页中断,page fault 。 我的C程序中有will cause page fault的字样,只要是写时拷贝,就会出现page fault 。 所以我们只需要在程序运行过程中监控page_fault,只要我们修改的变量的地址,引起了page fault,就证明fork 采用了COW 。
 
    看监控程序systemtap脚本:

  1. #! /usr/bin/env stap
  2. global fault_entry_time, fault_address, fault_access
  3. global time_offset
  4. probe begin { time_offset = gettimeofday_us() }
  5. probe vm.pagefault {
  6.   if(pid() == target() || ppid() == target())
  7.   {
  8.       t = gettimeofday_us() 
  9.       p = pid() 
  10.       fault_entry_time[p] = t
  11.       fault_address[p] = address
  12.       fault_access[p] = write_access ? "w" : "r"
  13.   } 
  14. } 
  15.                 
  16. probe vm.pagefault.return {
  17.   if(pid() == target() || ppid() == target())
  18.   { 
  19.       t=gettimeofday_us() 
  20.       p = pid() 
  21.       if (!(in fault_entry_time)) next 
  22.       e = t - fault_entry_time[p] 
  23.       if (vm_fault_contains(fault_type,VM_FAULT_MINOR)) {
  24.         ftype="minor" 
  25.       } else if (vm_fault_contains(fault_type,VM_FAULT_MAJOR)) {
  26.         ftype="major" 
  27.       } else {
  28.         next #only want to deal with minor and major page faults
  29.       } 
  30.       printf("%d:%d:%p:%s:%s:%d\n",
  31.       t - time_offset, p, fault_address[p], fault_access[p], ftype, e)
  32.                                                                                                                   
  33.       #free up memory
  34.       delete fault_entry_time[p]
  35.       delete fault_address[p]
  36.       delete fault_access[p]
  37.   }
  38. }
  39. probe timer.s(100){
  40.    exit();
  41. }
systemtap脚本的含义是跟踪指定进程和子进程,如果有page fault 会打印一条记录出来 。 
下面看现象:

  1. root@libin:~/program/C/process_share# g_var 's address is 804a060
  2. l_var 's address is bf8edf0c
  3. I begin to write now
  4. address at 804c060 value() will cause page falut
  5. address at 8054060 value() will cause page fault
  6. address at bf8eff0c value() will cause page falut
  7. address at bf8f7f0c value() will cause page falut
  8. .....
  1. root@libin:~/program/systemtap# 
  2. root@libin:~/program/systemtap# 
  3. root@libin:~/program/systemtap# stap pfaults.stp -x 9081
  4. 4767196:9081:0xb77ec72c:w:minor:35
  5. 4767230:9092:0xb77ec728:w:minor:23
  6. 4767239:9081:0xbf8edea8:w:minor:29
  7. .....
  8. 14768229:9092:0x0804c060:w:minor:13
  9.  
  10.  
  11.  
  12.  
  13.  
  14.  
  15.  
  16.  
  17.  
  18. 20768379:9092:0x08054060:w:minor:37
  19.  
  20.  
  21.  
  22.  
  23.  
  24.  
  25. 24768564:9092:0xbf8eff0c:w:minor:39
  26.  
  27.  
  28.  
  29.  
  30.  
  31. 28768745:9092:0xbf8f7f0c:w:minor:39
  32. ...
  33.  
 
这写个空格的出现是由于我手工敲的,因为中间有sleep,所以我有足够的时间敲回车。
产生了page_fault,证明了我们的推断。
 
另外我在调试的过程中发现,如果不调用memset,子进程退出后,父进程读访问数组指定位置的变量,也会出现page fault,有心的筒子可以自行验证。
 
 
提示: 代码在写博客的过程中有一些微调,输出格式有调整,也有其他的一些微调,所以可能输出和代码对应并不是100% 。 对此有困惑的筒子可以自行验证,总之我没有造假了,呵呵。
 
 
参考文献:
1 systemtap example
2 深入linux 内核架构
3 Linux Toolbox
 
本文转自:http://blog.chinaunix.net/uid-24774106-id-3361500.html?page=3
无觅相关文章插件,快速提升流量