前段时间在学习内核的进程管理方面的东西,看了进程创建和进程调度的代码,想写个大而全的东西,即有内核代码分析,又有一些实验在效果上证明内核的代码。 但是这篇文章很难产,感觉自己还是驾驭不了这个宏大的主题。 好久没写文章了,今天就放弃这个想法,写一个简单的东西。
我们都知道fork创建进程的时候,并没有真正的copy内存,因为我们知道,对于fork来讲,有一个很讨厌的东西叫exec系列的系统调用,它会勾引子进程另起炉灶。如果创建子进程就要内存拷贝的的话,一执行exec,辛辛苦苦拷贝的内存又被完全放弃了。
内核采用的策略是写时拷贝,换言之,先把页表映射关系建立起来,并不真正将内存拷贝。如果进程读访问,什么都不许要做,如果进程写访问,情况就不同了,因为父子进程的内存空间是独立的,不应该互相干扰。所以这时候不能在公用同一块内存了,否则子进程的改动会被父进程觉察到。
下图是linux toolbox里面的一张图,比较好,我就拷贝出来了(如果有侵权通知立删),很好的解释了COW的原理。
下面我们看下,fork一个进程,在kernel/fork.c文件中那些函数调到了。vfork,fork,pthread_create,最终,都会调用do_fork,所不同的就是传递的标志位不同,标志位又控制父子进程,或者父进程和线程他们哪些资源是共用的,哪些资源需要各存一份。
1. <stdio.h>
2. <stdlib.h>
3. <unistd.h>
4. <sys/types.h>
5. <sys/wait.h>
6. <string.h>
7.
8.
9.
10. int g_var[102400] = {0};
11. int main()
12. {
13. int l_var[102400] = {0};
14. (stderr,"g_var 's address is %lx\n",(unsigned long)g_var);
15.
16. (stderr,"l_var 's address is %lx\n",(unsigned long)l_var);
17.
18. (g_var,0,sizeof(g_var));
19. (l_var,0,sizeof(l_var));
20.
21. (15);
22.
23. int ret = fork();
24. if(ret < 0 )
25. {
26. (stderr,"fork failed ,nothing to do now!\n");
27. -1;
28. }
29.
30. if(ret == 0)
31. {
32. (10);
33. (stderr, "I begin to write now\n");
34.
35. (stderr,"address at %-10lx value(%-6d) will cause page falut\n",
36. (unsigned long)(g_var+2048),g_var[2048]);
37. [2048] = 4;
38. 39. (6);
40. (stderr,"address at %-10lx value(%-6d) will cause page fault\n",
41. (unsigned long)(g_var+10240),g_var[10240]);
42. [10240] = 8;
43.
44.
45. (4);
46. (stderr,"address at %-10lx value(%-6d) will cause page falut\n",
47. (unsigned long)(l_var+2048),l_var[2048]);
48. [2048] = 8;
49.
50. (4);
51. (stderr,"address at %-10lx value(%-6d) will cause page falut\n",
52. (unsigned long)(l_var+10240),l_var[10240]);
53. [10240] = 8;
54.
55. }
56. if(ret >0)
57. {
58. (-1,NULL,0);
59. (stderr,"child process exit, now check the value\n");
60. (stderr,"g_var[%-6d] = %-4d\ng_var[%-6d] = %-4d\n",
61. ,g_var[2048],10240,g_var[10240]);
62. (stderr,"l_var[%-6d] = %-4d\nl_var[%-6d] = %-4d\n",
63. ,l_var[2048],10240,l_var[10240]);
64.
65. ;
66. }
67.
68. }
这里面执行了一个fork系统调用,我们调用下systemtap脚本看下他都调用了kernel/fork.c里面的那些函数:systemtap脚本如下:
1. .function("*@kernel/fork.c")
2. {
3. if(pid() == target())
4. {
5. ("PID(%d) ,execname(%s) probe point:(%s) \n",pid(),execname(),pp());
6. }
7. }
8.
9. timer.s(60)
10. {
11. ();
12. }
1. :~/program/systemtap/process# stap fork_call.stp -x 7192
2. (7192) ,execname(fork_cow) probe point:(kernel.function("do_fork@/build/buildd/linux-2.6.32/kernel/fork.c:1364"))
3. (7192) ,execname(fork_cow) probe point:(kernel.function("copy_process@/build/buildd/linux-2.6.32/kernel/fork.c:978"))
4. (7192) ,execname(fork_cow) probe point:(kernel.function("dup_task_struct@/build/buildd/linux-2.6.32/kernel/fork.c:221"))
5. (7192) ,execname(fork_cow) probe point:(kernel.function("account_kernel_stack@/build/buildd/linux-2.6.32/kernel/fork.c:141"))
6. (7192) ,execname(fork_cow) probe point:(kernel.function("rt_mutex_init_task@/build/buildd/linux-2.6.32/kernel/fork.c:941"))
7. (7192) ,execname(fork_cow) probe point:(kernel.function("copy_flags@/build/buildd/linux-2.6.32/kernel/fork.c:923"))
8. (7192) ,execname(fork_cow) probe point:(kernel.function("posix_cpu_timers_init@/build/buildd/linux-2.6.32/kernel/fork.c:960"))
9. (7192) ,execname(fork_cow) probe point:(kernel.function("copy_files@/build/buildd/linux-2.6.32/kernel/fork.c:747"))
10. (7192) ,execname(fork_cow) probe point:(kernel.function("copy_fs@/build/buildd/linux-2.6.32/kernel/fork.c:727"))
11. (7192) ,execname(fork_cow) probe point:(kernel.function("copy_sighand@/build/buildd/linux-2.6.32/kernel/fork.c:799"))
12. (7192) ,execname(fork_cow) probe point:(kernel.function("copy_signal@/build/buildd/linux-2.6.32/kernel/fork.c:854"))
13. (7192) ,execname(fork_cow) probe point:(kernel.function("posix_cpu_timers_init_group@/build/buildd/linux-2.6.32/kernel/fork.c:826"))
14. (7192) ,execname(fork_cow) probe point:(kernel.function("copy_mm@/build/buildd/linux-2.6.32/kernel/fork.c:680"))
15. (7192) ,execname(fork_cow) probe point:(kernel.function("dup_mm@/build/buildd/linux-2.6.32/kernel/fork.c:624"))
16. (7192) ,execname(fork_cow) probe point:(kernel.function("mm_init@/build/buildd/linux-2.6.32/kernel/fork.c:448"))
17. (7192) ,execname(fork_cow) probe point:(kernel.function("mm_alloc_pgd@/build/buildd/linux-2.6.32/kernel/fork.c:403"))
18. (7192) ,execname(fork_cow) probe point:(kernel.function("mm_init_aio@/build/buildd/linux-2.6.32/kernel/fork.c:440"))
19. (7192) ,execname(fork_cow) probe point:(kernel.function("mm_init_owner@/build/buildd/linux-2.6.32/kernel/fork.c:951"))
20. (7192) ,execname(fork_cow) probe point:(kernel.function("dup_mmap@/build/buildd/linux-2.6.32/kernel/fork.c:278"))
21. (7192) ,execname(fork_cow) probe point:(kernel.function("copy_io@/build/buildd/linux-2.6.32/kernel/fork.c:774"))
22. (7192) ,execname(fork_cow) probe point:(kernel.function("__cleanup_sighand@/build/buildd/linux-2.6.32/kernel/fork.c:816"))
23. (7192) ,execname(fork_cow) probe point:(kernel.function("__cleanup_signal@/build/buildd/linux-2.6.32/kernel/fork.c:916"))
24. (7192) ,execname(fork_cow) probe point:(kernel.function("mm_release@/build/buildd/linux-2.6.32/kernel/fork.c:570"))
25. (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. (&oldmm->mm_users);
3. = oldmm;
4. ;
5. }
这个地方语意很怪,正常应该是CLONE_VM是1,我应该copy一份,但是正好相反,CLONE_XX意味值share_XX,意味着,不需要copy。
需要copy内存的话,真正干活的函数是dup_mm,pthread_create函数就不会走到dup_mm函数,因为他不需要copy一份父进程的内存空间,他是共用一份内存空间的。请看下面pthread_create引发的do_fork。
1. :~/program/C/process_share# ./pthread_cmp &
2. [3] 7787
3. :~/program/C/process_share# thread OUT
4. IN
5. thread OUT
6.
7. [2]- Done ./pthread_cmp
8. [3]+ Done ./pthread_cmp
1.
2. :~/program/systemtap/process# stap fork_call.stp -x 7787
3. (7787) ,execname(pthread_cmp) probe point:(kernel.function("do_fork@/build/buildd/linux-2.6.32/kernel/fork.c:1364"))
4. (7787) ,execname(pthread_cmp) probe point:(kernel.function("copy_process@/build/buildd/linux-2.6.32/kernel/fork.c:978"))
5. (7787) ,execname(pthread_cmp) probe point:(kernel.function("dup_task_struct@/build/buildd/linux-2.6.32/kernel/fork.c:221"))
6. (7787) ,execname(pthread_cmp) probe point:(kernel.function("account_kernel_stack@/build/buildd/linux-2.6.32/kernel/fork.c:141"))
7. (7787) ,execname(pthread_cmp) probe point:(kernel.function("rt_mutex_init_task@/build/buildd/linux-2.6.32/kernel/fork.c:941"))
8. (7787) ,execname(pthread_cmp) probe point:(kernel.function("copy_flags@/build/buildd/linux-2.6.32/kernel/fork.c:923"))
9. (7787) ,execname(pthread_cmp) probe point:(kernel.function("posix_cpu_timers_init@/build/buildd/linux-2.6.32/kernel/fork.c:960"))
10. (7787) ,execname(pthread_cmp) probe point:(kernel.function("copy_files@/build/buildd/linux-2.6.32/kernel/fork.c:747"))
11. (7787) ,execname(pthread_cmp) probe point:(kernel.function("copy_fs@/build/buildd/linux-2.6.32/kernel/fork.c:727"))
12. (7787) ,execname(pthread_cmp) probe point:(kernel.function("copy_sighand@/build/buildd/linux-2.6.32/kernel/fork.c:799"))
13. (7787) ,execname(pthread_cmp) probe point:(kernel.function("copy_signal@/build/buildd/linux-2.6.32/kernel/fork.c:854"))
14. (7787) ,execname(pthread_cmp) probe point:(kernel.function("copy_mm@/build/buildd/linux-2.6.32/kernel/fork.c:680"))
15. (7787) ,execname(pthread_cmp) probe point:(kernel.function("copy_io@/build/buildd/linux-2.6.32/kernel/fork.c:774"))
16. (7787) ,execname(pthread_cmp) probe point:(kernel.function("mm_release@/build/buildd/linux-2.6.32/kernel/fork.c:570"))
17. (7787) ,execname(pthread_cmp) probe point:(kernel.function("mmput@/build/buildd/linux-2.6.32/kernel/fork.c:509"))
18. (7787) ,execname(pthread_cmp) probe point:(kernel.function("__cleanup_sighand@/build/buildd/linux-2.6.32/kernel/fork.c:816"))
19. (7787) ,execname(pthread_cmp) probe point:(kernel.function("mm_release@/build/buildd/linux-2.6.32/kernel/fork.c:570"))
20. (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.
3. , fault_address, fault_access
4. global time_offset
5.
6. { time_offset = gettimeofday_us() }
7.
8. .pagefault {
9. if(pid() == target() || ppid() == target())
10. {
11. = gettimeofday_us()
12. = pid()
13. [p] = t
14. [p] = address
15. [p] = write_access ? "w" : "r"
16. }
17. }
18.
19. .pagefault.return {
20. if(pid() == target() || ppid() == target())
21. {
22. =gettimeofday_us()
23. = pid()
24. if (!(p in fault_entry_time)) next
25. = t - fault_entry_time[p]
26. if (vm_fault_contains(fault_type,VM_FAULT_MINOR)) {
27. ="minor"
28. } else if (vm_fault_contains(fault_type,VM_FAULT_MAJOR)) {
29. ="major"
30. } else {
31. next #only want to deal with minor and major page faults
32. }
33.
34. ("%d:%d:%p:%s:%s:%d\n",
35. - time_offset, p, fault_address[p], fault_access[p], ftype, e)
36.
37.
38. #free up memory
39. [p]
40. [p]
41. [p]
42. }
43. }
44.
45. .s(100){
46. exit();
47. }
systemtap脚本的含义是跟踪指定进程和子进程,如果有page fault 会打印一条记录出来 。
下面看现象:
1. :~/program/C/process_share# g_var 's address is 804a060
2. 's address is bf8edf0c
3. 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 )
8. .....
1. :~/program/systemtap#
2. :~/program/systemtap#
3. :~/program/systemtap# stap pfaults.stp -x 9081
4. :9081:0xb77ec72c:w:minor:35
5. :9092:0xb77ec728:w:minor:23
6. :9081:0xbf8edea8:w:minor:29
7.
8. .....
9. 14768229:9092:0x0804c060:w:minor:13
10. 11. 12. 13. 14. 15. 16. 17. 18. 19. 20768379:9092:0x08054060:w:minor:37
20. 21. 22. 23. 24. 25. 26. 24768564:9092:0xbf8eff0c:w:minor:39
27. 28. 29. 30. 31. 32. 28768745:9092:0xbf8f7f0c:w:minor:39
33. ...
34.
这写个空格的出现是由于我手工敲的,因为中间有sleep,所以我有足够的时间敲回车。
产生了page_fault,证明了我们的推断。
另外我在调试的过程中发现,如果不调用memset,子进程退出后,父进程读访问数组指定位置的变量,也会出现page fault,有心的筒子可以自行验证。
提示: 代码在写博客的过程中有一些微调,输出格式有调整,也有其他的一些微调,所以可能输出和代码对应并不是100% 。 对此有困惑的筒子可以自行验证,总之我没有造假了,呵呵。
参考文献:
1 systemtap example
2 深入linux 内核架构
3 Linux Toolbox