Part2:Virtual Memory
首先要熟悉x86的保护模式的内存管理体系,也就是分段和页转换。
Exercise 2
如果您还不熟悉分段与分页,请查看“Intel 80386参考手册”的第5章和第6章。 仔细阅读有关页面转换和基于页面的保护的部分(5.2和6.4)。 我们建议您浏览有关细分的部分; 虽然JOS使用分页硬件进行虚拟内存和保护,但在x86上无法禁用分段转换和基于段的保护,因此您需要对它有基本的了解。
Intel 80386 Reference Programmer's ManualTable of Contents
感觉x86的分段式特别复杂,配置起来很繁琐。 分页式相对来说就很清晰明了了,I like it.
虚拟、线性和物理内存
在x86术语中,虚拟地址由段选择器和段内偏移组成。 线性地址是段转换之后但在页转换之前获得的。 物理地址是在段和页面转换之后得到的,以及最终经过总线到达RAM的内容。
exercise 3
第一个窗口执行make qemu-gdb
, 第二个执行make gdb
. 然后第二个窗口执行c
在ctrl+c
终止程序。键入查看内存指令x/16xw 0xf0100000
:
(gdb) x/16xw 0xf0100000
0xf0100000 <_start+4026531828>: 0x1badb002 0x00000000 0xe4524ffe 0x7205c766
0xf0100010 <entry+4>: 0x34000004 0x2000b812 0x220f0011 0xc0200fd8
0xf0100020 <entry+20>: 0x0100010d 0xc0220f80 0x10002fb8 0xbde0fff0
0xf0100030 <relocated+1>: 0x00000000 0x112000bc 0x0056e8f0 0xfeeb0000
第二终端再按c
,保持程序的运行。 第一终端按ctrl+a c
出现qemu终端。
(qemu) xp/16xw 0x100000
0000000000100000: 0x1badb002 0x00000000 0xe4524ffe 0x7205c766
0000000000100010: 0x34000004 0x2000b812 0x220f0011 0xc0200fd8
0000000000100020: 0x0100010d 0xc0220f80 0x10002fb8 0xbde0fff0
0000000000100030: 0x00000000 0x112000bc 0x0056e8f0 0xfeeb0000
两个地址上的内容一致,故可以证明0xf0100000
虚拟地址映射到了0x100000
物理地址上。
一旦进入保护模式,所有的指针都会被解释成虚拟地址并由MMU翻译。
JOS内核通常需要将地址转换为不透明值或整数,而不需要对它们解引用,例如在物理内存分配器中。 有时这些是虚拟地址,有时它们是物理地址。 为了帮助规范代码,JOS源代码区分了两种情况:类型uintptr_t
表示不透明的虚拟地址,physaddr_t
表示物理地址。 这两种类型实际上只是32位整数(uint32_t
)的同义词,因此编译器不会阻止您将一种类型分配给另一种类型! 由于它们是整数类型(不是指针),如果您尝试对它们解引用,编译器就会给出警告或错误。
JOS内核可以通过首先将其转换为指针类型来解引用uintptr_t。 相反,内核不能明智地解引用物理地址,因为MMU会转换所有内存引用。 如果将physaddr_t转换为指针并解引用它,您可以加载并存储到结果地址(硬件会将其解释为虚拟地址),但您可能无法获得预期的内存位置。
Questions
Assuming that the following JOS kernel code is correct, what type should variable x have, uintptr_t or physaddr_t?
mystery_t x;
char* value = return_a_pointer();
*value = 10;
x = (mystery_t) value;
答: 是uintprt_t
第三句对*value进行了赋值,所以value肯定是一个虚拟地址,因为直接解引用物理地址是没有意义的。那么x肯定也是虚拟地址,要不然将虚拟地址赋值给一个物理地址也没有意义的。
引用计数
使用page_alloc时要小心。 它返回的页面的引用计数始终为0,因此只要您对返回的页面执行某些操作(例如将其插入页面表),pp_ref就应该递增。 有时这是由其他函数处理的(例如,page_insert),有时调用page_alloc的函数必须直接执行。
页表管理
现在,您将编写一组用于管理页表的例程:插入和删除线性到物理映射,以及在需要时创建页表页。
首先还复习一下MMU的知识。MMU (一)
Execise 4
In the file kern/pmap.c, you must implement code for the following functions.
- pgdir_walk()
- boot_map_region()
- page_lookup()
- page_remove()
page_insert()
check_page(), called from mem_init(), tests your page table management routines. You should make sure it reports success before proceeding.
需要复习一下LAB1的PART3部分,其处通过一个手写的页表,实现了4M的虚拟内存向物理内存的映射,理解页目录表,页目录项,页表,页表项的异同。
名词 | 说明 |
---|---|
页目录表 | 存放各个页目录项的表,页目录常驻内存,页目录表的物理地址存在寄存器CR3中 |
页目录项 | 存放各个二级页表起始物理地址 |
页表 | 存放页表项 |
页表项 | 页表项的高20位存放各页的对应的物理地址的高20位 |
错误
kernel panic at kern/pmap.c:841: assertion failed: page_insert(kern_pgdir, pp1, 0x0, PTE_W) < 0
这里的行号与源代码不太一致,不知道为什么,应该是死在了828行的assert
上,通过反汇编找到代码位置,f01016fe
,gdb进去。最后发现自己被坑了,在page_insert()
函数中,我写得return E_NO_MEM
, 而E_NO_MEM是一个大于1的枚举值,也就是如果发生错误,返回值是非负数,所以会一直卡在assert上。// Fill this function in pte_t *pte = pgdir_walk(pgdir, va, 1); if (!pte) { return -E_NO_MEM; }
`kernel panic at kern/pmap.c:837: assertion failed: PTE_ADDR(kern_pgdir[0]) == page2pa(pp0)
, 出现这个错误,应该是
insert不成功导致的,但是始终看不
insert哪里错了。最后发现是
pdgir_walk()`函数里就出现了错误。
原来当页表不存在时,创建页表给错了其物理地址部分*pde = PTE_ADDR(*pte) | PTE_P | PTE_W | PTE_U; // 设置页目录项 应该改为 *pde = PADDR(pte) | PTE_P | PTE_W | PTE_U; //设置页目录项
最终代码
pgdir_walk
pte_t *
pgdir_walk(pde_t *pgdir, const void *va, int create)
{
// Fill this function in
uint32_t pdx = PDX(va); // 页目录项索引
uint32_t ptx = PTX(va); // 页表项索引
pte_t *pde; // 页目录项指针
pte_t *pte; // 页表项指针
struct PageInfo *pp;
pde = &pgdir[pdx]; //获取页目录项
if (*pde & PTE_P) {
// 二级页表有效
// PTE_ADDR得到物理地址,KADDR转为虚拟地址
pte = (KADDR(PTE_ADDR(*pde)));
}
else {
// 二级页表不存在,
if (!create) {
return NULL;
}
// 获取一页的内存,创建一个新的页表,来存放页表项
if(!(pp = page_alloc(ALLOC_ZERO))) {
return NULL;
}
pte = (pte_t *)page2kva(pp);
pp->pp_ref++;
*pde = PADDR(pte) | (PTE_P | PTE_W | PTE_U); // 设置页目录项
}
// 返回页表项的虚拟地址
return &pte[ptx];
}
1.5.2. boot_map_region
static void
boot_map_region(pde_t *pgdir, uintptr_t va, size_t size, physaddr_t pa, int perm)
{
size_t pgs = size / PGSIZE;
if (size % PGSIZE != 0) {
pgs++;
} //计算总共有多少页
for (int i = 0; i < pgs; i++) {
pte_t *pte = pgdir_walk(pgdir, (void *)va, 1);//获取va对应的PTE的地址
if (pte == NULL) {
panic("boot_map_region(): out of memory\n");
}
*pte = pa | PTE_P | perm; //修改va对应的PTE的值
pa += PGSIZE; //更新pa和va,进行下一轮循环
va += PGSIZE;
}
}
1.5.3. page_insert
int
page_insert(pde_t *pgdir, struct PageInfo *pp, void *va, int perm)
{
// Fill this function in
pte_t *pte = pgdir_walk(pgdir, va, 1);
if (!pte) {
return -E_NO_MEM;
}
if (*pte & PTE_P) {
if (PTE_ADDR(*pte) == page2pa(pp)) {
// 插入的是同一个页面,只需要修改权限等即可
pp->pp_ref--;
}
else {
page_remove(pgdir, va);
}
}
pp->pp_ref++;
*pte = page2pa(pp)| perm | PTE_P;
return 0;
}
1.5.4. page_lookup
struct PageInfo *
page_lookup(pde_t *pgdir, void *va, pte_t **pte_store)
{
// Fill this function in
pte_t *pte = pgdir_walk(pgdir, va, 0);
if (!pte) {
return NULL;
}
if (pte_store) {
*pte_store = pte; // 通过指针的指针返回指针给调用者
}
// 难道不用考虑页表项是否存在
if (*pte & PTE_P) {
return (pa2page(PTE_ADDR(*pte)));
}
return NULL;
//return pa2page(PTE_ADDR(*pte));
}
1.5.5. page_remove
void
page_remove(pde_t *pgdir, void *va)
{
// Fill this function in
// 二级指针有点晕
pte_t *pte;
pte_t **pte_store = &pte;
struct PageInfo *pi = page_lookup(pgdir, va, pte_store);
if (!pi) {
return ;
}
page_decref(pi); // 减引用
**pte_store = 0; // 取消映射
tlb_invalidate(pgdir, va);
}
Comments | NOTHING