Lab5: Copy-on-Write Fork for xv6

Implement copy-on-write fork

  • 问题:fork的过程有一条就是子进程会拷贝父进程的内存空间,但是这个拷贝是有一定开销的,尤其是在需要拷贝的东西多的时候更明显。但是这就引出了一个问题——我们真的需要去拷贝吗?很显然,从逻辑上来看,只有父进程或子进程对内存空间有修改时,这种拷贝才是有意义的

  • 思路:我们将拷贝的时机推迟到某个进程修改内存的时候,这样就可以优化掉很多不必要的开销

  • 策略

    • fork时只需要为子进程添加一个指向原始页面的指针,这个页面将被标记为只读。这样当父进程或子进程尝试写入页面时,就会触发page fault,这个时候再由内核去重新分配内存空间,为进程提供一个可写的页面
    • 本来我们页面的释放是随着进程释放同步进行的,但是上面描述的策略中的进程不再持有真实的内存页面,而仅仅是一个引用,为了处理释放,我们可以采用引用计数的方法(类似于linux文件link计数器的原理),当我们的进程释放时,递减引用计数,当计数为0时调用内存的释放
  • 引用计数

struct {
  struct spinlock lock;
  struct run *freelist;

  // 页面引用计数锁
  struct spinlock reflock;
  char ref_count[PHYSTOP/PGSIZE];
} kmem;

void
kinit()
{
  initlock(&kmem.lock, "kmem");
  initlock(&kmem.reflock, "kmemref");
  freerange(end, (void*)PHYSTOP);
}
  • 初始化计数器
void *
kalloc(void)
{
  struct run *r;

  acquire(&kmem.lock);
  r = kmem.freelist;
  if(r) {
    kmem.freelist = r->next;
    acquire(&kmem.reflock);
	kmem.ref_count[(uint64)r / PGSIZE] = 1;       // 初始化为1
    release(&kmem.reflock);
  }   
  release(&kmem.lock);

  if(r)
    memset((char*)r, 5, PGSIZE); // fill with junk
  return (void*)r;
}
  • 修改释放内存的条件
void
kfree(void *pa)
{
  struct run *r;

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

  // 只有当引用计数为0时才释放内存,否则返回
  char refcnt = ksubref((void*)pa);
  if (refcnt > 0 )
    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);
}
  • 增加COW标志位
    • 记录每个PTE是否是COW映射:使用RISC-V的PTE中的RSW位(保留给软件使用)
    • image.png|475
    • #define PTE_COW (1L << 8)
  • 修改uvmcopy
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");
   
    if(*pte & PTE_W) 
      *pte = (*pte & (~PTE_W)) | PTE_COW;     // 清除父进程的W位,设置COW位
    
    pa = PTE2PA(*pte);
    kaddref((void *)pa);                      // 引用加一

    flags = PTE_FLAGS(*pte);                  // 子进程指向原始页面
    if(mappages(new, i, PGSIZE, (uint64)pa, flags) != 0){
      goto err;
    }
  }
  return 0;

 err:
  uvmunmap(new, 0, i / PGSIZE, 1);
  return -1;
}
  • 处理写错误
    • 先判断COW标志位,当该页面是COW页面时,就可以根据引用计数来进行处理
      • 如果计数大于1,那么就需要通过kalloc申请一个新页面,然后复制内容,之后对该页面进行映射,映射的时候清除COW标志位,设置PTE_W标志位
      • 如果计数等于1,那么就不需要申请新页面,只需要对这个页面的标志位进行修改就可以了
void
usertrap(void)
{
  int which_dev = 0;
  if((r_sstatus() & SSTATUS_SPP) != 0)
    panic("usertrap: not from user mode");
  // send interrupts and exceptions to kerneltrap(),
  // since we're now in the kernel.
  w_stvec((uint64)kernelvec);
  struct proc *p = myproc();
  // save user program counter.
  p->trapframe->epc = r_sepc();
  if(r_scause() == 8){
    // system call
    if(killed(p))
      exit(-1);
    // sepc points to the ecall instruction,
    // but we want to return to the next instruction.
    p->trapframe->epc += 4;
    // an interrupt will change sepc, scause, and sstatus,
    // so enable only now that we're done with those registers.
    intr_on();
    syscall();
  }
  // 15:写页面错
  else if(r_scause() == 15) 
  { 
    uint64 va0 = r_stval();   // 引起缺页的虚拟地址
    if(va0 > p->sz)           // 判断是否超出进程的地址空间
      p->killed = 1;    
    else if(cowhandler(p->pagetable,va0) !=0 )
      p->killed = 1;
  } 
  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);
  }
  if(killed(p))
    exit(-1);
  // give up the CPU if this is a timer interrupt.
  if(which_dev == 2)
    yield();
  usertrapret();
}
  • 对COW页的处理
int
cowhandler(pagetable_t pagetable, uint64 va)
{
    char *mem;
    if (va >= MAXVA)
      return -1;
    pte_t *pte = walk(pagetable, va, 0);
    if (pte == 0)
      return -1;
    // 检查标志位
    if ((*pte & PTE_COW) == 0 || (*pte & PTE_U) == 0 || (*pte & PTE_V) == 0)
      return -1;
    uint64 pa = PTE2PA(*pte);
    char refcnt = kgetref((void *)pa);
    if(refcnt == 1) {
       *pte = (*pte & (~PTE_COW)) | PTE_W;
       return 0;
    }
    if(refcnt > 1) 
    {
      if ((mem = kalloc()) == 0)   return -1;
      memmove((char*)mem, (char*)pa, PGSIZE);
      kfree((void*)pa);
      uint flags = PTE_FLAGS(*pte);
      // 设置新申请的页可写
      *pte = (PA2PTE(mem) | flags | PTE_W);
      // 清除新申请的页的COW标记
      *pte &= ~PTE_COW;
      return 0;
    }
    return -1;
}
  • copyout中增加页面错误处理
    • 因为copyout是在内核中调用的,缺页不会进入usertrap
int
copyout(pagetable_t pagetable, uint64 dstva, char *src, uint64 len)
{
  uint64 n, va0, pa0;
  pte_t * pte;
 
  while(len > 0){
    va0 = PGROUNDDOWN(dstva);
    pa0 = walkaddr(pagetable, va0); 
    if(pa0 == 0)
      return -1;
    struct proc *p = myproc();
    if(va0 >= MAXVA)
      return -1;
    if(va0 < PGSIZE)
      return -1;
    if((pte = walk(pagetable, va0, 0))==0) {
      p->killed = 1;     
      return -1;
    } 
    // 如果当前页是COW 页,且引用数大于1,就直接分配一个新页
    if ((va0 < p->sz) && (*pte & PTE_V) && (*pte & PTE_COW)&&(*pte & PTE_U)) 
    {
      char refcnt = kgetref((void *)pa0);
      if(refcnt == 1)
         *pte = (*pte &(~PTE_COW)) | PTE_W;
      else if(refcnt > 1)
      {
        char *mem;
        ksubref((void *)pa0);
        if ((mem = kalloc()) == 0) 
        {
          p->killed = 1;        
          return -1;
        } 
        memmove(mem, (char*)pa0, PGSIZE);
        uint flags = PTE_FLAGS(*pte);
        *pte = (PA2PTE(mem) | flags | PTE_W);
        *pte &= ~PTE_COW;
        pa0 = (uint64)mem;
      }
    }
    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;
}
  • 1
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值