MIT6.828 Lab2 part2 Virtual Memo
环境
Ubuntu20.04 64位系统
lab2地址:lab2
正文
本次实验需要我们对paging有一个比较深入的了解,如果只是一知半解可能连代码都不知道什么意思。所以在正式开始代码实验之前,我们先来介绍一下一些预先需要的知识。在part1当中,主要做的就是对物理内存分页,我们使用了PageInfo 和一个page_free_list来管理空闲的页。使用page_alloc()来从page_free_list中申请一个页。在part2中,我们要做的就是对页的管理,比如说新申请到的页插入到page table中。查找某个虚拟地址是否已经被映射
- 寻址的方式
在远古的8086下,还没有引入保护模式。寻址方式通过addr = cs * 4+ip来实现。在稍微后面的cpu就引入了保护模式。引入了segment selector的概念,在保护模式下方式是addr = selector+ ip来实现。这里需要了解一下GDT的知识。我这里就不再过多介绍了。这种寻址方式叫做Segmentation Mechanism。再后来就是paging了。在保护模式的基础上引入了paging,paging能够更好的处理memory fragmentation(内存碎片)问题。在MIT6.828当中,segmentation寻址是在flat model下的,就是说offset就等于 虚拟地址。下面是mit6.828的图:
我们在boot/boot.s当中将segment descriptor的segment base 设置为0,这样就达到了关闭segment translation的目的。一个C语言的指针得到的地址就是直接等于虚拟地址。在上图中的意思就是,virtual address == linear address。得到linear address后再经过paging mechanism就得到了physical address。
在我们本次lab中,开启paging以后,所有的地址都是虚拟地址了,虽然准确来说叫线性地址更合适,但是前面说了在本文中,virtual address 等于linear address。
- Paging
如果之前了解过paging感觉再说这个多此一举。不过好记性不如烂笔头。还是暂且记录下关键的一些内容,以后自己忘了也可以再看看。paging的寻址方式具体如下,摘自vx6 book:
paging
上面的图虽然很直观,不过还差一点。有一个问题值得我们去关注,page directory和page table当中存放的地址是物理地址还是虚拟地址?前面我们说了开启paging后所有的地址都是虚拟地址。然而,这个问题答案是在page diectory和page table 当中存放的地址都是物理地址(占据了20bit,剩下的12bit是flag)。所以这一点告诉我们的就是:当往page directory中插入page table或者page table中插入某个page的时候,往里面写入的地址都应该是物理地址。下图帮助理解:
paging的寻址过程
想在intel手册当中将page directory entry的内容和page table的内容都放出来,看了一下intel 手册当中讲的比较复杂,因为他说了一些4MB页大小的情况,在我们32bit下的实验是不需要关注的。我放一个OSDev那边关于paging的一些解释:OSdev Paging
回到part2的实验
对于执行在CPU上的代码,一旦我们进入了保护模式。我们就没有办法直接使用linear address或者物理地址了,因为所有的对内存地址都会被MMU翻译过,此时的指针地址也是虚拟地址。JOS提供了两种数据类型,uintptr_t是表示虚拟地址,physaddr_t表示物理地址。这两种数据类型都是unsinged int,这也就是说如果两者相互赋值编译器并不会报错。
有时候,我们只知道物理地址,并且企图去操作它所包含的数据。但是因为所有的地址都是虚拟地址,所以当我们的物理地址去直接使用的时候是无效的。JSO之所以将0xf000_0000映射到0x0000_0000是因为这样能够帮助内核来完成读写即使它只知道虚拟地址或者物理地址。为了将物理地址转为虚拟地址,只需要将物理地址pa加上0xf000_0000即可。即:va = pa+0xf000_0000。你可以使用函数KADDR(pa)来获得虚拟地址。同样的,一些内核的数据都被放在0xf000_000之后的地方,而这些数据的地址映射到了0x0000_0000之后的物理地址。pa = va - 0xf000_0000,所以在给定的虚拟地址,我们使用PADDR(va)来得到物理地址。
在接下来的一些lab当中,你可能需要将多个虚拟地址映射到同一个物理地址上。所在PageInfo这一结构当中,pp_ref表示了有多少个虚拟地址被映射到了物理地址。
接下来还有一些话就不是特别重要的,就不翻译了。
Exercise4:
实现kern/pamp.c中的下列函数:
pgdir_walk()
boot_map_region()
page_lookup()
page_remove()
page_insert()
个人感觉part2的几个函数实现相对来说难一点,不够如果对paging了解的比较深刻。我没有很详细的解释每一句代码的意思,结合注释以及一些我的个人理解,应该问题不大,干就完事儿了!
第一题 pgdir_walk()
pgdir是page director的指针,返回虚拟地址va所属的page table entry,返回一个指向这个page table entry的指针。其他的相关信息看注释。简要讲述一下这道题的思路。首先我们需要看看这个虚拟地址对应的page directory entry(也就是虚拟地址va对应的page table的地址)是否存在。如果存在就直接返回page table entry的地址。
如果不存在,那么就需要去创建对应的page table,接着把page table对应的地址和权限写入到page directory enrty中,然后把va对应的page table entry的地址返回。注意,此时我们并不关注page table entry里面所指向的页到底是哪个,我们只是返回page table entry的地址。对于页的地址,并不是pgdir_walk()要去关注的。对于页的权限(page table 也是一个页嘛),注释里说了可以放松点(permissive),因为再访问page table的时候还要权限检查,所以page directory里面的内容权限松一点问题不大。
这题可能稍微有点难理解,稍微多想一下应该没问题
pte_t *
pgdir_walk(pde_t *pgdir, const void *va, int create)
{
//pgdir: 页目录的地址
//va:返回va地址所对应的page table entry地址
//也就是说需要用va先找到page directory entry,然后再找到page table entry
//但是page table 所在的页可能还不存在,if create flase,return null
//否则创建一个page table page with page alloc
//如果创建失败,return null
//创建成功,count ++
//然后返回新创建的page table page
// Fill this function in
uint32_t pg_dir_index = PDX(va);
uint32_t pg_table_index = PTX(va);
pde_t* pg_dir_entry = &pgdir[pg_dir_index]; //address of page directory entry,physical address
pte_t* pg_table_va = NULL;
struct PageInfo* new_page = NULL;
if(*pg_dir_entry & PTE_P) {
physaddr_t pg_table_pa = PTE_ADDR(*pg_dir_entry); //获得pg_table_pa的物理地址
pg_table_va = KADDR(pg_table_pa); //将pg_table的物理地址转为虚拟地址
} else {
if( !create ) return NULL;
new_page = page_alloc(ALLOC_ZERO);
if( new_page == NULL) return NULL;
new_page->pp_ref ++;
*pg_dir_entry = page2pa(new_page) | PTE_U |PTE_P | PTE_W;
}
physaddr_t pg_table_pa = PTE_ADDR(*pg_dir_entry); //获得pg_table_pa的物理地址
pg_table_va = KADDR(pg_table_pa); //将pg_table的物理地址转为虚拟地址,因为page table entry还包含flags
return &pg_table_va[pg_table_index]; //返回page table entry的地址,physical address
}
第二题 boot_map_region()
这一题是将物理地址和虚拟地址映射起来。注意size是要被映射的内存范围,而不是要被映射的页的个数,所以首先需要计算要被映射的页的数目。然后想一下,如何将这这两个内存范围映射起来。首先是不是先要看看对应的page table entry是否存在。那这个很自然就可利用上面刚实现的函数pgdir_walk(),它会将虚拟地址对应的page table entry返回,如果虚拟地址对应的page table entry不存在,他也会将其所属的page table创建出来,再返回page table entry。
所以这道题的思路就是:先调用pgdir_walk(),获得va对应的page table entry,然后往里面写入对应的物理地址pa(page table entry里面装的是页的物理地址)以及权限。接着再去遍历下一个页即可。
static void
boot_map_region(pde_t *pgdir, uintptr_t va, size_t size, physaddr_t pa, int perm)
{
size_t nums_pages = size / PGSIZE;
for(int i = 0; i < nums_pages; i++) {
pte_t* pg_table_entry = pgdir_walk(pgdir,(void*) va,1);
if (pg_table_entry == NULL)
panic("boot_map_regin:out of memory \n");
//pgdir_walk得到的pg_table_entry此时里面还没有页的物理地址(physicall address of page )
*pg_table_entry = pa | PTE_P | perm;
pa += PGSIZE;
va += PGSIZE;
}
}
第三题 page_lookup()
这题也应该不难,找到虚拟地址va所映射的页的物理地址。啥意思?就是找到虚拟地址va对应的page table entry嘛,要找page table entry肯定就是用pgdir_walk()函数。不过还有点小问题,这道题返回值是PageInfo,所以找到对应的page table entry后还要转换为PageInfo再返回。还有一个就是,题目要求我们把虚拟地址对应的page table entry的地址放在pte_store当中(待会再解释,马上就用到了)。废话不多说,上代码:
struct PageInfo * page_lookup(pde_t *pgdir, void *va, pte_t **pte_store)
{
pte_t* pg_table_entry = pgdir_walk(pgdir,va,0);
if (pg_table_entry == NULL) return NULL;
struct PageInfo* result = pa2page(PTE_ADDR(*pg_table_entry));
if(*pte_store) { // == if (*pte_store != 0)
*pte_store = pg_table_entry;
}
return result;
}
第四题 page_remove()
让虚拟地址va不在映射到它属于的物理页。咋做?回想一下PageInfo的作用是什么,每一个物理页都对应一个PageInfo,它的pp_pref表示被映射它的虚拟地址的个数。如果取消某个虚拟地址的映射,只要将PageInfo的pp_ref-1,那么这个地址就取消映射到某个物理页了。如果pp_ref = 0,将这个物理页还有放回到page_free_list当中去。
当我们下次申请的时候,这个物理页就会分配给别的虚拟地址。
思路:我们用page_lookup()找到虚拟地址对应的物理页的PageInfo,还要给page_lookup()传一个二级指针,用于存放虚拟地址va对应的page table entry。因为我们remove一个page后,它对应的page table entry肯定要清除。
void
page_remove(pde_t *pgdir, void *va)
{
// Fill this function in
pte_t* pte;
struct PageInfo* result = page_lookup(pgdir,va,&pte);
if( result == NULL) return;
page_decref(result);
tlb_invalidate(pgdir,va);
*pte = 0; //将page table entry归0
}
第五题 page insert()
page_insert()顾名思义,就是往page table塞入一个page。所以我们要关注的就是,首先看看page table是否存在,如果不存在先创建page table (很熟悉了吧,这个工作较给pgdir_walk()函数,它会返回page table entry的地址)。只要往page table entry里面放入对应的物理页的物理地址pa就行。当然啦如果虚拟地址已经存在一个已经映射的物理页,那就要先从 page table中移除这个物理页,再重新写入新分配的物理页的地址
int
page_insert(pde_t *pgdir, struct PageInfo *pp, void *va, int perm)
{
// Fill this function in
pte_t* pg_table_entry = pgdir_walk(pgdir,va,1);
if ( pg_table_entry == NULL ) return -E_NO_MEM;
pp->pp_ref ++;
if ( *pg_table_entry & PTE_P ) { //判断虚拟地址va对应的page table entry是否存在
tlb_invalidate(pgdir,va);
page_remove(pgdir,va);
}
*pg_table_entry = page2pa(pp) | perm | PTE_P;
return 0;
}
最后的实验结果如下图所示,只要看到check_page()出现就说明你成功了。
实验结果