xv6实现内存延迟分配

106 阅读4分钟

xv6-lab:lazy page allocation

操作系统可以利用页表硬件的一种巧妙技巧就是延迟分配(lazy allocation)用户空间堆内存。在 xv6 中,应用程序通过 sbrk() 系统调用向内核请求堆内存。在原来的 xv6 内核中,sbrk() 会立即分配物理内存,并将其映射到进程的虚拟地址空间中。

但是,当程序请求较大内存时,这种方式会耗费较长时间。我们需要等待sbrk()系统调用使用kalloc()分配完物理页框,然后调用mappages(),更新每级页表的条目,如果分配的内存很大的话会导致程序运行在这里的时间很长,举个例子,一个1GB的内存由 262,144 个 4096 字节的页面组成。

此时我们可以采取延迟分配的技术来加速物理内存的分配,当调用 sbrk() 分配内存时,内核并不立即分配物理内存,而是只记录哪些用户地址被分配了,同时,在用户页表中将这些地址标记为无效,当进程第一次尝试访问某个尚未映射的延迟分配页面时,CPU会触发一个页错误,此时CPU会trap到usertrap()函数中。此时函数会根据尝试trap原因的寄存器的值得知,是缺页错误,所以此时要走对应新添加的延迟分配的分支,之后trapret回用户态之后就可以使用延迟分配的内存了。

这里主要讲思路,代码可以参考这篇博客

由于延迟分配内存不会在调用sbrk()时分配物理内存并处理映射更新,只需要修改当前进程的内存大小表示逻辑上已经分配了,所以我们需要修改sys_sbrk,使其不再调用 growproc(),而是只修改 p->sz 的值而不分配物理内存。

之后,我们还需要修改usertrap 用户态 trap 处理函数,为缺页异常添加检测,如果为缺页异常((r_scause() == 13 || r_scause() == 15)),且发生异常的地址是由于懒分配而没有映射的话,就为其分配物理内存,并在页表建立映射。我们可以在xv6的文档中得知异常原因的编号。

在此我们定义了两个函数,uvmlazytouch 函数负责分配实际的物理内存并建立映射。懒分配的内存页在被 touch 后就可以被使用了。uvmshouldtouch 用于检测一个虚拟地址是不是一个需要被 touch 的懒分配内存地址,具体检测的是:处于进程的地址范围之中,不是栈的 guard page,页表项不存在。

这是参考实现:

// kernel/vm.c

// touch a lazy-allocated page so it's mapped to an actual physical page.
void uvmlazytouch(uint64 va) {
  struct proc *p = myproc();
  char *mem = kalloc();
  if(mem == 0) {
    // failed to allocate physical memory
    printf("lazy alloc: out of memory\n");
    p->killed = 1;
  } else {
    memset(mem, 0, PGSIZE);
    if(mappages(p->pagetable, PGROUNDDOWN(va), PGSIZE, (uint64)mem, PTE_W|PTE_X|PTE_R|PTE_U) != 0){
      printf("lazy alloc: failed to map page\n");
      kfree(mem);
      p->killed = 1;
    }
  }
  // printf("lazy alloc: %p, p->sz: %p\n", PGROUNDDOWN(va), p->sz);
}

// whether a page is previously lazy-allocated and needed to be touched before use.
int uvmshouldtouch(uint64 va) {
  pte_t *pte;
  struct proc *p = myproc();
  
  return va < p->sz // within size of memory for the process
    && PGROUNDDOWN(va) != r_sp() // not accessing stack guard page (it shouldn't be mapped)
    && (((pte = walk(p->pagetable, va, 0))==0) || ((*pte & PTE_V)==0)); // page table entry does not exist
}

由于我们现在是延迟分配,在刚分配的时候是没有对应的映射的,所以要把一些原本在遇到无映射地址时会 panic 的函数的行为改为直接忽略这样的地址。因为我们此时的设计就是等到用户使用到这段内存再进行分配,而不是等到内核的一些其他函数在检测页表时,发现页表中建立了无效的映射而报错时崩溃。

这里参考博客修改了uvmummap():取消虚拟地址映射和uvmcopy():将父进程的页表以及内存拷贝到子进程这两个函数

此外我们还需要修改copyin和copyout函数,这两个函数在用户空间和内核空间中拷贝数据,此时可能会访问用户空间还未分配的虚拟页。如果目标虚拟页还没有分配物理内存,直接访问会导致缺页异常或出错。所以在真正访问虚拟地址前,先判断是否需要分配物理页,如果需要就分配,保证后续的内存访问是合法的。

举例:用户进程刚刚分配了一块虚拟内存,但还没实际访问。系统调用 write/read 时,内核通过 copyin/copyout 访问这块内存。由于还没分配物理页,uvmshouldtouch 返回 true,uvmlazytouch 负责分配物理页并建立映射。这样 copyin/copyout 就能顺利完成。

完成上述修改之后延迟分配内存的机制就在xv6中实现了。