使用多级页表
我们需要把页表放在内存里,并且需要有办法修改页表,比如在页表里增加一个页面的映射或者删除某个页面的映射。
最主要的是两个接口:
page_insert(),在页表里建立一个映射
page_remove(),在页表里删除一个映射
这些我们都在kern/mm/pmm.c里面编写。然后我们在虚拟内存空间的第一个大大页(Giga Page)随便建立一些映射来做测试。用来检查的函数page_ref()返回一个物理页面被多少个虚拟页面所对应。
static void check_pgdir(void) {
// assert(npage <= KMEMSIZE / PGSIZE);
// The memory starts at 2GB in RISC-V
// so npage is always larger than KMEMSIZE / PGSIZE
assert(npage <= KERNTOP / PGSIZE);
//boot_pgdir是页表的虚拟地址
assert(boot_pgdir != NULL && (uint32_t)PGOFF(boot_pgdir) == 0);
assert(get_page(boot_pgdir, 0x0, NULL) == NULL);
//get_page()尝试找到虚拟内存0x0对应的页,现在当然是没有的,返回NULL
struct Page *p1, *p2;
p1 = alloc_page();//拿过来一个物理页面
assert(page_insert(boot_pgdir, p1, 0x0, 0) == 0);//把这个物理页面通过多级页表映射到0x0
pte_t *ptep;
assert((ptep = get_pte(boot_pgdir, 0x0, 0)) != NULL);
assert(pte2page(*ptep) == p1);
assert(page_ref(p1) == 1);
ptep = (pte_t *)KADDR(PDE_ADDR(boot_pgdir[0]));
ptep = (pte_t *)KADDR(PDE_ADDR(ptep[0])) + 1;
assert(get_pte(boot_pgdir, PGSIZE, 0) == ptep);
//get_pte查找某个虚拟地址对应的页表项,如果不存在这个页表项,会为它分配各级的页表
p2 = alloc_page();
assert(page_insert(boot_pgdir, p2, PGSIZE, PTE_U | PTE_W) == 0);
assert((ptep = get_pte(boot_pgdir, PGSIZE, 0)) != NULL);
assert(*ptep & PTE_U);
assert(*ptep & PTE_W);
assert(boot_pgdir[0] & PTE_U);
assert(page_ref(p2) == 1);
assert(page_insert(boot_pgdir, p1, PGSIZE, 0) == 0);
assert(page_ref(p1) == 2);
assert(page_ref(p2) == 0);
assert((ptep = get_pte(boot_pgdir, PGSIZE, 0)) != NULL);
assert(pte2page(*ptep) == p1);
assert((*ptep & PTE_U) == 0);
page_remove(boot_pgdir, 0x0);
assert(page_ref(p1) == 1);
assert(page_ref(p2) == 0);
page_remove(boot_pgdir, PGSIZE);
assert(page_ref(p1) == 0);
assert(page_ref(p2) == 0);
assert(page_ref(pde2page(boot_pgdir[0])) == 1);
free_page(pde2page(boot_pgdir[0]));
boot_pgdir[0] = 0;//清除测试的痕迹
cprintf("check_pgdir() succeeded!\n");
}我们来看page_insert(),page_remove()的实现。注意它们都要调用两个对页表项进行操作的函数:get_pte()和page_remove_pte()
在entry.S里,我们虽然构造了一个简单映射使得内核能够运行在虚拟空间上,但是这个映射是比较粗糙的。
我们知道一个程序通常含有下面几段:
段:存放代码,需要是可读、可执行的,但不可写。
段:存放只读数据,顾名思义,需要可读,但不可写亦不可执行。
段:存放经过初始化的数据,需要可读、可写。
段:存放经过零初始化的数据,需要可读、可写。与 段的区别在于由于我们知道它被零初始化,因此在可执行文件中可以只存放该段的开头地址和大小而不用存全为 0的数据。在执行时由操作系统进行处理。
我们看到各个段需要的访问权限是不同的。但是现在使用一个Giga Page进行映射,它们都有相同的权限,在现在的映射下,我们甚至可以修改内核 段的代码!因为我们通过一个标志位 的页表项完成映射。而这会带来一个埋藏极深的隐患。
因此,我们考虑对这些段分别进行重映射,使得他们的访问权限被正确设置。虽然还是每个段都还是映射以同样的偏移量映射到相同的地方,但实现需要更加精细。
这里有一个小坑:对于我们最开始已经用特殊方式映射的一个Giga Page,该怎么对那里面的地址重新进行映射?这比较麻烦。我们可以放弃现有的页表,直接新建一个页表,在新页表里面完成重映射,然后把satp指向新的页表。这一部分的实现我们留作课后练习。
最后更新于
这有帮助吗?