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

写在前面

这次的实验与上一个实验(懒分配)很相似,实现写时复制,实验地址在这里。与以往不同的是,这次只有一个任务,虽然是 hard 难度,但写起来并没有很吃力,应该是我写的最快的一个实验了。思路很简单,主要是容易出现各种 bug,debug 的过程会比较煎熬,。我的代码在 github 上。

在 debug 的时候,有一些我觉得没啥问题的代码出现了错误,有的改完后也不是很理解~~ debug 的时候,发现别人都在强调进程安全问题,我也觉得很有道理,写的时候也加上了,然后发现并没有什么用。在实验都写完后,我把进程锁又删了,发现没什么问题,也能通过所有的测试用例。(理论上,我觉得应该加上读写锁的,保证进程安全;实际上,加了好像没啥用?!)

实验内容

实验只有一个任务,就是实现一个内存的写时复制机制(copy-on-write fork),也称为 COW。为什么要实现这个功能呢,主要原因是:

在 shell 中执行指令时,首先会 fork 一个子进程,然后在子进程中使用 exec 执行 shell 中的指令。在这个过程中,fork 需要完整的拷贝所有父进程的地址空间,但在 exec 执行时,又会完全丢弃这个地址空间,创建一个新的,因此会造成很大的浪费。

为了优化这个特定场景(fork 时)的内存利用率,我们可以在 fork 时,并不实际分配内存(与上一个实验的懒分配很像),而是让子进程和父进程共享相同的内存区域(页表不同,但指向的物理地址相同)。但为了保证进程之间的隔离性,我们不能同时对这块区域进行写操作,因此,设置共享的内存地址只有读权限。当需要向内存页中写入数据时,会触发缺页中断,此时再拷贝一个内存页,更新进程的页表,将内容写进去,然后重新执行刚才出错的指令。

在这个过程中,需要为每个物理内存页保存一个指向该内存页的页表数量。当为 0 时,表示没有进程使用该内存页,可以释放了;大于 1 时,每当有进程释放该内存页时,将对应的数值减一。

需要注意的是,这里要标记写入内存是 COW 场景。否则,如果真的有一个页面只能读不能写的话,就会出现问题。这里我们使用的是 PTE 页表项保留的标记位 RSW。

另一个知识点:在XV6中,除了 trampoline page 外,一个物理内存 page 只属于一个用户进程

任务说明

有两个场景需要处理 cow 的写入内存页场景:

  • 一个是用户进程写入内存,此时会触发 page fault 中断(15号中断是写入中断,只有这个时候会触发 cow,而13号中断是读页面,不会触发 cow);
  • 另一个是直接在内核状态下写入对应的内存,此时不会触发 usertrap 函数,需要另做处理。

总结起来,实验总共有以下四个步骤。

第一步,创建 page 的计数数组

首先对每个物理页面创建一个计数变量,保存在一个数组中,页面的数目就是数组的长度。这里有一个知识点:不是所有的物理内存都可以被用户进程映射到的,这里有一个范围,即 KERNBASE 到 PHYSTOP。具体映射可以从 xv6 手册中看到:

xv6 va和pa的映射关系

由于一个页表的大小(PGSIZE)是 4096,因此数组的长度可以定义为:(PHYSTOP - KERNBASE) / PGSIZE

  • 我们可以先在 kernel/kalloc.c 中定义一个用于计数的数组:
uint page_ref[(PHYSTOP - KERNBASE) / PGSIZE];

由于是全局变量,C 语言会自动初始化为 0。

  • 分配内存时增加数值:

kernel/riscv.h 中定义 COW 标记位和计算物理内存页下标的宏函数:

#define PTE_COW (1L << 8)  // copy on write
#define COW_INDEX(pa) (((uint64)(pa) - KERNBASE) >> 12)

带参数的宏,参数需要用括号括起来,避免参数是表达式时引起歧义

在 kalloc 时,设置值为 1:

void *
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
    page_ref[COW_INDEX(r)] = 1;
  }
  return (void*)r;
}
  • 在使用 kfree 释放内存页时,首先需要判断计数值是否大于 1:
void
kfree(void *pa)
{
  struct run *r;  

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

  //acquire(&ref_lock);
  if(page_ref[COW_INDEX(pa)] > 1) {
    page_ref[COW_INDEX(pa)]--;
    //release(&ref_lock);
    return;
  }
  page_ref[COW_INDEX(pa)] = 0;
  //release(&ref_lock);

  // 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);
}

第二步,uvmcopy

在创建好计数数组后,在 fork 时,直接使用原来的物理页进行映射:

在 kernel/vm.c 中修改 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");
    pa = PTE2PA(*pte);
    *pte = (*pte & ~PTE_W) | PTE_COW;
    flags = PTE_FLAGS(*pte);
    //if((mem = kalloc()) == 0)
    //  goto err;
    //memmove(mem, (char*)pa, PGSIZE);
    if(mappages(new, i, PGSIZE, pa, flags) != 0){
      //kfree(mem);
      goto err;
    }
    //acquire(&ref_lock);
    page_ref[COW_INDEX(pa)]++;
    //release(&ref_lock);
  }
  return 0;

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

需要在代码前面添加 extern 声明,用于引入外部的变量:

extern uint page_ref[]; // kalloc.c

第三步,处理中断 usertrap

和上一个实验相同,在 usertrap 中添加中断处理逻辑:

 else if(r_scause() == 15) {
    uint64 va = r_stval();
    if(va >= p->sz)
      p->killed = 1;
    else if(cow_alloc(p->pagetable, va) != 0)
      p->killed = 1;
  }

其中的 cow_alloc 函数,在 kalloc.c 中实现,并在 defs.h 中进行声明:

int
cow_alloc(pagetable_t pagetable, uint64 va) {
  va = PGROUNDDOWN(va);
  if(va >= MAXVA) return -1;
  pte_t *pte = walk(pagetable, va, 0);
  if(pte == 0) return -1;
  uint64 pa = PTE2PA(*pte);
  if(pa == 0) return -1;
  uint64 flags = PTE_FLAGS(*pte);
  if(flags & PTE_COW) {
    uint64 mem = (uint64)kalloc();
    if (mem == 0) return -1;
    memmove((char*)mem, (char*)pa, PGSIZE);
    uvmunmap(pagetable, va, 1, 1);
    flags = (flags | PTE_W) & ~PTE_COW;
    //*pte = PA2PTE(mem) | flags;
	if (mappages(pagetable, va, PGSIZE, mem, flags) != 0) {
      kfree((void*)mem);
      return -1;
    }
  }
  return 0;
}

这里,需要判断很多异常情况,测试用例会测试到。对错误的异常处理需要异常谨慎,,我就是在这里 debug 了很久,。。

第四步,内核写内存 copyout

这里直接调用上面写的 cow_alloc 函数即可(本来自己写了一下,发现一直出现各种问题,索性直接调用上面的函数,发现完全没问题,。)。

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 (cow_alloc(pagetable, va0) != 0)
      return -1;
    pa0 = walkaddr(pagetable, va0);
    if(pa0 == 0)
      return -1;
    n = PGSIZE - (dstva - va0);
    if(n > len)
      n = len;
    pte = walk(pagetable, va0, 0);
    if(pte == 0)
      return -1;
    memmove((void *)(pa0 + (dstva - va0)), src, n);

    len -= n;
    src += n;
    dstva = va0 + PGSIZE;
  }
  return 0;
}

最后注意在 defs.h 中添加 walk 声明。

总结

这个实验是我完成最快但 debug 最久的实验,。之前的实验很长时间都在考虑怎么实现,这个实验主要都是在 debug,。

文章同步在知乎

参考文献(博客)

  1. 【MIT6.S081】 lab6 cow pages
  2. MIT 6.S081 Lab6 CopyOnWrite
  • 3
    点赞
  • 13
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值