相比较于2.4的代码,0.11的比较简洁
写时复制也即copy on write,这种思想相当简单:父进程和子进程共享页面而不是复制页面。然而,只要页面被共享,它们就不能被修改。无论父进程和子进程何时试图写一个共享的页面,就产生一个错误。
就从这个错误开始:当用户试图往一个一个共享页面(用户内存页面)上写时,就触发一次页面异常(int 14)--这是我假想的情况,以此进行分析。
于是cpu强制跳转到页异常中断入口处_page_fault:
[主要关注蓝色代码,黑色可跳过]
14 _page_fault:
15 xchgl %eax,(%esp) #取堆栈中error_code到eax
16 pushl %ecx
17 pushl %edx
18 push %ds
19 push %es
20 push %fs
21 movl $0x10,%edx
22 mov %dx,%ds
23 mov %dx,%es
24 mov %dx,%fs
25 movl %cr2,%edx #取address到edx
26 pushl %edx #address-———┬—>调用do_wp_page的参数
27 pushl %eax #error_code——┘
28 testl $1,%eax #测试error_code的bit0(p)
29 jne 1f
30 call _do_no_page
31 jmp 2f
32 1: call _do_wp_page
33 2: addl $8,%esp
34 pop %fs
35 pop %es
36 pop %ds
37 popl %edx
38 popl %ecx
39 popl %eax
40 iret
当发生页异常时,cpu自动产生出错码(error_code)并将其压入堆栈,而异常时访问的线性地址就被保存在寄存器cr2中。
其中出错码是32位的长字。但只用最后3位--分别说明导致异常发生的原因:
bit0(P)--0表示不存在,1表示页保护
bit1(W/R)--0表示读操作,1写
bit2(U/S)--0超级用户下执行,1用户模式
这里我们的情况对应的是 bit0--1,bit1--1,bit--1 所以经过28行,经由29行直接跳转到32行--也即调用do_wp_page:
247 void do_wp_page(unsigned long error_code,unsigned long address)
248 {
...
255 un_wp_page((unsigned long *)
256 (((address>>10) & 0xffc) + (0xfffff000 &
257 *((unsigned long *) ((address>>20) &0xffc)))));
258
259 }
在0.11中,do_wp_page实际上只是调用了un_wp_page,而其参数:
(address>>20) &0xffc--页目录地址,
0xfffff000 & *(address>>20) &0xffc--页目录项,也即页表地址
(address>>10) & 0xffc--页表中的偏移地址
所以最终un_wp_page的参数是页表中表项的指针(如果对这个指针“*”取值操作将得到线性地址address对应的物理内存页面起始地址--4KB对齐)
221 void un_wp_page(unsigned long * table_entry)
222 {
223 unsigned long old_page,new_page;
224
225 old_page = 0xfffff000 & *table_entry;
226 if (old_page >= LOW_MEM && mem_map[MAP_NR(old_page)]==1) {
227 *table_entry |= 2;
228 invalidate();
229 return;
230 }
231 if (!(new_page=get_free_page()))
232 oom();
233 if (old_page >= LOW_MEM)
234 mem_map[MAP_NR(old_page)]--;
235 *table_entry = new_page | 7;
236 invalidate();
237 copy_page(old_page,new_page);
238 }
old_page是address对应的物理地址所在物理内存页面--4KB对齐
由于这里是用户试图往共享页面进行写操作,所以mem_map[MAP_NR(old_page)]应该大于1,226行if条件不满足直接到231行。
通过get_free_page()获取一页物理内存(4KB),new_page即为该内存页的物理地址。
234行,由于是用户内存页面,所以页面复制后,原来使用页面计数自减。
235行,更新指定页表项为新页面地址,并置页表项为:用户级/可读写/存在于内存
然后刷性页变换告诉缓冲,最后调用copy_page完成“写时复制”的复制操作。
54 #define copy_page(from,to) /
55 __asm__("cld ; rep ; movsl"::"S" (from),"D" (to),"c" (1024):"cx","di","si")
可见copy_page是写时复制最底层的操作了,它将物理页面起始地址old_page上1个物理内存页面(4KB)的内容复制到物理页面new_page上。
整理一下:_page_fault--->do_wp_page--->un_wp_page--->copy_page
特别要说明下,当cpu从此次异常处理返回用户空间,将会重新执行 原先引发页异常(写时复制) 那条指令(应该是那条写指令)。
这也是 为什么应该叫页面异常,而不是页面中断的原因(中断返回后会执行引发中断的指令的下一条指令)。
但是整个copy_on_write对于用户程序时“透明”的,好像用户程序从一开始就可以正常地进行写操作,而不用顾忌页面是否可写。