MIT6.828实验记录-Lab2

实验1完成了内核启动的相关操作,由实验1可知,首先BIOS(0xf0000~0x100000=64KB)将Boot Loader加载到0x7c00~0x7dff(512B)处,Boot Loader代码执行后将内核代码的ELF文件读取到0x100000其实的内存(1MB)

实验2为当前的操作系统编写内存管理相关代码。实验要求完成两个组件:

第一个组件是内核的物理内存分配器,使内核能够分配和释放内存。分配器将以4KB为单位分配物理内存,称为页。主要任务是维护数据结构,记录哪些物理页面是空闲的,哪些已分配,以及有多少进程正在共享每个分配的页面。

内存管理的第二个组件是虚拟内存,它将内核和用户软件使用的虚拟地址映射到物理内存中的地址。x86硬件的内存管理单元 (MMU) 在指令使用内存时执行映射,查询一组页表。根据提供的规范修改JOS以设置MMU的页表。

进行实验前首先需要知道实验中需要参考以下几个文件:

  • inc/memlayout.h
  • kern/pmap.c
  • kern/pmap.h
  • kern/kclock.h
  • kern/kclock.c

memlayout.h描述了必须通过修改pmap.c来实现的虚拟地址空间的布局。

memlayout.h和pmap.h定义了PageInfo用于跟踪哪些物理内存页面空闲的结构。

kclock.c和kclock.h 操作PC的电池供电时钟和CMOS RAM硬件(BIOS记录PC 包含的物理内存量等)。

pmap.c中的代码需要读取这个设备硬件,以便计算出有多少物理内存。

2.1 Part 1:物理页管理

操作系统必须跟踪物理RAM的哪些部分是空闲的,哪些部分当前正在使用。JOS以4KB为单位管理PC的物理内存, 以便它可以使用MMU来映射和保护每块分配的内存。编写物理页面分配器。它通过一个struct PageInfo对象链接列表来跟踪哪些页面是空闲的(与xv6不同的是,它们没有嵌入到空闲页面本身中),每个页面对应一个物理页面。需要先编写物理页分配器,然后才能编写其余的虚拟内存实现,因为页表管理代码需要分配物理内存来存储页表。

练习1要求实现文件中相关函数的代码,下面将按照函数给出的顺序进行代码编写:

  • boot_alloc函数,仅在JOS设置物理内存时使用。直接根据函数提示进行编写,有一点就是关于页面对齐,这一功能主要借助ROUNDUP函数实现,具体如下:

static void *
boot_alloc(uint32_t n)
{
    static char *nextfree;  // virtual address of next byte of free memory
    char *result;
    
    //cprintf("The value of nextfree before init = %08x\n",nextfree);
    // 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[];
        nextfree = ROUNDUP((char *) end, PGSIZE);
    }
    /** 
        ROUNDUP(a, b) :inc/x86.h-->inc/x86.h-->inc/types.h
        Round up a to the nearest integral multiple of B
    */
    //cprintf("The value of nextfree after init = %08x\n",nextfree);
   // 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.
    if(n == 0)
        return nextfree;
    if(n > 0){
        if(PADDR(ROUNDUP((char *)((uint32_t)nextfree + n), PGSIZE)) < npages * PGSIZE){
            result = nextfree;
            nextfree = ROUNDUP((char *)((uint32_t)nextfree + n), PGSIZE);
            return result;
        }else
            panic("boot_alloc: out of memory\n");
    }
    return NULL;
}
  • mem_init() 函数,练习1只要求到check_page_free_list(1)这一句,首先调用boot_alloc分配npages个PageInfoge大小的内存,然后再将分配的内存清空。具体如下:
//
    // 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:
    pages=(struct PageInfo *)boot_alloc(npages*sizeof(struct PageInfo));
    memset(pages,0,npages*sizeof(struct PageInfo));
    //
    // Now that we've allocated the initial kernel data structures, we set
    // up the list of free physical pages. Once we've done so, all further
    // memory management will go through the page_* functions. In
    // particular, we can now map memory using boot_map_region
    // or page_insert
    page_init();
    check_page_free_list(1);
  • page_init()函数,主要完成页面的初始化过程,设置空闲链表。结合注释和实验1中的内存物理地址空间图,如下:

  • 分析得到下列要点:
    • 物理内存中第0页,需要标记为已使用,目的是为了保留实模式IDT(中断描述表)和BIOS结构。也就是说初始化需要从第1页开始进行。
    • 从第1页到地址640KB(640KB/4KB=160,也就是代码中npages_basemem的值)之间(不含640K,也就是说到0x9FFFF)的区域是空闲的,可以进行初始化。
    • 640KB(0xA0000)到1M(0x100000)之间也不能用,这一部分是为了留给BIOS以及显存,大于1MB的部分,有一部分分配给了内核,需要计算出来,除去内核占用的部分之后的内存均可以初始化。关于内核之后的首个空闲页的计算要结合boot_alloc注解,(if n==0, returns the address of the next free page without allocating),最终代码实现如下:
page_init(void)
{
    size_t i;
    //npages_basemem = PGNUM(IOPHYSMEM); 
    for (i = 1; i < npages_basemem; i++) {
        pages[i].pp_ref = 0;
        pages[i].pp_link = page_free_list;
        page_free_list = &pages[i];
    }
    //Count the last available/free page after kernel
    size_t kern_page = PGNUM(PADDR(boot_alloc(0)));
    //cprintf("kern_page = %08x\n",kern_page);
    for(i = kern_page; i < npages; i++){
        pages[i].pp_ref = 0;
        pages[i].pp_link = page_free_list;
        page_free_list = &pages[i];
    }
}
  • page_alloc()函数,完成的主要工作是从空闲链表(page_free_list)取第1个空闲页,并更新链表头指向下一个空闲页位置,如果指定了alloc_flag,则将PageInfo结构对应的那4KB内存区域清零,需要用page2kva(page)获取对应页面的虚拟地址。代码如下:
struct PageInfo *
page_alloc(int alloc_flags)
{
    // Fill this function 
   /**
        page2kva(page);//Turn page address to kernel virtual address.
    */
    if(!page_free_list)
        return NULL;
    struct PageInfo *newpage = page_free_list;
    page_free_list = page_free_list -> pp_link;
    if(alloc_flags & ALLOC_ZERO)
        memset(page2kva(newpage), 0, PGSIZE);//Why memset virtual memory?
    newpage->pp_link = NULL;
    newpage->pp_ref = 0;
    return newpage;
}
  • 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_ref is nonzero or pp_link is not NULL\n");
    pp->pp_link = page_free_list;
    page_free_list = pp;
}

一开始忘记对page_alloc中的分配的页pp_link域置空(newpage->pp_link=NULL; newpage->pp_ref=0;)

出现了grade_lab2测试报错:

由于没有将页的链域置空导致page_free();回收空闲页中的对页面链域断言不通过。改正后,使用grade_lab2进行检测,结果如下(仅完成了物理页分配):

2.2 Part 2: 虚拟内存管理

练习2主要是阅读Intel 80386 Reference Manual中的第五章和第六章,熟悉内存保护模式的管理架构。其中,第五章主要讲解了80386计算机的地址转换过程:

  • 段转换:将逻辑地址(段选择器和段偏移量)转换为线性地址。
  • 页转换:将线性地址转换为物理地址。

转换过程体现在下面图中:

结合上图分析:

实验中用到的C指针其实就是虚拟地址中的offset,通过描述图表(DESCRIPTOR TABLE)和段选字(SELECTOR),通过分段机制转换为线性地址,因为jos中设置段基址为0,所以线性地址就等于offset。在开启分页前线性地址就是物理地址。开启分页后,线性地址需要经过MMU部件进行翻译才能得到物理地址。开启分页后,MMU部件会把线性地址分成3部分,分别是页目录索引(Directory)、页表索引(Table)和页内偏移(offset), 这3个部分把原本32位的线性地址分成了10+10+12的3个片段。每个页表的大小为4KB(因为页内偏移为12位)。如下图所示(位于inc/mmu.h中)

实验1第3部分中指出只映射了前4MB的物理内存,因为这足以让系统启动运行。总共创建1024个目录项只用了两个,每个页目录大小为4B,总共占用4KB内存。页目录项的结构和页表项的结构基本一致,高20位为物理页索引(ppn),用于定位页表物理地址,通过页表物理地址和页内偏移就可以找到物理地址,所以页目录(页表)项的低12位可以用于一些标识和权限控制。

系统在访问一个页面时就会自动地去判断页表的这些位,如果页面不存在或者权限不符,就会产生异常。在启用分页前,页目录所在的物理页面的首地址存放到CR3寄存器中,x86处理器在进行页式地址转换时会自动地从CR3中取得页目录物理地址,然后根据线性地址的高10位取页目录项,由页目录项所存储的地址(高20位)得到页表所在物理页的首地址。然后根据中间10位取得页表项,由页表项所存储的地址(高20位)找到物理页起始地址(Page Frame),将该地址 + 12位页内偏移得到真正的物理地址。

为了帮助检测和识别错误,80386包含验证存储器访问和指令执行是否符合保护标准的机制。这一部分内容主要在第6章中展开讲解。保护机制主要体现在五个方面:类型检查、限制检查、可寻址域的限制、程序入口点的限制、指令集限制。其中,与页面级保护有关的是:可寻址域的限制类型检查

在可寻址域的限制方面,页面特权的概念是通过将每个页面分配到两个级别之一来实现的:

    • 主管级别(U/S=0)——用于操作系统和其他系统软件及相关数据。
    • 用户级别(U/S=1)——用于应用程序和数据。

当前级别(U 或 S)与 CPL(描述符中一个描述特权级别的字段) 相关。如果 CPL 为 0、1 或 2,则处理器在主管级别执行。如果 CPL 为 3,则处理器在用户级别执行。当处理器在主管级别执行时,所有页面都是可寻址的,但是,当处理器在用户级别执行时,只有属于用户级别的页面是可寻址的。

在类型检查方面,系统在页寻址级别,定义了两种类型:

    • 只读访问 (R/W=0)
    • 读/写访问(R/W=1)

当处理器在主管级别执行时,所有页面都是可读和可写的。当处理器在用户级执行时,只有属于用户级并被标记为读写访问的页面是可写的;属于主管级别的页面从用户级别既不可读也不可写。(细节详见手册)

练习3要求熟悉qemu调试指令,在进入qemu调试时,先在ubuntu的terminal中执行make qemu-gdb(方便调试)之后弹出qemu窗体,再点击进入terminal按下ctrl+a松开再按c,弹出(qemu)则正常进入调试,如下所示:

按照练习要求,使用qemu调试模式下执行xp指令,然后在gdb中执行x指令,对结果进行观察,看结果是否一致。结果如下:

由上图结果可以看出,两个命令在执行结果上是一致的。

同样,练习3还指出qemu还提供了info pg(显示当前页表,详细的表示,包括映射的内存范围、权限和标志),info mem(显示映射了哪些虚拟地址范围以及具有哪些权限的概述),结果如下:

因为在0x7c00处,刚刚进入bootloader,此时还未开启分页,所以直接跳转到0x100000处,再次执行info pg指令,结果如下:

注:

  • PDE(Page Directory Entry)、PTE(Page Table entry)
  • D —— Dirty,是否被修改;
  • A —— Accessed,最近是否被访问;
  • P —— Present,判断对应物理页面是否存在,存在为1,否则为0;
  • W —— Write,该位用来判断对所指向的物理页面是否可写,1可写,0不可写;

执行info men指令的结果如下:

注:根据实验1中的第3部分中的注解也可以理解上图的信息,在kern/entry.S设置CR0_PG标志之前(未进入保护模式),内存引用被视为物理地址(进入保护模式前线性地址与物理地址是相同的)。一旦CR0_PG设定,内存引用是得到由虚拟内存硬件到物理地址转换的虚拟地址。 entry_pgdir将 0xf0000000 到 0xf0400000 范围内的虚拟地址转换为物理地址 0x00000000 到 0x00400000,并将虚拟地址 0x00000000 到 0x00400000 转换为物理地址 0x00000000 到 0x00400000。

JOS内核通常将地址作为不透明值或整数进行操作,而不是对地址进行解引用,例如在物理内存分配器中,有时是虚拟地址,有时是物理地址。为了区分地址类型,JOS源区分了两种情况:类型uintptr_t表示虚拟地址、physaddr_t表示物理地址。这两种类型实际上只是32位整数 ( uint32_t) 的同义词,因此编译器不会阻止将一种类型分配给另一种类型。由于它们是整数类型(不是指针),如果尝试进行解引用,编译器会报错。

JOS内核可以通过将uintptr_t投影为指针类型对它进行解引用相比较而言,因为内存管理单元(MMU)会对所有地址进行解引用,所以,内核无法对一个物理地址进行解引用。如果将physaddr_t转换为指针并对它进行解引用,可能能够加载并存储到结果地址(硬件会将其解释为虚拟地址),但无法获得预期的内存位置。

Q:假设下面的 JOS 内核代码是正确的,变量x应该是什么类型,uintptr_t还是physaddr_t?

mystery_t x;
char* value = return_a_pointer();
*value = 10;
x = (mystery_t) value;

A:根据return_a_pointer()函数判断,value是一个指针类型,是一个虚拟地址,x是vlaue的一个解引用,因此可以判断x是uintptr_t类型。(上面标为蓝色的那句话)

JOS内核无法绕过虚拟地址转换,因此无法直接加载和存储到物理地址。为了读取和写入它知道物理地址的内存,内核需要重新映射从物理地址0开始到虚拟地址0xf0000000的所有物理内存。物理地址到虚拟地址的转换其实就是在物理地址上加上0xf0000000,这个加法的实现是借助kern/pmap.h中的KADDR(pa)宏实现的。

给定存储内核数据结构的内存的虚拟地址,JOS内核有时也需要能够找到物理地址。内核全局变量和boot_alloc()分配的内存位于加载内核的区域,从0xf0000000开始,也就是映射所有物理内存的区域。因此,要将这个区域中的虚拟地址转换为物理地址,内核可以简单地减去0xf0000000。这一减法是通过kern/pmap.h中的PADDR(va) 完成的。

实验中,可能需要将同一个物理页面映射到多个虚拟地址(或多个地址的环境中),物理页面被引用的次数将会记录到pp_ref字段中。

练习4要求完成相关函数代码,实现对页表的管理,包括:插入和删除线性到物理的映射,在需要页表时进行创建,并完成页面检查。

pgdir_walk():根据虚拟地址找到页表项地址。当物理页面不存在时,如果指定了create标志,分配新的页,并设置页目录项为新分配的页的地址。

pte_t *
pgdir_walk(pde_t *pgdir, const void *va, int create)
{
    pde_t *pde;
    pte_t *pte;
    pde = &pgdir[PDX(va)];//Get page directory entry 
    if(!(*pde & PTE_P)){    //Page directory not exists
        struct PageInfo *new_page = page_alloc(ALLOC_ZERO);
        if(!new_page || !create)
            return NULL;
        new_page->pp_ref++;
        //*pde = page2pa(new_page) | PTE_P | PTE_U;  //Omit PTE_W see below
        *pde = page2pa(new_page) | PTE_P | PTE_U | PTE_W;//Trun new_page to pde
    }
    if(PTE_ADDR(*pde) < npages * PGSIZE){
        pte = (pte_t *)KADDR(PTE_ADDR(*pde));//Page table entry
        return &pte[PTX(va)];
    }
    return NULL;
}

对于入口标志的检验采用&操作,即,将要检测的页目录入口低12位(假设二进制为000000000101)与PTE_P(inc/mmu.h中为0x001,转换为而二进制000000000001)进行与操作后,得到000000000001,仅保留要检测的第一位的信息,为1,证明标志位P为1。

对页目录入口标志进行设置时,采用|操作,即,将要检测的页目录入口低12位(假设二进制为000000000100)与PTE_P(inc/mmu.h中为0x001,转换为而二进制000000000001)进行或操作后,得到000000000101,完成对入口地址中标志位PTE_P的设置。

boot_map_region():将在[va, va+size)范围的虚拟地址,映射到在[pa, pa+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
    int page_num=PGNUM(size);///计算实际需要分配多少页
    int i;
    for(i=0;i<page_num;i++){
        pte_t *pte=pgdir_walk(pgdir,(void *)va,1);
        ///assert(!pte);///不能用,一但出错直接终止了
        if(!pte)
            panic("boot_map_regin panic:pte error");
        *pte=pa|perm|PTE_P;
        va+=PGSIZE;
        pa+=PGSIZE;
    }
}

page_lookup():查找虚拟地址va对应的页表项,如果找到则返回页表项对应的页信息结构(PageInfo)。

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))///如果页表项地址不存在,或者对应页表项的页不存在
        return NULL;
    if(pte_store)
        *pte_store=pte;
    return pa2page(PTE_ADDR(*pte));
}

page_remove():清除页表中虚拟地址va对应的物理页映射。将PageInfo的引用数pp_ref减1,并设置对应页表项的值为0,最后调用tlb_invalidate使tlb中该页缓存失效。

//
// 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;
    struct PageInfo *page=page_lookup(pgdir,va,&pte);///查找页表项
    if(!page)///如果页表项不存在
        return;
    page_decref(page);
    *pte=0;
    tlb_invalidate(pgdir,va);
}

page_insert():映射虚拟地址va到pp对应的物理页。如果之前该虚拟地址已经存在映射,则要先移除原来的映射。

注:boot_map_region映射的物理页不改变对应的pp_ref,一个物理页被这个函数映射与它是否被使用没有任何关系;而通过page_insert映射的物理页,同时表明该物理页被使用了一次,对应pp_ref加1。

//
// 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)
{
    pte_t *pte= pgdir_walk(pgdir, va, 1);
    if(!pte)
        return -E_NO_MEM;
    pp->pp_ref++;   //Must before remove
    if(*pte & PTE_P)
        page_remove(pgdir, va);
    *pte = page2pa(pp) | perm | PTE_P;
    return 0;

}

练习4测试过程出现的问题较多,具体如下:

pgdir_walk()中入口标志未设置PTE_P:

一个无法直接从报错信息直接知道的错误:

定位到第795行查看,找到具体出错原因如下:

即,page_insert()中,先移除映射关系(page_remove)导致页已经被释放了,又对引用数增加而出现的错。

pgdir_walk()中入口标志未设置PTE_U:

修改上述所有错误后,成功通过页管理测试:

2.3 Part 3:内核地址空间

JOS将线性地址分成内核部分和用户部分,正如上图中所示,左侧标注U开头的部分(低地址)是用户部分,K开头的部分(高地址)是内核部分。在访问上,用户仅能访问用户地址空间的内容(不能超过ULIM)。否则,用户代码可能会重写内核数据,导致系统崩溃等问题。实验中特别提到PTE_W访问控制位,对于内核代码和用户代码均有效。地址范围[UTOP,ULIM)的部分,是内核的只读数据结构部分,内核代码和用户代码均有只读权限。低于UTOP部分的内容,可以由用户空间设置访问权限进行访问。

练习5要求参考inc/memlayout.h补充kern/pmap.c中的mem_init未完成的代码来设置地址空间的内核部分。

//
    // Now we set up virtual memory

    //
    // 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:
    //PGSIZE error
    boot_map_region(kern_pgdir,UPAGES,PGSIZE,PADDR(pages),PTE_U);
    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-->kern/pmap.h
    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); 

Q1:地址映射出现问题.

A1:为了测试到底什么地方出现问题在check_kern_pgdir中将check_va2pa和PADDR的结果进行输出((在断言前添加cprintf).

再次进行测试,结果如下:

可以发现经过check_va2pa对实际分配结果进行测试得出的值为4294967295.应该是直接溢出到虚拟内存的最大地址(0xffffffff)了.所以再次添加cprintf,再次进行验证.

// check pages array
    n = ROUNDUP(npages*sizeof(struct PageInfo), PGSIZE);
    for (i = 0; i < n; i += PGSIZE){
        cprintf("max_address=%u  ",0xffffffff);
        cprintf("check_va2pa=%u  ",check_va2pa(pgdir, UPAGES + i));
        cprintf("PADDR=%u\n",PADDR(pages) + i);
        assert(check_va2pa(pgdir, UPAGES + i) == PADDR(pages) + i); 
    }  

输出结果如下:

又上图可知直接映射到了最高地址.查看inc/memlayout.c中的内存分布图,readonly(RO)区的大小为PTSIZE,而实现中代码中映射的大小被自己写成了PGSIZE.

为此,将新添加的第一部分代码修改为:

//
    // Now we set up virtual memory

    //
    // 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:
    boot_map_region(kern_pgdir,UPAGES,PTSIZE,PADDR(pages),PTE_U);

再次运行:

Q2:通过上面的输出可以发现,访问权限出现了问题.

A2:跳转到出现问题的地方进行查看:

可以看出,当大于KERNBASE部分的页目录的可写访问权限有误.而页目录访问权限是在pgdir_walk中进行设置的,所以修改pgdir_walk中对于页目录访问权限的设置.

pte_t *
pgdir_walk(pde_t *pgdir, const void *va, int create)
{
    // Fill this function in
    int pde_index=PDX(va);///页目录索引
    pde_t *pde=&pgdir[pde_index];///根据页目录索引获取指定的页目录入口
    if(!(*pde&PTE_P)){  /// Present位,如果页目录项不存在
        if(create==false)   ///如果页不存在并且不要求新建
            return NULL;
        else{
            struct PageInfo *new_page=page_alloc(ALLOC_ZERO);
            if(!new_page)
                return NULL;
            else{
                new_page->pp_ref++;
                ///练习5在下面一行补充|PTE_W
                *pde=page2pa(new_page)|PTE_P|PTE_U|PTE_W;///设置页目录项为新分配的页的地址>,并设置入口标志
            }   
        }   
    }   
    pte_t *pte=(pte_t *)KADDR(PTE_ADDR(*pde));///页表入口
    int pte_index=PTX(va);///页表索引
    return &pte[pte_index];
}

注:对于pgdir_walk函数的实现是在上一个练习中实现的,当时,设置页目录访问权限时,Hint2提示过让页面目录中的权限比严格必要的权限更为宽松,由于疏忽未设置PTE_W位。但当时并未对页目录的PTE_W位进行检测。最终,在练习5中check_kern_pgdir测试时,出现了上面的错误。

将上面两个问题解决后,再次进行检测,结果如下:

Question

Q2:页面目录中的哪些条目(行)此时已被填充?它们映射哪些地址以及它们指向何处?

A2:练习5中完成所有地址映射之后,check_kern_pgdir之前,添加如下代码进行测试:

///
//Test for Question after Exercise 5
    cprintf("Page directory\n");
    int index;
    for(index = 1023; index >= 0; index--){

        pte_t pte = kern_pgdir[index];
        cprintf("index = %u  ",index);
        cprintf("pte = %08x  ",pte);
        cprintf("address = %08x\n",PTE_ADDR(pte));
    }
///
    check_kern_pgdir();

运行结果部分截图如下:

根据运行结果填写表格:

Entry

Base Virtual Address

Point to(logically):

1023

0x003be000

Page table for top 4MB of phys memory

1022

0x003bf000

Page table for top 4MB of phys memory

...

...

Page table for top 4MB of phys memory

960

0x003ff000

Page table for top 4MB of phys memory

959

0x003fe000

Kernel Stack & Invalid Memory

958

0x00000000

NULL

957

0x0011a000

Page Table

956

0x003fd000

Read-Only PAGES

955

0x00000000

NULL

...

...

NULL

2

0x00000000

NULL

1

0x00000000

NULL

0

0x00000000

NULL

Q3:将内核和用户环境放置在相同的地址空间中。为什么用户程序不能读写内核内存?什么具体机制保护内核内存?

A3:内核空间内存的页表项(高于KERNBASE的部分)的perm没有设置PTE_U,要访问内核需要CPL为0-2。而用户程序的CPL为3,用户没有足够的权限去访问内核空间。

Q4:此操作系统可以支持的最大物理内存量是多少?为什么?

A4:2GB,因为PTSIZE(Page table size)最大为4MB,而每个PageInfo大小为8B,所以可以最多可以存储512K个PageInfo结构体,而每个PageInfo对应4KB内存,所以最多512K*4KB= 2GB内存。

//Print size of PageInfo
cprintf("PageInfo size = %x\n",sizeof(struct PageInfo));

Q5:如果我们实际上拥有最大数量的物理内存,那么管理内存有多少空间开销?这个开销是如何分解的?

A5: 如果有最大的2GB内存,则物理页有2GB/4KB=512K个,每个PageInfo结构占用8B,则一共是512K*8B=4MB。页表包括512K个页表项,每个页表项占用4B共需要512K*4B=2MB存储。另外,页目录本身占用4KB存储,所以额外消耗的内存为4MB+2MB+4KB=6MB + 4KB。

Q6:重新访问kern/entry.S和kern/entrypgdir.c中的页表设置。在打开分页后,EIP仍然是一个很小的数字(略高于 1MB)。什么时候过渡到在KERNBASE之上的EIP上运行?是什么让可以在启用分页和开始在高于KERNBASE 的EIP上运行之间继续以低EIP执行?为什么需要这种转变?

A6:kern/entry.S 中的jmp *%eax语句之后就开始跳转到高地址运行。在entry.S中cr3加载的是entry_pgdir,为保证系统正常运行,它将虚拟地址 [0, 4M)和[KERNBASE, KERNBASE+4M)都映射到了物理地址 [0, 4M)。在新的kern_pgdir加载后,并没有映射低位的虚拟地址[0, 4M),所以这一步跳转是必要的。

Challenge1

阅读《Intel® 64 and IA-32 ArchitecturesSoftware Developer’s Manual》,思考如何使用页目录条目PTE_PS位代替用物理页保存KERNBASE映射的页表来节省空间。

首先,注释掉mem_init中对KERNBASE以上的空间的地址映射(boot_map_region())。未进行优化时,分配二级页表的页面大小为4KB,共需要64个PDE表项和64*1K个PTE表项,共需要64*4B+64K*4B=256KB+256B内存。为减少内存首先开启CR4的PSE位,开启后每个PDE表项对应4MB内存,仅需64个PDE表项,共需要64*4B=256B内存。

开启CR4的PSE位后,设置页目录的PTE_PS位,完成该操作的代码如下:

//
    // 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);
    //Challenge1///
    ///turn on PSE of cr4
    uint32_t cr4 = rcr4();
    cr4 |= CR4_PSE;
    lcr4(cr4);
    //Set PDE
    uintptr_t va = KERNBASE;
    physaddr_t pa = 0;
    size_t i;
    for(i = 0; i< 64; i++){
        kern_pgdir[PDX(va)] = pa | PTE_W | PTE_P | PTE_PS;
        va += PTSIZE;
        pa += PTSIZE;
    }
    /Challenge1/
	///check_kern_pgdir();

注:因为check_kern_pgdir();会对PTE进行检查,而当前的优化省略了PTE表项,为保证系统能够继续执行,这里去掉check_kern_pgdir();检查。

进入QEMU调试模式,查看当前分页情况:

注:由上图可知,已经完成映射,S即为PS位。

Challenge2

扩展JOS命令,实现showmappings命令。

关于命令相关设置需要对kern/monitor.h和kern/monitor.c文件进行修改.首先,向command添加命令提示,修改如下:

//kern/monitor.c
static struct Command commands[] = { 
    { "help", "Display this list of commands", mon_help },
    { "kerninfo", "Display information about the kernel", mon_kerninfo },
    { "showmappings","Display information about physical page mappings",mon_showmappings},
};

添加函数声明:

//kern/monitor.h
// Functions implementing monitor commands.
int mon_help(int argc, char **argv, struct Trapframe *tf);
int mon_kerninfo(int argc, char **argv, struct Trapframe *tf);
int mon_backtrace(int argc, char **argv, struct Trapframe *tf);
int mon_showmappings(int argc, char **argv, struct Trapframe *tf);///新添加

实现函数:

int
mon_showmappings(int argc, char **argv, struct Trapframe *tf)
{
    char flag[0x100] = {
        [0]= '-',
        [PTE_W] = 'W',
        [PTE_U] = 'U',
        [PTE_A] = 'A',
        [PTE_D] = 'D',
        [PTE_PS] = 'S',
    };
    char *arg1 = argv[1];
    char *arg2 = argv[2];
    //char *arg3=argv[3];
    if(arg1 == NULL || arg2 == NULL){
        cprintf("showmappings need two arguments!\n");
        return 0;
    }
    char *str1,*str2;
    uintptr_t va_1 = strtol(arg1,&str1,16);
    uintptr_t va_2 = strtol(arg2,&str2,16);
    //判断输入格式,即判断是否全部为16进制格式字符
    if(*str1 || *str2){
        cprintf("Arguments's format error!\n");
        return 0;
    }
    if(va_1 > va_2){
        cprintf("The first argument must smaller than the second argument!\n");
        return 0;
    }
    pte_t *pgdir = (pde_t *)PGADDR(PDX(UVPT), PDX(UVPT), 0);
    cprintf("  va range  entry  flag    pa range\n");
    cprintf("---------------------------------------------------------------\n");
    while(va_1 <= va_2){
        pte_t pde = pgdir[PDX(va_1)];//根据目录索引(入口)获取页表
        if(pde&PTE_P){
            char flag_w = flag[pde & PTE_W];//判断页目录权限位,并记录权限标识
            char flag_u = flag[pde & PTE_U];
            char flag_a = flag[pde & PTE_A];
            char flag_d = flag[pde & PTE_D];
            char flag_s = flag[pde & PTE_PS];
            pde = PTE_ADDR(pde);//获取入口地址
            //打印低地址部分信息
            if(va_1 < KERNBASE){
                cprintf("[%08x - %08x]", va_1, va_1 + PTSIZE - 1);
                cprintf(" PDE[%03x] --%c%c%c--%c%cP\n", PDX(va_1), flag_s, flag_d, flag_a, flag_u, flag_w);
                pte_t *pte = (pte_t *)(pde + KERNBASE);
                size_t i;
                for(i = 0; i < 1024 && va_1 <= va_2; va_1 += PGSIZE, i++){
                    if(pte[i]&PTE_P){
                        flag_w = flag[pte[i] & PTE_W];//判断页表是否权限位,并>记录权限标识
                        flag_u = flag[ pte[i] & PTE_U];
                        flag_a = flag[ pte[i] & PTE_A];
                        flag_d = flag[ pte[i] & PTE_D];
                        flag_s = flag[pte[i] & PTE_PS];
                        cprintf(" |-[%08x - %08x]", va_1, va_1+PGSIZE-1);
                        cprintf(" PTE[%03x] --%c%c%c--%c%cP", i, flag_s, flag_d, flag_a, flag_u, flag_w);
                        cprintf(" [%08x - %08x]\n", PTE_ADDR(pte[i]), PTE_ADDR(pte[i]) + PGSIZE - 1);
                    }//if
                }//for
                continue;
            }//if
            //打印高地址部分信息
            cprintf("[%08x - %08x]", va_1, va_1 + PTSIZE-1);
            cprintf(" PDE[%03x] --%c%c%c--%c%cP", PDX(va_1), flag_s, flag_d, flag_a, flag_u, flag_w);
            cprintf(" [%08x - %08x]\n", pde, pde + PTSIZE-1);
        }//if
         va_1 += PTSIZE;
    }//while
    return 0;
}

测试

测试结果待验证。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值