MIT 6.s081 Lab3 Page tables

2,686 阅读6分钟

该实验只需要在kernel文件夹下进行修改,主要是proc.cvm.cdefs.h这几个文件,需要先阅读参考资料,并配合其中讲解代码的几节理解相关代码。
总体来说,跟着实验的提示可以按部就班的完成,只是这个实验调试起来有点麻烦。

Print a page table

这个实验很简单,根据提示仿照vm.c里的freewalk()函数来编写即可。但需要注意,在第三级页表不需要判断(pte2 & (PTE_R|PTE_W|PTE_X)) == 0
需要注意的是PTE2PA()返回值一定是uint64,不可以用pagetable_t

代码

void vmprint(pagetable_t p)
{
  printf("page table %p\n", p);
  for(int i = 0; i < 512; i++){
    pte_t pte = p[i];
    if((pte & PTE_V) && (pte & (PTE_R|PTE_W|PTE_X)) == 0){
      uint64 pa = PTE2PA(pte);
      printf("..%d: pte %p pa %p\n", i, pte, pa);
      pagetable_t p1 = (pagetable_t)pa;
      for(int j = 0; j < 512; j++){
        pte_t pte1 = p1[j];
        if((pte1 & PTE_V) && (pte1 & (PTE_R|PTE_W|PTE_X)) == 0){
          uint64 pa1 = PTE2PA(pte1);
          printf(".. ..%d: pte %p pa %p\n", j, pte1, pa1);
          pagetable_t p2 = (pagetable_t)pa1;
          for(int k = 0; k < 512; k++){
            pte_t pte2 = p2[k];
            if(pte2 & PTE_V){
              uint64 pa2 = PTE2PA(pte2);
              printf(".. .. ..%d: pte %p pa %p\n", k, pte2, pa2);
            }
          }
        }  
      }
    }
  }
}

运行结果

image.png

A kernel page table per process

实验要求每个进程需要维护一个内核页表,所以先根据提示先在proc.hstruct proc增加一个变量pagetable_t

// Per-process state
struct proc {
  struct spinlock lock;

  // p->lock must be held when using these:
  enum procstate state;        // Process state
  struct proc *parent;         // Parent process
  void *chan;                  // If non-zero, sleeping on chan
  int killed;                  // If non-zero, have been killed
  int xstate;                  // Exit status to be returned to parent's wait
  int pid;                     // Process ID

  // these are private to the process, so p->lock need not be held.
  uint64 kstack;               // Virtual address of kernel stack
  uint64 sz;                   // Size of process memory (bytes)
  pagetable_t pagetable;       // User page table
  pagetable_t kernel_pagetable;// Kernel page table
  struct trapframe *trapframe; // data page for trampoline.S
  struct context context;      // swtch() here to run process
  struct file *ofile[NOFILE];  // Open files
  struct inode *cwd;           // Current directory
  char name[16];               // Process name (debugging)
};

然后,我们需要实现每个进程的内核页表的初始化,进程状态为RUNNABLE时切换为该进程的内核页表,以及进程结束后相应的清除。要实现这些要求,需要修改vm.cproc.c

修改vm.c

首先,根据提示,我们要实现一个类似kvminit()的函数,为每个进程创建一个内核页表。不同于kvminit(),因为需要给myproc()->kernel_pagetable赋值,所以这个函数需要返回pagetable_t
注意,需要在defs.h里添加prockvminit()的声明,以便于调用。

pagetable_t
prockvminit()
{
  pagetable_t p = (pagetable_t) kalloc();
  
  if(p == 0) return 0;
  
  memset(p, 0, PGSIZE);

  // uart registers
  prockvmmap(p, UART0, UART0, PGSIZE, PTE_R | PTE_W);

  // virtio mmio disk interface
  prockvmmap(p, VIRTIO0, VIRTIO0, PGSIZE, PTE_R | PTE_W);

  // CLINT
  prockvmmap(p, CLINT, CLINT, 0x10000, PTE_R | PTE_W);

  // PLIC
  prockvmmap(p, PLIC, PLIC, 0x400000, PTE_R | PTE_W);

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

  // map kernel data and the physical RAM we'll make use of.
  prockvmmap(p, (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.
  prockvmmap(p, TRAMPOLINE, (uint64)trampoline, PGSIZE, PTE_R | PTE_X);
  
  return p;
}

kvminit()里需要调用kvmmap(),而kvmmap()默认使用kernel_pagetable,所以我们还需要实现一个prockvmmap()函数,把进程的kernel_pagetable传进去后,再做相关映射。

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

此外,当切换进程时,需要将进程的kernel_pagetable放入satp寄存器,应当仿照kvminithart()编写prockvminithart()

void 
prockvminithart(pagetable_t pagetable)
{
  w_satp(MAKE_SATP(pagetable));
  sfence_vma();
}

修改proc.c

首先,需要调用prockvminit()为进程创建内核页表,根据提示应修改allocproc(),在创建好进程的用户页表后调用。
内核页表需要有进程内核栈的映射,将procinit()里的相关代码复制过来修改,接在内核页表创建之后即可。
需要注意的是,procinit()kSTACK的物理地址是刚分配的,这里不需要进行分配,直接调用kvmpa()函数计算。

// An empty user page table.
  p->pagetable = proc_pagetable(p);
  if(p->pagetable == 0){
    freeproc(p);
    release(&p->lock);
    return 0;
  }
  
  // An proc kernel page table.
  p->kernel_pagetable = prockvminit();
  if(p->kernel_pagetable == 0){
    freeproc(p);
    release(&p->lock);
    return 0;
  }
  
  // Map kstack to kernel page table
  uint64 va = KSTACK((int) (p - proc));
  uint64 pa = kvmpa(va);
  prockvmmap(p->kernel_pagetable, va, pa, PGSIZE, PTE_R | PTE_W);

接下来,需要在scheduler()里把RUNNABLE的进程的内核页表放入satp寄存器,这里调用之前写好的prockvminithart()

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;
        prockvminithart(p->kernel_pagetable);
        swtch(&c->context, &p->context);
	
        // Process is done running for now.
        // It should have changed its p->state before coming back.
        kvminithart();
        c->proc = 0;

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

最后需要修改freeproc(),因为不要求清除物理地址的页,所以参照vm.c下的freewalk()去掉panic,使程序可以正常运行。

// free a proc kernel page table
// no memory page
void
free_proc_kernel_pagetable(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);
      free_proc_kernel_pagetable((pagetable_t)child);
      pagetable[i] = 0;
    }
  }
  kfree((void*)pagetable);
}

Simplify copyin/copyinstr

根据题目要求,我们需要把vm.c里的copyin()copyinstr()函数改为调用vmprint.c里的copyin_new()copyinstr_new(),然后再修改forkexecsbrk几个功能让程序能正常运行。
copyin()copyinstr()直接改成调用copyin_new()copyinstr_new()就行,但要注意defs.h里没有声明copyin_new()copyinstr_new(),要自行添加。

int
copyin(pagetable_t pagetable, char *dst, uint64 srcva, uint64 len)
{
  return copyin_new(pagetable, dst, srcva, len);
}

int
copyinstr(pagetable_t pagetable, char *dst, uint64 srcva, uint64 max)
{
  return copyinstr_new(pagetable, dst, srcva, max);
}

forkexecsbrk修改的地方则是当进程的用户页表有修改时,进程的内核页表映射需要相对应的修改。因此,要先实现用户页表->内核页表的复制。

用户页表->内核页表

vm.c中的uvmcopy()为子进程创建了新的物理页,再将父进程用户页表映射的物理页复制过去,最后建立新的物理页与子进程用户页表之间的映射。
参考uvmcopy(),进程用户页表->内核页表不需要创建新的物理页,只需要获取每一个页表项的flags,然后建立物理页与内核页表的映射。 需要注意的是,uvmcopy()是对整个页的复制,所以只需要一个参数传入页表中有效页表项的大小,而考虑到实现sbrk功能时,只需要增加新物理页的映射,所以做了相应的修改。 根据提示,内核页表的flagsPTE_U不可为真,否则会无法在内核模式中读取,需要相应处理。 此外,在建立物理页和页表项映射时,使用mappages()会弹出panic:"remap",所以使用在mappages()基础上,删去了panic条件的kmappages()函数。

int
kvmcopy(pagetable_t old, pagetable_t new, uint64 oldsz, uint64 newsz)
{
  pte_t *pte;
  uint64 pa, i;
  uint flags;
  
  oldsz = PGROUNDUP(oldsz);
  if(newsz <= oldsz) return 0;
  
  for(i = oldsz; i < newsz; i += PGSIZE){
    if((pte = walk(old, i, 0)) == 0)
      panic("kvmcopy: pte should exist");
    if((*pte & PTE_V) == 0)
      panic("kvmcopy: page not present");
    pa = PTE2PA(*pte);
    flags = PTE_FLAGS(*pte);
    flags &= (~PTE_U);
    if(kmappages(new, i, PGSIZE, pa, flags) != 0){
      goto err;
    }
  }
  return 0;

 err:
  uvmunmap(new, 0, i / PGSIZE, 0);
  return -1;
}
int
kmappages(pagetable_t pagetable, uint64 va, uint64 size, uint64 pa, int perm)
{
  uint64 a, last;
  pte_t *pte;

  a = PGROUNDDOWN(va);
  last = PGROUNDDOWN(va + size - 1);
  for(;;){
    if((pte = walk(pagetable, a, 1)) == 0)
      return -1;
    *pte = PA2PTE(pa) | perm | PTE_V;
    if(a == last)
      break;
    a += PGSIZE;
    pa += PGSIZE;
  }
  return 0;
}

fork

fork里的功能很好实现,在子进程复制了父进程的物理页,建立了自己的用户页表后,调用kvmcopy()即可,详细代码可参考fork()里对uvmcopy()的调用。

int
fork(void)
{
...
  // 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;

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

exec

同样,exec()里找到p->pagetable被修改的地方,在后面调用kvmcopy()即可。

int
exec(char *path, char **argv)
{
  ...
  // Commit to the user image.
  oldpagetable = p->pagetable;
  p->pagetable = pagetable;
  p->sz = sz;
  // Copy to the proc kernel pagetable
  if(kvmcopy(pagetable, p->kernel_pagetable, 0, sz) == -1)
    goto bad;
  
  p->trapframe->epc = elf.entry;  // initial program counter = main
  p->trapframe->sp = sp; // initial stack pointer
  proc_freepagetable(oldpagetable, oldsz);
  
  
  if(p->pid == 1) vmprint(p->pagetable);
  
  return argc; // this ends up in a0, the first argument to main(argc, argv)

 bad:
  if(pagetable)
    proc_freepagetable(pagetable, sz);
  if(ip){
    iunlockput(ip);
    end_op();
  }
  return -1;
}

sbrk

sbrk稍微复制一点,在sysproc.c中可以知道sys_sbrk()调用的是growproc()函数,在defs.h下查找到其是在proc.c下。
因为sbrk的功能是动态分配内存,不够时增加,空闲了则减少,所以需要分情况讨论。
n>0,也就是增加内存时,所以需要防止其超过PLIC限制,再调用kvmcopy()建立新分配内存与内核页表间的映射。
n<0,减少内存时,我本来是考虑直接调用uvmdealloc(),这样会比较方便,但是这样运行usertests会出错。
经过对uvmdealloc()的仔细研究,我才明白,这个函数调用uvmunmap()时,do_free参数一直是1,也就是会删除物理页,而这步是用户页表已经做过的事情,不要内核页表这里再做一次,所以会出错。 最后,我只能仿照uvmdealloc()里的写法,直接调用uvmunmap(),完成了实验。

int
growproc(int n)
{
  uint sz;
  struct proc *p = myproc();

  sz = p->sz;
  if(n > 0){
    if(sz + n >= PLIC || (sz = uvmalloc(p->pagetable, sz, sz + n)) == 0) {
      return -1;
    }
    
    if(kvmcopy(p->pagetable, p->kernel_pagetable, p->sz, sz) == -1)
      return -1;
    
  } else if(n < 0){
    sz = uvmdealloc(p->pagetable, sz, sz + n);
    uvmunmap(p->kernel_pagetable, PGROUNDUP(sz), (PGROUNDUP(p->sz) - PGROUNDUP(sz)) / PGSIZE,0);
  }
  p->sz = sz;
  return 0;
}

userinit()

最后,根据提示,userinit()刚创建的用户页表,也需要同步内核页表。

// Set up first user process.
void
userinit(void)
{
  struct proc *p;

  p = allocproc();
  initproc = p;
  
  // allocate one user page and copy init's instructions
  // and data into it.
  uvminit(p->pagetable, initcode, sizeof(initcode));
  p->sz = PGSIZE;
  
  // user page to kernel page
  kvmcopy(p->pagetable, p->kernel_pagetable, 0, PGSIZE);

  // prepare for the very first "return" from kernel to user.
  p->trapframe->epc = 0;      // user program counter
  p->trapframe->sp = PGSIZE;  // user stack pointer

  safestrcpy(p->name, "initcode", sizeof(p->name));
  p->cwd = namei("/");

  p->state = RUNNABLE;

  release(&p->lock);
}

Make grade

image.png