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

本文介绍了如何在XV6操作系统中实现写时复制机制,以优化fork过程中的内存利用率。通过创建页的计数数组,跟踪物理内存页的使用情况,仅在写入时复制内存页,减少不必要的内存开销。在用户进程写入、内核写入以及处理中断时,文章详细阐述了各个关键步骤的实现和调试过程,强调了处理进程安全和避免bug的重要性。

写在前面

这次的实验与上一个实验(懒分配)很相似,实现写时复制,实验地址在这里。与以往不同的是,这次只有一个任务,虽然是 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
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值