[MIT 6.S081] Lab 5: xv6 lazy page allocation

Lab 5: xv6 lazy page allocation

Eliminate allocation from sbrk() (easy)

要点

  • 更改 sbrk() 函数值增加进程空间 myproc()->sz, 不分配内存

步骤

  1. 修改 sys_sbrk() 函数, 将原本调用的 growproc() 函数注释掉, 而是直接增加 myproc()->sz.
uint64
sys_sbrk(void)
{
  int addr;
  int n;

  if(argint(0, &n) < 0)
    return -1;
  addr = myproc()->sz;
  // lab5-1
  myproc()->sz += n;    // increase size but not allocate memory
//  if(growproc(n) < 0)
//    return -1;
  return addr;
}

测试

在 xv6 中运行 echo hi, 输出如下:
在这里插入图片描述
可以看到 SCAUSE 寄存器的值为 15, 如下在RISC-V privileged instructions中图所示, 15 对应 Store/AMO page fault, 而根据 sepc 的值在 user/sh.asm 文件中可以找到对应汇编代码, 可以看到是一个 sw 指令, 用于向内存中写入 1 个字, 由于上述修改使得没有实际分配物理内存, 从而引发 page fault.
在这里插入图片描述
在这里插入图片描述

Lazy allocation (moderate)

要点

  • kernel/trap.c 中添加代码来处理 page fault 分配物理内存.

步骤

  1. kernel/trap.cusertrap() 中添加对 page fault 的处理.
    根据指导书提示, 当 r_scause() 的值为 13 和 15 时为需要处理的 page fault 情况.
    然后参考 growproc() 函数中调用的 uvmalloc() 代码, 调用 kalloc() 函数为引发 page fault 时记录在寄存器 STVAL 中出错的地址(由 r_stval() 获得)分配一个物理页, 然后调用 mappages() 函数在用户页表中添加虚拟页到物理页的映射. 此处需要使用 PGROUNDDOWN() 来对虚拟地址向下取整.
    对于中途出现的错误, 则是将 p->killed 置 1, 标记当前进程并在随后被杀死.
void
usertrap(void)
{
  int which_dev = 0;
  // ...
  if(r_scause() == 8){   // lab5-2
  // ...
  } else if (r_scause() == 13 || r_scause() == 15) {    // lab5-2
    char *pa;
    if((pa = kalloc()) != 0) {    // 分配物理页
      uint64 va = PGROUNDDOWN(r_stval());   // 引发page fault的虚拟地址向下取整
      memset(pa, 0, PGSIZE);
      // 进行页表映射
      if(mappages(p->pagetable, va, PGSIZE, (uint64)pa, PTE_W|PTE_R|PTE_U) != 0) {
          // 页表映射失败
          kfree(pa);
          printf("usertrap(): mappages() failed\n");
          p->killed = 1;
      }
    } else {    // 分配物理页失败
      printf("usertrap(): kalloc() failed\n");
      p->killed = 1;
    }
  } else if((which_dev = devintr()) != 0){
    // ok
  } else {
    // ...
  }

  if(p->killed)
    exit(-1);

  // give up the CPU if this is a timer interrupt.
  if(which_dev == 2)
    yield();

  usertrapret();
}
  1. 在 xv6 的原本实现中, 由于是 Eager Allocation, 所以不存在分配的虚拟内存不对应物理内存的情况, 因此在 uvmunmap() 函数中取消映射时对于虚拟地址对应的 PTE 若是无效的, 便会引发 panic. 而此处改为 Lazy allocation 后, 对于用户进程的虚拟空间, 便会出现有的虚拟内存并不对应实际的物理内存的情况, 即页表中的 PTE 是无效的. 因此此时便不能引发 panic, 而是选择跳过该 PTE.
void
uvmunmap(pagetable_t pagetable, uint64 va, uint64 npages, int do_free)
{
  uint64 a;
  pte_t *pte;

  if((va % PGSIZE) != 0)
    panic("uvmunmap: not aligned");

  for(a = va; a < va + npages*PGSIZE; a += PGSIZE){
    if((pte = walk(pagetable, a, 0)) == 0)
      panic("uvmunmap: walk");
    if((*pte & PTE_V) == 0) {
      continue;     // lab5-2
//      panic("uvmunmap: not mapped");  // lab5-2
    }
    if(PTE_FLAGS(*pte) == PTE_V)
      panic("uvmunmap: not a leaf");
    if(do_free){
      uint64 pa = PTE2PA(*pte);
      kfree((void*)pa);
    }
    *pte = 0;
  }
}

遇到问题

  • 执行 echo hi 引发 freewalk: leaf 的 panic. 如图所示:
    在这里插入图片描述
    解决: 经过排查, 是因为 usertrap() 中处理 page fault 时未对 va 使用 PGROUNDDOWN() 向下取整. 开始笔者观察源码 mappages() 发现其中会对 va 调用 PGROUNDDOWN() 向下取整, 便想在这里省去该步骤, 但仔细分析源码会发现, 其中的终止页 last 是通过 va 计算出来的, 若 va 未向下取整, 则 last==a+1 而非 last==a, 这会导致多映射一页, 从而页面释放时多映射的一页未被取消映射, 从而引发 panic.
    在这里插入图片描述

测试

在 xv6 中运行 echo hi, 可以正常打印出 hi, 如下图所示.
在这里插入图片描述

Lazytests and Usertests (moderate)

要点

  • 处理 sbrk() 负数参数的情况
  • 引发 page fault 的地址低于用户栈和高于分配地址的情况杀死进程
  • 处理 fork() 时的父进程向子进程拷贝内存的情况
  • 处理 read()/write() 使用未分配物理内存的情况
  • kalloc() 失败时杀死进程(上一部分实验已处理).

步骤

  1. 处理 kernel/sysproc.csys_sbrk() 参数为负数的情况.
    处理的方法可以直接参考原本调用的 growproc() 函数, 因为在 Lazy allocation 的情况下减少内存同样是将多余的内存进行释放, 此处便调用了 uvmdealloc() 函数.
    此处笔者认为应该对 addr+n 进行进一步判断, 要求其值要大于等于 PGROUNDUP(p->trapframe->sp), 因为 sys_sbrk() 函数主要是用于分配用户空间的堆, 因此不能缩减到用户栈及其以下结构. 但是原 growproc() 实际上并未考虑该情况. 经过测试发现是否添加该判断都能通过测试.
    至于不考虑 addr+n>MAXVA-2*PGSIZE 的情况, 虽然不能将内存覆盖到 trapframetrampoline 的虚拟空间, 但由于 addrn 均为 int 类型, 因此不会出现这种情况.
    此外, 由于 addrnint 类型, 因此要进行防溢出操作 addr+n>=addr, 避免 addr+n 由正变负造成 p->sz 变小的情况.
uint64
sys_sbrk(void)
{
  int addr;
  int n;
  struct proc *p;

  if(argint(0, &n) < 0)
    return -1;
  p = myproc();
  addr = p->sz;
  // lab5-1
  if(n >= 0 && addr + n >= addr){
    p->sz += n;    // increase size but not allocate memory
  } else if(n < 0 && addr + n >= PGROUNDUP(p->trapframe->sp)){
    // handle negative n and addr must be above user stack - lab5-3
    p->sz = uvmdealloc(p->pagetable, addr, addr + n);
  } else {
    return -1;
  }

//  if(growproc(n) < 0)
//    return -1;
  return addr;
}
  1. 进一步修改 kernel/vm.c 中的 uvmunmap() 函数, 将 pte==0 的情况由引发 panic 改为 continue 跳过.
    之所以这样操作, 原因在于 uvmunmap() 获取 pte 调用的 walk() 函数. walk() 用于根据虚拟地址 va 得到其相应页表中对应的 PTE. 而 xv6 的页表结构是有三级页目录, 因此可能在 L2 或 L1 的 PTE 可能就是无效的了, 此时便未分配低级的页目录, 自然也就未分配 va 对应的物理内存, 此时会根据 alloc 参数选择是否构建新的页目录, 但在 uvmunmap() 调用的 walk()alloc 参数为 0, 因为取消映射页表时不应该再新建页目录了. 此时 walk() 便返回 0.
    由于 Lazy allocation, 自然很可能出现虚拟地址对应的 L2 或 L1 的 PTE 就是未分配(无效)的情况, 所以此时不应该引发 panic, 同样应该跳过处理.
    简而言之, 对于 pte==0 的情况, 一般是 sbrk() 申请了较大内存, L2 或 L1 中的 PTE 就未分配, 致使 L0 页目录就不存在, 虚拟地址对应的 PTE 也就不存在; 而对于之前提到的 (*pte&PTE_V)==0 的情况, 一般是 sbrk() 申请的内存不是特别大, 当前分配的 L0 页目录还有效, 但虚拟地址对应 L0 中的 PTE 无效. 这两种情况都是 Lazy Allocation 未实际分配内存所产生的情况, 因此在取消映射时都应该跳过而非 panic.
void
uvmunmap(pagetable_t pagetable, uint64 va, uint64 npages, int do_free)
{
  uint64 a;
  pte_t *pte;

  if((va % PGSIZE) != 0)
    panic("uvmunmap: not aligned");

  for(a = va; a < va + npages*PGSIZE; a += PGSIZE){
    if((pte = walk(pagetable, a, 0)) == 0) {
      continue;  // lab5-3
//      panic("uvmunmap: walk");    // lab5-3
    }
    if((*pte & PTE_V) == 0) {
      continue;     // lab5-2
//      panic("uvmunmap: not mapped");  // lab5-2
    }
    if(PTE_FLAGS(*pte) == PTE_V)
      panic("uvmunmap: not a leaf");
    if(do_free){
      uint64 pa = PTE2PA(*pte);
      kfree((void*)pa);
    }
    *pte = 0;
  }
}
  1. 处理 kernel/proc.cfork() 函数中父进程向子进程拷贝时的 Lazy allocation 情况.
    容易分析得到, fork() 是通过 uvmcopy() 来进行父进程页表即用户空间向子进程拷贝的. 而对于 uvmcopy() 的处理和 uvmunmap() 是一致的, 只需要将 PTE 不存在和无效的两种情况由引发 panic 改为 continue 跳过即可.
int
uvmcopy(pagetable_t old, pagetable_t new, uint64 sz)
{
  pte_t *pte;
  uint64 pa, i;
  uint flags;
  char *mem;

  for(i = 0; i < sz; i += PGSIZE){
    if((pte = walk(old, i, 0)) == 0) {
      continue;     // lab5-3
//      panic("uvmcopy: pte should exist"); // lab5-3
    }
    if((*pte & PTE_V) == 0) {
      // lab5-3
      continue;
//        panic("uvmcopy: page not present");
    }
    // ...
}
  1. 处理 page fault 的虚拟地址超过 p->sz 或低于用户栈的情况.
    对于这两种情况, 处理方式比较简单, 即杀死进程.
    对于超过 p->sz 的情况比较简单, 而对于低于用户栈的情况, 此处需要额外说明一下, 使用va<p->trapframe->sp, va<PGROUNDDOWN(p->trapframe->sp)va<PGROUNDUP(p->trapframe->sp) 三种条件都能通过测试. 首先使用 p->trapframe->sp 是因为当前已经进入内核模式使用内核栈, 用户栈指针存到了 p->trapframe->sp字段中. 而之所以以上三种都行, 是因为理论上这里的 Lazy allocation 只处理用户堆, 用户栈在 exec 系统调用时已经进行了分配, 因此 PGROUNDDOWN(p->trapframe->sp) < p->trapframe->sp <= PGROUNDUP(p->trapframe->sp), 因此 va 引发 page fault 只可能出现在 PGROUNDDOWN(p->trapframe->sp) 以下, 所以三个条件实际上是等价的.
void
usertrap(void)
{
  // ..  
  if(r_scause() == 8){
    // system call
    // ...
  } else if (r_scause() == 13 || r_scause() == 15) {    // lab5-2
    char *pa;
    uint64 va = r_stval();
    // kill the process if va higher than size or below the user stack - lab5-3
    if(va >= p->sz){
      printf("usertrap(): invalid va=%p higher than p->sz=%p\n",
             va, p->sz);
      p->killed = 1;
      goto end;
    }
    if(va < PGROUNDUP(p->trapframe->sp)) {  // lab5-3
      printf("usertrap(): invalid va=%p below the user stack sp=%p\n",
             va, p->trapframe->sp);
      p->killed = 1;
      goto end;
    }
    if ((pa = kalloc()) == 0) {
        printf("usertrap(): kalloc() failed\n");
        p->killed = 1;
        goto end;
    }
    memset(pa, 0, PGSIZE);
    if (mappages(p->pagetable, PGROUNDDOWN(va), PGSIZE, (uint64) pa, PTE_W | PTE_R | PTE_U) != 0) {
        kfree(pa);
        printf("usertrap(): mappages() failed\n");
        p->killed = 1;
        goto end;
    }
  } else if((which_dev = devintr()) != 0){
    // ok
  } else {
    printf("usertrap(): unexpected scause %p pid=%d\n", r_scause(), p->pid);
    printf("            sepc=%p stval=%p\n", r_sepc(), r_stval());
    p->killed = 1;
  }
end:    // lab5-3
  if(p->killed)
    exit(-1);

  // give up the CPU if this is a timer interrupt.
  if(which_dev == 2)
    yield();

  usertrapret();
}
  1. 处理 read()/write() 使用未分配物理内存的情况. 通过这两个函数最终会调用 copyin()copyout() 进行用户空间到内核空间的读写. 而这两个函数对虚拟地址 va 的处理最终是通过 walkaddr() 函数得到物理地址而完成的.
    对于函数的原本逻辑, PTE 无效或者不存在以及无 PTE_U 标志位都会返回 0 表示失败. 但是在 Lazy allocation 的情况下, PTE 无效和不存在是可以被允许的, 因此要对这两种情况进行处理. 但并不是两种情况的所有情形都是允许的, 由于 Lazy allocation 是针对用户堆空间的, 因此需要判断虚拟地址 va 是否在用户堆空间的范围.
    若满足, 则考虑如何分配内存, 与前面不同的是, readwrite 为系统调用, 执行到此处时已经在内核模式, 因此这里不会引发 page fault, 因此这里应该和 usertrap() 中的处理类似, 对虚拟地址分配相应的物理页即可.
uint64
walkaddr(pagetable_t pagetable, uint64 va)
{
  pte_t *pte;
  uint64 pa;
  struct proc *p=myproc();  // lab5-3

  if(va >= MAXVA)
    return 0;

  pte = walk(pagetable, va, 0);
  // lazy allocation - lab5-3
  if(pte == 0 || (*pte & PTE_V) == 0) {
    // va is on the user heap  
    if(va >= PGROUNDUP(p->trapframe->sp) && va < p->sz){
        char *pa;
        if ((pa = kalloc()) == 0) {
            return 0;
        }
        memset(pa, 0, PGSIZE);
        if (mappages(p->pagetable, PGROUNDDOWN(va), PGSIZE,
                     (uint64) pa, PTE_W | PTE_R | PTE_U) != 0) {
            kfree(pa);
            return 0;
        }
    } else {
        return 0;
    }
  }
  if((*pte & PTE_U) == 0)
    return 0;
  pa = PTE2PA(*pte);
  return pa;
}

遇到问题

  • 运行 echo hi 执行正常但运行 lazytests 第一个测试用例出现 uvmunmap: walk 的 panic, 如下图所示:
    在这里插入图片描述
    解决: 观察 user/lazytests.c 的代码就会发现, 其中使用 sbrk() 申请了 1GB 的虚拟内存, 便会出现 PTE 不存在的情形, 即 uvmunmap()pte==0 的情况. 具体见上述 #步骤2.
  • 运行 lazytests 出现 freewalk: leaf 的 panic, 如下图所示:
    在这里插入图片描述
    解决: 出现该 panic 证明有 L0 的 PTE 未取消映射. 最终发现是因为笔者在 usertrap() 函数中的编写错误, 对于出错杀死进程(p->killed=1)的情况, 应该直接跳至 if(p->killed) 处, 在未使用 goto 语句时, 笔者误将 va 判断出错后没有跳出当前判断体, 仍进行了内存分配, 从而导致了上述问题. 最后笔者选择使用 goto end 语句来进行错误跳转, 使得条理相对清晰不容易出错.

测试

  • 在 xv6 中执行 lazytests:
    在这里插入图片描述
    在这里插入图片描述
  • make grade 测试:
    在这里插入图片描述
  • 4
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值