MIT 6.S081 2021: Lab Copy-on-Write Fork for xv6

虽然Copy-on-Write原理是很显而易见的,但是在具体实现中需要处理的细节特别多,经常会在莫名其妙的地方出现错误,再加上使用gdb调试内核本身就是一件不容易的事情,所以这个Copy-on-Write实验还是很有难度的。首先来看一下什么是copy-on-write,两张图解释:

 

1.uvmcopy

根据提示,首先要修改uvmcopy() ,把子进程的虚拟地址映射到父进程的物理页表上,同时清理父子进程页表上的PTE_W位。这里还是比较简单的,先根据要求,尝试一下写出代码:

 

不是最终代码

获取父进程每页对应的PTE表项,清除掉父进程的PTE_W位,提取出它的flag,在给子进程的虚拟地址建立页表映射的时候使用这个flag,这样子进程的各个PTE和父进程就相同了。

2.usertrap

然后根据提示,我们需要修改usertrap() 函数,当遇到page fault时要为子进程新分配一个页并更新映射。

首先要明确几点:当发生page fault时,引发page fault的地址存储在寄存器stval中,引发pagefault的原因存储在寄存器scause中,引发page fault的指令地址在sepc中。因为父子进程已经是共用数据了,因此使用if(r_scause()==15)来判断是不是出现了page fault。(反正是只用15没问题)尝试实现处理COW操作的函数(不是最终版):

 

 

首先记得做好错误检测,如果传入了非法地址要及时退出。make grade是真的会测试非法地址的!然后使用walk()得到指向stval所在页表的指针,如果得到的不是有效地址也要及时返回。使用kalloc()分配一个页,其地址赋值给pa2,page fault页的物理地址赋值给pa1。注意kalloc()不能保证一定能分配出一块内存,所以要记得检测pa2是不是0,如果分配不出来内存要退出。

最后使用memmove直接复制本页所有内容,然后使用PA2PTE宏来创建页表项,并打开所有的PTE位,存入刚刚walk()得到的内存地址中,这样就覆盖了原来的表项。

3.reference count

在COW中,会出现大量的多个虚拟地址对应同一个物理页表的情况。如果某一个进程乱释放物理页表,会导致其他进程的页表也被释放。所以需要对每一个页表都设置引用计数。初始状态计数默认为1。调用kfree()释放内存时,先把计数减去1,再检查计数,如果为0,说明这个页表已经是彻底没用了,可以直接释放。(这就像ext文件系统里的inode一样)

按照提示,在kalloc.c里初始化一个数组int refcount[PHYSTOP/PGSIZE],初始化为0。PHYSTOP是xv6可用的全部内存值,因此refcount存储了所有页表的计数。

修改kalloc(),在这里初始化计数为1:

void *
kalloc(void)
{
  struct run *r;
  acquire(&kmem.lock);
  r = kmem.freelist;
  if(r)
  {
    int pn=(uint64)r/PGSIZE;
    if(refcount[pn]!=0)
      panic("ref kalloc");
    refcount[pn]=1;
    kmem.freelist = r->next;
  }
  release(&kmem.lock);
​
  if(r)
    memset((char*)r, 5, PGSIZE); // fill with junk
  return (void*)r;
}

记得错误检查:如果计数不为0,说明这个空闲页表被引用,必然是出现了严重的bug,直接打出panic。还要注意,refcount显然是所有进程共有的,为了避免出现race condition,一定要在锁中操作。

修改kfree():

void
kfree(void *pa)
{
  struct run *r;
​
  if(((uint64)pa % PGSIZE) != 0 || (char*)pa < end || (uint64)pa >= PHYSTOP)
    panic("kfree");
  
  acquire(&kmem.lock);
  int pn=(uint64)pa/PGSIZE;
  if(refcount[pn]<1)
    panic("kfree ref");
  refcount[pn]-=1;
  int tmp=refcount[pn];
  release(&kmem.lock);
​
  if(tmp>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);
  
}

只要有函数调用kfree(),就说明需要对页表pa的refcount减去1。注意错误处理:如果refcount已经是0,说明系统出现了多次释放同一块内存的bug。减完之后再检测一下refcount是不是0,如果是0则释放即可。记得访问refcount的时候要加锁。

现在make qemu一下,xv6竟然不能boot,为什么呢?因为系统在boot时调用了kinit()初始化内存,kinit()调用freerange(),而freerange()使用了kfree()来清空内存。这时的refcount是0,已经是空闲页了,kfree()无法释放它。因此需要修改freerange():

void
freerange(void *pa_start, void *pa_end)
{
  char *p;
  p = (char*)PGROUNDUP((uint64)pa_start);
  for(; p + PGSIZE <= (char*)pa_end; p += PGSIZE)
  {
    refcount[(uint64)p/PGSIZE]=1;
    kfree(p);
  }
    
}

 

4.使用reference count

每当一个进程fork()一次,它每个页表的reference count都应该加1,因为有一个子进程使用了它的物理页表。因此可以设计一个incref函数用来给refcount+1:

void incref(uint64 pa)
{
  int pn=pa/PGSIZE;
  acquire(&kmem.lock);
  if(pa>PHYSTOP||refcount[pn]<1)
    panic("incref");
  
  refcount[pn]+=1;
  release(&kmem.lock);
}

修改uvmcopy(),调用incref(pa),每mappage一次都需要增加一次计数:

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);
    *pte=(*pte)&~PTE_W;
    flags = PTE_FLAGS(*pte);
    // if((mem = kalloc()) == 0)
    //   goto err;
    // memmove(mem, (char*)pa, PGSIZE);
    incref(pa);
    if(mappages(new, i, PGSIZE, (uint64)pa, flags) != 0){
      //kfree(mem);
      goto err;
    }
  }
  return 0;

现在如果直接执行cowtest的话会出现内存不足的bug。因为在COW操作完成之后,子进程不再和父进程共用发生过page fault的页表,因此父进程中原来的页表引用需要减去1。还要修改cowfault(),对父进程的页表进行一次kfree()操作来减少一次引用计数:

int cowfault(pagetable_t pagetable,uint64 va)
{
  if(va>=MAXVA)
    return -1;
  pte_t *pte=walk(pagetable,va,0);
  if(pte==0)
    return -1;
  //检测地址是否合法
  if((*pte&PTE_U)==0||(*pte&PTE_V)==0)
    return -1;
  uint64 pa1=PTE2PA(*pte);
  uint64 pa2=(uint64)kalloc();
  if(pa2==0)
  {
    printf("kalloc failed\n");
    return -1;
  }
​
  memmove((void*)pa2,(void*)pa1,PGSIZE);
​
  *pte=PA2PTE(pa2)|PTE_V|PTE_U|PTE_R|PTE_W|PTE_X;
  kfree((void*)pa1);
  return 0;
}

5.copyout

按照提示,我们需要给copyout()增加COW的功能。首先要获取dstva所在页表的虚拟地址va0和表项pte。如果此页表的PTE_W是无效的不允许写入,则需要COW创建一个新的页表给子进程写入。有了前面的代码,我们可以直接调用cowfault为va0分配一个新的独立页。

这时va0对应的表项已经被更新,对应的物理地址已经被改变,所以使用PTE2PA获取新分配的物理地址,稍后传给memmove()。

int
copyout(pagetable_t pagetable, uint64 dstva, char *src, uint64 len)
{
  uint64 n, va0, pa0;
  
​
  while(len > 0){
    va0 = PGROUNDDOWN(dstva);
    if(va0>=MAXVA)
      return -1;
    pa0 = walkaddr(pagetable, va0);
    pte_t* pgfault=walk(pagetable,va0,0);
    if(pgfault==0||(*pgfault&PTE_V)==0||(*pgfault&PTE_U)==0)
      return -1;
    if(pa0 == 0)
      return -1;
    if((*pgfault&PTE_W)==0)
    {
      if(cowfault(pagetable,va0)<0)
      {
        return -1;
      }
    }
    pa0=PTE2PA(*pgfault);
    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;
}

这里还是要注意错误处理!注意va0是否合法,pgfault是否存在,如果存在PTE_U和PTE_V是否合法,cowfault是否能正常返回。

posted @ 2021-11-19 22:43  LunaCancer  阅读(967)  评论(0编辑  收藏  举报