tags:mit-6.828 os
本文主要介绍lab2,讲的是操作系统内存管理,从内容上分为三部分:
- 第一部分讲的是物理内存管理,要进行内存管理首先需要知道哪些物理内存是空闲的,哪些是被使用的。还需要实现一些函数对这些物理内存进行管理。
- 第二部分讲的是虚拟内存。一个虚拟地址如何被映射到物理地址,将实现一些函数来操作页目录和页表从而达到映射的目的。
- 第三部分讲的是内核的地址空间。将结合第一部分和第二部分的成果,来对内核地址空间进行映射。
Part 1: Physical Page Management
通过lab1可以总结出如下的物理内存分布图:
大致上可以分为三部分:
- 0x00000~0xA0000:这部分叫做basemem,是可用的。
- 接着是0xA0000~0x100000:这部分叫做IO Hole,不可用。
- 再接着就是0x100000以上的部分:这部分叫做extmem,可用。
kern/pmap.c中的i386_detect_memory()统计有多少可用的物理内存,将总共的可用物理内存页数保存到全局变量npages中,basemem部分可用的物理内存页数保存到npages_basemem中。
Exercise 1:
需要我们写一个物理内存页的allocator。要求实现kern/pmap.c文件中的boot_alloc(),mem_init(),page_init(),page_alloc(),page_free()。check_page_free_list()和check_page_alloc()中会有一些测试用例,如果没有通过两个函数则说明代码有问题,一种类似TDD的开发流程。
从lab1知道,进入内核后首先调用的是i386_init(),该函数会调用mem_init()。mem_init()调用其他工具函数实现内核内存管理。该函数首先调用i386_detect_memory()来计算有多少可以的物理内存页保存到npages和npages_basemem中。然后调用boot_alloc()。
boot_alloc()实现如下:
static void * boot_alloc(uint32_t n) { static char *nextfree; // virtual address of next byte of free memory char *result; // Initialize nextfree if this is the first time. // 'end' is a magic symbol automatically generated by the linker, // which points to the end of the kernel's bss segment: // the first virtual address that the linker did *not* assign // to any kernel code or global variables. if (!nextfree) { extern char end[]; //在/kern/kernel.ld中定义的符号,位于bss段的末尾 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((char *)result + n, PGSIZE); cprintf("boot_alloc memory at %x, next memory allocate at %x\n", result, nextfree); return result; }
该函数维护一个static的指针nextfree,初始值是end,end是定义在/kern/kernel.ld中定义的符号,位于bss段的末尾。也就是说从内核的末尾开始分配物理内存。需要添加的代码是:
result = nextfree; nextfree = ROUNDUP((char *)result + n, PGSIZE); cprintf("boot_alloc memory at %x, next memory allocate at %x\n", result, nextfree); return result;
每次调用都返回nextfree,然后根据参数n更新nextfree的值,使其指向下一个空闲地址处。
mem_init()调用boot_alloc(),将返回值赋给全局变量kern_pgdir,kern_pgdir保存的是内核页目录的物理地址。
接着根据mem_init()中的注释:
// Allocate an array of npages 'struct PageInfo's and store it in 'pages'. // The kernel uses this array to keep track of physical pages: for // each physical page, there is a corresponding struct PageInfo in this // array. 'npages' is the number of physical pages in memory. Use memset // to initialize all fields of each struct PageInfo to 0.
在mem_init()中补充如下代码:
pages = (struct PageInfo*)boot_alloc(sizeof(struct PageInfo) * npages); //分配足够大的空间(PGSIZE的倍数)保存pages数组 memset(pages, 0, sizeof(struct PageInfo) * npages);
这段代码分配足够的的内存空间保存pages数组,pages数组的每一项是一个PageInfo结构,对应一个物理页的信息,定义在inc/memlayout.h中。
接下来mem_init()调用page_init()。
page_init()实现如下:
// -------------------------------------------------------------- // Tracking of physical pages. // The 'pages' array has one 'struct PageInfo' entry per physical page. // Pages are reference counted, and free pages are kept on a linked list. // -------------------------------------------------------------- // // Initialize page structure and memory free list. // After this is done, NEVER use boot_alloc again. ONLY use the page // allocator functions below to allocate and deallocate physical // memory via the page_free_list. // void page_init(void) { // The example code here marks all physical pages as free. // However this is not truly the case. What memory is free? // 1) Mark physical page 0 as in use. // This way we preserve the real-mode IDT and BIOS structures // in case we ever need them. (Currently we don't, but...) // 2) The rest of base memory, [PGSIZE, npages_basemem * PGSIZE) // is free. // 3) Then comes the IO hole [IOPHYSMEM, EXTPHYSMEM), which must // never be allocated. // 4) Then extended memory [EXTPHYSMEM, ...). // Some of it is in use, some is free. Where is the kernel // in physical memory? Which pages are already in use for // page tables and other data structures? // // Change the code to reflect this. // NB: DO NOT actually touch the physical memory corresponding to // free pages! // 这里初始化pages中的每一项,建立page_free_list链表 // 已使用的物理页包括如下几部分: // 1)第一个物理页是IDT所在,需要标识为已用 // 2)[IOPHYSMEM, EXTPHYSMEM)称为IO hole的区域,需要标识为已用。 // 3)EXTPHYSMEM是内核加载的起始位置,终止位置可以由boot_alloc(0)给出(理由是boot_alloc()分配的内存是内核的最尾部),这块区域也要标识 size_t i; size_t io_hole_start_page = (size_t)IOPHYSMEM / PGSIZE; size_t kernel_end_page = PADDR(boot_alloc(0)) / PGSIZE; //这里调了半天,boot_alloc返回的是虚拟地址,需要转为物理地址 for (i = 0; i < npages; i++) { if (i == 0) { pages[i].pp_ref = 1; pages[i].pp_link = NULL; } else if (i >= io_hole_start_page && i < kernel_end_page) { pages[i].pp_ref = 1; pages[i].pp_link = NULL; } else { pages[i].pp_ref = 0; pages[i].pp_link = page_free_list; page_free_list = &pages[i]; } } }
这个函数的主要作用是初始化之前分配的pages数组,并且构建一个PageInfo链表,保存空闲的物理页,表头是全局变量page_free_list。具体实现看注释。
接着实现page_alloc():
// Allocates a physical page. If (alloc_flags & ALLOC_ZERO), fills the entire // returned physical page with '\0' bytes. Does NOT increment the reference // count of the page - the caller must do these if necessary (either explicitly // or via page_insert). // // Be sure to set the pp_link field of the allocated page to NULL so // page_free can check for double-free bugs. // // Returns NULL if out of free memory. // // Hint: use page2kva and memset struct PageInfo * page_alloc(int alloc_flags) { struct PageInfo *ret = page_free_list; if (ret == NULL) { cprintf("page_alloc: out of free memory\n"); return NULL; } page_free_list = ret->pp_link; ret->pp_link = NULL; if (alloc_flags & ALLOC_ZERO) { memset(page2kva(ret), 0, PGSIZE); } return ret; }
该函数的作用是:从page_free_list指向的链表中取一个PageInfo结构返回,根据参数alloc_flags决定要不要将这块内存初始化为0。需要注意的是,不需要增加PageInfo的pp_ref字段。
和page_alloc()对称的是page_free()实现如下:
// Return a page to the free list. // (This function should only be called when pp->pp_ref reaches 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: pp->pp_ref is nonzero or pp->pp_link is not NULL\n"); } pp->pp_link = page_free_list; page_free_list = pp; }
该函数重新将参数pp对应的物理页设置为空闲状态。
重新回到mem_init()的流程中来,在调用page_init()后,会调用check_page_free_list(1)和check_page_alloc()。这两个函数通过一系列断言,判断我们的实现是否符合预期。需要注意的是check_page_free_list()中的这段代码:
if (only_low_memory) { // Move pages with lower addresses first in the free // list, since entry_pgdir does not map all pages. struct PageInfo *pp1, *pp2; struct PageInfo **tp[2] = { &pp1, &pp2 }; for (pp = page_free_list; pp; pp = pp->pp_link) { int pagetype = PDX(page2pa(pp)) >= pdx_limit; *tp[pagetype] = pp; tp[pagetype] = &pp->pp_link; } //执行该for循环后,pp1指向(0~4M)中地址最大的那个页的PageInfo结构。pp2指向所有页中地址最大的那个PageInfo结构 *tp[1] = 0; *tp[0] = pp2; page_free_list = pp1; }
刚开始也没看明白,最后在纸上涂涂画画了半天才搞明白,关键是要理解for循环结束后pp1和pp2所指向的地址的具体含义。这段代码的作用就是调整page_free_list链表的顺序,将代表低地址的PageInfo结构放到链表的表头处,这样的话,每次分配物理地址时都是从低地址开始。
这样第一部分就结束了,现在pages数组保存这所有物理页的信息,page_free_list链表记录这所有空闲的物理页。可以用page_alloc()和page_free()进行分配和回收。
执行完mem_init()后的物理内存如下:
Part 2: Virtual Memory
这部分主要的目的是实现一些函数操作页目录和页表从而达到实现虚拟地址到物理地址映射的目的。
Selector +--------------+ +-----------+ ---------->| | | | | Segmentation | | Paging | Software | |-------->| |----------> RAM Offset | Mechanism | | Mechanism | ---------->| | | | +--------------+ +-----------+ Virtual Linear Physical
这张图展示了x86体系中虚拟地址,线性地址,物理地址的转换过程。在boot/boot.S中我们设置了全局描述符表(GDT),设置所有段的基地址都是0x0,所有虚拟地址的offset和线性地址都是相等的。
在lab1中已经安装了一个简易的页目录和页表,将虚拟地址[0, 4MB)映射到物理地址[0, 4MB),[0xF0000000, 0xF0000000+4MB)映射到[0, 4MB)。具体实现在kern/entry.S中,临时的页目录线性地址为entry_pgdir,定义在kern/entrypgdir.c中。
线性地址到物理地址的转换过程可以用下图表示:
通过这张图我们知道页目录和和页表实际上构成了一棵树结构,任何映射关系我们都只需要修改这棵树结构就能实现。
页表条目结构如下:
每一位的具体含义可以参考Intel 80386 Reference Manual。
JOS内核有时候在仅知道物理地址的情况下,想要访问该物理地址,但是没有办法绕过MMU的线性地址转换机制,所以没有办法用物理地址直接访问。JOS将虚拟地址0xf0000000映射到物理地址0x0处的一个原因就是希望能有一个简便的方式实现物理地址和线性地址的转换。在知道物理地址pa的情况下可以加0xf0000000得到对应的线性地址,可以用KADDR(pa)宏实现。在知道线性地址va的情况下减0xf0000000可以得到物理地址,可以用宏PADDR(va)实现。
Exercise 4
该实验要求我们实现:
- pgdir_walk()
- boot_map_region()
- page_lookup()
- page_remove()
- page_insert()
pagedir_walk():
参数:
- pgdir:页目录虚拟地址
- va:虚拟地址
- create:布尔值
返回值:页表条目的地址
作用:给定pgdir,指向一个页目录,该函数返回一个指针指向虚拟地址va对应的页表条目(PTE)。
// Given 'pgdir', a pointer to a page directory, pgdir_walk returns // a pointer to the page table entry (PTE) for linear address 'va'. // This requires walking the two-level page table structure. // // The relevant page table page might not exist yet. // If this is true, and create == false, then pgdir_walk returns NULL. // Otherwise, pgdir_walk allocates a new page table page with page_alloc. // - If the allocation fails, pgdir_walk returns NULL. // - Otherwise, the new page's reference count is incremented, // the page is cleared, // and pgdir_walk returns a pointer into the new page table page. // // Hint 1: you can turn a PageInfo * into the physical address of the // page it refers to with page2pa() from kern/pmap.h. // // Hint 2: the x86 MMU checks permission bits in both the page directory // and the page table, so it's safe to leave permissions in the page // directory more permissive than strictly necessary. // // Hint 3: look at inc/mmu.h for useful macros that manipulate page // table and page directory entries. // pte_t * pgdir_walk(pde_t *pgdir, const void *va, int create) { // Fill this function in pde_t* pde_ptr = pgdir + PDX(va); if (!(*pde_ptr & PTE_P)) { //页表还没有分配 if (create) { //分配一个页作为页表 struct PageInfo *pp = page_alloc(1); if (pp == NULL) { return NULL; } pp->pp_ref++; *pde_ptr = (page2pa(pp)) | PTE_P | PTE_U | PTE_W; //更新页目录项 } else { return NULL; } } return (pte_t *)KADDR(PTE_ADDR(*pde_ptr)) + PTX(va); //这里记得转为pte_t*类型,因为KADDR返回的的是void*类型。调了一个多小时才发现 }
boot_map_region()
参数:
- pgdir:页目录指针
- va:虚拟地址
- size:大小
- pa:物理地址
- perm:权限
作用:通过修改pgdir指向的树,将[va, va+size)对应的虚拟地址空间映射到物理地址空间[pa, pa+size)。va和pa都是页对齐的。
// Map [va, va+size) of virtual address space to physical [pa, pa+size) // in the page table rooted at pgdir. Size is a multiple of PGSIZE, and // va and pa are both page-aligned. // Use permission bits perm|PTE_P for the entries. // // This function is only intended to set up the ``static'' mappings // above UTOP. As such, it should *not* change the pp_ref field on the // mapped pages. // // Hint: the TA solution uses pgdir_walk 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 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; } }
思路很简单,看注释即可。
page_insert()
参数:
- pgdir:页目录指针
- pp:PageInfo结构指针,代表一个物理页
- va:线性地址
- perm:权限
返回值:0代表成功,-E_NO_MEM代表物理空间不足。
作用:修改pgdir对应的树结构,使va映射到pp对应的物理页处。
// Map the physical page 'pp' at virtual address 'va'. // The permissions (the low 12 bits) of the page table entry // should be set to 'perm|PTE_P'. // // Requirements // - If there is already a page mapped at 'va', it should be page_remove()d. // - If necessary, on demand, a page table should be allocated and inserted // into 'pgdir'. // - pp->pp_ref should be incremented if the insertion succeeds. // - The TLB must be invalidated if a page was formerly present at 'va'. // // Corner-case hint: Make sure to consider what happens when the same // pp is re-inserted at the same virtual address in the same pgdir. // However, try not to distinguish this case in your code, as this // frequently leads to subtle bugs; there's an elegant way to handle // everything in one code path. // // RETURNS: // 0 on success // -E_NO_MEM, if page table couldn't be allocated // // Hint: The TA solution is implemented using pgdir_walk, page_remove, // and page2pa. // 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); //拿到va对应的PTE地址,如果va对应的页表还没有分配,则分配一个物理页作为页表 if (pte == NULL) { return -E_NO_MEM; } pp->pp_ref++; //引用加1 if ((*pte) & PTE_P) { //当前虚拟地址va已经被映射过,需要先释放 page_remove(pgdir, va); //这个函数目前还没实现 } physaddr_t pa = page2pa(pp); //将PageInfo结构转换为对应物理页的首地址 *pte = pa | perm | PTE_P; //修改PTE pgdir[PDX(va)] |= perm; return 0; }
page_lookup()
参数:
- pgdir:页目录地址
- va:虚拟地址
- pte_store:一个指针类型,指向pte_t *类型的变量
返回值:PageInfo*
作用:通过查找pgdir指向的树结构,返回va对应的PTE所指向的物理地址对应的PageInfo结构地址。
// Return the page mapped at virtual address 'va'. // If pte_store is not zero, then we store in it the address // of the pte for this page. This is used by page_remove and // can be used to verify page permissions for syscall arguments, // but should not be used by most callers. // // Return NULL if there is no page mapped at va. // // Hint: the TA solution uses pgdir_walk and pa2page. // struct PageInfo * page_lookup(pde_t *pgdir, void *va, pte_t **pte_store) { // Fill this function in struct PageInfo *pp; pte_t *pte = pgdir_walk(pgdir, va, 0); //如果对应的页表不存在,不进行创建 if (pte == NULL) { return NULL; } if (!(*pte) & PTE_P) { return NULL; } physaddr_t pa = PTE_ADDR(*pte); //va对应的物理 pp = pa2page(pa); //物理地址对应的PageInfo结构地址 if (pte_store != NULL) { *pte_store = pte; } return pp; }
如果将page_insert()函数看作是“增”,那么page_lookup()就是“查”。查找一个虚拟地址va,对应的物理地址信息。
page_remve()
参数:
- pgdir:页目录地址
- va:虚拟地址
作用:修改pgdir指向的树结构,解除va的映射关系。
// Unmaps the physical page at virtual address 'va'. // If there is no physical page at that address, silently does nothing. // // Details: // - The ref count on the physical page should decrement. // - The physical page should be freed if the refcount reaches 0. // - The pg table entry corresponding to 'va' should be set to 0. // (if such a PTE exists) // - The TLB must be invalidated if you remove an entry from // the page table. // // Hint: The TA solution is implemented using page_lookup, // tlb_invalidate, and page_decref. // void page_remove(pde_t *pgdir, void *va) { // Fill this function in pte_t *pte_store; struct PageInfo *pp = page_lookup(pgdir, va, &pte_store); //获取va对应的PTE的地址以及pp结构 if (pp == NULL) { //va可能还没有映射,那就什么都不用做 return; } page_decref(pp); //将pp->pp_ref减1,如果pp->pp_ref为0,需要释放该PageInfo结构(将其放入page_free_list链表中) *pte_store = 0; //将PTE清空 tlb_invalidate(pgdir, va); //失效化TLB缓存 }
如果将page_insert()函数看作是“增”,page_lookup()是“查”,那么page_remove()就是“删”,删除线性地址va的映射关系,删除过后不可使用该虚拟地址,否则会出现页错误,lab3将处理该错误。
至此如果一切顺利,将通过mem_init()中check_page()的所有assert。
Part 3: Kernel Address Space
JOS将线性地址空间分为两部分,由定义在inc/memlayout.h中的ULIM分割。ULIM以上的部分用户没有权限访问,内核有读写权限。
/* * Virtual memory map: Permissions * kernel/user * * 4 Gig --------> +------------------------------+ * | | RW/-- * ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ * : . : * : . : * : . : * |~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~| RW/-- * | | RW/-- * | Remapped Physical Memory | RW/-- * | | RW/-- * KERNBASE, ----> +------------------------------+ 0xf0000000 --+ * KSTACKTOP | CPU0's Kernel Stack | RW/-- KSTKSIZE | * | - - - - - - - - - - - - - - -| | * | Invalid Memory (*) | --/-- KSTKGAP | * +------------------------------+ | * | CPU1's Kernel Stack | RW/-- KSTKSIZE | * | - - - - - - - - - - - - - - -| PTSIZE * | Invalid Memory (*) | --/-- KSTKGAP | * +------------------------------+ | * : . : | * : . : | * MMIOLIM ------> +------------------------------+ 0xefc00000 --+ * | Memory-mapped I/O | RW/-- PTSIZE * ULIM, MMIOBASE --> +------------------------------+ 0xef800000 * | Cur. Page Table (User R-) | R-/R- PTSIZE * UVPT ----> +------------------------------+ 0xef400000 * | RO PAGES | R-/R- PTSIZE * UPAGES ----> +------------------------------+ 0xef000000 * | RO ENVS | R-/R- PTSIZE * UTOP,UENVS ------> +------------------------------+ 0xeec00000 * UXSTACKTOP -/ | User Exception Stack | RW/RW PGSIZE * +------------------------------+ 0xeebff000 * | Empty Memory (*) | --/-- PGSIZE * USTACKTOP ---> +------------------------------+ 0xeebfe000 * | Normal User Stack | RW/RW PGSIZE * +------------------------------+ 0xeebfd000 * | | * | | * ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ * . . * . . * . . * |~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~| * | Program Data & Heap | * UTEXT --------> +------------------------------+ 0x00800000 * PFTEMP -------> | Empty Memory (*) | PTSIZE * | | * UTEMP --------> +------------------------------+ 0x00400000 --+ * | Empty Memory (*) | | * | - - - - - - - - - - - - - - -| | * | User STAB Data (optional) | PTSIZE * USTABDATA ----> +------------------------------+ 0x00200000 | * | Empty Memory (*) | | * 0 ------------> +------------------------------+ --+ * * (*) Note: The kernel ensures that "Invalid Memory" is *never* mapped. * "Empty Memory" is normally unmapped, but user programs may map pages * there if desired. JOS user programs map pages temporarily at UTEMP. */
Exercise 5
该实验需要我们填充mem_init()中缺失的代码,使用part2的增删改函数初始化内核线性地址空间。
在mem_init()函数的check_page();
后添加如下语句:
////////////////////////////////////////////////////////////////////// // Map 'pages' read-only by the user at linear address UPAGES // Permissions: // - the new image at UPAGES -- kernel R, user R // (ie. perm = PTE_U | PTE_P) // - pages itself -- kernel RW, user NONE // Your code goes here: // 将虚拟地址的UPAGES映射到物理地址pages数组开始的位置 boot_map_region(kern_pgdir, UPAGES, PTSIZE, PADDR(pages), PTE_U); ////////////////////////////////////////////////////////////////////// // Use the physical memory that 'bootstack' refers to as the kernel // stack. The kernel stack grows down from virtual address KSTACKTOP. // We consider the entire range from [KSTACKTOP-PTSIZE, KSTACKTOP) // to be the kernel stack, but break this into two pieces: // * [KSTACKTOP-KSTKSIZE, KSTACKTOP) -- backed by physical memory // * [KSTACKTOP-PTSIZE, KSTACKTOP-KSTKSIZE) -- not backed; so if // the kernel overflows its stack, it will fault rather than // overwrite memory. Known as a "guard page". // Permissions: kernel RW, user NONE // Your code goes here: // 'bootstack'定义在/kernel/entry. boot_map_region(kern_pgdir, KSTACKTOP-KSTKSIZE, KSTKSIZE, PADDR(bootstack), PTE_W); ////////////////////////////////////////////////////////////////////// // Map all of physical memory at KERNBASE. // Ie. the VA range [KERNBASE, 2^32) should map to // the PA range [0, 2^32 - KERNBASE) // We might not have 2^32 - KERNBASE bytes of physical memory, but // we just set up the mapping anyway. // Permissions: kernel RW, user NONE // Your code goes here: boot_map_region(kern_pgdir, KERNBASE, 0xffffffff - KERNBASE, 0, PTE_W);
执行完mem_init()后kern_pgdir指向的内核页目录代表的虚拟地址空间到物理地址空间映射可以用下图来表示:
如何仔细看图和上面的代码,会觉得奇怪,UPAGES开始的这一页是什么时候映射的?实际上早在mem_init()开始的时候就有这么一句
kern_pgdir[PDX(UVPT)] = PADDR(kern_pgdir) | PTE_U | PTE_P;
,页目录表的低PDX(UVPT)项指向页目录本身,也就是说虚拟地址UVPT开始处的0x400000字节映射到物理地址PADDR(kern_pgdir)处。 至此,lab2的所有实验都已完成。如果顺利运行./grade-lab2会看到:
该实验大体上做三件事:
- 提供管理物理内存的数据结构和函数
- 提供修改页目录和页表树结构结构的函数,从而达到映射的目的
- 用前面两部分的函数建立内核的线性地址空间
具体代码在:https://github.com/gatsbyd/mit_6.828_jos
如有错误,欢迎指正:
15313676365