xv6 6.S081 Lab5: cow

xv6 6.S081 Lab5: cow

cow代码在这里。完成了lazy后,cow的实现就非常明了了……

写在前面

经典写在前面😜。cowcopy-on-write的缩写(不是母牛┗|`O′|┛ 嗷~~),从字面上来看就是只在要写的时候复制内存。考虑这样一个情况:调用fork()后,子进程是需要复制所有的父进程内存还是说当且仅当子进程或者父进程要的时候才复制呢?答案显而易见了。这就是cow的核心思想。

按照惯例,这篇博客OS实验xv6 6.S081 开坑中给出了另一些有用的参考资料。

实验介绍

给出经典的官方指导书
实验内容没啥可说的,就是在xv6中实现我们在写在前面里谈到的cow。

开始!

在这里插入图片描述

为了实现cow,MIT给出了一种方案:基于cow的fork()函数只在子进程中创建指向父进程物理页面的页表,而不创建真实的物理页面;在调用fork()函数后,子进程和父进程的pte(page table entry)均被置为不可写,并且予以一个COW标记,表示该pte是属于cow的,这样,当其中一个进程要写的时候,就会在trap.c中捕捉到写错误同时发现va对应的pte是被COW标记的,就会对原物理页进行复制操作,并修改该pte映射的物理页为被复制的物理页。此外还需要注意一个细节:我们应该为每一块物理页面添加一个引用指针,用于记录它被进程引用的次数。当其引用次数为0的时候,我们就应该将其释放。这种情况对应着两个进程都复制了原物理页,那么原物理页就没有存在的必要了,调用kfree释放即可。整个过程可以用下图描述。
在这里插入图片描述
下面,按照Hints,我们先更改fork中调用的uvmcopy函数,使其只复制页表而不复制物理页面,并且修改子进程和父进程的pte为不可写,并标记为PTE_COW。代码如下:

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);
    /** 将Parent和Child的PTE权限均改为不可写,且均为COW Page  */
    *pte = (*pte & ~PTE_W) | PTE_COW;
    flags = PTE_FLAGS(*pte);
    /** 
     * 不重新分配物理内存,指向pa即可
     * flags = PTE_FLAGS(*pte);
     * if((mem = kalloc()) == 0)
     * goto err;
     * memmove(mem, (char*)pa, PGSIZE); */
    if(mappages(new, i, PGSIZE, (uint64)pa, flags) != 0){
      /** kfree(mem); */
      printf("uvmcopy():can not map page\n");
      goto err;
    }
    addref("uvmcopy()",(void*)pa);
    /** True  */
    /* printf("origin shoot: %p, new shoot: %p\n", walkaddr(old, i), walkaddr(new, i));
    printf("origin perm: %x, new perm %x \n", PTE_FLAGS(*walk(old,i,0)), PTE_FLAGS(*walk(new,i,0)));
    printf("origin perm & PTE_COW: %d, new perm & PTE_COW %d \n", PTE_FLAGS(*walk(old,i,0)) & PTE_COW, PTE_FLAGS(*walk(new,i,0)) & PTE_COW); */
  }
  return 0;

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

接着,我们需要处理trap.c中的内容以处理PTE_COW的写错误。这里写错误的标记为r_scause() == 15。在if(pa)后面,便是复制页面的操作了,这里要记住va映射到的被复制的页面的权限应该是可写且非PTE_COW的。

else if (r_scause() == 15) {
    pte_t* pte; 
    uint64 va = PGROUNDDOWN(r_stval());
    
    if (va >= MAXVA){
      printf("va is larger than MAXVA!\n");
      p->killed = 1;
      goto end;
    }
    
    if (va > p->sz){
      printf("va is larger than sz!\n");
      p->killed = 1;
      goto end;
    }
    
    pte = walk(p->pagetable, va, 0);
    
    if(pte == 0 || ((*pte) & PTE_COW) == 0 || ((*pte) & PTE_V) == 0 || ((*pte) & PTE_U)==0){
      printf("usertrap: pte not exist or it's not cow page\n");
      p->killed=1;
      goto end;
    }

    //printf("------------------------------\n");
    //printf("pte addr: %p, pte perm: %x\n",pte, PTE_FLAGS(*pte));
    if(*pte & PTE_COW){
      //printf("usertrap():got page COW faults at %p\n", va);
      char *mem;
      if((mem = kalloc()) == 0)
      {
        printf("usertrap(): memery alloc fault\n");
        p->killed = 1;
        goto end;
      }
      memset(mem, 0, PGSIZE);
      uint64 pa = walkaddr(p->pagetable, va);
      if(pa){
        memmove(mem, (char*)pa, PGSIZE);
        int perm = PTE_FLAGS(*pte);
        perm |= PTE_W;
        perm &= ~PTE_COW;
        if(mappages(p->pagetable, va, PGSIZE, (uint64)mem, perm) != 0){
          printf("usertrap(): can not map page\n");
          kfree(mem); 
          p->killed = 1;
          goto end;
        }
        /** mem处是新的页,添加一处引用,原来的物理地址减少一处引用  */
        kfree((void*) pa);
      }
      else
      {
        printf("usertrap(): can not map va: %p \n", va);
        p->killed = 1;
        goto end;
      }
    }
    else
    {
      printf("usertrap(): not caused by cow \n");
      p->killed = 1;
      goto end;
    }
    
  } 

接下来,根据Hint:Next, ensure that each physical page is freed when the last PTE reference to it goes away (but not before!), perhaps by implementing reference counts in kalloc.c,我们要在kalloc.c中实现页面引用。这里的实现方式有一点trick,经过计算,kalloc最多可分配32723的物理页面,因此,我直接开辟了一个32723大小的ref数组,用以记录。相关代码如下:

#define NPAGE 32723

char reference[NPAGE];

int
getrefindex(void *pa){
  int index = ((char*)pa - (char*)PGROUNDUP((uint64)end)) / PGSIZE;
  return index;
}

int
getref(void *pa){
  return reference[getrefindex(pa)];
}


void
addref(char *tip, void *pa){
  reference[getrefindex(pa)]++;
}

void
subref(char *tip,void *pa){
  int index = getrefindex(pa);
  if(reference[index] == 0)
    return;
  reference[index]--;
}

之后我们在free_range()中初始化reference数组:

void
freerange(void *pa_start, void *pa_end)
{
  char *p;
  p = (char*)PGROUNDUP((uint64)pa_start);
  printf("start ~ end:%p ~ %p\n", p, pa_end);
  for(; p + PGSIZE <= (char*)pa_end; p += PGSIZE){
    /** 初始化ref_count  */
    reference[getrefindex(p)] = 0;
    kfree(p);
  }
}

在调用kalloc后,物理页应该被引用1次

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

  acquire(&kmem.lock);
  r = kmem.freelist;
  if(r)
    kmem.freelist = r->next;
  release(&kmem.lock);
  /** implementation of ref count  */
  /** r is the start of physical page  */
  if(r){
    memset((char*)r, 5, PGSIZE); // fill with junk
    int index = getrefindex((void *)r);
    reference[index] = 1;
  }
  /** r出去后会被修改 */
  return (void*)r;
}

接下来是重点reference的减少一定要在kfree中进行,因为还有许多程序要用到kfree,如果在kfree之外进行,那就需要修改非常多的地方,这要感谢我同学ljj的指点,可惜他不写博客。代码如下:

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

  if(((uint64)pa % PGSIZE) != 0 || (char*)pa < end || (uint64)pa >= PHYSTOP)
    panic("kfree");
  
  // printf("----------------\n");
  // printf("r->ref_count befroe: %d\n",((struct run *)pa)->ref_count);
  // Fill with junk to catch dangling refs.
  /** implementation of ref count  */
  //int ref_count = ((struct run *)pa)->ref_count;
  /** 
   * 
   * 大坑:一定要在kfree中减,因为其他很多程序也会调用kfree,如此一来,那些程序就无法kfree掉
   * 鸣谢:ljj同学
   * */
  subref("kfree()", (void *) pa);
  int ref_count = getref(pa);
  if(ref_count == 0){
    //printf("!\n");
    memset(pa, 1, PGSIZE);
    // printf("r->ref_count after: %d\n",((struct run *)pa)->ref_count);
    // printf("----------------\n");
    r = (struct run*)pa;
    //r->ref_count = ref_count;
    acquire(&kmem.lock);
    r->next = kmem.freelist;
    kmem.freelist = r;
    release(&kmem.lock);

  }
}

最后,根据Hint:Finally, modify copyout() to use the same scheme as page faults when it encounters a COW page,我们去修改copyout。为什么要修改copyout呢,个人认为是因为copyout是将Kernel Page复制给User Page,而用户进程要求页面是可读写的,因此要做出修改。代码如下:

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){
      //printf("copyout(): va is greater than MAXVA\n");
      return -1;
    }
    pte = walk(pagetable, va0, 0);
    if(*pte & PTE_COW){
      //printf("copyout(): got page COW faults at %p\n", va0);
      char *mem;
      if((mem = kalloc()) == 0)
      {
        printf("copyout(): memery alloc fault\n");
        return -1;
      }
      memset(mem, 0, sizeof(mem));
      uint64 pa = walkaddr(pagetable, va0);
      if(pa){
        memmove(mem, (char*)pa, PGSIZE);
        int perm = PTE_FLAGS(*pte);
        perm |= PTE_W;
        perm &= ~PTE_COW;
        if(mappages(pagetable, va0, PGSIZE, (uint64)mem, perm) != 0){
          printf("copyout(): can not map page\n");
          kfree(mem); 
          return -1;
        }
        kfree((void*) pa);
      }
    }
    pa0 = walkaddr(pagetable, va0);
    if(pa0 == 0)
      return -1;
    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;
}

至此,我们完成了cow,运行make grade,测试成功!(PS:记得要在根目录下添加time.txt文件,该文件标识了该实验你做了多少个小时,不添加这个文件的话不能拿满分┭┮﹏┭┮)
OK,起飞🛫
在这里插入图片描述

  • 5
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 3
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值