MIT6.828 Lab2: 第2部分 Virtual Memory

来源:互联网 发布:js将base64转换为图片 编辑:程序博客网 时间:2024/05/20 00:50

Virtual, Linear, and Physical Addresses
  在x86体系中,虚拟地址(Virtual Address)是由两部分组成,一个是段选择子(segment selector),另一个是段内偏移(segment offset)。线性地址(Linear Address)指的是通过段地址转换机构把虚拟地址进行转换之后得到的地址。物理地址(Physical Addresses)是分页地址转换机构把线性地址进行转换之后得到的真实的内存地址,这个地址将会最终送到你的内存芯片的地址总线上。
这里写图片描述

  我们所编写的C语言程序中的指针的值是虚拟地址中段内偏移部分的值。在boot/boot.S文件中,我们引入了一个全局描述符表,这个表通过把所有段的基址设置为0,界限设置为0xffffffff的方式,关闭了分段管理的功能。因此虚拟地址中的段选择子字段的内容已经没有任何意义,线性地址的值总是等于虚拟地址中段内偏移的值。
  回顾一下lab1中的part 3,我们引入了一个简单的页表,使得内核可以运行与0xf0100000的虚拟地址空间,尽管它所在的真实位置是物理地址0x00100000处,刚刚好在ROM BIOS之上。这个页表仅仅映射了4MB的内存空间。在我们这个JOS操作系统中,我们希望把这种映射扩展到物理内存的头256MB空间上,并且把这部分物理空间映射到从0xf0000000开始的虚拟空间中,以及一些其他的虚拟地址空间中。

  Exercise3:
  通过GDB,我们只能通过虚拟地址来查看内存所存放的内容,但是如果我们能够访问物理内存的话,肯定会更有帮助的。我们可以看一下QEMU中的一些常用指令,特别是xp指令,可以允许我们去访问物理内存地址。

  “QEMU中有一个内置的监控器(moniter),首先通过在运行着QEMU软件的terminal里面输入 ctrl-a c,可以让我们切换到这个监控器。” 这个是官方给出的做法,但是我是通过xshell远程连接,貌似快捷键冲突,没有响应的。后来发现在lab目录下面输入如下指令,一样可以打开moniter:

  qemu-system-i386 -hda obj/kern/kernel.img -monitor stdio -gdb tcp::26000 -D qemu.log

  打开monitor后,就可以输入如下比较常见的指令:

  xp/Nx paddr – 查看paddr物理地址处开始的,N个字的16进制的表示结果。

  info registers – 展示所有内部寄存器的状态。

  info mem – 展示所有已经被页表映射的虚拟地址空间,以及它们的访问优先级。

  info pg – 展示当前页表的结构。(我这版本的qemu没有该指令)

  在之后的实验中,你将会经常遇到一种情况,多个不同的虚拟地址被同时映射到相同的物理页上面。这时我们需要记录一下每一个物理页上存在着多少不同的虚拟地址来引用它,这个值存放在这个物理页的PageInfo结构体的pp_ref成员变量中。当这个值变为0时,这个物理页才可以被释放。通常来说,任意一个物理页p的pp_ref值等于它在所有的页表项中,被位于虚拟地址UTOP之下的虚拟页所映射的次数(UTOP之上的地址范围在启动的时候已经被映射完成了,之后不会被改动)。

  当我们使用page_alloc函数的时候需要注意。它所返回的页的引用计数值总是0,所以pp_ref应该被马上加一。

Page Table Management
  现在你应该可以着手开始编写管理页表的程序了:包括插入和删除线性地址到物理地址的映射关系,以及创建页表等操作。

  Exercise 4:
  完成kern/pmap.c中的下面几个子函数的编码
    pgdir_walk() boot_map_region() page_lookup() page_remove() page_insert()
  check_page()子函数将会被用来检查你所编写的这些程序是否正确。

  回答:

  首先完成pgdir_walk函数,函数原型 pgdir_walk(pde_t *pgdir, const void *va, int create),该函数的功能在注释中解释道:
  给定一个页目录表指针 pgdir ,该函数应该返回线性地址va所对应的页表项指针。
    所以在这里我们应该完成以下几个步骤:
    1. 通过页目录表求得这个虚拟地址所在的页表页对应的页目录中的页目录项地址 pg_dir_entry。(8)
    2. 判断这个页目录项对应的页表页是否已经在内存中。 (9)
    3. 如果在,计算这个页表页的基地址page_table,然后返回va所对应页表项的地址 &page_table[PTX(va)] (22-23)
    4. 如果不在则,且create为true则分配新的页,并且把这个页的信息添加到页目录项pg_dir_entry中。(13-18)
    5. 如果create为false,则返回NULL。(11-12)
    修改代码:

pte_t *pgdir_walk(pde_t *pgdir, const void *va, int create){        assert(pgdir != NULL);        pde_t *pg_dir_entry = NULL;        pte_t *pg_table = NULL;        struct PageInfo *new_page = NULL;        pg_dir_entry = &pgdir[PDX(va)];        if (!(*pg_dir_entry & PTE_P)) {                if (!create)                        return NULL;                else {                        new_page = page_alloc(ALLOC_ZERO);                        if (new_page == NULL)                                return NULL;                        new_page->pp_ref++;                        *pg_dir_entry = (page2pa(new_page) | PTE_P |PTE_W | PTE_U);                }        }        pg_table = KADDR(PTE_ADDR(*pg_dir_entry));        return &pg_table[PTX(va)];}

  接下来完成boot_map_region函数,函数原型 static void boot_map_region(pde_t *pgdir, uintptr_t va, size_t size, physaddr_t pa, int perm),这个函数的功能在注释中被这样解释:

  把虚拟地址空间范围[va, va+size)映射到物理空间[pa, pa+size)的映射关系加入到页表pgdir中。这个函数主要的目的是为了设置虚拟地址UTOP之上的地址范围,这一部分的地址映射是静态的,在操作系统的运行过程中不会改变,所以这个页的PageInfo结构体中的pp_ref域的值不会发生改变。

  这个函数要完成的步骤如下:
  1. 需要完成一个循环,在每一轮中,把一个虚拟页和物理页的映射关系存放到响应的页表项中。直到把size个字节的内存都分配完。

static voidboot_map_region(pde_t *pgdir, uintptr_t va, size_t size, physaddr_t pa, int perm){        uint32_t i;        pte_t *pt_table_entry = NULL;        uint32_t page_num = size / PGSIZE;        for (i = 0; i < page_num; i++) {                pg_table_entry = pgdir_walk(pgdir, (void *) va, 1);                assert(pg_page_entry != NULL);                *pg_table_entry = pa | perm | PTE_P;                va += PGSIZE;                pa += PGSIZE;        }}

  接下来再继续查看page_insert(),函数原型如下 page_insert(pde_t *pgdir, struct PageInfo *pp, void *va, int perm),功能上是完成:把一个物理内存中页pp与虚拟地址va建立映射关系。

  这个函数的主要步骤如下:
  1. 首先通过pgdir_walk函数求出虚拟地址va所对应的页表项。(5)
  2. 修改pp_ref的值。(10)
  3. 查看这个页表项,确定va是否已经被映射,如果被映射,则删除这个映射。(11-13)
  4. 把va和pp之间的映射关系加入到页表项中。(15-16)

intpage_insert(pde_t *pgdir, struct PageInfo *pp, void *va, int perm){        pte_t *entry = NULL;        entry = pgdir_walk(pgdir, va, 1);        if (entry == NULL)                return -E_NO_MEM;        pp->pp_ref++;        if(*entry & PTE_P) {                tlb_invalid(pgdir, va);                page_remove(pgdir, va);        }        *entry = (page2pa(pp) | perm | PTE_P);        pgdir[PDX(va)] = entry;        return 0;}

  这里要注意,pp->pp_ref++这条语句,一定要放在page_remove之前,这是为了处理一种特殊情况:pp已经映射到va上了。

  接下来继续完成page_lookup()函数,函数原型:struct PageInfo * page_lookup(pde_t *pgdir, void *va, pte_t **pte_store), 函数的功能为:

  返回虚拟地址va所映射的物理页的PageInfo结构体的指针,如果pte_store参数不为0,则把这个物理页的页表项地址存放在pte_store中。
  这个函数的功能就很容易实现了,我们只需要调用pgdir_walk函数获取这个va对应的页表项,然后判断这个页是否已经在内存中,如果在则返回这个页的PageInfo结构体指针。并且把这个页表项的内容存放到pte_store中。

struct PageInfo *page_lookup(pde_t *pgdir, void *va, pte_t **pte_store){        pte_t *entry = NULL;        struct PageInfo *ret = NULL;        entry = pgdir_walk(pgdir, va, 0);        if (entry == NULL)                return NULL;        if (!(*entry & PTE_P))                return NULL;        ret = pa2page(PTE_ADDR(*entry));        if (pte_store != NULL)                *pte_store = entry;        return ret;}

  最后一个就是page_remove函数,它的原型是:void page_remove(pde_t *pgdir, void *va),功能就是把虚拟地址va和物理页的映射关系删除。
  注释里面还提示了要注意的几个细节:
  1. pp_ref值要减一
  2. 如果pp_ref减为0,要把这个页回收
  3. 这个页对应的页表项应该被置0

voidpage_remove(pde_t *pgdir, void *va){        pte_t *entry = NULL;        struct PageInfo *page = page_lookup(pgdir, va, &entry);        if (page == NULL)                return;        page_decref();        tlb_invalidate(pgdir, va);        *entry = 0;}
0 0
原创粉丝点击