XV6学习 (7) Lab lazy: Lazy allocation

2,120

代码在github上。

这一个实验是要利用缺页异常来实现懒分配(lazy allocation)。用户态程序通过sbrk系统调用来在堆上分配内存,而sbrk则会通过kalloc函数来申请内存页面,之后将页面映射到页表当中。

当申请小的空间时,上述过程是没有问题的。但是如果当进程一次申请很大的空间,如数GB的空间,再使用上述策略来一页页地申请映射的话就会非常的慢(1GB/4KB=262,144)。这时候就引入了lazy allocation技术,当调用sbrk时不进行页面的申请映射,而是仅仅增大堆的大小,当实际访问页面时,就会触发缺页异常,此时再申请一个页面并映射到页表中,这是再次执行触发缺页异常的代码就可以正常读写内存了。

通过lazy allocation技术,就可以将申请页面的开销平摊到读写内存当中去,在sbrk中进行大量内存页面申请的开销是不可以接受的,但是将代价平摊到读写操作当中去就可以接受了。

总体来说这一个实验的难度并不大,理解了上一个trap的实验以及缺页异常就能比较轻松地完成了。

Eliminate allocation from sbrk() (easy)

这一个就是要修改sbrk函数,使其不调用growproc函数进行页面分配,关键就是p->sz += n将堆大小增大,然后注释掉growprocif(n < 0)是后面部分的内容。

uint64
sys_sbrk(void)
{
  int addr;
  int n;
  if(argint(0, &n) < 0)
    return -1;

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

Lazy allocation (moderate)

接下来就是真正实现Lazy allocation:当系统发生缺页异常时,就会进入到usertrap函数中,此时scause寄存器保存的是异常原因(13为page load fault,15为page write fault),stval是引发缺页异常的地址。

usertrap判断scause为13或15后,就可以读取stval获取引发异常的地址,之后调用lazy_alloc对该地址的页面进行分配即可。在这里不需要进行p->trapframe->epc += 4操作,因为我们要返回发生异常的那条指令并重新执行。

void
usertrap(void)
{
  ...
  } else if((which_dev = devintr()) != 0){
    // ok
  } else if (r_scause() == 13 || r_scause() == 15) {
    // 13: page load fault; 15: page write fault
    // printf("page fault\n");
    uint64 addr = r_stval();
    if (lazy_alloc(addr) < 0) {
      p->killed = 1;
    }
  } 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;
  }
  ...
}

lazy_alloc函数中,首先判断地址是否合法,之后通过PGROUNDDOWN宏获取对应页面的起始地址,然后调用kalloc分配页面,memset将页面内容置0,最后调用mappages将页面映射到页表中去。

int
lazy_alloc(uint64 addr) {
  struct proc *p = myproc();
  // page-faults on a virtual memory address higher than any allocated with sbrk()
  // this should be >= not > !!!
  if (addr >= p->sz) {
    // printf("lazy_alloc: access invalid address");
    return -1;
  }

  if (addr < p->trapframe->sp) {
    // printf("lazy_alloc: access address below stack");
    return -2;
  }
  
  uint64 pa = PGROUNDDOWN(addr);
  char* mem = kalloc();
  if (mem == 0) {
    // printf("lazy_alloc: kalloc failed");
    return -3;
  }
  
  memset(mem, 0, PGSIZE);
  if(mappages(p->pagetable, pa, PGSIZE, (uint64)mem, PTE_W|PTE_X|PTE_R|PTE_U) != 0){
    kfree(mem);
    return -4;
  }
  return 0;
}

Lazytests and Usertests (moderate)

这一部分就是要强化上面写的的lazy allocation,使其能在一些特殊情况下工作。

Handle negative sbrk() arguments.

这一个就是在上面的sys_sbrk函数中的if(n < 0)部分,当参数为负数时,调用uvmdealloc取消分配。

Kill a process if it page-faults on a virtual memory address higher than any allocated with sbrk().

这一个即lazy_alloc函数中的addr >= p->sz部分,当访问的地址大于堆的大小时就说明访问了非法地址,注意这里是>=而不是>

Handle the parent-to-child memory copy in fork() correctly.

fork函数中通过uvmcopy进行地址空间的拷贝,我们只要将其中panic的部分改为continue就行了,当页表项不存在时并不是说明出了问题,直接跳过就可以了。

Handle the case in which a process passes a valid address from sbrk() to a system call such as read or write, but the memory for that address has not yet been allocated.

当进程通过readwrite等系统调用访问未分配页面的地址时,并不会通过页表硬件来访问,也就是说不会发生缺页异常;在内核态时是通过walkaddr来访问用户页表的,因此在这里也要对缺页的情况进行处理。 当出现pte == 0 || (*pte & PTE_V) == 0时,就说明发生了缺页,这时只要调用lazy_alloc进行分配,之后再次使用walk就能正确得到页表项了。

uint64
walkaddr(pagetable_t pagetable, uint64 va)
{
  pte_t *pte;
  uint64 pa;

  if(va >= MAXVA)
    return 0;

  pte = walk(pagetable, va, 0);
  if(pte == 0 || (*pte & PTE_V) == 0) {
    if (lazy_alloc(va) == 0) {
      pte = walk(pagetable, va, 0);
    } else {
      return 0;
    }
  }
  if((*pte & PTE_U) == 0)
    return 0;
  pa = PTE2PA(*pte);
  return pa;
}

Handle out-of-memory correctly: if kalloc() fails in the page fault handler, kill the current process.

kalloc失败时,lazy_alloc就会返回负值,此时判断返回值然后p->killed = 1就行了。

Handle faults on the invalid page below the user stack.

这一个可以通过addr < p->trapframe->sp判断,当地址小于栈顶地址时就说明发生了非法访问。