Chinaunix首页 | 论坛 | 博客
  • 博客访问: 3665371
  • 博文数量: 146
  • 博客积分: 3918
  • 博客等级: 少校
  • 技术积分: 8581
  • 用 户 组: 普通用户
  • 注册时间: 2010-10-17 13:52
个人简介

个人微薄: weibo.com/manuscola

文章分类

全部博文(146)

文章存档

2016年(3)

2015年(2)

2014年(5)

2013年(42)

2012年(31)

2011年(58)

2010年(5)

分类: LINUX

2012-09-27 22:45:23

    前段时间在学习内核的进程管理方面的东西,看了进程创建和进程调度的代码,想写个大而全的东西,即有内核代码分析,又有一些实验在效果上证明内核的代码。 但是这篇文章很难产,感觉自己还是驾驭不了这个宏大的主题。 好久没写文章了,今天就放弃这个想法,写一个简单的东西。
 
    我们都知道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 (!(p 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(0 ) will cause page falut
  5. address at 8054060 value(0 ) will cause page fault
  6. address at bf8eff0c value(0 ) will cause page falut
  7. address at bf8f7f0c value(0 ) 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



阅读(13700) | 评论(13) | 转发(8) |
给主人留下些什么吧!~~

zhangweiguo8284242013-12-17 14:45:48

我最近在sparc的机器上发现了一个很奇怪的问题(系统是Solaris),有一个进程,占用内存大概有30M,fork之后在子进程里面给一个结构体变量赋值(这个结构体在父进程中也有),给第一个元素赋值的时间长达13-18ms不等。
但是在x86的机器上这个时间是微秒级别。
我的问题是fork的写时复制会不会针对不同的CPU有不同的行为,比如说可不可能在sparc的CPU上复制的时候是整体复制,而在x86的CPU上是写哪个复制哪个?

hk23056212012-10-13 22:17:14

Bean_lee: 汗一个,我算不上啥大牛,小牛都算不上,跟很多前辈学了一些东西,哪敢托大啊。真的是多多交流,共同进步。.....
恩恩,一起学习,一起进步.

Bean_lee2012-10-13 16:36:01

hk2305621: 我发现 chinaunix 里的大牛, 不仅技术牛逼, 而且很低调. 让我想起了一部电视里的台词“人分三等, 一等人有能力没脾气,二等人有能力有脾气,三等人没能.....
汗一个,我算不上啥大牛,小牛都算不上,跟很多前辈学了一些东西,哪敢托大啊。真的是多多交流,共同进步。

hk23056212012-10-13 16:12:17

Bean_lee: 互相学习,共同提高.....
我发现 chinaunix 里的大牛, 不仅技术牛逼, 而且很低调. 让我想起了一部电视里的台词“人分三等, 一等人有能力没脾气,二等人有能力有脾气,三等人没能力,脾气还挺大”.  大牛们都是一等人呀. 看来我不止得向你们学习技术了...

Bean_lee2012-10-13 15:55:01

hk2305621: 呵呵, 过来瞻仰下. 对systemptap的使用, 很犀利了. 我第一次听到这个东东,记录下, 抽空了解下.  我接触linux内核也有快一年了. 感觉还是迷迷糊糊的. 向各位学.....
互相学习,共同提高