内存分页机制是实现虚拟内存的基础,它通过把现有的物理内存按一定的大小分成多个区域,这样的内存区域被称作“页面”。我们把每个页面大小设定为4kb,也就是4096字节。并通过页目录和页表来描述内存的分页使用情况。在开启分页之前要先设置好页目录和页表的内容,并为cr3寄存器设置页目录所在的物理内存地址。这样程序就会通过cr3寄存器找到页目录,进而找到页表,来管理和查看内存的使用情况。页目录和页表的格式如下:
P:页面是在内存中1,还是不在内存中0,如果不在内存中触发页面失效异常。
R/W:页面是只读0,还是可写1。
U/S:是普通用户0,还是超级用户1。
X:保留,设置为0。
D:保留,设置为0。
A:页面没有被存取0,还是已经被存取1。
Not Used:提供用户自定义。
PageTable's Physical Address:页表所在物理内存地址的高20位。
Page's Physical Address:页面所在物理内存地址的高20位。
由于页目录和页表的地址域只有20位,所以对内存分页时每个页面要以4kb对齐。将页面的物理
地址的高20位存入页表中,再将页表的物理地址的高20位存入页目录中,最后将页目录的物理地址存入cr3寄存器中。我们预计为每一个任务都分配4G的逻辑内存,于是page_no(页面数)= 4G / 4K = 1M也就是说一共需要1M个页面,这也正是页表中20个地址位所能够表示的数量( )。我们需要在内存中定义1024个页目录,每个页目录中存放了一组页表的首地址,每组有1024个页表,每个页表中存放了页面物理地址的高20位,如下图:
在CPU开启分页之后,对于一个给定的逻辑地址,CPU在寻址时首先查看当前使用的GDT或LDT,做权限和长度检查,并将GDT或LDT中的基地址相加得到一个线性地址,再把这个线性地址除以页面大小(4096)等到页面号,再检查此页是否在内存中,如果此页不在内存中或权限不够,触发失效异常。在这个页面失效异常的处理程序中将再次检查这个失效页是尚未分配还是已经分配,如果是尚未分配,则为其分配一个新页面;如果是已经分配,说明此页面被换出到交换分区中,这时需要将此页面从交换分区中换回到内存当中。逻辑地址的高10位(22 ~ 31bit)表示的是页目录的索引号,中间10位(12 ~ 21bit)表示的是当前页表的索引号,低12位(0 ~ 11bit)表示的是页内偏移地址。
例如:对于一个逻辑地址0xEF32F4A8,CPU对其地址解析的过程如下:
从cr3寄存器中读出页目录所在地址。
计算出逻辑地址0xEF32F4A8高10位的值为0x3BC。
取得页目录第0x3BC项的值,假设其值为0xEF5EAC007。
取得此页目录值的高20位得到页表地址0xEF5EAC000。
计算出逻辑地址0xEF32F4A8中间10位的值为0x32F。
取得0xEF5EAC000处页表组的第0x32F项的值,假设其值为0x1FEC5007。
取得此页表值的高20位等到页面地址0x1FEC5000。
计算出逻辑地址0xEF32F4A8的低12位的值为0x4A8即:页内偏移地址。
将页面地址0x1FEC5000加上内偏移地址0x4A8得到的就是实际物理地址0x1FEC54A8。
需要说明的是,为了使逻辑地址被CPU处理起来更加的简单,我们采用的LDT段地址均为0 ~ 4G,也就是说逻辑地址与线性地址一致,也就省去了CPU从逻辑地址到线性地址的转换过程。当CPU遇到一个不存在的页或页表或者权限冲突,页面无效异常程序被执行。CR2存储了导致这个异常的逻辑地址,错误码被压入栈,格式如下:
在开启分页之前首先要处理好默认的页目录和页表,并设置好cr3寄存器的值。也就是说在开启分页之前要先让cr3载入正确的页目录地址。
定义页目录的起始地址:
#define PAGE_DIR (0x700000) #define PAGE_TABLE (PAGE_DIR + 0x1000) //页目录开始于 [0x700000, 0x701000) ,大小为1024个(每个4字节)共4096字节 u32 *page_dir = ((u32 *) PAGE_DIR); //页表1开始于0x700000 + 0x1000 = 0x701000 u32 *page_table = ((u32 *) PAGE_TABLE);
这里的page_dir和page_table与alloc.c中的MMAP一样是由系统内核静态分配的。我们设定小于16M的内存已经被使用了(实际上操作系统内核使用内存1M,其它为MMAP、PAGE_DIR、PAGE_TAB所使用),所以将page_dir的前4项设置为常驻内存,另外任务A和任务B在被安装到系统中时,它们都申请了一部分内存用于存放pcb的内容和它们所使用的代码段、数据段。这一区域的内存也要被设置为已在内存中,所以暂时将32M以下的内存也设置为已在内存中(这不是一个好主意,我们将在下一节中学习如何利用缺页异常来动态管理页面),所以page_dir的前8项被设置:
//用于内存地址计算 u32 address = 0; //处理所有页目录 //页表开始于 [0x701000, 0xb01000) ,大小为4M for (int i = 0; i < 1024; i++) { if (i < 8) { for (int j = 0; j < 1024; j++) { page_table[j] = address | 7; address += MM_PAGE_SIZE; } page_dir[i] = ((u32) page_table | 7); page_table += 1024; } else { page_dir[i] = (6); } }
设置页目录的值到cr3寄存器:
__asm__ volatile("movl %%eax, %%cr3" :: "a"(PAGE_DIR));
开启内存分页,其实就是将cr0寄存器的31位置为1:
__asm__ volatile( "movl %cr0, %eax;" "orl $0x80000000, %eax;" "movl %eax, %cr0;" );
再将任务B的代码中做一点修改,让其尝试访问32M以上的内存:
void run_B() { //尝试读入32M以上的内存区域 char *ps = (char *) 0x2000000; //这里是真正读内存,会触发缺页异常 char ch = *ps; while (1) { } }
然后在安装任务A和任务B时,为它们tss中的cr3寄存器都置为PAGE_DIR(同样这也不是一个好注意,但这里我们也只是为了学习分页机制的原理,关于让每个任务都有自己能够独立使用的逻辑内存的问题我们会在下一节中学习):
pcb_A->tss.cr3 = PAGE_DIR; pcb_B->tss.cr3 = PAGE_DIR;
再在int.S中加入处理缺页异常的处理程序:
//页错误 _int_0x0e: cli //C函数中参数和变量所用的栈 push %ebp mov %esp, %ebp //调用C函数来处理这个异常 call int_page_error leave sti iret
在int_page_error中只是显示了缺页异常的提示信息,并没有对缺页做任何的处理:
void int_page_error() { printf("int_page_error.\n"); hlt(); }
最后在Makefile中加入page.c的相关编译选项,编译并运行查看结果:
可以看到,任务A正常运行并在屏幕的右下角显示了一个字符,但任务B要访问32M以上的内存区域,但这一内存页并不在内存中,于是出现了“int_page_error.”的缺页异常。
源代码的下载地址为:
https https://github.com/magicworldos/lidqos.git git git@github.com:magicworldos/lidqos.git subverion https://github.com/magicworldos/lidqos branch v0.15
Copyright © 2015-2023 问渠网 辽ICP备15013245号