lab3 Page tables

111 阅读5分钟

Print a page table

该函数的作用是为了打印当前进程的页表,通过hint可以知道如何得知当前页表的PTE是否指向下一级页表, 写一个很简单的递归函数就行

void _vmprint(pagetable_t pagetable, int level) {
  for (int i = 0; i < 512; i++) {
    pte_t pte = pagetable[i];
	if (pte & PTE_V) {
      uint64 pa = PTE2PA(pte);
      for (int j = 0; j < level; j++) {
		if (j) printf(" ");
		printf("..");
	  }
	  printf("%d: pte %p pa %p\n", i, pte, pa);
	  if ((pte & (PTE_R | PTE_W | PTE_X)) == 0) {
	    _vmprint((pagetable_t)pa, level+1);
	  }
	}
  }
}

void vmprint(pagetable_t pagetable) {
  printf("page table %p\n", pagetable);
  _vmprint(pagetable, 1);
}

A kernel page table per process

这个实验的主要目的就是为每一个进程单独的分配一个内核页表,为下一个做准备

在struct proc中添加一个内核页表字段

// kernel/proc.h

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 kpagetable;      // kernel pagetable
  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)
};

实现一个修改版的kvminit创建核页表,并构建基础映射

//kernel/vm.c

pagetable_t ukvminit() {
  pagetable_t kpagetable = (pagetable_t) kalloc();

  memset(kpagetable, 0, PGSIZE);

  ukvmmap(kpagetable, UART0, UART0, PGSIZE, PTE_R | PTE_W);

  ukvmmap(kpagetable, VIRTIO0, VIRTIO0, PGSIZE, PTE_R | PTE_W);

  ukvmmap(kpagetable, CLINT, CLINT, 0x10000, PTE_R | PTE_W);

  ukvmmap(kpagetable, PLIC, PLIC, 0x400000, PTE_R | PTE_W);

  ukvmmap(kpagetable, KERNBASE, KERNBASE, (uint64)etext-KERNBASE, PTE_R | PTE_X);

  ukvmmap(kpagetable, (uint64)etext, (uint64)etext, PHYSTOP-(uint64)etext, PTE_R | PTE_W);

  ukvmmap(kpagetable, TRAMPOLINE, (uint64)trampoline, PGSIZE, PTE_R | PTE_X);
  
  return kpagetable;
}

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

分配自己的内核栈并建立映射

由于所有内核态的页表都共享一个内核页表,对于不同的内核进程,就需要创建不同的内核栈

xv6 在启动过程中,会在 procinit() 中为所有可能的 64 个进程位都预分配好内核栈 kstack,具体为在高地址空间里,每个进程使用一个页作为 kstack,并且两个不同 kstack 中间隔着一个无映射的 guard page 用于检测栈溢出错误。具体参考 xv6 book 的 Figure 3.3

image.png

根据hint在proc单独创建一个内核栈并建立映射

// kernel/proc.c

static struct proc *
allocproc(void) {
  //......
  if(p->pagetable == 0){
    freeproc(p);
    release(&p->lock);
    return 0;
  }

    char *pa = kalloc();
    if (pa == 0)
    panic("kalloc");
    uint64 va = KSTACK((int) (p - proc));
    kvmmap_plus(p->kpagetable, 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;
}

取消为每个进程但独创建的内核页表

// kernel/proc.c

void
procinit(void)
{
  struct proc *p;
  
  initlock(&pid_lock, "nextpid");
  for(p = proc; p < &proc[NPROC]; p++) {
      initlock(&p->lock, "proc");

      // 下面是注释的内容
      // 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));
      kvmmap(va, (uint64)pa, PGSIZE, PTE_R | PTE_W);
      p->kstack = va;*/
  }
  kvminithart();
}

修改scheduler()来加载进程的内核页表到核心的satp寄存器

这里有一个问题就是什么时候要切换satp寄存器呢,根据scheduler注释可知swth函数就直接切换进程了,所以要在这之前切换satp寄存器中的内容

// kernel/proc.c

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;

        // 切换到进程独立的内核页表
        w_satp(MAKE_SATP(p->kernelpgtbl));
        sfence_vma(); // 清除快表缓存
        
        // 调度,执行进程
        swtch(&c->context, &p->context);

        // 切换回全局内核页表
        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
  }
}

释放内核页表以及内核栈

注意这里将进程的状态设置为UNUSED一定要在清理玩所有数据之后,不然会出现walk错误,一定要注意,之前也是在这里出问题的

static void
freeproc(struct proc *p)
{
  if(p->trapframe)
    kfree((void*)p->trapframe);
  p->trapframe = 0;
  // delete user pagetable
  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;
    // 释放进程的内核栈
  if(p->kstack)
  uvmunmap(p->kpagetable, p->kstack, 1, 1);
  p->kstack = 0;
  
  if(p->kpagetable) {
  proc_freewalk(p->kpagetable);
  }

  p->kpagetable = 0;
  p->state = UNUSED;
}

void proc_freewalk(pagetable_t pagetable) {
  for (int i = 0; i < 512; i++) {
    pte_t pte = pagetable[i];
	if (pte & PTE_V) {
	  pagetable[i] = 0;
	  if ((pte & (PTE_R | PTE_W | PTE_X)) == 0) {
	    uint64 child = PTE2PA(pte);
		proc_freewalk((pagetable_t)child);
	  }
	}
  }
  kfree((void*)pagetable);
}

simplify copyin/copyinstr

在上一个实验中,已经使得每一个进程都拥有独立的内核态页表了,这个实验的目标是,在进程的内核态页表中维护一个用户态页表映射的副本,这样使得内核态也可以对用户态传进来的指针(逻辑地址)进行解引用。这样做相比原来 copyin 的实现的优势是,原来的 copyin 是通过软件模拟访问页表的过程获取物理地址的,而在内核页表内维护映射副本的话,可以利用 CPU 的硬件寻址功能进行寻址,效率更高并且可以受快表加速。

先写一个复制userpage的函数

void u2kvmcopy(pagetable_t upagetable, pagetable_t kpagetable, uint64 oldsz, uint64 newsz) {
  oldsz = PGROUNDUP(oldsz);
  for (uint64 i = oldsz; i < newsz; i += PGSIZE) {
    pte_t* pte_from = walk(upagetable, i, 0);
	pte_t* pte_to = walk(kpagetable, i, 1);
	if(pte_from == 0) panic("u2kvmcopy: src pte do not exist");
	if(pte_to == 0) panic("u2kvmcopy: dest pte walk fail");
	uint64 pa = PTE2PA(*pte_from);
  // 这里很重要一定要 &(~PTE_U)
	uint flag = (PTE_FLAGS(*pte_from)) & (~PTE_U);
	*pte_to = PA2PTE(pa) | flag;
  }
}

做内存映射

在fork中添加复制函数

int
fork(void)
{
  int i, pid;
  struct proc *np;
  struct proc *p = myproc();

  // Allocate process.
  if((np = allocproc()) == 0){
    return -1;
  }

  // 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 saved user registers.
  *(np->trapframe) = *(p->trapframe);

  // Cause fork to return 0 in the child.
  np->trapframe->a0 = 0;

  // increment reference counts on open file descriptors.
  for(i = 0; i < NOFILE; i++)
    if(p->ofile[i])
      np->ofile[i] = filedup(p->ofile[i]);
  np->cwd = idup(p->cwd);

 // 添加映射
  u2kvmcopy(np->pagetable, np->kpagetable, 0, np->sz);

  safestrcpy(np->name, p->name, sizeof(p->name));

  pid = np->pid;

  np->state = RUNNABLE;

  release(&np->lock);

  return pid;
}

在exec中添加映射以及检查边界是否越界

int
exec(char *path, char **argv)
{
   
  // ....
  // Load program into memory.
  for(i=0, off=elf.phoff; i<elf.phnum; i++, off+=sizeof(ph)){
    if(readi(ip, 0, (uint64)&ph, off, sizeof(ph)) != sizeof(ph))
      goto bad;
    if(ph.type != ELF_PROG_LOAD)
      continue;
    if(ph.memsz < ph.filesz)
      goto bad;
    if(ph.vaddr + ph.memsz < ph.vaddr)
      goto bad;
    uint64 sz1;
    if((sz1 = uvmalloc(pagetable, sz, ph.vaddr + ph.memsz)) == 0)
      goto bad;
    if(sz1 >= PLIC) { // 添加检测,防止程序大小超过 PLIC
      goto bad;
    }
    sz = sz1;
    if(ph.vaddr % PGSIZE != 0)
      goto bad;
    if(loadseg(pagetable, ph.vaddr, ip, ph.off, ph.filesz) < 0)
      goto bad;
  }
  iunlockput(ip);
  end_op();
  ip = 0;

  p = myproc();
  uint64 oldsz = p->sz;

  // Allocate two pages at the next page boundary.
  // Use the second as the user stack.
  sz = PGROUNDUP(sz);
  uint64 sz1;
  if((sz1 = uvmalloc(pagetable, sz, sz + 2*PGSIZE)) == 0)
    goto bad;
  sz = sz1;
  uvmclear(pagetable, sz-2*PGSIZE);
  sp = sz;
  stackbase = sp - PGSIZE;

  u2kvmcopy(pagetable, p->kpagetable, 0, sz);

\\...

在growproc中添加映射并检查是否越界

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

  sz = p->sz;
  if(n > 0){
    // 检查是否越界
	if(PGROUNDUP(sz + n) >= PLIC) return -1;
    if((sz = uvmalloc(p->pagetable, sz, sz + n)) == 0) {
      return -1;
    }
    // 添加映射
	u2kvmcopy(p->pagetable, p->kpagetable, sz - n, sz);
  } else if(n < 0){
    sz = uvmdealloc(p->pagetable, sz, sz + n);
  }
  p->sz = sz;
  return 0;
}

在userinit中添加映射

对于 init 进程,由于不像其他进程,init 不是 fork 得来的,所以需要在 userinit 中也添加同步映射的代码。

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;
  // 添加映射
  u2kvmcopy(p->pagetable, p->kpagetable, 0, p->sz);

  // 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);
}