当前位置:   article > 正文

【MIT 6.S081】Lab3: page tables_mit6s081 lab3

mit6s081 lab3


本Lab简单优化了系统的页表功能,使得程序在内核态时可以直接解析用户态的指针。
笔者用时约8h

Print a page table

第一部分是为系统添加一个打印给定页表的函数vmprint,该函数接收一个参数pagetable(根页表的物理地址),递归遍历整张页表,打印有效的表项。
参考freewalk函数(定义在kernel/vm.c:331),每次遍历512个表项,若表项有效,则打印相关信息(第几级、第几项、pte内容和pte内容对应的物理地址),且若为一二级页表则继续递归,直到第三级页表返回。参考代码如下:

void 
vmprint_helper(pagetable_t pgtbl, int level) 
{
  // there are 2^9 = 512 PTEs in a page table.
  for(int i = 0; i < 512; i++){
    pte_t pte = pgtbl[i];
    if(pte & PTE_V){
      for (int j = 0; j < level; j ++ ) {
        if (j) printf(" ");
        printf("..");
      }
      printf("%d: pte %p pa %p\n", i, pte, PTE2PA(pte));
      if ((pte & (PTE_R|PTE_W|PTE_X)) == 0) {
        // this PTE points to a lower-level page table.
        uint64 child = PTE2PA(pte);
        vmprint_helper((pagetable_t)child, level + 1);
      }
    }
  }
}

void
vmprint(pagetable_t pgtbl) 
{
  printf("page table %p\n", pgtbl);
  vmprint_helper(pgtbl, 1);
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27

A kernel page table per process

如标题,第二部分的内容是为每一个进程添加一个单独的内核页表副本,为下一节直接解引用用户态指针做铺垫。

首先需要在进程的结构体(定义在kernel/proc.h中)struct proc中添加一个字段维护内核页表副本,如下图所示
在这里插入图片描述

然后,由于我们需要在分配进程时需要为每一个进程初始化一个内核页表的副本,于是需要参考kvminit函数(定义在kernel/vm.c:66),编写一个初始化进程中内核页表副本的函数proc_kvminit,代码如下所示。该函数内容与kvminit函数基本一致。其中的uvmmapkvmmap函数(定义在kernel/vm.c:171)类似,映射给定的虚拟地址和物理地址范围,唯一不同点是前者修改的是传入的指定页表而不仅仅是全局的内核页表。

void 
uvmmap(pagetable_t pagetable, uint64 va, uint64 pa, uint64 sz, int perm) 
{
  if(mappages(pagetable, va, sz, pa, perm) != 0)
    panic("uvmmap");
}

/*
 * create a direct-map page table for the given process.
 */
pagetable_t
proc_kvminit() 
{
  pagetable_t pgtbl = (pagetable_t) kalloc();
  if (pgtbl == 0) return 0;
  memset(pgtbl, 0, PGSIZE);
  
  // uart registers
  uvmmap(pgtbl, UART0, UART0, PGSIZE, PTE_R | PTE_W);

  // virtio mmio disk interface
  uvmmap(pgtbl, VIRTIO0, VIRTIO0, PGSIZE, PTE_R | PTE_W);

  // CLINT
  uvmmap(pgtbl, CLINT, CLINT, 0x10000, PTE_R | PTE_W);

  // PLIC
  uvmmap(pgtbl, PLIC, PLIC, 0x400000, PTE_R | PTE_W);

  // map kernel text executable and read-only.
  uvmmap(pgtbl, KERNBASE, KERNBASE, (uint64)etext-KERNBASE, PTE_R | PTE_X);

  // map kernel data and the physical RAM we'll make use of.
  uvmmap(pgtbl, (uint64)etext, (uint64)etext, PHYSTOP-(uint64)etext, PTE_R | PTE_W);

  // map the trampoline for trap entry/exit to
  // the highest virtual address in the kernel.
  uvmmap(pgtbl, TRAMPOLINE, (uint64)trampoline, PGSIZE, PTE_R | PTE_X);

  return pgtbl;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41

接着在allocproc函数(定义在kernel/proc.c:95)中调用上述定义的proc_kvminit函数,实现在进程分配时初始化进程中的内核页表副本。同时,还需要将procinit中对进程内核栈对应的页表项初始化代码段移动到allocproc函数中,如下所示。这里需要注意的是,原始代码中对进程context字段的修改一定要放在最下面。(暂时不知道为啥,等知道了再补一下原因)

  ...

  // initialize the process kernel page table
  p->kernel_pagetable = proc_kvminit();
  if(p->kernel_pagetable == 0){
    freeproc(p);
    release(&p->lock);
    return 0;
  }

  // initialize the process kernel stack in kernel process kernel page table
  char *pa = kalloc();
  if(pa == 0)
    panic("kalloc");
  uint64 va = KSTACK((int) (p - proc));
  uvmmap(p->kernel_pagetable, va, (uint64)pa, PGSIZE, PTE_R | PTE_W);
  p->kstack = va;

  // Set up new context to start executing at forkret,
  // which returns to user space.
  memset(&p->context, 0, sizeof(p->context));
  p->context.ra = (uint64)forkret;
  p->context.sp = p->kstack + PGSIZE;


  return p;
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26

接下来在scheduler函数(定义在kernel/proc.c:489)中,当调度进程执行时,将进程对应的内核页表加载到satp寄存器中,且调用sfence_vma进行刷新。在进程执行完,调用将页表切换回全局的内核页表,代码段如下所示。

    // load process's kernel page table and flush the TLB
    w_satp(MAKE_SATP(p->kernel_pagetable));
    sfence_vma();

    swtch(&c->context, &p->context);

    // load kernel page table when process done
    kvminithart();
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

最后,还需要在freeproc函数(定义在kernel/proc.c:155)中释放进程所维护的内核页表副本。需要将进程中内核页表维护的内核栈物理空间释放掉,调用uvmunmap函数(定义在kernel/vm.c:230)即可。同时还需要将维护的内核页表副本销毁掉,由于freewalk函数只销毁第一级和第二级页表表项,需要自己写一个类似的函数来销毁第三级页表的表项,如下所示。

// Recursively free process's kernel page-table pages.
void 
proc_freewalk(pagetable_t pagetable)
{
  // there are 2^9 = 512 PTEs in a page table.
  for(int i = 0; i < 512; i++){
    pte_t pte = pagetable[i];
    if((pte & PTE_V) && (pte & (PTE_R|PTE_W|PTE_X)) == 0){
      // this PTE points to a lower-level page table.
      uint64 child = PTE2PA(pte);
      proc_freewalk((pagetable_t)child);
      pagetable[i] = 0;
    } else if(pte & PTE_V){
      pagetable[i] = 0;
    }
  }
  kfree((void*)pagetable);
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18

freeproc函数中的相关代码段如下

  // free process's kernel page table
  uvmunmap(p->kernel_pagetable, p->kstack, 1, 1);
  p->kstack = 0;
  proc_freewalk(p->kernel_pagetable);
  p->kernel_pagetable = 0;

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

Simplify copyin/copyinstr

这一部分需要实现的是将每一个进程的用户空间映射添加到进程维护的内核页表副本中(上一节创建的),由于用户空间的虚拟地址从0开始,且内核的虚拟地址从较高的地址开始(文档里说是PLIC,但是xv6book里面的图3.3是从CLINT开始的,暂时不知道为啥),所以给用户空间的映射留下了一些虚拟空间进行映射(0~PLIC-1)。
我们需要在fork函数、exec函数、growproc函数与userinit函数中,为进程维护的内核页表添加上用户空间的映射,因为这些函数都更改了用户映射。

首先,我仿照uvmcopy函数(定义在kernel/vm.c:384),定义了一个函数uvm2ukvm,它接收两个页表,一个是用户进程页表,一个是用户进程中维护的内核页表,并接收需要映射的起始虚拟地址和末尾虚拟地址,将这个范围内的用户空间虚拟地址复制到进程维护的内核页表中。注意需要将PTE_U标志位置为0,否则内核无法访问。

void uvm2ukvm(pagetable_t upgtbl, pagetable_t ukpgtbl, uint64 st, uint64 ed)
{
  pte_t *pte_u, *pte_uk;
  uint64 pa, i;
  uint flags;

  for (i = st; i < ed; i += PGSIZE) {
    if((pte_u = walk(upgtbl, i, 0)) == 0)
      panic("uvm2ukvm: pte_u should exist");
    if((*pte_u & PTE_V) == 0)
      panic("uvm2ukvm: page not present");
    pa = PTE2PA(*pte_u);
    flags = PTE_FLAGS(*pte_u);
    flags &= (~PTE_U);

    if((pte_uk = walk(ukpgtbl, i, 1)) == 0)
      panic("uvm2ukvm: pte_uk should exist");
    *pte_uk = PA2PTE(pa) | flags;
  }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20

fork函数中(定义在kernel/proc.c:289),调用以上函数,添加一行代码即可。

  ...
  // Copy user memory from parent to child.
  if(uvmcopy(p->pagetable, np->pagetable, p->sz) < 0){
    freeproc(np);
    release(&np->lock);
    return -1;
  }
  np->sz = p->sz;

  uvm2ukvm(np->pagetable, np->kernel_pagetable, 0, np->sz);
  ...
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11

exec函数中(定义在kernel/exec.c:13),也是一样的添加上一行代码即可。

  ...
  // Commit to the user image.
  oldpagetable = p->pagetable;
  p->pagetable = pagetable;
  p->sz = sz;
  uvm2ukvm(p->pagetable, p->kernel_pagetable, 0, sz);
  p->trapframe->epc = elf.entry;  // initial program counter = main
  p->trapframe->sp = sp; // initial stack pointer
  proc_freepagetable(oldpagetable, oldsz);
  ...
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10

growproc函数中,当申请增长内存时,需要判断增长后的虚拟地址上界是否超过PLIC的起始地址,如果超过则返回-1,否则也是调用上述函数将增长的地址范围复制一份到进程维护的内核页表中即可。

  ...
    if (PGROUNDUP(sz + n) > PLIC) {
      return -1;
    }
    if((sz = uvmalloc(p->pagetable, sz, sz + n)) == 0) {
      return -1;
    }
    uvm2ukvm(p->pagetable, p->kernel_pagetable, sz - n, sz);
  ...
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

userinit函数中第一次初始化进程页表时,也要进行复制。

  ...
  // allocate one user page and copy init's instructions
  // and data into it.
  uvminit(p->pagetable, initcode, sizeof(initcode));
  uvm2ukvm(p->pagetable, p->kernel_pagetable, 0, PGSIZE);
  ...
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

最后,把copyin函数和copyinstr函数体中的内容改成调用copyin_newcopyinstr_new函数即可。

声明:本文内容由网友自发贡献,转载请注明出处:【wpsshop博客】
推荐阅读
相关标签
  

闽ICP备14008679号