【MIT 6.S081】Lab6: Copy-on-Write Fork for xv6

COW

  • 概述
  • Implement copy-on write
  • 问题

本Lab为xv6添加一个copy on write的功能。
笔者用时约6h(太菜啦 不难但是细节多

概述

xv6中原始fork系统的实现是,当产生一个子进程时,直接把父进程页表中的每一页复制给子进程的页表,这样子做无疑有许多物理空间被浪费,因为并不是每一个空间都会在之后被修改。一个经典的思路就是copy on write,也就是一开始父子进程共享一块物理空间,当某一个物理空间需要被写的时候,再进行复制。
具体的做法就是,当父进程fork出一个子进程的时候,关闭父进程所有页表项的写权限,然后进行复制,复制时父子进程映射到相同的物理地址空间。当代码对没有写权限的页表项进行写操作时,有两种情况,分为内核态与用户态,如果在用户态,则会产生page fault;如果在内核态,则是在copyout函数中;两种情况的处理方式相同,都是为错误的虚拟地址重新分配一页物理内存,具体细节见下文。

Implement copy-on write

在写代码之前最好先把细节都想清楚,不然就会像笔者一样产生一堆bug >_<

由于fork函数中会调用uvmcopy函数对页表进行复制,故首先修改uvmcopy函数,将父进程的物理页映射到子进程中,而不是分配新物理页,并清空父子进程对应PTE中的PTE_W标志。具体代码如下,其中的updateref函数之后会讲。

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)
      panic("uvmcopy: pte should exist");
    if((*pte & PTE_V) == 0)
      panic("uvmcopy: page not present");
    pa = PTE2PA(*pte);

    // remove the PTE_W flag
    flags = PTE_FLAGS(*pte);
    if ((flags & PTE_W) != 0) {
      flags ^= PTE_W;
      *pte ^= PTE_W;
    }

    updateref((void*) pa, 1);

    // map child's va to parent's pa
    if(mappages(new, i, PGSIZE, pa, flags) != 0){
      kfree((void*) pa);
      goto err;
    }
  }
  return 0;

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

接下来需要修改usertrap函数,以便在page fault出现时进行处理。类似lazy lab,需要先判断是否为page fault,这里只需要处理scause为15的情况(13对应load的page fault,15对应store的)。首先需要判断当前错误的虚拟地址对应的PTE是否是不可写状态(文档里说重新用一个位来标志当前页面是否为cow页,但是在这里我直接用是否不可写来判断了,好像不太严谨但是也能过测试),之后便是分配新的物理页,并复制旧物理页内容到新物理页中,把旧物理页映射删除(这里用uvmunmap是有必要的,后面会讲)。

else if(r_scause() == 15) {
    uint64 va = PGROUNDDOWN(r_stval());
    pte_t* pte = walk(p->pagetable, va, 0);
    uint64 flags = PTE_FLAGS(*pte);

    if (((*pte) & PTE_W) == 0) {
      uint64 pa = (uint64) kalloc();
      if (pa == 0) {
        p->killed = 1;
      } else {
        uint64 oldpa = walkaddr(p->pagetable, va);
        memmove((void*) pa, (void*) oldpa, PGSIZE);
        
        uvmunmap(p->pagetable, va, 1, 1);
        mappages(p->pagetable, va, PGSIZE, pa, flags | PTE_W);
      }
    }
  }

copyout函数中也差不多,处理内核可能发生的写失败问题(内核只有在这个函数中才会写用户空间),处理代码如下所示。

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;

    // ======= mycode
    pte_t* pte = walk(pagetable, va0, 0);
    if (((*pte) & PTE_W) == 0) {
      uint64 flags = PTE_FLAGS(*pte);
      uint64 oldpa = pa0;
      pa0 = (uint64) kalloc();
      if (pa0 == 0) return -1;
      memmove((void*) pa0, (void*) oldpa, PGSIZE);

      uvmunmap(pagetable, va0, 1, 1);
      mappages(pagetable, va0, PGSIZE, pa0, flags | PTE_W);
    }
    // =======

    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;
}

最后还有一个重点,就是对于每一页物理页,我们不能在kfree函数调用时就直接把它回收,因为它可能还被其他进程所共享。一种解决方案是,为每一页物理页保留引用该页面的进程数,在kalloc函数中将该物理页的引用数置为1;在父进程fork时也就是uvmcopy函数中;增加物理页的引用计数(也就是之前说的updateref函数);在kfree中,减少该物理页的引用计数(如果为0则不减,一开始会在kinit中调用kfree进行初始化所以会为0),如果减少之后引用计数不为0,则不回收该物理页。
由于该引用计数数组需要被多个CPU共享,于是需要加个自旋锁,定义数据结构如下。其中,数组ref的大小其实只需要(PHYSTOP - end) / PGSIZE

struct {
  struct spinlock lock;
  int ref[(PHYSTOP-KERNBASE) / PGSIZE];
} kmemref;

然后定义两个函数,方便引用计数的读与修改。其中updateref函数将物理页r的引用计数增加k,getref函数获取某个物理页的引用计数。

void 
updateref(void* r, int k)
{
  acquire(&kmemref.lock);
  kmemref.ref[((uint64)r - KERNBASE) / PGSIZE] += k;
  release(&kmemref.lock);
}

int
getref(void* r) 
{
  acquire(&kmemref.lock);
  int res = kmemref.ref[((uint64)r - KERNBASE) / PGSIZE];
  release(&kmemref.lock);

  return res;
}

最后修改kallockfree函数,如下所示。这里重新解释一下uvmunmap调用的必要性,其实就是利用uvmunmap会调用kfree函数的特点,将对应物理页的引用计数减一,这样就很方便啦。

void
kfree(void *pa)
{
  struct run *r;

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

  // mycode
  if (getref(pa) > 0)
    updateref(pa, -1);
  if (getref(pa) > 0) return;
  // mycode end

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

  r = (struct run*)pa;

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

void *
kalloc(void)
{
  struct run *r;

  acquire(&kmem.lock);
  r = kmem.freelist;
  if(r) {
    kmem.freelist = r->next;
    // mycode
    updateref((void*)r, 1);
  }
  release(&kmem.lock);

  if(r) {
    memset((char*)r, 5, PGSIZE); // fill with junk
  }
  
  return (void*)r;
}

问题

不知道为啥把uvmcopy改成下面这样就不对呀,有没有大佬浇浇

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)
      panic("uvmcopy: pte should exist");
    if((*pte & PTE_V) == 0)
      panic("uvmcopy: page not present");
    pa = PTE2PA(*pte);

    // remove the PTE_W flag
    flags = PTE_FLAGS(*pte);
    if ((flags & PTE_W) != 0) {
      flags ^= PTE_W;
      *pte ^= PTE_W;
    }


    // map child's va to parent's pa
    if(mappages(new, i, PGSIZE, pa, flags) != 0){
    // 删了这里
    //   kfree((void*) pa);
      goto err;
    }
    // 改了这里
    updateref((void*) pa, 1);
  }
  return 0;

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

你可能感兴趣的:(MIT,6.S081,操作系统,os)