MIT6.828——Lab2(麻省理工操作系统实验)
Lab2
Lab2 是关于操作系统存储管理的细节。主要是建立内存模型,页表,物理地址映射等。
在Lab2之前,请复习好前序知识:
Part1 物理内存管理
在开始做题之前,需要了解一下一些常用的函数,宏以及内存布局,建议复习一下LAB1中的简单内存模型,LAB2预备知识中的相关。这里有几个很有用的地址变换工具,具体实现可以查看mmu.h
和pmap.h
,提前掌握这些小工具对于理解地址变换和后续的程序编写有很大帮助。
名称 | 参数 | 作用 |
---|---|---|
PADDR | 内核虚拟地址kva | 将内核虚拟地址kva转成对应的物理地址 |
KADDR | 物理地址pa | 将物理地址pa转化为内核虚拟地址 |
page2pa | 页信息结构struct PageInfo | 通过空闲页结构得到这一页起始位置的物理地址 |
pa2page | 物理地址pa | 通过物理地址pa获取这一页对应的页结构体struct PageInfo |
page2kva | 页信息结构struct PageInfo | 通过空闲页结构得到这一页起始位置的虚拟地址 |
PDX | 线性地址la | 获得该线性地址la对应的页目录项索引 |
PTX | 线性地址la | 获得该线性地址la在二级页表中对应的页表项索引 |
PTE_ADDR(pte) | 页表项或页目录项的值 | 获得对应的页表基址或者物理地址基址(低12位为0) |
- 首先关于第一个函数
boot_alloc()
这是在内存管理机制还没建立起来时,系统内存的分配函数。在page等建立以后当使用page_alloc()
而不该再使用该函数。根据函数的注释,先记录当前的free指针,然后将free指针偏移n单元即可,注意内存对齐(使用ROUNDUP函数)。
static void *
boot_alloc(uint32_t n)
{
static char *nextfree; // virtual address of next byte of free memory
char *result;
if (!nextfree) {
extern char end[];
nextfree = ROUNDUP((char *) end, PGSIZE);
}
// Allocate a chunk large enough to hold 'n' bytes, then update
// nextfree. Make sure nextfree is kept aligned
// to a multiple of PGSIZE.
// LAB 2: Your code here.
result = nextfree;
nextfree = ROUNDUP(result + n, PGSIZE);
return result;
}
-
第二个函数是初始化内存管理了,只需要做到check_page_free_list(1)之前即可。
首先使用
i386_detect_memory
获取物理内存大小;之后创建内核的页目录,使用的是boot_alloc()
,大小是1页(4KB);然后将内核页目录安装到一个页目录项中;之后创建空闲物理页数组pages。void mem_init(void) { uint32_t cr0; size_t n; i386_detect_memory(); ////////////////////////////////////////////////////////////////////// // create initial page directory. kern_pgdir = (pde_t *) boot_alloc(PGSIZE); memset(kern_pgdir, 0, PGSIZE); ////////////////////////////////////////////////////////////////////// // Permissions: kernel R, user R // UVPT是 User read-only virtual page table的虚拟地址 // PDX获得页目录项索引 // 将内核页目录安装到内核页目录中(参考前一篇文章中类似的搞法) /* ULIM, MMIOBASE-->+------------------------------+ 0xef800000 | Cur. Page Table (User R-) | R-/R- PTSIZE UVPT ---->+------------------------------+ 0xef400000 此处PTSIZE=4096*1024 =4MB 为一个页目录项能映射的内存大小 */ kern_pgdir[PDX(UVPT)] = PADDR(kern_pgdir) | PTE_U | PTE_P; // 分配pages数组,一共有npages个物理页,每个页使用struct PageInfo结构记录,并填充0 // Your code goes here: size_t sizes = sizeof(struct PageInfo) * npages; pages = (struct PageInfo*)boot_alloc(sizes); memset(pages, 0, sizes); page_init(); check_page_free_list(1);
-
第三个函数,建立page相关的数据结构。首先哪些物理内存是free的?根据注释,首先物理内存的第0页需要被标记为已使用;IO-hole需要被标记为已使用,不能被分配出去;扩展地址包含内核地方不能被分配出去,剩下的空间就可标记为free并后续可以分配出去。
void page_init(void) { // npages_basemem :Amount of base memory (in pages) //第0页不能被后续分配出去 pages[0].pp_ref = 1; pages[0].pp_link = NULL; size_t i; //内核的尾端所在的页索引号(那物理地址进行计算) size_t kernel_end_page = PADDR(boot_alloc(0)) / PGSIZE; for (i = 1; i < npages; i++) { //IO-hole和内核部分不能被分配出去 if (i >= npages_basemem && i < kernel_end_page) { pages[i].pp_ref = 1; pages[i].pp_link = NULL; } else { //建立free物理页链表 pages[i].pp_ref = 0; pages[i].pp_link = page_free_list; page_free_list = &pages[i]; } } }
-
第四个函数,是后续应该使用的内存分配函数
page_alloc
,根据前面我们知道,page_free_list
指着第一个空闲页,因此只需要从这个链表上摘取一个下来即可。这里通过前面的几个函数或者宏,可以将struct PageInfo
轻松地对应到物理地址或者虚拟地址。// 分配一个物理页 // If (alloc_flags & ALLOC_ZERO) 用0填充该页 // 不要增加页引用数 // 链接域要设为NULL // 如果内存不够了,返回NULL // Hint: use page2kva and memset struct PageInfo * page_alloc(int alloc_flags) { //page_free_list=NULL 说明没有内存可供分配 if (page_free_list == NULL) { cprintf("page_alloc: out of free memory\n"); return NULL; } //摘下那一页 struct PageInfo *addr = page_free_list; page_free_list = page_free_list->pp_link; addr->pp_link = NULL; if (alloc_flags & ALLOC_ZERO) { //得到这个info结构描述的那个物理页的虚拟地址,才能使用memset memset(page2kva(addr), 0, PGSIZE); } //返回这个空闲页的info结构 return addr; }
-
第五个函数,作用是释放一个页。也就是将一个
struct PageInfo
结构,重新挂回page_free_list
。注意不能释放一个引用值不为0的页,或者链接值不为空的页。void page_free(struct PageInfo *pp) { // Fill this function in // Hint: You may want to panic if pp->pp_ref is nonzero or // pp->pp_link is not NULL. if (pp->pp_ref != 0 || pp->pp_link != NULL) { panic("page_free: can not free the memory"); return; } //挂入链表 pp->pp_link = page_free_list; page_free_list = pp; }
Part2 虚拟内存
这一部分的前序知识,可以看上一篇文章Lab2内存管理准备知识。于是开始建立页表管理。
-
第一个函数,用于给定一个页目录和虚拟地址,返回对于的页表项指针。就是一个访问二级页表找值的过程,上一篇文章中详细地写到了。
/* 给定一个指向页目录的指针,这个函数返回 指向线性地址va的页表项的指针 这需要访问二级页表 对应的页表不一定存在,如果create参数为false则直接返回NULL否则,该函数申请新的一页来做页表,并增 加页的引用计数值。 */ pte_t * pgdir_walk(pde_t *pgdir, const void *va, int create) { // Fill this function in // 得到页目录索引对应的页目录项 pde_t *dir = pgdir + PDX(va); //检查这一页表是否存在 if (!(*dir & PTE_P)) { if (!create) return NULL; //申请新的一页 struct PageInfo* pp = page_alloc(1); if (pp == NULL) return NULL; pp->pp_ref++; //得到这一页的起始物理地址,并安装到页目录中 *dir = page2pa(pp) | PTE_P | PTE_U | PTE_W; } // 页表的起始物理地址转为虚拟地址+在页表项中的索引---->一个指向页表项的指针 return (pte_t *) KADDR(PTE_ADDR(*dir)) + PTX(va); }
-
第二个函数,建立起一段虚拟地址空间和物理地址空间的映射关系,也就是填充页表的值。
/* 将虚拟地址空间[va, va+size),映射到物理地址空间[pa, pa+size) 物理地址和虚拟地址都是页对齐的。 映射的过程就是填页表的过程。 */ static void boot_map_region(pde_t *pgdir, uintptr_t va, size_t size, physaddr_t pa, int perm) { // Fill this function in // 空间大小为多少页(对齐) size_t pieces = ROUNDUP(size, PGSIZE) / PGSIZE; for (size_t i = 0; i < pieces; i++) { //得到这个虚地址对于的页表项 pte_t *pte = pgdir_walk(pgdir, (void *) va, 1); if (pte == NULL) { panic("boot_map_region: out of memory!\n"); } //页表项放上物理地址(页的起始地址) *pte = pa | PTE_P | perm; //下一页 va += PGSIZE; pa += PGSIZE; } }
-
第三个函数,查找一个虚拟地址对应的页。
/* 得到虚拟地址va对应的页结构,如果pte_store不为空,就存入这一页的地址 va还没有对应到某个页,就返回NULL */ 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 || !(*pte & PTE_P)) { cprintf("page_lookup: can not find out the page.\n"); return NULL; } // 存储记录 if (pte_store) { *pte_store = pte; } // 得到页的物理地址对应的PageInfo结构 return pa2page(PTE_ADDR(*pte)); }
-
第四个函数,取消一个映射关系
/* 取消虚拟地址va映射到的物理页 物理页的引用计数应该减少(为0是释放) 这个地址对应的页表项(如果有)应该清空 TLB失效 */ void page_remove(pde_t *pgdir, void *va) { // Fill this function in // pte_store会存入页表项 pte_t *pte_store; struct PageInfo *pp = page_lookup(pgdir, va, &pte_store); // pp不为空说明有这一项 if (pp) { page_decref(pp); // 页表项清空 *pte_store = 0; tlb_invalidate(pgdir, va); } }
-
第五个函数
/* 将物理地址pp映射到虚拟地址va 权限设置为 perm|PTE_P 如果va以及和一个物理地址关联了,那么应该使用page_remove()并刷TLB pp所在的物理页的引用计数增加 */ 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; } pp->pp_ref++; //已经存在映射关系 if (*pte & PTE_P) { page_remove(pgdir, va); tlb_invalidate(pgdir, va); } //得到该页的物理地址,并安装进页表 *pte = page2pa(pp) | PTE_P | perm; return 0; }
继续完善```mem_init()``
void
mem_init(void)
{
/* ... ... */
check_page_free_list(1);
check_page_alloc();
check_page();
// pa:PADDR(pages)---->va:UPAGES size=PTSIZE
boot_map_region(kern_pgdir, UPAGES, PTSIZE, PADDR(pages), PTE_U);
// pa:PADDR(bootstack)---->va:KSTACKTOP - KSTKSIZE size=KSTKSIZE
boot_map_region(kern_pgdir, KSTACKTOP - KSTKSIZE, KSTKSIZE, PADDR(bootstack), PTE_W);
// pa:0---->va:KERNBASE size=0xffffffff - KERNBASE
boot_map_region(kern_pgdir, KERNBASE, 0xffffffff - KERNBASE, 0, PTE_W);
// Check that the initial page directory has been set up correctly.
check_kern_pgdir();
/* ... ... */
}
现在可以来一段总结了
这便是JOS目前建立起来的内存映射了,左侧是物理地址空间,右边是虚拟地址空间。比如说UVPT,在代码中有这样一段
kern_pgdir[PDX(UVPT)] = PADDR(kern_pgdir) | PTE_U | PTE_P;
而PDX(UVPT)=1110 1111 01
因此地址区间0xef400000~0xef7fffff
共计4MB被映射到PADDR(kern_pgdir)
处。而正如JOS一开始所说,只会使用256MB的内存,映射关系也满足。
总结
- 内存映射这块,需要好好地阅读代码,文章中没有详细地列出JOS内存布局,虚拟内存的布局在
memlayout.h
中 - 为了更好地理解这部分,需要熟悉保护模式分页模式下地寻址Lab2内存管理准备知识
- 要区分好物理地址和虚拟地址,页表,页目录这些里面装地内容是什么
- 要有一个内存模型总体上的概念