MIT6.828 lab2 内存管理

Part A 物理页管理

Exercise1

补全在kern/pmap.c下的几个函数。

boot_alloc()
mem_init() (only up to the call to check_page_free_list(1))
page_init()
page_alloc()
page_free()

boot_alloc()

在JOS中,一开始的物理内存布局如下图所示


物理内存布局

虚拟内存布局


虚拟内存布局

在代码中,所有的变量的地址都是虚拟地址,JOS中虚拟地址到物理地址的转换很简单:

虚拟地址 = 物理地址 + KERNBASE(0xF0000000)

boot_alloc()中的end代表的就是内核代码的最上端,即内核代码的末尾,此位置往上的物理内存都可以分配。当申请n字节大小空间的内存时,将当前nextfree保存在result当做函数返回值,然后将其向后移动ROUNDUP(n, PGSIZE),此时[result, nextfree)的空间就分配出来了。因为是按页管理内存,所以分配的内存大小需要页对齐。

static void *
boot_alloc(uint32_t n)
{
    static char *nextfree;  // virtual address of next byte of free memory
    char *result;
    // end: bss段的末尾,正好是kernel的末尾的指针,第一个未使用的虚拟地址
    if (!nextfree) {
        extern char end[];
        nextfree = ROUNDUP((char *) end, PGSIZE);
    }
    // cprintf("nextfree: %x\n", nextfree);
    // LAB 2: Your code here.
    // 返回的是上一次的地址,然后再将nextfree往后移动,相当于分配空间
    result = nextfree;
    if(n != 0) {
        nextfree = ROUNDUP(nextfree + n, PGSIZE);
    }
    return result;
}

mem_init() 只需要完成到调用check_page_free_list(1)之前

在内核代码中每个物理页都由一个PageInfo的数据结构来标识,一共有npages个物理页。所有的PageInfo组成一个pages数组。所以在mem_init需要先对pages结构进行物理内存分配。
之后所出现的物理页其实是指PageInfo,代码中对物理页的操作其实都是操作PageInfo这个结构

struct PageInfo {
    // Next page on the free list.
    struct PageInfo *pp_link;

    // pp_ref is the count of pointers (usually in page table entries)
    // to this page, for pages allocated using page_alloc.
    // Pages allocated at boot time using pmap.c's
    // boot_alloc do not have valid reference count fields.

    uint16_t pp_ref;
};
void
mem_init(void)
{
    ...
    // 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.
    // Your code goes here:
    // npages: 还有多少页物理内存,每个页都要有一个PageInfo
    pages = (struct PageInfo*)boot_alloc(sizeof(struct PageInfo) * npages);
    // cprintf("npages: %d\n", npages);
    memset(pages, 0, npages * sizeof(struct PageInfo));
    //////////////////////////////////////////////////////////////////////
    ...
}

page_init()

分配完内存后自然就要对数据结构进行初始化,即将物理内存中的每一页都与pageInfo关联,其中分为可用页和不可用页。物理内存页到pages数组下标的映射关系为: 地址/PGSIZE(4k)。根据提示可知,一共有两大块空闲物理内存块。[1, npages_basemem)。第二块就是从内核代码往后,这个地址可以用boot_alloc(0)取到,即分配了pages内存之后的地址。但这个地址在代码中是虚拟地址,所以需要将其转换成物理地址,可以用PADDR()宏来转换。所以第二块范围就是[PADDR(boot_alloc(0)/PGSIZE),npages)。找出这些空闲页后需要用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!
    size_t i;
    for (i = 1; i < npages_basemem; i++) {
        pages[i].pp_ref = 0;
        // 构成一个链表
        pages[i].pp_link = page_free_list;
        page_free_list = &pages[i];
    }
    
    size_t first_free_page = (size_t)PADDR(boot_alloc(0))/PGSIZE;
    // cprintf("npages_basemem: %d\n first_free_page: %d\n", npages_basemem, first_free_page);
    for (i = first_free_page; i < npages; i++) {
        pages[i].pp_ref = 0;
        pages[i].pp_link = page_free_list;
        page_free_list = &pages[i];
    }
    pages[0].pp_ref = 1;
    pages[0].pp_link = NULL;
    for(i = npages_basemem; i < first_free_page; i++) {
        pages[i].pp_ref = 1;
        pages[i].pp_link = NULL;
    }
}

page_alloc()

空闲物理页的分配。在空闲物理页链表中取出一个物理页即可。返回的是PageInfo*,这个怎么与物理内存中的物理页对应呢?

  • 注意: 两个指针相减,结果并不是两个指针数值上的差,而是把这个差除以指针指向类型的大小的结果。

可以用page2pa(PageInfo*)的宏,因为pages数组是连续的物理内存,所以直接将PageInfo* pp 的地址减去pages就可以知道在数组中的下标是多少。在乘以4K就可以得到物理地址了: (pp-pages) << PGSHIFT。PGSHIFT = 1<<12 = 4096 = 4k。

struct PageInfo *
page_alloc(int alloc_flags)
{
    // Fill this function in
    if (page_free_list == NULL) {
        return NULL;
    }
    struct PageInfo* pageInfo = page_free_list;
    page_free_list = page_free_list->pp_link;
    pageInfo->pp_link = NULL;
    if (alloc_flags & ALLOC_ZERO) {
        // 内核虚拟地址空间映射到物理地址空间,直接减去kernbase
        memset(page2kva(pageInfo), 0, PGSIZE);
        // cprintf("page2kva(pageInfo): %x %x %d\n", pageInfo, page2kva(pageInfo), PGSIZE);
    }
    return pageInfo;
}

page_free()

这个比较简单。页面释放,将物理页重新插入到page_free_list中。前提是要保证该页面没有被引用,并且也不在空闲链表中。

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. 
    // cprintf("pp->pp_ref: %d pp->pp_link: %d\n", pp->pp_ref, pp->pp_link == NULL);
    assert(pp->pp_ref == 0 && pp->pp_link == NULL);
    pp->pp_link = page_free_list;
    page_free_list = pp;
}

Part B 虚拟内存

Exercise2

阅读Intel 80386 Reference Manual的第5第6章。

在x86结构下,使用的是分段分页机制,虚拟地址转换为物理地址需要中间还需要经历线性地址(分段的过程)。


虚拟地址-线性地址-物理地址

下图是具体的地址结构转换过程。
具体地址结构

在JOS中,虚拟地址=线性地址,为什么呢?因为在boot/boot.S中把所有的段地址都设置成了0 到0xffffffff,即段基址都等于0,相当于0+offset,所以就没有分段的效果了。这样我们就可以专注于实现分页机制了。

Exercise3

使用qemu-debug下的xp命令查看物理地址的内容。因为gdb只能获取到虚拟地址,所以需要使用qemu的下的debug模式才能查看物理地址。
我觉得直接在程序中用cprintf也可以查看。

Exercise4

补全kern/pmap.c下的这些函数,实现页表管理。

        pgdir_walk()
        boot_map_region()
        page_lookup()
        page_remove()
        page_insert()

在补全这些函数之前,需要先明白一个图的含义。JOS采用的是二级页表机制,主要由五个元素组成,页目录表-页目录项(PDE, page diretory entry),页表-页表项(PTE, page table entry),物理页。PDE和PTE存储的都是地址。
其中一个页目录项对应一个页表,一个页表项对应一个物理页。页目录表的地址存储在CR3寄存器中。


二级页表机制

pgdir_walk()

根据(页目录表,虚拟地址,创建标志)找到该虚拟地址所对应的物理页的虚拟地址。
通过PDX获得va的页目录项在页目录表中的偏移取得PDE,如果该PDE所指向的PT是空的话且create == 1,那就创建一个页目录表,即申请一页的物理内存。并设置为用户可读可写。然后再根据PTX获得va在页表项在页表中的偏移获取PTE,返回此PTE的地址。
PTE_ADDR(*pde)的作用是去掉后面的权限位。

页表项的结构

pte_t *
pgdir_walk(pde_t *pgdir, const void *va, int create)
{
    // Fill this function in
    
    int pde_index = PDX(va);
    int pte_index = PTX(va);
    pde_t *pde = &pgdir[pde_index];
    if (!(*pde & PTE_P)) {
        if (create) {
            struct PageInfo *page = page_alloc(ALLOC_ZERO);
            if (!page) return NULL;

            page->pp_ref++;
            *pde = page2pa(page) | PTE_P | PTE_U | PTE_W;
        } else {
            return NULL;
        }   
    }   

    pte_t *p = (pte_t *) KADDR(PTE_ADDR(*pde));
    return &p[pte_index];
}

boot_map_region()

之前的pgdir_walk是取到页表项,但页表项还未真正的映射到物理页上,此函数将从va开始的大小为size的地址按页从物理地址pa开始映射。相当于对页表项赋值。

static void
boot_map_region(pde_t *pgdir, uintptr_t va, size_t size, physaddr_t pa, int perm)
{
    // Fill this function in
    // 页表项映射到物理地址, 页表本身需要物理地址存储,pgdir_walk得到页表存储的虚拟地址
    size_t i;
    
    for (i = 0; i < size; va += PGSIZE, pa += PGSIZE, i += PGSIZE) {
        pte_t* pte = pgdir_walk(pgdir, (void*)va, 1);
        // 不需要 *pte & PTE_P
        if (pte == NULL) {
            panic("error");
        }
        *pte = pa | perm | PTE_P;
    }
}

page_lookup()

返回页表项所对应的物理页的虚拟地址,并把页表项存储在pte_store中。**pte_store二级指针相当于传入指针的引用。

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 == NULL || !(*pte & PTE_P)) {
        return NULL;
    }
    if (pte_store) {
        *pte_store = pte;
    }
    return (struct PageInfo*)pa2page(PTE_ADDR(*pte));
}

page_remove()

清空页表项对应的物理页,并把物理页引用减减。

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);
    if(pp == NULL || !(*pte_store & PTE_P)) 
        return;
    page_decref(pp);
    *pte_store = 0;
    tlb_invalidate(pgdir, va);
}

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 == NULL) {
        return -E_NO_MEM;
    }
    pp->pp_ref++;
    if (*pte & PTE_P) {
        page_remove(pgdir, va);
    }
    *pte = page2pa(pp) | perm | PTE_P;
    // cprintf("page_insert: %x\n", *pte);
    return 0;
}

Part 3 内核地址空间

JOS的内核空间为[UTOP, KERNBASE),一共为256MB。
填充完整mem_init(),将虚拟内核地址空间映射到物理地址上。

  1. [UPAGES, UPAGES+PTSIZE)这段空间是pages数组的空间,将其映射到PADDR(pages)上。
  2. [KSTACKTOP-KSTKSIZE, KSTACKTOP)是内核栈的空间,将其映射到PADDR(bootstack)
  3. [KERNBASE, 2^32-1),其中32位系统无法计算 2^32,但 2^32-1 == -KERNBASE。这段地址从物理地址0开始映射。
boot_map_region(kern_pgdir, UPAGES, PTSIZE, PADDR(pages), PTE_U);
PTE_U);
boot_map_region(kern_pgdir, KSTACKTOP-KSTKSIZE, KSTKSIZE, PADDR(bootstack), PTE_W);
boot_map_region(kern_pgdir, KERNBASE, -KERNBASE, 0, PTE_W);

你可能感兴趣的:(MIT6.828 lab2 内存管理)