自映射机制
这是扩展知识。 上一小节讲述了通过boot_map_segment函数建立了基于一一映射关系的页目录表项和页表项,这里的映射关系为:
virtual addr (KERNBASE~KERNBASE+KMEMSIZE) = physical_addr (0~KMEMSIZE)
这样只要给出一个虚地址和一个物理地址,就可以设置相应PDE和PTE,就可完成正确的映射关系。
如果我们这时需要按虚拟地址的地址顺序显示整个页目录表和页表的内容,则要查找页目录表的页目录表项内容,根据页目录表项内容找到页表的物理地址,再转换成对应的虚地址,然后访问页表的虚地址,搜索整个页表的每个页目录项。这样过程比较繁琐。
我们需要有一个简洁的方法来实现这个查找。ucore做了一个很巧妙的地址自映射设计,把页目录表和页表放在一个连续的4MB虚拟地址空间中,并设置页目录表自身的虚地址<-->物理地址映射关系。这样在已知页目录表起始虚地址的情况下,通过连续扫描这特定的4MB虚拟地址空间,就很容易访问每个页目录表项和页表项内容。
具体而言,ucore是这样设计的,首先设置了一个常量(memlayout.h):
VPT=0xFAC00000, 这个地址的二进制表示为:
1111 1010 1100 0000 0000 0000 0000 0000
高10位为1111 1010 11,即10进制的1003,中间10位为0,低12位也为0。在pmm.c中有两个全局初始化变量
pte_t * const vpt = (pte_t *)VPT;
pde_t * const vpd = (pde_t *)PGADDR(PDX(VPT), PDX(VPT), 0);
并在pmm_init函数执行了如下语句:
boot_pgdir[PDX(VPT)] = PADDR(boot_pgdir) | PTE_P | PTE_W;
这些变量和语句有何特殊含义呢?其实vpd变量的值就是页目录表的起始虚地址0xFAFEB000,且它的高10位和中10位是相等的,都是10进制的1003。当执行了上述语句,就确保了vpd变量的值就是页目录表的起始虚地址,且vpt是页目录表中第一个目录表项指向的页表的起始虚地址。此时描述内核虚拟空间的页目录表的虚地址为0xFAFEB000,大小为4KB。页表的理论连续虚拟地址空间0xFAC00000~0xFB000000,大小为4MB。因为这个连续地址空间的大小为4MB,可有1M个PTE,即可映射4GB的地址空间。
但ucore实际上不会用完这么多项,在memlayout.h中定义了常量
#define KERNBASE 0xC0000000
#define KMEMSIZE 0x38000000 // the maximum amount of physical memory
#define KERNTOP (KERNBASE + KMEMSIZE)
表示ucore只支持896MB的物理内存空间,这个896MB只是一个设定,可以根据情况改变。则最大的内核虚地址为常量
#define KERNTOP (KERNBASE + KMEMSIZE)=0xF8000000
所以最大内核虚地址KERNTOP的页目录项虚地址为
vpd+0xF8000000/0x400000*4=0xFAFEB000+0x3E0*4=0xFAFEBF80
最大内核虚地址KERNTOP的页表项虚地址为:
vpt+0xF8000000/0x1000*4=0xFAC00000+0xF8000*4=0xFAFE0000
需要注意,页目录项和页表项是4字节对齐的。从上面的设置可以看出KERNTOP/4M后的值是4字节对齐的,所以这样算出来的页目录项和页表项地址的最后两位一定是0。
在pmm.c中的函数print_pgdir就是基于ucore的页表自映射方式完成了对整个页目录表和页表的内容扫描和打印。注意,这里不会出现某个页表的虚地址与页目录表虚地址相同的情况。
print_pgdir函数使得 ucore 具备和 qemu 的info pg相同的功能,即print pgdir能 够从内存中,将当前页表内有效数据(PTE_P)印出来。拷贝出的格式如下所示:
PDE(0e0) c0000000-f8000000 38000000 urw
|-- PTE(38000) c0000000-f8000000 38000000 -rw
PDE(001) fac00000-fb000000 00400000 -rw
|-- PTE(000e0) faf00000-fafe0000 000e0000 urw
|-- PTE(00001) fafeb000-fafec000 00001000 -rw
上面中的数字包括括号里的,都是十六进制。
主要的功能是从页表中将具备相同权限的 PDE 和 PTE 项目组织起来。比如上表中:
PDE(0e0) c0000000-f8000000 38000000 urw
• PDE(0e0):0e0表示 PDE 表中相邻的 224 项具有相同的权限; • c0000000-f8000000:表示 PDE 表中,这相邻的两项所映射的线性地址的范围; • 38000000:同样表示范围,即f8000000减去c0000000的结果; • urw:PDE 表中所给出的权限位,u表示用户可读,即PTE_U,r表示PTE_P,w表示用 户可写,即PTE_W。
PDE(001) fac00000-fb000000 00400000 -rw
表示仅 1 条连续的 PDE 表项具备相同的属性。相应的,在这条表项中遍历找到 2 组 PTE 表项,输出如下:
|-- PTE(000e0) faf00000-fafe0000 000e0000 urw
|-- PTE(00001) fafeb000-fafec000 00001000 -rw
注意:
- PTE 中输出的权限是 PTE 表中的数据给出的,并没有和 PDE 表中权限做与运算。 2. 整个print_pgdir函数强调两点:第一是相同权限,第二是连续。 3. print_pgdir中用到了vpt和vpd两个变量。可以参 考VPT和PGADDR两个宏。
自映射机制还可方便用户态程序访问页表。因为页表是内核维护的,用户程序很难知道自己页表的映射结构。VPT 实际上在内核地址空间的,我们可以用同样的方式实现一个用户地址空间的映射(比如 pgdir[UVPT] = PADDR(pgdir) | PTE_P | PTE_U,注意,这里不能给写权限,并且 pgdir 是每个进程的 page table,不是 boot_pgdir),这样,用户程序就可以用和内核一样的 print_pgdir 函数遍历自己的页表结构了。