最近一段时间在阅读Linux的源代码,想把看到的东西写出来,觉得内存这一部分最简单,就先写了出来。请指正!
内存最低4K的地址是一张页目录(page_dir),页目录共1024项,每项4字节。目录项的结构如下:
____________________________________|32-12位为页框地址 | |U|R|p|| | |S|W| ||_________________|______ |_|_ |_| |
随后的16K,用来做了4张页表,页表项结构和页目录项结构一样。页表的每一项指向一个物理页面,也就是指向内存中的一个4K大小的空间。有了这4张页表,已经能寻址16M的内存了。下面就是在系统初始化的时候在head.s程序中设置一张页目录和四张页表的代码。此时页目录中仅前4项有效,正是指向位于其下面的4张页表,而这4张页表寻址了内存的最低16M。
198 setup_paging:199 movl 24*5,%ecx /* 5 pages - pg_dir+4 page tables */200 xorl %eax,%eax201 xorl %edi,%edi /* pg_dir is at 0x000 */202 cld;rep;stosl203 movl $pg0+7,_pg_dir /* set present bit/user r/w */204 movl $pg1+7,_pg_dir+4 /* --------- " " --------- */205 movl $pg2+7,_pg_dir+8 /* --------- " " --------- */206 movl $pg3+7,_pg_dir+12 /* --------- " " --------- */207 movl $pg3+4092,%edi208 movl xfff007,%eax /* 16Mb - 4096 + 7 (r/w user,p) */209 std210 1: stosl /* fill pages backwards - more efficient :-) */211 subl x1000,%eax212 jge 1b |
以后每次有fork新进程,都要为新进程分配内存。但具体是怎么做的呢,我也想知道,一起看吧。当执行fork时,它使用int0x80调用sys_fork函数,sys_fork的代码位于system_call.s中,很短如下:
208 _sys_fork:209 call _find_empty_process210 testl %eax,%eax211 js 1f212 push %gs213 pushl %esi214 pushl %edi215 pushl %ebp216 pushl %eax217 call _copy_process218 addl ,%esp219 1: ret |
看到其中调用了两个函数,find_empty_process and copy_process,这两个函数在fork.c文件里实现的。find_empty_process是为将要创建的新进程找一个pid,保存在last_pid里,然后调用copy_process,这是sys_fork真正的主程序,其中有如此句:
77 p = (struct task_struct *) get_free_page(); |
先为新进程分配一张物理页面,用来存放进程的PCB结构,即task_struct结构。光给新进程一张物理页面来存放它的task_struct,显然是不能满足它的。我们知道,在创建之初,新进程是和其父进程共享代码和数据的。这是人为定的,不过这样的好处不言而喻。因此在创建的时候就没有必要将其代码和数据全部copy到新内存地址里,而只为新进程创建页目录项和页表就可以了。代码如下:
115 if (copy_mem(nr,p)) { /*copy_mem调用memory.c里的copy_page_tables*/116 task[nr] = NULL;117 free_page((long) p);118 return -EAGAIN;119 } |
copy_mem为新进程分配页表空间,并把父进程的页表内容copy到新进程的页表空间里,这样新进程的页表的每一项指向的物理页面和其父进程页表的相应每一项指向的物理页面是一样的。少说了一些,不能只copy页表就完事了。32位线性地址转换为物理地址的时候,最先要找到32位线性地址对应的页目录项,再用页目录项找到页表地址。新进程有了自己的页表,并且页表也都指向了物理地址,现在少的就是页目录项了。新进程在创建的时候,在4G线性空间里给其分配了64M的线性空间,是通过设置LDT来完成的:
130 set_ldt_desc(gdt+(nr<<1)+FIRST_LDT_ENTRY,&(p->ldt)); |
这64M的线性地址是从nr*64M的地址处开始的,这个地址正好可以被映射到页目录里的一项,这项的地址是:((nr*64M)>>20)&0xffc。只要从这里开始,在页目录里建一些页目录项,指向新创建的进程的页表地址(copy_mem调用copy_page_tables()来做的)。到这里,copy_mem的工作可以说是完成了,不过一定不能少了这一句:
177 this_page &= ~2; (memory.c) |
由于新进程和其父进程共享物理内存页面,因此把这些物理页面重新都设成只读是必要的。上面这句是放在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 long error_code,unsigned long address)248 {249 #if 0250 /* 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 #endif255 un_wp_page((unsigned long *)256 (((address>>10) & 0xffc) + (0xfffff000 &257 *((unsigned long *) ((address>>20) &0xffc)))));258 259 } |