MIT 6s081 lab6:Copy-on-Write Fork for xv6

lab6:Copy-on-write fork

作业地址:Lab: Copy-on-Write Fork for xv6 (mit.edu)

实现 fork 懒复制机制,在进程 fork 后,不立刻复制内存页,而是将虚拟地址指向与父进程相同的物理地址。在父子任意一方尝试对内存页进行修改时,才对内存页进行复制。 物理内存页必须保证在所有引用都消失后才能被释放,这里需要有引用计数机制。

一开始做的时候没有上锁,一直没通过全部测试。后来参考了下面这位大佬的博客后意识到了竞争问题,加入自旋锁后顺利通过测试。

[mit6.s081] 笔记 Lab6: Copy-on-write fork | fork 懒拷贝 | Miigon’s blog

fork时不立刻复制内存

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");
        panic("uvmcopy: page not present");
    pa = PTE2PA(*pte);
    
    // 如果原来是可写的,那就要改成不可写,并且加上COW标志
    if(*pte & PTE_W){
      *pte &= ~PTE_W; // 去掉写flag
      *pte |= PTE_C; //标记为写时复制状态
    }
    flags = PTE_FLAGS(*pte);
    
    if(mappages(new, i, PGSIZE, (uint64)pa, flags) != 0){
      // kfree(mem);
      goto err;
    }
    // 添加这个物理地址的引用计数
    acquire(&pgreflock);
    modify(GET_IDX(pa), 1);
    release(&pgreflock);    

  }
  return 0;

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

在riscv.h中加入对标志位PTE_C的定义:

#define PTE_V (1L << 0) // valid
#define PTE_R (1L << 1)
#define PTE_W (1L << 2)
#define PTE_X (1L << 3)
#define PTE_U (1L << 4) // 1 -> user can access

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

这样,fork 时就不会立刻复制内存,只会创建一个映射了。这时候如果尝试修改懒复制的页,会出现 page fault 被 usertrap() 捕获。接下来需要在 usertrap() 中捕捉这个 page fault,并在尝试修改页的时候,执行真正的复制操作。

捕获写操作并执行复制

在 usertrap() 中添加对 page fault 的检测,检测接收的页是否是一个懒复制页(有PTE_C标志,并且虚拟地址合法),如果此时物理页的引用次数大于1,那么需要进行复制,将旧物理页的引用次数减一,分配的新物理页的引用次数加一;若引用次数为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(p->killed)
      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 sstatus &c registers,
    // so don't enable until done with those registers.
    intr_on();
 
    syscall();
  } else if((which_dev = devintr()) != 0){
    // ok
  } else {
  } 
  else if(r_scause() == 15){ // page fault
    // 12 : instruction page fault
    // 13 :load page fault
    // 15 :store/AMO page Fault

    uint64 va = r_stval(); // get the virtual address that caused the page fault.
    pte_t *pte = walk(p->pagetable, va, 0);
    uint flags = PTE_FLAGS(*pte);
    uint64 old_pa = PTE2PA(*pte);
    
    if(flags & PTE_C && uvmcheckcowpage(va)) // 是写时复制场景
    {
      acquire(&pgreflock);
      int count = modify(GET_IDX((old_pa)),0);
      release(&pgreflock);
      if(count > 1) { // 引用次数超过1
        char * pa = kalloc(); // alloc physial memory ,分配一页物理内存
        if(pa == 0){ // 申请失败
          p->killed = 1; // 杀死进程
          exit(-1);
        }
        else{
          memset(pa, 0, PGSIZE); // 记录之前指向的内存地址的值,然后拷贝过去
          memmove(pa, (void * )old_pa, PGSIZE); 
        
          kfree((void*)old_pa); //对于旧的地址,直接尝试删除
        
          //  增加新地址的引用次数,在kalloc中完成
          uint flags_new = (flags | (PTE_W)) & (~PTE_C); // 把原来的flag,清空掉PTE_C标志,加上PTE_W标志
          *pte = PA2PTE(pa) | flags_new | PTE_V; //修改pte
        }
      }
      else{
        uint flags_new = (flags | (PTE_W)) & (~PTE_C); 
        *pte = PA2PTE(old_pa) | flags_new | PTE_V;
      }
    }
    else{
      p->killed = 1;
      exit(-1);
    } 
  }
  else {
    printf("r_scause() == %d\n", r_scause());
    printf("usertrap(): unexpected scause %p pid=%d\n", r_scause(), p->pid);
    printf("            sepc=%p stval=%p\n", r_sepc(), r_stval());
    p->killed = 1;
  }

  if(p->killed)
    exit(-1);

  // give up the CPU if this is a timer interrupt.
  if(which_dev == 2)
    yield();

  usertrapret();
}

同时 copyout() 由于是软件访问页表,不会触发缺页异常,所以需要手动添加同样的监测代码,检测接收的页是否是一个懒复制页(有PTE_C标志,并且虚拟地址合法),如果此时物理页的引用次数大于1,那么需要进行复制,将旧物理页的引用次数减一,分配的新物理页的引用次数加一;若引用次数为1,直接返回该页。

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;
    // 这里可能会往一个cow page的页进行写操作,这时需要及时复制,和trap类似的操作
    pte_t *pte = walk(pagetable, va0, 0);
    if((*pte & PTE_U) == 0) return -1;
    uint flags = PTE_FLAGS(*pte);

    if((flags & PTE_C) && (flags & PTE_V) && uvmcheckcowpage(dstva)) // 是写时复制场景
    {
      acquire(&pgreflock);
      int count = modify(GET_IDX((pa0)),0);
      release(&pgreflock);

      if(count > 1) { // 引用次数超过1
        char * pa = kalloc(); // alloc physial memory ,分配一页物理内存
        if(pa == 0){ // 申请失败
          return -1;
        }
        else{
          memset(pa, 0, PGSIZE);
          // 记录之前指向的内存地址的值,然后拷贝过去
          uint64 old_pa = pa0;
          memmove(pa, (void * )old_pa, PGSIZE); 
        
          kfree((void*)old_pa); //对于旧的地址,直接尝试删除
          uint flags_new = (flags | (PTE_W)) & (~PTE_C); // 把原来的flag,清空掉PTE_C标志,加上PTE_W标志
          *pte = PA2PTE(pa) | flags_new | PTE_V;
          pa0 = (uint64)pa; // 更新读写的位置
        }
      }
      else{  //此时引用次数为1
        uint flags_new = (flags | (PTE_W)) & (~PTE_C); 
        *pte = PA2PTE(pa0) | flags_new | PTE_V;
      }
      
    } 
    
    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;
}

提供工具方法:uvmcheckcowpage

int uvmcheckcowpage(uint64 va) {
  struct proc *p = myproc();
  return va < p->sz;
}

到这里,就已经确定了大体的逻辑了:在 fork 的时候不复制数据只建立映射+标记,在进程尝试写入的时候进行实复制并重新映射为可写。

接下来,还需要做页的生命周期管理,确保在所有进程都不使用一个页时才将其释放

在原本的 xv6 实现中,一个物理页的生命周期内,可以支持以下操作:

  • kalloc(): 分配物理页
  • kfree(): 释放回收物理页

而在支持了懒分配后,由于一个物理页可能被多个进程(多个虚拟地址)引用,并且必须在最后一个引用消失后才可以释放回收该物理页,所以一个物理页的生命周期内,现在需要支持以下操作:

  • kalloc,分配物理页时,引用次数加一
  • fork,将旧的物理页的引用次数加一
  • 写时复制场景usertrap\copyout:把旧的物理页的引用次数减一,新分配的物理页次数加一
  • kfree:对物理页的引用次数减一,当计数为0,才真正释放物理页

记录物理页的引用次数

在kalloc.c中添加一个全局数组,用于记录物理页的引用次数,并提供Modify方法,用于修改引用计数数组

int rc_array[(PHYSTOP - KERNBASE) / PGSIZE];
struct spinlock pgreflock; // 用于 rc_array 数组的锁,防止竞态条件引起内存泄漏

int modify(int x,int c)
{
  if(x < 1){
    return -1;
  }

  rc_array[x] += c;
  if(rc_array[x] < 0) rc_array[x] = 0;

  return rc_array[x];
}

修改kfree函数:引用次数为0才能删去

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

  if(((uint64)pa % PGSIZE) != 0 || (char*)pa < end || (uint64)pa >= PHYSTOP)
  {
    panic("kfree");
  }
    
  // acquire(&pgreflock);
  // 添加对引用次数的判断,如果引用次数为1,才能删去
  acquire(&pgreflock);
  int count = modify(GET_IDX(pa), -1); // 把自己的引用计数减一,如果引用计数还是大于0,说明不能删除,则返回。
  release(&pgreflock);

  if(count <= 0){
    // Fill with junk to catch dangling refs.
    // 如果引用计数为-1,说明还未分配空间,那么往下执行,分配空间
    // 如果引用计数为0,说明空间已经不被使用了,释放空间
    memset(pa, 1, PGSIZE);
    
    r = (struct run*)pa;

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

}

修改Kalloc函数,分配物理页时将引用次数加一

void * // 添加对引用次数+1的功能
kalloc(void)
{
  struct run *r;

  acquire(&kmem.lock);
  r = kmem.freelist;

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

  if(r)
    memset((char*)r, 5, PGSIZE); // fill with junk

  // lab6 add 
  if(r){
    acquire(&pgreflock);
    modify(GET_IDX(r), 1);
    release(&pgreflock);

  }
    
  return (void*)r;
}

在usertrap和copyout函数中,加入判断,当物理页的引用次数为1时,说明此时只有一个进程使用这个物理页,那么就不用进行复制,直接返回这个物理页即可

加锁防止竞争

在rc_array添加了自旋锁pgreflock,在使用modify方法时,都使用acquire("&pgreflock")release(&pgreflock)来对操作进行保护,防止竞争导致的内存泄露。

如果出现了内存泄露问题,在运行Make grade后,系统会提示usertests调用前后的空闲页数量不同。

FAILED -- lost some free pages 32447 (out of 32448)

在以下四种场景调用Modify操作中加入锁进行保护。

  • kalloc
  • fork
  • 写时复制场景usertrap\copyout
  • kfree

提交

$ make qemu-gdb
(7.1s) 
== Test   simple == 
  simple: OK 
== Test   three == 
  three: OK 
== Test   file == 
  file: OK 
== Test usertests == 
$ make qemu-gdb
(78.3s) 
== Test   usertests: copyin == 
  usertests: copyin: OK 
== Test   usertests: copyout == 
  usertests: copyout: OK 
== Test   usertests: all tests == 
  usertests: all tests: OK 
== Test time == 
time: OK 
Score: 110/110

你可能感兴趣的:(MIT6s081,risc-v,linux,c语言)