399 void mem_init(l start_mem, l end_mem)
400 {
401 int i;
402
403 high_memory = end_mem;
404 for (i=0 ; i405 mem_map[i] = used;
406 i = map_nr(start_mem);
407 end_mem -= start_mem;
408 end_mem >>= 12;
409 while (end_mem-->0)
410 mem_map[i++]=0;
411 }
其实前面所有的申请内存的程序里都最终使用了一个函数get_free_page(),不管申请多少的内存,最终还是要按页面来申请:
63 unsigned l get_free_page(void)
64 {
65 register unsigned l __res asm("ax");
66
67 __asm__("std ; repne ; scasb\n\t"
68 "jne 1f\n\t"
69 "movb ,1(%%edi)\n\t"
70 "sall ,%%ecx\n\t"
71 "addl %2,%%ecx\n\t"
72 "movl %%ecx,%%edx\n\t"
73 "movl 24,%%ecx\n\t"
74 "leal 4092(%%edx),%%edi\n\t"
75 "rep ; stosl\n\t"
76 "movl %%edx,%%eax\n"
77 "1:"
78 :"=a" (__res)
79 :"" (0),"i" (low_mem),"c" (paging_pages),
80 "d" (mem_map+paging_pages-1)
81 :"di","cx","dx");
82 return __res;
83 }
这个函数就是在物理内存中找一张没有使用的页面并返回其物理地址。这是一段gcc内联汇编,它在mem_map数组中的最后一项一直向前找,只要找一项的值不为0,则用这个数组下标计算出物理地址返回,并把那一项的值设为1。用下标计算物理地址的方法我想是这样的:index*4096+low_men (std;repne;scasb,这三句是依次检查mem_map里的每一项的值,如果全部不为0,也即没有物理内存可以用,立即返回0。movb ,1(%%edi)这句就是把mem_map数组里找到的可用的一项的标志设为1。此时ecx里的值就是数组下标,因此sall ,%%ecx就是index*4096,addl %2,%%ecx即把刚才的index*4096+low_mem。73,74,5三句是把相应的物理内存空间内容全部清0。movl %%edx,%%eax显然是返回值了)。
有件事一定要做,那就是在返回之前把那个物理页面的内容全部清0。清0的事情让get_free_page做了,回收就简单了,只要把mem_map数组的相应项置为0就可以了,从下面可以看出来,free_page确实只做了这件事:
89 void free_page(unsigned l addr)
90 {
91 if (addr < low_mem) return;
92 if (addr >= high_memory)
93 panic("trying to free n page");
94 addr -= low_mem;
95 addr >>= 12;
96 if (mem_map[addr]--) return;
97 mem_map[addr]=0;
98 panic("trying to free free page");
99 }
进程退出时,会调用sys_exit,sys_exit只是调用了一下do_exit,回收内存的工作就在这里完成的。
106 free_page_tables(get_base(current->ldt[1]),get_limit(0x0f));
107 free_page_tables(get_base(current->ldt[2]),get_limit(0x17));
free_page_tables释放进程的代码段和数据段占用的内存,它内部使用循环,调用free_page完成最终的工作。
由于新进程和其父进程共享物理内存页面,因此把这些物理页面重新都设成只读是必要的。上面这句是放在copy_page_tables函数里面的循环中的。copy_mem主要是靠调用这个程序来完成工作的。分析到这里,我终于可以小舒一口气了。不如回顾一下:系统初始化的时候在内存起始处建一张页目录(page_dir),以后所有的进程都使用这张页目录。并为系统建了4张页表。以后每有新进程产生,便为之分配空间存放pcb(即struct task_struct),然后为之通过复制父进程的页表来创建自己的页表,并创建相应的页目录项。
程序运行了,问题又来了。终于读到了“写时复制”和请求调页的部分。当程序访问的线性地址没有被映射到一个物理页面,或欲写操作的线性地址映射的物理页面仅是只读,都会产生一个页异常,然后就会转去页异常中断处理程序(int 14)执行,页异常中断处理程序(page.s)如下:
14 _page_fault:
15 xchgl %eax,(%esp)
16 pushl %ecx
17 pushl %edx
18 push %ds
19 push %es
20 push %fs
21 movl x10,%edx
22 mov %dx,%ds
23 mov %dx,%es
24 mov %dx,%fs
25 movl %cr2,%edx
26 pushl %edx
27 pushl %eax
28 testl ,%eax
29 jne 1f
30 call _do_no_page
31 jmp 2f
32 1: call _do_wp_page
33 2: addl ,%esp
34 pop %fs
35 pop %es
36 pop %ds
37 popl %edx
38 popl %ecx
39 popl %eax
40 iret
根据error_code判断是缺页还是写保护引起的异常,然后去执行相应的处理程序段,先看写保护的处理吧。
247 void do_wp_page(unsigned l error_code,unsigned l address)
248 {
249 #if 0
250 /* we cannot do this yet: the estdio library writes to code space */
251 /* stupid, stupid. i really want the libc.a from gnu */
252 if (code_space(address))
253 do_exit(sigsegv);
254 #endif
255 un_wp_page((unsigned l *)
256 (((address>>10) & 0xffc) + (0xfffff000 &
257 *((unsigned l *) ((address>>20) &0xffc)))));
258
259 }
程序就一个函数调用,很少有这么简单的函数,哈哈!address很显然是程序想要访问但引起出错的线性地址了。(0xfffff000&*((unsigned l *)((address>>20)&0xffc))计算出32位线性地址对应页表的地址,再加上一个((address>>10) & 0xffc),就是加上页表内的偏移量,即得到页表内的一个页表项。看un_wp_page()就更明白了。
221 void un_wp_page(unsigned l * table_entry)
222 {
223 unsigned l 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 &nb
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 }
225-229做了个判断,如果此物理页面没有被共享,则只要将可写位置1(227)。不然就进入231行去。
在物理内存中分配一页空间,把原页面的内容copy到新页面里(copy_page),再把那个引起出错的address映射到这个新页面的物理地址上去(235行)。至此,写保护出错的处理完成了,可以返回去执行原进程里引起出错的那条指令了。
上面所述,就是所谓的“写时复制(copy on write)”。如果是缺页异常的话,则执行do_no_page,最简单的办法就是直接申请一张物理页面,对应到这个引起出错的address,如下:
372 address &= 0xfffff000;
373 tmp = address - current->start_code;
374 if (!current->executable || tmp >= current->end_data) {
375 get_empty_page(address);
376 return;
377 }
如果这样了之,那也太不负责任了,只是在!current->executable || tmp >= current->end_data的情况下,才这样做。这是怎样的情况呢?!current->executable有待阅读,tmp >= current->end_data很简单,在程序体已全部读入内存后,这可能是动态内存分配所要求的内存空间。否则就尝试去和别的进程共享一下,如下:
378 if (share_page(tmp))
379 return;
如果共享不成,那也只好自己申请一张页面了,如下:
380 if (!(page = get_free_page()))
381 oom();