MIT6.828-OS lab2:Memory Management 记录

这一章主要讲内存分配。

github:https://github.com/viktorika/mit-os-lab

introduction

在这个实验中,你将会为你的操作系统写内存管理代码。内存管理有两个部件。

为了kernel能够分配并且释放内存,第一个部件是kernel的物理内存分配器allocator。你的allocator将以4096B为操作单位,称为页。您的任务将是维护数据结构,该数据结构记录哪些物理页是空闲的,哪些是已分配的,以及多少进程正在共享每个分配的页。 您还将编写例程来分配和释放内存页面。

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

 

Getting started

在这个和将来的lab中,你将逐步建立起你的kernel。你需要把你lab1的提交记录merge到lab2里,你可能需要处理冲突。lab2包含了新的源文件,你应该浏览它。

memlayout描述了虚拟内存的布局,你必须修改pmap.c来实现它。memlayout.h和pmap.h定义了PageInfo数据结构,您将使用该结构来跟踪哪些物理内存页可用。kclock.c和kclock.h操纵PC的电池供电时钟和CMOS RAM硬件(BIOS记录PC包含的物理内存量...等等的地方)。 pmap.c中的代码需要读取该设备的硬件才能确定有多少物理内存,但是这部分代码已为您完成:您无需了解CMOS硬件如何工作的详细信息。

请特别注意memlayout.h和pmap.h,因为此练习要求您使用和理解它们包含的许多定义。您可能也想查看inc / mmu.h,因为它也包含许多对本练习有用的定义。

 

Lab Requirements 

在本实验室和后续实验室中,请执行实验室中描述的所有常规练习以及至少一个挑战性问题。(当然,某些挑战性问题比其他挑战性问题更具挑战性!)此外,针对实验室提出的问题写下简短的答案,并简短描述(例如,一到两段)您为解决所选挑战性问题所做的工作。 如果您实施了多个挑战问题,则只需在本文中描述其中一个问题,当然,欢迎您做更多的事情。(PS:本质上我是不喜欢浪费时间的,要是挑战性问题是跟操作系统有关系的那我应该会全部尝试完成,要是没有关系我估计就会跳过了)。

 

Hand-In Procedure 

和以前一样,您可以在lab目录中运行make grade以使用分级程序测试内核。 您可以更改完成实验所需的任何内核源文件和头文件,但是不用说您不得更改或破坏分级代码。

 

Part 1: Physical Page Management

操作系统必须跟踪物理RAM的哪些部分空闲以及当前正在使用哪些部分。 JOS以页面粒度管理PC的物理内存,以便它可以使用MMU映射和保护分配的每个内存。

现在,您将编写物理页面分配器。 它通过struct PageInfo对象的链接列表来跟踪哪些页面是空闲的,每个页面对应于一个物理页面。 您需要先编写物理页分配器,然后才能编写其余的虚拟内存实现,因为页表管理代码将需要分配用于存储页表的物理内存。

Exercise 1. 在文件kern / pmap.c中,您必须为以下函数实现代码(可能按给定的顺序)。

check_page_free_list()和check_page_alloc()测试您的物理页面分配器。 您应该启动JOS并查看check_page_alloc()是否报告成功。 修正您的代码,使其通过。 您会发现添加自己的assert()来验证您的假设正确是有帮助的。

所有的lab练习中,你都必须自己搞清楚自己需要做些什么,自己找到需要添加代码的地方,通常注释会给你规范和提示。

在做这个练习之前,首先我们得了解MMU的工作过程,首先分页管理我们是通过一个二级表结构来实现的,第一级是页目录表,第二级是页表,为什么要分两级,因为每个进程(包括kernel)都需要有自己的线性地址空间,如果我们只有一级那么每多一个进程,内存就得多一份页表拷贝,假设是32位地址空间为4G,一个页表项代表一页4k,一个页表项大小4B,每多一个进程就得多消耗4M的内存来存这份页表,但是我们多了一级页目录表的情况下,如果我们把页表拆成1000份子页表,然后用页目录表去指向这1000份子页表,每多一个进程只是多了一份页目录表,消耗大大减少。而当前页目录表是存在cr3寄存器,切换进程的时候会修改cr3寄存器。

一般来说读取一个内存里的值是这样的一个过程,MMU先找到TLB,看看是否命中,命中就直接去找页表项,否则就通过刚才的二级查找方式来找到页表项并且缓存在TLB里。然后如果页表项里有允许cache的标记,则让CPU去cache里查找是否命中,命中则直接返回,否则从物理内存中读取给到CPU,同时也返回给cache去缓存该内存的值(缓存并不只是缓存这个值,会把附近的值都缓存过来,叫cache line)。(PS:这就是一般的查找过程,但是不一定相同)

最后再说一下这个kernel的设计,(PS:你可以跳过这一段直接看下面做练习,若实在做不了就来看这一段,主要是讲解一下这个练习相关的内存设计)。

主要有下面几点:

1.一个线性地址里面是包含的对应的页表信息,前10位是页目录表的index,接着10位是页表的index,这是直接对应的。

2.内核虚拟地址=物理地址+KERNBASE。

3.每个页都有一个pageInfo结构与他对应,放在pages数组。且页的物理地址和pages数组也是线性对应的。

4.页目录项和页表项的前20位是对应的物理地址,比如页目录项的前20位是他指向的页表的物理地址,而页表项的前20位指的是他指向的页面的物理地址

 

下面开始做这个练习。先来看看boot_alloc,这是它的注释。

就是说这个函数只用在建立虚拟内存系统时调用(初始化的时候调用,在page_free_list建立之前),建立完后是用page_alloc来分配内存,这个函数如果参数n>0则分配连续的足够大的空间去容纳n字节,不初始化内存,返回一个内核的虚拟内存地址。n==0则返回下一个空闲页面不分配内存。如果我们没有足够内存,那么应该调用panic系统调用。再看下面。

nextfree是空闲内存的下一个字节的虚拟地址,第一次调用时需要初始化nextfree,end是linker自动生成的符号,指向内核bss段的末尾,这是linker未分配给任何内核代码或全局变量的第一个虚拟地址。这里还用了ROUNDUP我们看一下。

直接找到这两个宏,ROUNDUP是a向上对n取整,ROUNDDOWN是向下取证。简单来说上面的nextfree向上取整应该是为了内存对齐一下。再看我们该写的代码部分。

if((uint32_t)nextfree + n > KERNBASE + npages * PGSIZE)
    panic("boot_alloc: out of memory\n");
result = nextfree;
nextfree = ROUNDUP((char *)(nextfree + n), PGSIZE);
return result;

 

根据mem_init先调用了i386_detect_memory,i386_detect_memory里计算了内存,然后此时的虚拟地址比物理地址多了个KERNBASE(从PADDR宏推导),我估计数组越界的判定应该是这样判,如果写错了请大佬指出。

再看看mem_init。

此函数建立两级页表,kern_pgdir是它根的线性地址(页目录表的地址)。这个函数仅仅设置kernel部分的地址空间(即地址>=UTOP),user部分的地址空间以后再设置,从UTOP到ULIM,允许用户读取但不能写入。高于ULIM的空间用户不可读写。

先计算有多少物理页,然后这句panic在你需要测试这个函数的时候去掉。

首先通过刚才的boot_alloc函数分配内存,然后对这块内存初始化。

我做完所有内容回过头来看这里,还是不怎么明白,只能理解为kern_pgdir是个页目录的同时也是一个页表,然后他就做了一层映射,之后的分配的页表映射到了这个二级目录当中。但是这个意义何在?就是单纯为了把页表用的页映射在页表吗?有没有大神来解释解释。

又到我们的代码时间,我们需要分配一个npages的数组用于PageInfo结构,并且把他存储到pages里,内核使用这个数组去跟踪物理页面,对于每个物理页面,此数组都有一个对应的PageInfo结构。 npages是内存中的物理页数。这个应该没啥难度吧。代码如下:

pages = (struct PageInfo *)boot_alloc(sizeof(PageInfo) * npages);

继续看下面。

练习1是到check_page_free_list完成了就好,所以还需要完成的是page_init,page_alloc和page_free函数,这里说到一旦初始化好了空闲物理页,之后就通过page_*函数来做内存管理,并且现在可以使用boot_map_region和page_insert来映射内存。先来看page_init。

初始化pages数组,将空闲页面放到page_free_list,已经使用过的页面引用计数标记为1,调用这个函数后不能再次使用boot_alloc,只能通过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!

    // 已经使用的区间,左闭右开
    // 0用于实模式IDT和BIOS结构
    // 还有空洞是不能分配的
    // 空洞后一直到下一个可分配的物理内存之前都是不可用的
    // 这个地方我踩了好久,一开始不知道怎么辨别kernel这里有多少个地方被用了,结果看大神的直接把前面的都认为不可用才恍然大大悟
    physaddr_t nextfree_paddr = PADDR((pde_t *)boot_alloc(0)); 
    physaddr_t used_interval[2][2] = {{0, PGSIZE}, {IOPHYSMEM, nextfree_paddr}};
    const int kUsed_interval_length = 2;
    int used_interval_pointer = 0;
    for(int i = 0; i < npages; ++i){
        while(used_interval_pointer < kUsed_interval_length && used_interval[used_interval_pointer][1] <= page2pa(pages + i))
            used_interval_pointer++;

        //空闲-----used_interval_pointer越界或者当前页面的物理地址小于used_interval[0]
        if(used_interval_pointer >= kUsed_interval_length || page2pa(pages + i) < used_interval[used_interval_pointer][0]){
            pages[i].pp_ref = 0;
            pages[i].pp_link = page_free_list;
            page_free_list = pages + i;
        }
        else{
        //否则就是在used_interval的其中一个区间,也就是已经被使用了
            pages[i].pp_ref = 1;
            pages[i].pp_link = NULL;
        }
    }
}

就是把他注释里提到的几个地方标记不可用,其他都是可用的。再来看看page_alloc。

分配一个物理页,返回一个PageInfo结构,如果alloc_flags&ALLOC_ZERO为真,用'\0'填充返回的物理页面。不自增页面的引用计数。如果没有足够的空闲内存返回NULL。这个就很简单了吧,照着写就完了。

struct PageInfo *
page_alloc(int alloc_flags)
{
    // Fill this function in
    if(!page_free_list) return NULL;
    struct PageInfo *new_page = page_free_list;
    page_free_list = page_free_list->pp_link;
    new_page->pp_link = NULL;
    if(alloc_flags & ALLOC_ZERO)
        memset(page2kva(new_page), 0, PGSIZE);
    return new_page;
}

同样还有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
    if(!pp || pp->pp_ref) return;
    pp->pp_link = page_free_list;
    page_free_list = pp;
}

至此的话是可以通过到check_page_free_list的了。

 

Part 2: Virtual Memory

在做之前,请熟悉x86的保护模式内存管理体系结构:即分段和页面转换。

Exercise 2.如果尚未阅读《英特尔80386参考手册》的第5章和第6章,请参阅。 仔细阅读有关页面转换和基于页面的保护的部分(5.2和6.4)。 我们建议您还浏览有关细分的部分; 尽管JOS将分页用于虚拟内存和保护,但是不能在x86上禁用段转换和基于段的保护,因此您需要对其有基本的了解。

Virtual, Linear, and Physical Addresses

在x86术语中,虚拟地址由段selector和段内的偏移量组成。 线性地址是段转换后但页面转换前的内容。 物理地址是段和页面转换后最终获得的地址,最终是在硬件总线上输出到RAM的地址。

C指针实际上是虚拟地址的“偏移”部分。 在boot / boot.S中,我们安装了全局描述符表(GDT),该表通过将所有段基址设置为0并将限制设置为0xffffffff,有效地禁用了段转换。 因此,selector无效,线性地址始终等于虚拟地址的偏移量。 在实验3中,我们将需要与分段进行更多的交互才能设置特权级别,但是对于内存转换,我们可以在整个JOS实验中忽略分段,而只专注于页面转换。

回想一下,在实验1的第3部分中,我们安装了一个简单的页表,即使内核实际上已加载在ROM BIOS上方的物理内存中,即0x00100000,该内核也可以在其链接地址0xf0100000上运行。 该页表仅映射了4MB的内存。 在本实验中,您将在虚拟内存布局中为JOS进行设置,我们将对其进行扩展以映射从虚拟地址0xf0000000开始的前256MB物理内存,并映射许多其他虚拟内存区域。

Exercise 3.尽管GDB只能通过虚拟地址访问QEMU的内存,但是在设置虚拟内存时检查物理内存通常很有用。 查看实验工具指南中的QEMU Monitor命令,尤其是xp命令,该命令可让您检查物理内存。 要访问QEMU监视器,请在终端中按Ctrl-a c(相同的绑定将返回到串行控制台)。

使用QEMU监视器中的xp命令和GDB中的x命令来检查相应物理和虚拟地址处的内存,并确保看到相同的数据。

我们的补丁版QEMU提供了一个info pg命令,该命令也被证明是有用的:它显示了当前页表的紧凑但详细的表示形式,包括所有映射的内存范围,权限和标志。 Stock QEMU还提供了一个info mem命令,该命令显示了映射了哪些虚拟内存范围以及具有哪些权限的概述。

从CPU上执行的代码开始,一旦进入保护模式(我们在boot / boot.S做的第一件事),就无法直接使用线性或物理地址。 所有内存引用都被解释为虚拟地址,并由MMU转换,这意味着C中的所有指针都是虚拟地址。

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

总结一下:

Question

1.假设以下JOS内核代码正确,则变量x应该具有哪种类型uintptr_t或physaddr_t?

answer:应该是uintptr_t,毕竟value是虚拟地址,然后value又赋值了给x,那么x应该也是虚拟地址。

JOS内核有时需要读取或修改仅知道物理地址的内存。 例如,将映射添加到页表可能需要分配物理内存以存储页目录,然后初始化该内存。 但是,内核与其他任何软件一样,无法绕过虚拟内存转换,因此无法直接加载并存储到物理地址。 JOS重映射从虚拟地址0xf0000000处的物理地址0开始的所有物理内存的原因之一是为了帮助内核读写仅知道物理地址的内存。 为了将物理地址转换为内核可以实际读取和写入的虚拟地址,内核必须在物理地址上添加0xf0000000才能在重映射区域中找到其对应的虚拟地址。 您应该使用KADDR(pa)来操作。

给定存储内核数据结构的内存的虚拟地址,JOS内核有时有时还需要能够找到物理地址。 boot_alloc()分配的内核全局变量和内存位于加载内核的区域中,从0xf0000000开始,该区域正是我们映射所有物理内存的区域。 因此,要将该区域中的虚拟地址转换为物理地址,内核可以简单地减去0xf0000000。 您应该使用PADDR(va)来操作。

Reference counting

在未来的实验中,您经常会在多个虚拟地址(或多个环境的地址空间)中同时映射相同的物理页面。 您将在与物理页面对应的结构PageInfo的pp_ref字段中保留对每个物理页面的引用数的计数。 当物理页面的计数变为零时,该页面可以被释放,因为不再使用该页面。 通常,此计数应等于物理页在所有页表中出现在UTOP之下的次数(UTOP之上的映射大部分是在启动时由内核设置的,永远不应释放,因此无需引用计数他们)。我们还将使用它来跟踪指向页目录页面的指针数量,进而跟踪页目录对页表页面的引用数量。

使用page_alloc时要小心。 它返回的页面将始终具有0的引用计数,因此一旦对返回的页面执行了某些操作(例如将其插入到页面表中),就应将pp_ref递增。 有时这是由其他函数(例如,page_insert)处理的,有时,调用page_alloc的函数必须直接执行此操作。

Page Table Management

现在,您将编写一组例程来管理页表:插入和删除线性地址到物理地址的映射,并在需要时创建页表页面。

Exercise 4. 在文件kern / pmap.c中,您必须实现以下函数的代码。

这次得完成到mem_init的check_page这个地方,必须保证能完成check_page。

首先是pgdir_walk:

给一个页目录指针pgdir,返回线性地址va对应的页表项。如果相关的页表不存在,当create==false时返回NULL,否则通过page_alloc分配一个新的页表页。如果分配失败则返回NULL,否则新页的引用计数+1,并且返回一个在新页表页里的指针。

pte_t *
pgdir_walk(pde_t *pgdir, const void *va, int create)
{
    // Fill this function in
    //算出页目录表项
    pde_t *pgdir_entry = pgdir + PDX(va);
    if(!(*pgdir_entry & PTE_P)){
        //不存在
        if(create){
            struct PageInfo *new_pageinfo = page_alloc(ALLOC_ZERO);
            //分配失败
            if(!new_pageinfo) return NULL;
            //成功则引用+1,并且设置标记,将页表项指向这个新分配的页面
            new_pageinfo->pp_ref = 1;
            *pgdir_entry = page2pa(new_pageinfo) | PTE_P | PTE_U | PTE_W;
        }
        else
            return NULL;
    }   
    //再算出页表项
    pte_t *pg_address = KADDR(PTE_ADDR(*pgdir_entry));
    return pg_address + PTX(va);
}

再来看看boot_map_region:

映射虚拟地址[va, va+size)到物理地址[pa, pa+size],使用权限位为perm|PTE_P,只是在UTOP上方做静态映射,所以不能修改pp_ref字段。

static void
boot_map_region(pde_t *pgdir, uintptr_t va, size_t size, physaddr_t pa, int perm)
{
    // Fill this function in
    uintptr_t cur_va = va; 
    uintptr_t cur_pa = pa; 
    //记住要向上取整
    unsigned length = (size + PGSIZE - 1) / PGSIZE;
    //循环的时候要记住不能直接用物理地址来判定,因为物理地址容易越界返回值变会从0开始,然后无限循环
    for(unsigned i = 0; i < length; ++i){
        //一页页来映射
        //首先求页表项
        pte_t *pg_entry = pgdir_walk(pgdir, (void *)cur_va, true);
        if(!pg_entry) continue;
        *pg_entry = cur_pa | perm | PTE_P;
        cur_va += PGSIZE;
        cur_pa += PGSIZE;
    }   
}

这里细节挺多的,很容易出错,注意看我的注释就好,再看看page_lookup:

返回一个虚拟地址va映射的页面。如果pte_store不是0,那么将对应的页表项地址存到pte_store的地址里(用于结果返回)。如果没有页面映射在va那么返回NULL。提示:使用pgdir_walk和pa2page。

struct PageInfo *
page_lookup(pde_t *pgdir, void *va, pte_t **pte_store)
{
    // Fill this function in
    pte_t *pg_entry = pgdir_walk(pgdir, va, 0);
    //不存在
    if(!pg_entry || !(*pg_entry & PTE_P)) return NULL;
    physaddr_t pg_paddr= PTE_ADDR(*pg_entry);
    struct PageInfo *pg_entry_info = pa2page(pg_paddr);
    if(pte_store)
        *pte_store = pg_entry;
    return pg_entry_info;
}

基本上他的注释写的我没啥疑惑,应该算好写的了,再看看page_remove:

解除物理页在虚拟地址va的映射。如果不存在物理页,那么什么都不做。细节:引用计数必须-1,当引用计数归0时页面必须被释放。页表项应该设置为0(如果PTE标记存在)。如果从页表中删除条目则TLB必须无效。提示:可以使用page_lookup,tlb_invalidate,和page_decref。

void
page_remove(pde_t *pgdir, void *va)
{
    // Fill this function in
    pte_t *pg_entry = NULL;
    struct PageInfo* pg_entry_info = page_lookup(pgdir, va, &pg_entry);
    if(!pg_entry_info) return;
    //引用-1
    page_decref(pg_entry_info);
    //页表项清0
    *pg_entry = 0;
    //TLB无效
    tlb_invalidate(pgdir, va);
}

这个应该是最简单的了。最后是page_insert:

把物理内存页pp映射在虚拟地址va,页表项权限设置为perm|PTE_P。如果已经有一个页面在va,它应该先调用page_remove()删除,如有必要,应按需分配页表并将其插入“ pgdir”。插入成功pp->ref应该自增。如果页面以前位于“ va”,则TLB必须无效。特殊情况提示:确保考虑到当同样的pp重新插入到同样的虚拟地址在同样的页目录里会发生什么。然而,尽量不要在您的代码中区分这种情况。因为这经常会导致细微的错误。有一种优雅的方法可以在一个代码路径中处理所有内容。成功返回0,页表无法分配则返回-E_NO_MEM,提示:使用pgdir_walk,page_remove和page2pa。

int
page_insert(pde_t *pgdir, struct PageInfo *pp, void *va, int perm)
{
    // Fill this function in
    pte_t* pg_entry = pgdir_walk(pgdir, va, 1);
    if(!pg_entry) return -E_NO_MEM;
    physaddr_t pg_paddr = page2pa(pp);
    //先对引用计数自增1,否则同样的pp插入到同样的va会被释放到空闲链表
    pp->pp_ref += 1;
    if(*pg_entry & PTE_P)
        //如果已经存在页面
        //先删除
        page_remove(pgdir, va);
    //映射地址
    *pg_entry = pg_paddr | perm | PTE_P;
    pgdir[PDX(va)] |= perm;
    return 0;
}

这个稍微难搞一点,首先他提示了同样的pp重新插入到同样的va会有问题,虽然提示了,但我一开始还是踩了上去,还好他的check函数很强,报了错误出来,然后我看了一下就知道是remove放回了空闲链表的问题。其余的就不多说了。

 

Part 3: Kernel Address Space

JOS将处理器的32位线性地址空间分为两部分。 我们将在实验3中开始加载和运行的用户环境(进程)将控制下部的布局和内容,而内核始终保持对上部的完全控制。 分隔线由inc / memlayout.h中的符号ULIM任意定义,为内核保留大约256MB的虚拟地址空间。 这解释了为什么我们需要在实验1中为内核提供如此高的链接地址:否则,内核的虚拟地址空间中就没有足够的空间可以同时在其下面的用户环境中进行映射。 对于本部分和以后的实验,您可以参考inc / memlayout.h中的JOS内存布局图,是很有帮助的。

Permissions and Fault Isolation

由于内核和用户内存都存在于每个环境的地址空间中,因此我们将不得不使用x86页表中的权限位来允许用户代码仅访问地址空间的用户部分。 否则,用户代码中的错误可能会覆盖内核数据,从而导致崩溃或更微妙的故障。 用户代码也可能能够窃取其他环境的私有数据。 用户环境将没有对ULIM之上的任何内存的许可,而内核将能够读写此内存。 对于地址范围[UTOP,ULIM),内核和用户环境都具有相同的权限:它们可以读取但不能写入该地址范围。 此地址范围用于将某些内核数据结构只读给用户环境。 最后,UTOP下的地址空间供用户环境使用; 用户环境将设置访问该内存的权限。

Initializing the Kernel Address Space

现在,您将在UTOP上方设置地址空间:地址空间的内核部分。 inc / memlayout.h显示了您应该使用的布局。 您将使用刚刚编写的函数来设置适当的线性地址到物理地址的映射。

Exercise 5.填充完mem_init的代码。

您的代码现在应该通过check_kern_pgdir()和check_page_installed_pgdir()检查。

接口已经写完了,这个练习只是调用三次映射接口而已,没什么难度,前提是你接口写的正确。。。。直接看代码吧。

//
    // 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, sizeof(struct PageInfo) * npages, PADDR(pages), PTE_U | PTE_W);

    //
    // 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:
    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, 0xffffffffu - KERNBASE, 0, PTE_W);

没啥好说的吧,它的注释写得很清楚了。make grade是全通过了。这里还提到了个guard page的概念,内核栈被分为两部分,一部分是有物理内存支持的,另一部分是没有的,没有的那部分称为guard page,一旦溢出到这个地方就会报错。

Question

2.此时,页目录中的哪些条目(行)已填写? 他们映射什么地址,并指向何处? 换句话说,请尽可能填写此表:

answer:emmmm,不填了吧,就四个部分,页目录表本身,pages,kernel stack,KERNBASE开始的所有部分。

3.(从第3讲开始)我们将内核和用户环境放在了相同的地址空间中。 为什么用户程序无法读取或写入内核的内存? 哪些特定机制可以保护内核内存?

answer:这个的保护机制应该就是表项里的权限位了。

4.该操作系统可以支持的最大物理内存量是多少? 为什么?

answer:分配物理页是通过PageInfo结构来分配的,那么支持多少取决于能有多少个PageInfo,而PageInfo是映射在UPAGES里,UPAGES留的位置有一个PTSIZE也就是0x400000B,一个pageInfo是8B,一个物理页有4KB,那么计算得2GB。

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

answer:这个问题我有点摸不着头脑,首先是开销,一个页表4KB可以存1000个页,也就是说一个页表等于4MB内存,2GB的话需要512个页表,那么全部页表都分配的情况下就需要4KB×512=2MB内存,然后还需要页目录表,页目录表内核必须要有一个,一个页目录表4KB,假设有n个进程,那么极限情况下管理内存需要2MB+(n+1)*4KB内存,还有pages部分的内存,pages需要512*8B=4M,也就是需要6M+(n+1)*4KB内存,开销分解?没明白想问什么。。。。

6.重新访问kern / entry.S和kern / entrypgdir.c中的页表设置。 在打开分页后,EIP仍然是一个很小的数字(略超过1MB)。 在什么时候我们要过渡到在KERNBASE之上的EIP上运行? 在启用分页与开始在高于KERNBASE的EIP之间运行期间,什么东西使我们能够以较低的EIP继续执行? 为什么需要这种过渡?

answer:jump *eax跳转的时候会跳到ip为KERNBASE的地方执行指令,因为entrypgdir.c里将[KERNBASE,KERNBASE+4MB)和[0,4MB)映射到物理内存[0,4MB],所以是可以执行的。为什么需要这种过渡?指令一开始在低位,内核空间在高位,自然需要这种过渡。

最后再粘贴个make grade的图。

--------------------------------------------------------------------------------------------------------------------------------

TODO   challenge(PS:等我变强回来搞)

--------------------------------------------------------------------------------------------------------------------------------

总结

1.c指针的本质是段内偏移,逻辑地址通过selector(查GDT)得到段机制+offset转换为线性地址,线性地址通过分页管理得到物理地址。

2.页表和页目录表的表项都是前面物理地址位+后面的标记位组成,标记位有关于该页的各种消息。

3.MMU工作一般是先查TLB,查不到再查两级页表,查到了再看表项有没有cache标记,有的话让cpu查cache,查不到再去物理内存里读出来。

  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值