Lab 3 page tables

Print a page table

该实验的任务是编写一个vmprint()函数来打印列表,参数和一些必要的添加代码在题目中已经给出。

关键是实现vmprint()函数,在此,hint告诉我们可以参考freewalk()函数。观察freewalk,我们可以看到对页表进行了一个循环,在循环中使用递归来调用函数。对于vmprint要实现的功能,我们可以看到确实是使用递归,于是在freewalk的基础上进行修改。

打印第一行显示vmprint的参数,在此,只有一次,不在循环中出现,尝试引入一个新的参数后依然不能实现该功能,于是引入一个新的函数vmprint_t()用于执行除第一步之外的打印,也就是循环打印。增加一个新的参数level,判断目前在第几级,因为页表只有三级,所以level必须小于等于3时,才执行递归。显然在不同级的打印中,“..”的出现次数不一样,于是判断在第几层,进行打印。

  1. void
  2. vmprint_t(pagetable_t pagetable, int level)
  3. {
  4. for(int i = 0; i < 512; i++){
  5. pte_t pte = pagetable[i];
  6. if((pte & PTE_V)){
  7. if(level == 1) printf(".. ");
  8. if(level == 2) printf(".. .. ");
  9. if(level == 3) printf(".. .. .. ");
  10. uint64 child = PTE2PA(pte);
  11. printf("%d: pte %p pa %p\n", i, pte, PTE2PA(pte));
  12. if(level <= 3) vmprint_t((pagetable_t)child, level + 1);
  13. }
  14. }
  15. }
  16. void
  17. vmprint(pagetable_t pagetable)
  18. {
  19. printf("page table %p\n", pagetable);
  20. vmprint_t(pagetable, 1);
  21. }

A kernel page table per process

该实验的难度相当高,在此,根据题目给出的hints一步步将问题拆分并解决问题。

首先看看描述,本题是修改内核让每一个进程在内核执行时使用自己的内核页表副本。在源代码中,我们是只有一个内核页表kernel_pagetable的,每个进程都使用这个全局内核页表。

hint1:在struct proc中为进程的内核页表添加字段

于是,在struct proc中添加代码:

pagetable_t kpagetable;      // Kernel page table

hint2:为新进程生成一个内核页表。这里给出的提示是参考kvminit,创造一个新的页表而不是修改kernel_pagetable。于是观察函数kvminit,这是一个无返回值的函数,要创造一个新的页表,必须返回一个新建的页表。此外,kvminit函数使用了kvmmap函数,而在kvmmap函数中,是直接使用全局内核页表kernel_pagetable的,但是对于创造一个新的页表的函数,行不通,所以修改kvmmap函数,引入一个新的参数,类型为页表。

其实在这里有考虑过直接修改kvminit和kvmmap,但是想想还是保留与操作全局内核页表相关的函数为好,在带代码里,全局内核页表仍保留而并非删除。

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

pagetable_t
kvmcreate()
{
  pagetable_t kpagetable = (pagetable_t) kalloc();
  memset(kpagetable, 0, PGSIZE);

  kkvmmap(kpagetable, UART0, UART0, PGSIZE, PTE_R | PTE_W);
  kkvmmap(kpagetable, VIRTIO0, VIRTIO0, PGSIZE, PTE_R | PTE_W);
  kkvmmap(kpagetable, CLINT, CLINT, 0x10000, PTE_R | PTE_W);
  kkvmmap(kpagetable, PLIC, PLIC, 0x400000, PTE_R | PTE_W);
  kkvmmap(kpagetable, KERNBASE, KERNBASE, (uint64)etext-KERNBASE, PTE_R | PTE_X);
  kkvmmap(kpagetable, (uint64)etext, (uint64)etext, PHYSTOP-(uint64)etext, PTE_R | PTE_W);
  kkvmmap(kpagetable, TRAMPOLINE, (uint64)trampoline, PGSIZE, PTE_R | PTE_X);

  return kpagetable;
}

为了保证函数能顺利运行,要在defs.h中声明。

还有,我们需要在allocproc中调用这个函数,在此模仿用户页表的创建。

// An empty kernel page table.
p->kpagetable = kvmcreate();
if(p->kpagetable == 0){
  freeproc(p);
  release(&p->lock);
  return 0;
}

hint3:确保每一个进程的内核页表都关于该进程的内核栈有一个映射。且在未修改的XV6中,所有的内核栈都在procinit中设置。而在此需要迁移到allocproc中。

只需之间将代码迁移过去即可。

// Allocate a page for the process's kernel stack.
// Map it high in memory, followed by an invalid
// guard page.
char *pa = kalloc();
if(pa == 0)
    panic("kalloc");
uint64 va = KSTACK((int) (p - proc));
kkvmmap(p->kpagetable, va, (uint64)pa, PGSIZE, PTE_R | PTE_W);
p->kstack = va;

这个操作的含义是不在procinit中为每个进程分配内核栈,而是在allocproc创建页表的过程中分配内核栈。

hint4:修改scheduler函数来加载进程的内核页表到satp寄存器,这里参考kvminithart。

直接参考kvminithart的内部实现,将其中w_satp函数的参数修改为进程对应的内核页表即可。接着进行swtch的操作,如果没有进程运行,那么调用kvminithart函数回到全局的内核页表。

void
scheduler(void)
{
  struct proc *p;
  struct cpu *c = mycpu();

  c->proc = 0;
  for(;;){
    // Avoid deadlock by ensuring that devices can interrupt.
    intr_on();

    int found = 0;
    for(p = proc; p < &proc[NPROC]; p++) {
      acquire(&p->lock);
      if(p->state == RUNNABLE) {
        // Switch to chosen process.  It is the process's job
        // to release its lock and then reacquire it
        // before jumping back to us.
        p->state = RUNNING;
        c->proc = p;

        // change satp
        w_satp(MAKE_SATP(p->kpagetable));
        sfence_vma();

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

        // No process, change back kernel_pagetable
        kvminithart();

        // Process is done running for now.
        // It should have changed its p->state before coming back.
        c->proc = 0;

        found = 1;
      }
      release(&p->lock);
    }
#if !defined (LAB_FS)
    if(found == 0) {
      intr_on();
      asm volatile("wfi");
    }
#else
    ;
#endif
  }
}

hint5:在freeproc中释放一个进程的内核页表。

观察freeproc函数中的这一段代码:

if(p->pagetable)
  proc_freepagetable(p->pagetable, p->sz);

可以看到这里是释放进程页表的代码。

根据提示我们知道需要一种不必释放叶子物理內存页面的方法来释放页表。我们可以看到在freewalk函数中实现了对页表页面的释放,经过修改,不释放叶子物理内存页面,就可以满足要求。

// Recursively free page-table pages.
// No leaf mappings must already have been removed.
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;
    }
  }
  kfree((void*)pagetable);
}

这里还需要注意一个问题,需要释放内核栈,否则会出现报错:“panic: kvmmap”。

为什么需要释放内核栈?页表内存储的内核栈地址本身就是虚拟地址,要对其指向的物理地址进行释放。

if(p->kstack){
    pte_t *pte = walk(p->kpagetable, p->kstack, 0);
    if(pte == 0) panic("freeproc: walk");
    kfree((void*)PTE2PA(*pte));
}
p->kstack = 0;

完整的freeproc代码如下:

static void
freeproc(struct proc *p)
{
  if(p->trapframe)
    kfree((void*)p->trapframe);
  p->trapframe = 0;
  if(p->kstack){
    pte_t *pte = walk(p->kpagetable, p->kstack, 0);
    if(pte == 0) panic("freeproc: walk");
    kfree((void*)PTE2PA(*pte));
  }
  p->kstack = 0;
  if(p->kpagetable)
    proc_freewalk(p->kpagetable);
  p->kpagetable = 0;
  if(p->pagetable)
    proc_freepagetable(p->pagetable, p->sz);
  p->pagetable = 0;
  p->sz = 0;
  p->pid = 0;
  p->parent = 0;
  p->name[0] = 0;
  p->chan = 0;
  p->killed = 0;
  p->xstate = 0;
  p->state = UNUSED;
}

还要注意一个问题,此时执行make qemu会出现报错:“panic: kvmpa”。需要修改kvmpa中的

pte = walk(kernel_pagetable, va, 0);

改为:

pte = walk(myproc()->kpagetable, va, 0);

因为在这里我们需要获取的不是全局内核页表,而是每个进程对应的内核页表。

Simplify copyin/copyinstr

这个实验难度挺大,在这里写一下大概思路。

实验要求是将每个进程的用户页表复制到进程内核页表,使得copyin可以直接解引用用户指针。于是需要写一个复制的函数。

// copy the user page to kernel page
void
u2kvmcopy(pagetable_t upagetable, pagetable_t kpagetable, uint64 oldsz, uint64 newsz)
{
  pte_t *pte_to, *pte_from;
  uint64 pa;
  uint flag;

  if(newsz < oldsz)
  {
    return;
  }
  oldsz = PGROUNDUP(oldsz);
  for(uint64 va=oldsz; va<newsz; va+=PGSIZE)
  {
    pte_from = walk(upagetable, va, 0);
    pte_to = walk(kpagetable, va, 1);
    if(pte_from == 0)
      panic("u2kvmcopy: src pte do not exist");
    if(pte_to == 0)
      panic("u2kvmcopy: dst pte walk fail");
    pa = PTE2PA(*pte_from);
    flag = PTE_FLAGS(*pte_from) & (~PTE_U);
    *pte_to = flag | PA2PTE(pa);
  }
}

紧接着,按照提示,在更改用户映射的每一处,都要调用函数来进行修改,包括fork(),exec()sbrk()中调用的growproc(),此外在userinit中也要复制第一个进程的用户页表,这里直接调用u2kvmcopy()即可。

另外,要防止虚拟地址超过PLIC,对sz做判断,大于便返回错误。

根据提示,要将copyin的内容替换为copyin_new,将copyinstr替换成copyinstr_new,这里也很简单,将原来的内容注释掉,return用于替换的那两个函数。