MIT6.828——Lab2(麻省理工操作系统实验)

Lab2

Lab2 是关于操作系统存储管理的细节。主要是建立内存模型,页表,物理地址映射等。
在Lab2之前,请复习好前序知识:

Lab2内存管理准备知识

MIT6.828——Lab1 PartA

MIT6.828——Lab1 PartB

Part1 物理内存管理

在开始做题之前,需要了解一下一些常用的函数,宏以及内存布局,建议复习一下LAB1中的简单内存模型,LAB2预备知识中的相关。这里有几个很有用的地址变换工具,具体实现可以查看mmu.hpmap.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的内存,映射关系也满足。

总结

  1. 内存映射这块,需要好好地阅读代码,文章中没有详细地列出JOS内存布局,虚拟内存的布局在memlayout.h
  2. 为了更好地理解这部分,需要熟悉保护模式分页模式下地寻址Lab2内存管理准备知识
  3. 要区分好物理地址和虚拟地址,页表,页目录这些里面装地内容是什么
  4. 要有一个内存模型总体上的概念
posted @ 2021-11-01 20:06  OasisYang  阅读(1498)  评论(0编辑  收藏  举报