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

本Lab为xv6添加一个copy on write的功能。
笔者用时约6h(太菜啦 不难但是细节多

概述

xv6中原始fork系统的实现是,当产生一个子进程时,直接把父进程页表中的每一页复制给子进程的页表,这样子做无疑有许多物理空间被浪费,因为并不是每一个空间都会在之后被修改。一个经典的思路就是copy on write,也就是一开始父子进程共享一块物理空间,当某一个物理空间需要被写的时候,再进行复制。
具体的做法就是,当父进程fork出一个子进程的时候,关闭父进程所有页表项的写权限,然后进行复制,复制时父子进程映射到相同的物理地址空间。当代码对没有写权限的页表项进行写操作时,有两种情况,分为内核态与用户态,如果在用户态,则会产生page fault;如果在内核态,则是在copyout函数中;两种情况的处理方式相同,都是为错误的虚拟地址重新分配一页物理内存,具体细节见下文。

Implement copy-on write

在写代码之前最好先把细节都想清楚,不然就会像笔者一样产生一堆bug >_<

由于fork函数中会调用uvmcopy函数对页表进行复制,故首先修改uvmcopy函数,将父进程的物理页映射到子进程中,而不是分配新物理页,并清空父子进程对应PTE中的PTE_W标志。具体代码如下,其中的updateref函数之后会讲。

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

    // remove the PTE_W flag
    flags = PTE_FLAGS(*pte);
    if ((flags & PTE_W) != 0) {
      flags ^= PTE_W;
      *pte ^= PTE_W;
    }

    updateref((void*) pa, 1);

    // map child's va to parent's pa
    if(mappages(new, i, PGSIZE, pa, flags) != 0){
      kfree((void*) pa);
      goto err;
    }
  }
  return 0;

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

接下来需要修改usertrap函数,以便在page fault出现时进行处理。类似lazy lab,需要先判断是否为page fault,这里只需要处理scause为15的情况(13对应load的page fault,15对应store的)。首先需要判断当前错误的虚拟地址对应的PTE是否是不可写状态(文档里说重新用一个位来标志当前页面是否为cow页,但是在这里我直接用是否不可写来判断了,好像不太严谨但是也能过测试),之后便是分配新的物理页,并复制旧物理页内容到新物理页中,把旧物理页映射删除(这里用uvmunmap是有必要的,后面会讲)。

else if(r_scause() == 15) {
    uint64 va = PGROUNDDOWN(r_stval());
    pte_t* pte = walk(p->pagetable, va, 0);
    uint64 flags = PTE_FLAGS(*pte);

    if (((*pte) & PTE_W) == 0) {
      uint64 pa = (uint64) kalloc();
      if (pa == 0) {
        p->killed = 1;
      } else {
        uint64 oldpa = walkaddr(p->pagetable, va);
        memmove((void*) pa, (void*) oldpa, PGSIZE);
        
        uvmunmap(p->pagetable, va, 1, 1);
        mappages(p->pagetable, va, PGSIZE, pa, flags | PTE_W);
      }
    }
  }

copyout函数中也差不多,处理内核可能发生的写失败问题(内核只有在这个函数中才会写用户空间),处理代码如下所示。

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;

    // ======= mycode
    pte_t* pte = walk(pagetable, va0, 0);
    if (((*pte) & PTE_W) == 0) {
      uint64 flags = PTE_FLAGS(*pte);
      uint64 oldpa = pa0;
      pa0 = (uint64) kalloc();
      if (pa0 == 0) return -1;
      memmove((void*) pa0, (void*) oldpa, PGSIZE);

      uvmunmap(pagetable, va0, 1, 1);
      mappages(pagetable, va0, PGSIZE, pa0, flags | PTE_W);
    }
    // =======

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

最后还有一个重点,就是对于每一页物理页,我们不能在kfree函数调用时就直接把它回收,因为它可能还被其他进程所共享。一种解决方案是,为每一页物理页保留引用该页面的进程数,在kalloc函数中将该物理页的引用数置为1;在父进程fork时也就是uvmcopy函数中;增加物理页的引用计数(也就是之前说的updateref函数);在kfree中,减少该物理页的引用计数(如果为0则不减,一开始会在kinit中调用kfree进行初始化所以会为0),如果减少之后引用计数不为0,则不回收该物理页。
由于该引用计数数组需要被多个CPU共享,于是需要加个自旋锁,定义数据结构如下。其中,数组ref的大小其实只需要(PHYSTOP - end) / PGSIZE

struct {
  struct spinlock lock;
  int ref[(PHYSTOP-KERNBASE) / PGSIZE];
} kmemref;

然后定义两个函数,方便引用计数的读与修改。其中updateref函数将物理页r的引用计数增加k,getref函数获取某个物理页的引用计数。

void 
updateref(void* r, int k)
{
  acquire(&kmemref.lock);
  kmemref.ref[((uint64)r - KERNBASE) / PGSIZE] += k;
  release(&kmemref.lock);
}

int
getref(void* r) 
{
  acquire(&kmemref.lock);
  int res = kmemref.ref[((uint64)r - KERNBASE) / PGSIZE];
  release(&kmemref.lock);

  return res;
}

最后修改kallockfree函数,如下所示。这里重新解释一下uvmunmap调用的必要性,其实就是利用uvmunmap会调用kfree函数的特点,将对应物理页的引用计数减一,这样就很方便啦。

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

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

  // mycode
  if (getref(pa) > 0)
    updateref(pa, -1);
  if (getref(pa) > 0) return;
  // mycode end

  // Fill with junk to catch0 dangling refs.
  memset(pa, 1, PGSIZE);

  r = (struct run*)pa;

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

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

  acquire(&kmem.lock);
  r = kmem.freelist;
  if(r) {
    kmem.freelist = r->next;
    // mycode
    updateref((void*)r, 1);
  }
  release(&kmem.lock);

  if(r) {
    memset((char*)r, 5, PGSIZE); // fill with junk
  }
  
  return (void*)r;
}

问题

不知道为啥把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);

    // remove the PTE_W flag
    flags = PTE_FLAGS(*pte);
    if ((flags & PTE_W) != 0) {
      flags ^= PTE_W;
      *pte ^= PTE_W;
    }


    // map child's va to parent's pa
    if(mappages(new, i, PGSIZE, pa, flags) != 0){
    // 删了这里
    //   kfree((void*) pa);
      goto err;
    }
    // 改了这里
    updateref((void*) pa, 1);
  }
  return 0;

 err:
  uvmunmap(new, 0, i / PGSIZE, 1);
  return -1;
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
### 回答1: 在xv6中,copy-on-write fork是一种优化技术,它可以在子进程创建时避免不必要的内存复制。具体来说,当父进程调用fork()创建子进程时,子进程会共享父进程的内存页表,而不是复制一份父进程的内存。只有当子进程尝试修改共享的内存时,才会发生实际的复制操作。这种技术可以减少内存使用和复制时间,提高系统性能。 ### 回答2: xv6是一个操作系统教学项目,这是一个现代化风格的UNIX第六版。copy-on-write forkxv6中实现的一种机制,它与fork系统调用有关。这种机制可减少在进行进程复制时所涉及的空间和时间开销,从而增加操作系统的效率。 在fork系统调用中,操作系统会复制原始进程,创建一个独立的进程。传统方法是,操作系统会将原有进程的内存空间全部复制一份给新进程,并在新进程中对地址进行修正。这样做会消耗大量的空间和时间,尤其是当进程较大时,复制整个内存空间会非常耗时。 copy-on-write fork的实现与传统方法不同。当原始进程需要创建新进程时,操作系统会将进程的内存空间标记为只读状态,并保留原内存页的映射关系。这样,当进程尝试写入内存时,操作系统将会产生一个缺页异常。在此时,操作系统会创建一个新页,将原内存页的内容复制到新页中,并在新页上进行写入操作。这样可以减少空间和时间开销,因为新页仅在需要写入时被复制,而不是在进程创建时。 copy-on-write fork有许多优点。首先,这种机制使系统更高效。使用copy-on-write fork可以显著降低进程复制的时间和空间开销。其次,这种机制还可以提高系统的可扩展性。当进程需要更多内存时,操作系统会重新映射新的内存页,而不是将整个进程复制一次。因此,系统可以更轻松地扩展。 总之,copy-on-write forkxv6中非常有用的一个机制。它可以减少进程复制所需的时间和空间开销,从而提高操作系统的效率和可扩展性。 ### 回答3: 在操作系统课程xv6中,实现了一种名为“copy-on-write fork”的操作,这种操作可以让父进程和子进程在初始时共享相同的物理内存。当父进程或子进程试图修改内存时,内存页会被复制并分配新的物理内存,以避免父进程和子进程之间的竞争条件。 这种“copy-on-write”技术可以减少系统中的内存浪费,并且在分配内存时减少了复制操作,从而提高了系统的性能。在实现中,当父进程调用fork()创建一个新的子进程时,子进程将直接引用父进程的地址空间。父进程和子进程都共享相同的物理内存,但是它们各自有自己的页目录和页表来管理地址空间和虚拟内存。 当父进程或子进程尝试读取数据时,它们可以访问共享的物理内存。然而,当父进程或子进程试图修改数据时,操作系统会将所涉及的内存页复制到另一个物理内存地址,并使涉及的进程引用新的物理内存地址。这样,父进程和子进程将各自拥有自己的数据副本,一个进程修改数据不会影响另一个进程。 这种技术在许多操作系统中都有广泛应用,因为它可以提供更高效的内存管理和更好的性能。实现“copy-on-writefork操作在操作系统课程中具有教育意义,因为它可以让学生更深入了解xv6的内部机制和操作系统的基本理论。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值