MIT-6.S081-2020实验(xv6-riscv64)五:lazy

实验文档

概述

这次实验主要实现Lazy allocation的功能,即进程在动态分配内存的时候先不分配,等到要用到发生缺页中断的时候再实际分配,核心是实现缺页中断的处理。xv6的文档介绍了三种缺页中断的应用,第一为Copy on write,即fork的时候先不复制内存,等到要用到发生缺页中断的时候再实际分配;第二为硬盘虚拟内存,就是当内存不够大的时候将一部分硬盘区域当作内存交换区,虚拟地址只映射到一个无效位置,当访问该虚拟地址发生缺页中断时再把一个页的内容保存进磁盘,然后从磁盘中加载当前这个虚拟地址指向的实际内容;第三就是本实验的内容。

内容

Eliminate allocation from sbrk()

这个任务非常简单,没啥好说的:

uint64
sys_sbrk(void)
{
  int addr;
  int n;

  if(argint(0, &n) < 0)
    return -1;
  struct proc *p = myproc();
  addr = p->sz;
  if (n < 0) p->sz = uvmdealloc(p->pagetable, p->sz, p->sz + n);
  else p->sz += n;
  // if(growproc(n) < 0)
  //   return -1;
  return addr;
}

对n小于0情况的处理是第三个任务的内容,这里可以忽略。

Lazy allocation

这个任务要求实现对缺页中断的处理,因为在sbrk的时候仅仅指扩大了进程的虚拟地址区域,所以在访问这些虚拟地址时会发生缺页中断,这里就需要在发生缺页中断的时候分配物理内存然后映射,中断处理函数usertrap()对缺页中断进行处理:

	......
    syscall();
  } else if((which_dev = devintr()) != 0){
    // ok
  } else {
      if (r_scause() == 13 || r_scause() == 15) {
          uint64 va = r_stval(); if (handle_page(va, p) == -1) p->killed = 1;
      } else {
          printf("usertrap(): unexpected scause %p pid=%d
", r_scause(), p->pid);
          printf("            sepc=%p stval=%p
", r_sepc(), r_stval());
          p->killed = 1;
      }
  }
  if(p->killed)
    exit(-1);

这里把缺页中断的实际处理过程抽象成了一个函数,实际上仅从任务2考虑是没有必要的,但是任务3中还需要对copyin、copyout这些函数中发生缺页的情况进行处理,所以抽象成一个函数方便各处调用。

handle_page函数我写在proc.c里,因为这里已经包含了所需要的头文件:

int handle_page(uint64 va, struct proc *p) {
    uint64 base =  PGROUNDDOWN(va);
    if (va >= p->sz || va < p->trapframe->sp) return -1;
    char *mem = kalloc();
    if (mem == 0) return -1;
    memset(mem, 0, PGSIZE);
    if(mappages(p->pagetable, base, PGSIZE, (uint64)mem, PTE_W|PTE_X|PTE_R|PTE_U) != 0) {
        kfree(mem); return -1;
    }
    return 0;
}

这些return -1的情况也是任务3的内容,任务2可以忽略,主要都是借鉴函数uvmalloc。然后修改一下uvmunmap(),即把一些因为缺页导致的panic跳掉了,因为这些页从来就没分配过,也就不用释放:

  for(a = va; a < va + npages*PGSIZE; a += PGSIZE){
    if((pte = walk(pagetable, a, 0)) == 0) continue;
      // panic("uvmunmap: walk");
    if((*pte & PTE_V) == 0) continue;
      // panic("uvmunmap: not mapped");
    if(PTE_FLAGS(*pte) == PTE_V)
      panic("uvmunmap: not a leaf");
    ......

Lazytests and Usertests

这个任务主要是把上个任务遗留的一些不合法情况进行处理。

第一是sbrk的参数为负数的问题,根据growproc函数的内容,对参数为负数的情况就是释放参数绝对值大小的内存,仿造growproc()就行了,见上面的代码。uvmdealloc本身不用修改,因为内部就是调用uvmunmap的。

第二是缺页中断中当虚拟地址不合法时应该直接返回并杀掉进程,不合法包含两种情况,一是虚拟地址太大,大出了进程所申请的内存(不管实际有没有分配),因为进程虚拟地址从0开始,所以只要保证虚拟地址小于p->sz即可;而是虚拟地址太小,比进程的栈顶还低(注意栈是从高往低增长的),这就需要知道栈顶的位置,查看测试程序usertests,发现它获取栈顶的方法就是读sp寄存器,但是缺页中断的处理是在内核态,sp指向的也是内核栈的栈顶,想要获得用户栈的栈顶,可以借助进程的中断帧来实现,即读取p->trapframe->sp,需要保证虚拟地址大于等于这个值。杀掉进程可以观察usertrap函数的其他位置,发现只要令p->killed=1即可,见上面的代码。

第三是如果申请物理内存失败时也要杀掉进程,加上映射失败,照着uvmalloc里写就行了。

第四是fork的时候复制到缺页的虚拟地址时的处理,注意到fork的这部分是调用的uvmcopy,所以改uvmcopy,和uvmunmap一样,缺页导致的panic跳掉:

  for(i = 0; i < sz; i += PGSIZE){
    if((pte = walk(old, i, 0)) == 0) continue;
      // panic("uvmcopy: pte should exist");
    if((*pte & PTE_V) == 0) continue;
      // panic("uvmcopy: page not present");
    ......

第五是read和write文件的时候如果传入了一个缺页的虚拟地址(在将文件读入内存和将内存写入文件时需要传入地址),追踪这两个函数的过程可以发现最终处理地址调用的是copyin、copyinstr和copyout函数,注意到这几个函数会先walk一下传入的虚拟地址,如果得不到物理地址就直接返回失败,而不会经过缺页中断的过程,所以直接加入代码让其在判断得不到物理地址的情况下调用handle_page函数即可:

    va0 = PGROUNDDOWN(srcva);
    pa0 = walkaddr(pagetable, va0);
    if(pa0 == 0) {
        if (handle_page(va0, myproc()) == -1) return -1; else pa0 = walkaddr(pagetable, va0);
      // return -1;
    }
	......

总结一下,缺页中断的发生时刻应该是在MMU访问到一个PTE_V位为0的PTE时,在xv6中这个PTE的其他位是没有意义的,而在riscv-pk(用在spike模拟器上的代理内核)则让PTE的其他位指向一个标记结构体,里面包含了这个缺页的信息,比如该缺页是否是因为内存被置换到硬盘上了,置换到了哪个位置等信息,这样就使得该系统可以处理多种原因导致的缺页中断,而xv6应该是不支持硬盘虚拟内存的。

原文地址:https://www.cnblogs.com/YuanZiming/p/14223188.html