XV6学习 (4)Lab pgtbl

这一个实验主要是学习XV6的页表(分页机制),关于分页机制的相关内容已经写在XV6学习 (3)里面了。
代码放在Github上。

这一个就是要实现一个vmprint()函数来遍历页表并打印,可以仿照freewalk()函数来写。

void printwalk(pagetable_t pagetable, uint level) {
  char* prefix;
  if (level == 2) prefix = "..";
  else if (level == 1) prefix = ".. ..";
  else prefix = ".. .. ..";

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

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

在这里是通过pte & (PTE_R|PTE_W|PTE_X)来判断当前PTE是不是指向下一级页表。

A kernel page table per process (hard)

这一题是要为每个进程分配一个独立的内核页表,而不是使用全局的内核页表。这一题主要是为了下一题做准备。

因此,首先就是要建立一个函数来创建内核页表。这个函数内部只要仿照kvminit函数,给对应的页面创建映射就行了。

pagetable_t
proc_kpagetable() {
  pagetable_t kpagetable;
  kpagetable = uvmcreate();
  if(kpagetable == 0)
    return 0;

  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 pagetable ,uint64 va, uint64 pa, uint64 sz, int perm)
{
  if(mappages(pagetable, va, sz, pa, perm) != 0)
    panic("ukvmmap");
}

之后,将procinit函数中的内核栈的映射移动到allocproc函数中。在allocproc函数中先创建一个内核页表,之后将内核栈映射到对应位置上就可以了。

static struct proc*
allocproc(void)
{
  ...
  // Allocate a trapframe page.
  if((p->trapframe = (struct trapframe *)kalloc()) == 0){
    release(&p->lock);
    return 0;
  }

  // An empty user page table.
  p->pagetable = proc_pagetable(p);
  if(p->pagetable == 0){
    freeproc(p);
    release(&p->lock);
    return 0;
  }

  // create the kernel page table.
  p->kpagetable = proc_kpagetable(p);
  if(p->kpagetable == 0){
    freeproc(p);
    release(&p->lock);
    return 0;
  }

  // init the kernel stack.
  char *pa = kalloc();
  if(pa == 0)
    panic("kalloc");
  uint64 va = KSTACK((int) (p - proc));
  ukvmmap(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;
}

scheduler函数中在进程切换之后对内核页表也进行切换,记得用sfence_vma刷新TLB。

// switch the kernel pagetable.
w_satp(MAKE_SATP(p->kpagetable));
sfence_vma();

最后一步就是在freeproc的时候对内核页表和内核栈也进行释放。

static void
freeproc(struct proc *p)
{
  if(p->trapframe)
    kfree((void*)p->trapframe);
  p->trapframe = 0;

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

  if(p->pagetable)
    proc_freepagetable(p->pagetable, p->sz);
  if(p->kpagetable)
    proc_freekpagetable(p->kpagetable);
  ...
}

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

Simplify copyin/copyinstr (hard)

这一个就是利用上一步的进程内核页表,将进程的地址空间映射到内核页表中,来简化copy_in操作,使得copy_in不需要去查找进程的页表来进行地址转换。

之所以能进行这个映射就是因为进程的地址空间是从 0 开始增长的,而内核需要的地址空间是从PLIC开始增长的(CLINT仅在内核初始化的时候使用,之后就不需要了)。因此,进程的地址空间是可以从 0 增长到PLIC的,而这里就需要在growproc中对进程的地址空间进行限制,避免其超出PLIC

if (PGROUNDUP(sz + n) >= PLIC) return -1;

在XV6中,会涉及到进程页表改变的只有三个地方:fork exec sbrk,因此要在对进程页表改变后,将其同步到内核页表中。

// copy page table
void
ukvmcopy(pagetable_t pagetable, pagetable_t kpagetable, uint64 oldsz, uint64 newsz)
{
  pte_t *src, *dest;
  uint64 cur;

  if (newsz < oldsz)
    return;

  oldsz = PGROUNDUP(oldsz);
  for(cur = oldsz; cur < newsz; cur += PGSIZE){
    if ((src = walk(pagetable, cur, 0)) == 0)
      panic("ukvmcopy: pte not exist");
    if ((dest = walk(kpagetable, cur, 1)) == 0)
      panic("ukvmcopy: pte alloc failed");

    uint64 pa = PTE2PA(*src);
    *dest = PA2PTE(pa) | (PTE_FLAGS(*src) & (~PTE_U));
  }
}

页表的同步就通过上面的ukvmcopy函数来实现,在上述三个函数对页表进行改变后,就需要调用这个函数进行同步。

这里有一个问题就是在newsz < oldsz的时候,即释放内存的时候,没有对页表项进行删除,后面需要完善。

posted @ 2020-12-23 10:33  星見遥  阅读(2921)  评论(0编辑  收藏  举报