fork,你拿什麼證明你的寫時拷貝(COW)

    前段時間在學習內核的進程管理方面的東西,看了進程創建和進程調度的代碼,想寫個大而全的東西,即有內核代碼分析,又有一些實驗在效果上證明內核的代碼。 但是這篇文章很難產,感覺自己還是駕馭不了這個宏大的主題。 好久沒寫文章了,今天就放棄這個想法,寫一個簡單的東西。
 
    我們都知道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.                 sleep(6);
  30.                 fprintf(stderr,"address at %-10lx value(%-6d) will cause page fault\n",
  31.                                 (unsigned long)(g_var+10240),g_var[10240]);
  32.                 g_var[10240] = 8;


  33.                 sleep(4);
  34.                 fprintf(stderr,"address at %-10lx value(%-6d) will cause page falut\n",
  35.                                 (unsigned long)(l_var+2048),l_var[2048]);
  36.                 l_var[2048] = 8;

  37.                 sleep(4);
  38.                 fprintf(stderr,"address at %-10lx value(%-6d) will cause page falut\n",
  39.                                  (unsigned long)(l_var+10240),l_var[10240]);
  40.                 l_var[10240] = 8;
  41.               
  42.         }
  43.         if(ret >0)
  44.         {
  45.                 waitpid(-1,NULL,0);
  46.                 fprintf(stderr,"child process exit, now check the value\n");
  47.                 fprintf(stderr,"g_var[%-6d] = %-4d\ng_var[%-6d] = %-4d\n",
  48.                                 2048,g_var[2048],10240,g_var[10240]);
  49.                 fprintf(stderr,"l_var[%-6d] = %-4d\nl_var[%-6d] = %-4d\n",
  50.                                 2048,l_var[2048],10240,l_var[10240]);

  51.                 return 0;
  52.         }

  53. }

    這裏面執行了一個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. 20768379:9092:0x08054060:w:minor:37






  10. 24768564:9092:0xbf8eff0c:w:minor:39





  11. 28768745:9092:0xbf8f7f0c:w:minor:39
  12. ...



這寫個空格的出現是由於我手工敲的,因爲中間有sleep,所以我有足夠的時間敲回車。
產生了page_fault,證明了我們的推斷。

另外我在調試的過程中發現,如果不調用memset,子進程退出後,父進程讀訪問數組指定位置的變量,也會出現page fault,有心的筒子可以自行驗證。


提示: 代碼在寫博客的過程中有一些微調,輸出格式有調整,也有其他的一些微調,所以可能輸出和代碼對應並不是100% 。 對此有困惑的筒子可以自行驗證,總之我沒有造假了,呵呵。


參考文獻:
1 systemtap example
2 深入linux 內核架構

3 Linux Toolbox


原文鏈接:http://blog.chinaunix.net/uid-24774106-id-3361500.html

發佈了357 篇原創文章 · 獲贊 168 · 訪問量 52萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章