# Implement copy-on-write fork

大意是子进程和父进程共享物理内存,只有需要对共享页写操作时再分配新的自己的物理页。

根据提示,为了区分当前页面是否是 copy-on-write 页面,在 riscv.h 中设置 PTE 保留的第 8 位作为标志。

#define PTE_C (1L << 8) // copy-on-write

修改 kalloc.c 如下:

int ref[PHYSTOP/PGSIZE+1];// 物理页面的引用计数
//...
void
freerange(void *pa_start, void *pa_end)// 分配时初始化
{
  char *p;
  p = (char*)PGROUNDUP((uint64)pa_start);
  for(; p + PGSIZE <= (char*)pa_end; p += PGSIZE){
      ref[(uint64)p/PGSIZE]=1;// 初始的引用计数为 1
      kfree(p);
  }
}
// Free the page of physical memory pointed at by pa,
// 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)
{
  if(((uint64)pa % PGSIZE) != 0 || (char*)pa < end || (uint64)pa >= PHYSTOP)
    panic("kfree");
  acquire(&kmem.lock);
  int remain=--ref[(uint64)pa/PGSIZE];// 引用计数 - 1
  release(&kmem.lock);
  if(remain>0){// 仍然有进程在使用该物理页,不释放
     return;
  }
  // 释放页面
  struct run *r;
  // 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);
}
// Allocate one 4096-byte page of physical memory.
// Returns a pointer that the kernel can use.
// Returns 0 if the memory cannot be allocated.
void *
kalloc(void)// 分配页面
{
  struct run *r;
  acquire(&kmem.lock);
  r = kmem.freelist;
  if(r){
    kmem.freelist = r->next;
    if(ref[(uint64)r/PGSIZE]!=0) panic("kalloc: ref count");// 未分配时引用计数必为 0
    ref[(uint64)r/PGSIZE]=1;// 初始计数为 1
  }
  release(&kmem.lock);
  if(r){
      memset((char*)r, 5, PGSIZE); // fill with junk
  }
  return (void*)r;
}
void ref_increase(uint64 pa){// 使引用计数 + 1 的函数
    acquire(&kmem.lock);
    if(pa%PGSIZE!=0||pa>=PHYSTOP||ref[pa/PGSIZE]<1) panic("ref increase err");
    ref[pa/PGSIZE]++;
    release(&kmem.lock);
}

在 trap.c 中修改如下:

// handle an interrupt, exception, or system call from user space.
// called from trampoline.S
//
void
usertrap(void)
{
  // ...
  if(r_scause() == 8){
    // system call
	// ...
  } else if(r_scause() == 15||r_scause()==13){//13 和 15 都是 page fault
      //page fault
      uint64 va=r_stval();
      if(!cow_pagefault(p->pagetable,va)){//copy-on-write 处理失败
          setkilled(p);// 杀死进程
      }
  } 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());
    setkilled(p);
  }
  // ...
}

vm.c 修改如下:

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);// 获取当前页面标志位
    if(flags&PTE_W){// 有写标志,说明不是 copy-on-write 页
      flags &= ~PTE_W;// 取消写标志
      flags |= PTE_C;//copy-on-write 标志
    }
    flags |= PTE_R;// 设置可读
    *pte = PA2PTE(pa)|flags;//pte 标志设置为 flags
    mappages(new, i, PGSIZE, pa, flags);// 将新页映射到同一个物理地址 pa
    ref_increase(pa);// 引用计数 + 1
  }
  return 0;
}
// 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;
  pte_t *pte;
  while(len > 0){
    va0 = PGROUNDDOWN(dstva);
    if(va0 >= MAXVA)// 越界
      return -1;
    pte = walk(pagetable, va0, 0);
    if(pte == 0 || (*pte & PTE_V) == 0 || (*pte & PTE_U) == 0)// 无效页或者用户不能访问的页
      return -1;
    if((*pte & PTE_W)==0){// 无写标志,可能是 copy-on-write 页导致的只读
        if(!cow_pagefault(pagetable,va0)) 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;
}
int cow_pagefault(pagetable_t pagetable,uint64 va){// 处理对 copy-on-write 页的写操作
    if(va>=MAXVA) return 0;// 越界
    pte_t *pte;
    if(!(pte=walk(pagetable,va,0))){// 无效 va
        return 0;
    }
    if(!(*pte&PTE_V)||!(*pte&PTE_C)){// 无效页或者不属于 copy-on-write 页
        return 0;
    }
    va = PGROUNDDOWN(va);
    char*mem=kalloc();// 分配一个物理页
    if(!mem){
        return 0;
    }
    uint64 pa = PTE2PA(*pte);
    memmove(mem, (char*)pa, PGSIZE);// 复制到新页
    uint flags = PTE_FLAGS(*pte);
    *pte = PA2PTE((uint64)mem) | flags;// 复制旧页的标志位到新页
    *pte |= PTE_W;// 新页可写
    *pte &= ~PTE_C;// 新页取消 copy-on-write 标志
    kfree((void*)pa);// 源页引用计数 - 1
    return 1;
}

# Test

一开始没有注意到 scause=13 也是 page fault,导致 usertest 部分一直卡住无输出,查看日志才知道有大量 unexcepted scause。

更新于