Loading

MIT6.S081 ---- Lab cow

Lab cow

The problem

xv6 中的 fork 系统调用复制所有的父进程的用户空间内存到子进程。如果父进程用户空间内存很大,复制会很耗时。有时这个复制是不必要的,如果 fork 之后子进程调用 exec,会释放掉复制的内存,可能大部分复制的内存都没有使用。如果父子进程共享一页,其中一个要写这个页,这才真正需要复制。

The Solution

COW fork 的目标是延迟为子进程分配物理页直到需要复制的时候。

COW fork 只为子进程创建一个页表,其对应用户内存的 PTEs 指向父进程的物理页。COW fork() 标记父子进程的所有的用户 PTEs 不可写。当其中一个进程想要写 COW pages时,CPU产生一个 page fault。内核的 page-fault handler 处理这种情况:

  • 为错误进程分配一个物理页。
  • 复制原指向的页的内容到新页。
  • 修改 PTE,指向新页的物理地址,并标记为可写。
  • 当 page fault handler 返回时,用户空间能够写复制的页。

释放物理页的时候需注意:一个物理页可能有多个进程页表引用,只有当引用数为 \(1\) 时才可以释放。

Implement copy-on write

vm.c

  • 将父子进程的 PTE 标记为 COW page 和 不可写。
    需要判断该页是否是 PTE_U,因为 trapframe 页在 fork 处理完用户页的映射后,会单独处理。且 uvmcopy 的参数 \(sz\) 是进程用户空间内存的大小,不包含 trapframe。
  • 父进程的物理页引用数加 \(1\)
  • 子进程的 PTE 指向父进程的物理页(不为子进程分配物理页)。
// Given a parent process's page table, copy
// its memory into a child's page table.
// Copies only the page table and
// clear their PTE_W.
// returns 0 on success, -1 on failure.
// frees any allocated pages on failure.
int
uvmcopy(pagetable_t old, pagetable_t new, uint64 sz)
{
  pte_t *pte;
  uint64 pa, i;
  uint flags;

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

    // trapframe(PTE_U=0) copied separately after call uvmcopy in fork()
    // uvmcopy only pay attention to user memory(PTE_U set)
    if ((flags & PTE_W) && (PTE_U | flags)) {
      flags = (flags & (~PTE_W)) | PTE_RSW_COW;
      *pte = ((*pte) & (~PTE_W)) | PTE_RSW_COW;
    }

    krefinc(pa);

    if(mappages(new, i, PGSIZE, pa, flags) != 0){
      goto err;
    }
  }
  return 0;

 err:
  uvmunmap(new, 0, i / PGSIZE, 1);
  return -1;
}

trap.c:

  • cowpagefault() 处理写不可写的 COW page。
  • 通过 kalloc() 分配新的页。
  • 重要优化:在复制之前可以判断该页的引用数,如果只有本进程在用,则可以直接设置该页可写。
  • 将旧页的内容复制到新页上。
  • 设置 PTE :可写,取消 COW page 标记,用新页的物理地址。
  • 释放旧页(引用为 \(1\) 回收物理页,否则引用数减 \(1\))。
// only handler cow fault.
// write the COW page(PTE_RSW_COW) of PTE_W on 0.
int
cowpagefault(pagetable_t pagetable, uint64 va)
{
  pte_t *pte;
  uint64 oldpa, newpa;

  // va >= MAXVA cause walk() panic
  if (va >= MAXVA)
    return -1;

  if ((pte = walk(pagetable, va, 0)) == 0)
    return -1;

  if ((*pte & PTE_RSW_COW) == 0)
    return -1;

  if (*pte & PTE_W)
    return -1;

  // guard page, trampoline, trapframe
  if ((*pte & PTE_U) == 0) {
    printf("PTE_U=0\n");
    return -1;
  }

  oldpa = PTE2PA(*pte);

  // Important optimization: if a store page fault and
  // the ref which pointed this phy-page is one, no copy.
  if (krefnum(oldpa) == 1) {
    *pte = (*pte | PTE_W) & (~PTE_RSW_COW);
    return 0;
  }

  if ((newpa = (uint64)kalloc()) == 0) {
    printf("cowpagefault: kalloc fault.\n");
    return -1;
  }

  // copy the old page to the new page
  memmove((void*)newpa, (void*)oldpa, PGSIZE);

  *pte = (PA2PTE(newpa) | PTE_FLAGS(*pte) | PTE_W) & (~PTE_RSW_COW);

  kfree((void*)oldpa);

  return 0;
}

trap.c : usertrap():
完善 trap path。

else if (r_scause() == 15) {
  if (cowpagefault(p->pagetable, r_stval()) < 0) {
    p->killed = 1;
  }
}

kalloc.c

  • 释放物理页时判断引用计数,引用数为 \(1\) 时才可以真正释放物理页。
  • 基于 kfree() 的设计,在空闲物理页链表的初始化 freerange 中,必须先为引用计数设置为 \(1\) , 否则 kfree()panic
// Free the page of physical memory pointed at by v,
// which normally should have been returned by a
// call to kalloc().  (The exception is when
// initializing the allocator; see kinit above.)
void
kfree(void *pa)
{
  struct run *r;

  if(((uint64)pa % PGSIZE) != 0 || (char*)pa < end || (uint64)pa >= PHYSTOP)
    panic("kfree");


  int phypageindex = PHYPAGEINDEX((uint64)pa);
  uint16 refcnt;

  acquire(&kmem.lock);
  refcnt = kmem.ppagerefcnt[phypageindex];
  if (refcnt < 1)
    panic("kfree ref = 0.");

  kmem.ppagerefcnt[phypageindex]--;
  release(&kmem.lock);

  if (refcnt > 1)
    return;

  // Fill with junk to catch dangling refs.
  memset(pa, 1, PGSIZE);

  r = (struct run*)pa;

  acquire(&kmem.lock);
  r->next = kmem.freelist;
  kmem.freelist = r;
  release(&kmem.lock);
}

void
krefinc(uint64 pa)
{
  int phypageindex = PHYPAGEINDEX(pa);

  acquire(&kmem.lock);
  if (pa >= PHYSTOP || kmem.ppagerefcnt[phypageindex] < 1)
    panic("krefinc pa invalid.");
  kmem.ppagerefcnt[phypageindex]++;
  release(&kmem.lock);
}

uint16
krefnum(uint64 pa)
{
  int phypageindex = PHYPAGEINDEX(pa);
  uint16 sz;

  acquire(&kmem.lock);
  sz = kmem.ppagerefcnt[phypageindex];
  release(&kmem.lock);

  return sz;
}

vm.c

copyout() 将内核中的数据复制到用户空间,获取虚拟地址 dstva 对应的物理地址的时候,并不是 MMU 在转换地址,而是内核通过 walkaddr 模拟硬件读取页表内容转换虚拟地址为物理地址,内核直接对物理地址进行操作,所以 copyout 时需要对 COW page 进行复制。否则会直接对不可写的 COW page 进行修改而不会出现 page-fault ,出现难以排查的 bug。

// Copy from kernel to user.
// Copy len bytes from src to virtual address dstva in a given page table.
// Return 0 on success, -1 on error.
int
copyout(pagetable_t pagetable, uint64 dstva, char *src, uint64 len)
{
  uint64 n, va0, pa0;

  while(len > 0){
    va0 = PGROUNDDOWN(dstva);

    pa0 = walkaddr(pagetable, va0);
    if(pa0 == 0)
      return -1;

    pte_t *pte = walk(pagetable,va0, 0);
    if (!(*pte & PTE_W) && (*pte & PTE_RSW_COW)) {
      if (cowpagefault(pagetable, va0) < 0)
        return -1;
      pa0 = PTE2PA(*pte);
    }

    n = PGSIZE - (dstva - va0);
    if(n > len)
      n = len;
    memmove((void *)(pa0 + (dstva - va0)), src, n);

    len -= n;
    src += n;
    dstva = va0 + PGSIZE;
  }
  return 0;
}

Code

Code: Lab cow

posted @ 2022-01-25 17:09  seaupnice  阅读(298)  评论(0编辑  收藏  举报