MIT6.S081 Lab5: Copy-on-Write Fork for xv6

目录

前言:

本节实验要求:

Implement copy-on write (hard)

你的工作:

这是一个合理的攻克计划:

提示:

5.1 Implement copy-on write (hard)

5.1.1 准备工作

5.1.2 修改uvmcopy()

5.1.3 修改usertrap()

5.1.4 修改copyout()

5.1.5 修改kfree()

前言:

        Lab6只有一个实验,目的是为了实现写时复制。写时复制(Copy-on-write,简称COW)是一种计算机程序设计领域的优化策略。其核心思想是,如果有多个调用者(callers)同时请求相同资源(如内存或磁盘上的数据存储),他们会共同获取相同的指针指向相同的资源,直到某个调用者试图修改资源的内容时,系统才会真正复制一份专用副本(private copy)给该调用者,而其他调用者所见到的最初的资源仍然保持不变。这过程对其他的调用者都是透明的。此作法主要的优点是如果调用者没有修改该资源,就不会有副本(private copy)被创建,因此多个调用者只是读取操作时可以共享同一份资源。

        Lab6的工作是当父进程fork出子进程后,先不给子进程分配内存空间,而是让它和父进程共享内存空间,当子进程使用到其中的几个页面时,CPU将强制产生页面错误,此时再分配空间给子进程的页面,进行复制。

本节实验要求:

Implement copy-on write (hard)

你的工作:

        您的任务是在xv6内核中实现copy-on-write fork。如果修改后的内核同时成功执行cowtestusertests程序就完成了。

        为了帮助测试你的实现方案,我们提供了一个名为cowtest的xv6程序(源代码位于user/cowtest.c)。cowtest运行各种测试,但在未修改的xv6上,即使是第一个测试也会失败。因此,最初您将看到:

$ cowtest
simple: fork() failed
$

        “simple”测试分配超过一半的可用物理内存,然后执行一系列的fork()fork失败的原因是没有足够的可用物理内存来为子进程提供父进程内存的完整副本。

        完成本实验后,内核应该通过cowtestusertests中的所有测试。即:

$ cowtest
simple: ok
simple: ok
three: zombie!
ok
three: zombie!
ok
three: zombie!
ok
file: ok
ALL COW TESTS PASSED
$ usertests
...
ALL TESTS PASSED
$

这是一个合理的攻克计划:

  1. 修改uvmcopy()将父进程的物理页映射到子进程,而不是分配新页。在子进程和父进程的PTE中清除PTE_W标志。
  2. 修改usertrap()以识别页面错误。当COW页面出现页面错误时,使用kalloc()分配一个新页面,并将旧页面复制到新页面,然后将新页面添加到PTE中并设置PTE_W
  3. 确保每个物理页在最后一个PTE对它的引用撤销时被释放——而不是在此之前。这样做的一个好方法是为每个物理页保留引用该页面的用户页表数的“引用计数”。当kalloc()分配页时,将页的引用计数设置为1。当fork导致子进程共享页面时,增加页的引用计数;每当任何进程从其页表中删除页面时,减少页的引用计数。kfree()只应在引用计数为零时将页面放回空闲列表。可以将这些计数保存在一个固定大小的整型数组中。你必须制定一个如何索引数组以及如何选择数组大小的方案。例如,您可以用页的物理地址除以4096对数组进行索引,并为数组提供等同于kalloc.ckinit()在空闲列表中放置的所有页面的最高物理地址的元素数。
  4. 修改copyout()在遇到COW页面时使用与页面错误相同的方案。

提示:

  • lazy page allocation实验可能已经让您熟悉了许多与copy-on-write相关的xv6内核代码。但是,您不应该将这个实验室建立在您的lazy allocation解决方案的基础上;相反,请按照上面的说明从一个新的xv6开始。
  • 有一种可能很有用的方法来记录每个PTE是否是COW映射。您可以使用RISC-V PTE中的RSW(reserved for software,即为软件保留的)位来实现此目的。
  • usertests检查cowtest不测试的场景,所以别忘两个测试都需要完全通过。
  • kernel/riscv.h的末尾有一些有用的宏和页表标志位的定义。
  • 如果出现COW页面错误并且没有可用内存,则应终止进程。

5.1 Implement copy-on write (hard)

        根据实验指导书的提示,我们需要在两个情况下进行写时复制:

  • 当用户进程向内存写入时,如果此时的页面还没有被分配到内存,则会触发中断进行分配
  • 当在内核态情况下,调用copyout()对页面进行写入时,也需要对页面进行内存分配

5.1.1 准备工作

        这一小节主要是完成准备工作,需要创建一个记录了每个页面被引用情况的数组,每当有新的子进程被fork出来,对应的页面引用数都要加1,只有一个页面的引用数归0时该页面才会被放回空闲列表。

        首先我们先查阅xv6手册,看看有多少的页面需要创建数组索引:

img

        可以看到, 只有从KERNBASE到PHYSTOP范围内的虚拟内存,才需要映射到物理内存中,因此,我们只需要以这段空间的页面数作为数组大小即可,这一部分在kernel/kalloc.c中完成:

// kernel/kalloc.c

struct {
  struct spinlock lock;
  struct run *freelist;
  char *ref_page;
  int page_cnt;
  char *end;
} kmem;

void
kinit()
{
  initlock(&kmem.lock, "kmem");
  // 计算有多少页面需要被索引
  kmem.page_cnt = pagecnt(end, (void*)PHYSTOP);
  kmem.ref_page = end;
  for(int i = 0; i < kmem.page_cnt; ++i)
  {
    kmem.ref_page[i] = 0;
  }
  kmem.end = kmem.ref_page + kmem.page_cnt;
  
  freerange(kmem.end, (void*)PHYSTOP);
}

int
pagecnt(void *pa_start, void *pa_end)
{
  char *p;
  int cnt = 0;
  p = (char*)PGROUNDUP((uint64)pa_start);
  for(; p + PGSIZE <= (char*)pa_end; p += PGSIZE)
    cnt++;
  return cnt;
}

        之后,需要对pte新增一个标志位,该标志位用来表示是否页面是出于COW(写时复制)的状态:

// kernel/riscv.h

#define PTE_V (1L << 0) // valid
......
#define PTE_COW (1L << 8)

5.1.2 修改uvmcopy()

        这一小节需要完成对uvmcopy()函数的修改,在原代码中,这个函数实现的是为子进程申请一个内存空间,并将父进程内存空间的内容复制到子进程中。而写时复制则要求将子进程的虚拟空间映射到父进程的内存空间中,此外,还需要完成对对应页表项的标志位修改:

// kernel/vm.c

int
uvmcopy(pagetable_t old, pagetable_t new, uint64 sz)
{
  pte_t *pte;
  uint64 pa, i;
  uint flags;

  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标志位置为1
    *pte |= PTE_COW;
    flags = PTE_FLAGS(*pte);
    // if((mem = kalloc()) == 0)//分配物理页面
    //   goto err;
    // memmove(mem, (char*)pa, PGSIZE);//把src的内容复制到dst上
    // 将子进程的页面映射到父进程的物理地址上
    if (mappages(new, i, PGSIZE, (uint64)pa, flags) != 0) {
      goto err;
    }
    incr((void *)pa);
  }
  return 0;

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

5.1.3 修改usertrap()

        由于此时子进程和父进程共享了同一块物理内存,因此,当子进程需要写入时,就必须要重新将对应的页面映射到一块新的物理内存中,这个函数主要完成的就是在写入时,对于处于共享的页面触发中断,对其分配内存:

// kernel/trap.c

void
usertrap(void)
{
  int which_dev = 0;

  ......
  
  if(r_scause() == 8){
    ......
  } else if((which_dev = devintr()) != 0){
    // ok
  } 
  // 如果发生了写中断时触发内存分配 
  else if (r_scause() == 15) {
    // 获取导致中断发生的页面地址
    uint64 va = r_stval();
    // 判断是否需要写时复制
    if (is_cow_fault(p->pagetable, va)) {
      // 对页面进行分配
      if (cow_alloc(p->pagetable, va) < 0) {
        printf("usertrap(): cow_alloc failed!");
        p->killed = 1;
      }
    }
    else {
      printf("usertrap(): unexpected scause %p pid=%d\n", r_scause(), p->pid);
      printf("            sepc=%p stval=%p\n", r_sepc(), r_stval());
      p->killed = 1;
    }
  }
  ......
  usertrapret();
}

// kernel/vm.c

int is_cow_fault(pagetable_t pagetable, uint64 va)
{
  // 根据标志位判断当前页表项是否符合条件
  va = PGROUNDDOWN(va);
  pte_t *pte = walk(pagetable, va, 0);
  if(pte == 0)
    return 0;
  if((*pte & PTE_V) == 0)
    return 0;
  if((*pte & PTE_U) == 0)
    return 0;
  if((*pte & PTE_COW))
    return 1;
  return 0;
}

// 模仿uvmcopy的原代码即可
int cow_alloc(pagetable_t pagetable, uint64 va)
{
  va = PGROUNDDOWN(va); 
  pte_t *pte;
  char *mem;
  // 返回页表中对应的虚拟地址的页表项
  if ((pte = walk(pagetable, va, 0)) == 0) {
    panic("uvmcopy: pte should exist");
  }
  uint64 pa = PTE2PA(*pte);

  uint flags = PTE_FLAGS(*pte);
  flags &= ~(PTE_COW);
  flags |= PTE_W;

  // 分配物理页面
  if ((mem = kalloc()) == 0) {
    goto err;
  }
  // 把src的内容复制到dst上
  memmove(mem, (char*)pa, PGSIZE);
  // 先解除映射关系
  uvmunmap(pagetable, va, 1, 1);
  if (mappages(pagetable, va, PGSIZE, (uint64)mem, flags) != 0) {
    kfree(mem);
    goto err;
  }
  return 0;

  err:
    return -1;
}

5.1.4 修改copyout()

        另一种需要写时复制的场景是调用copyout函数,因此我们需要修改copyout函数,由于这个函数不会触发中断,需要已进入函数体就进行判断:

// kernel/vm.c

int
copyout(pagetable_t pagetable, uint64 dstva, char *src, uint64 len)
{
  uint64 n, va0, pa0;

  while(len > 0){
    va0 = PGROUNDDOWN(dstva);
    // 判断是否需要写时复制
    if (is_cow_fault(pagetable, va0)) {
      if(cow_alloc(pagetable, va0) < 0) {
        printf("copyout: cow_alloc fail!\n");
        return -1;
      }
    }
    ......
  }
  return 0;
}

5.1.5 修改kfree()

         最后就是修改kfree函数了,由于每个页面都存在引用计数,因此不能和原代码一样直接释放,而是需要等到引用计数归0后才可以释放:

// kernel/kalloc.c

void
kfree(void *pa)
{
  int index = page_index((uint64)pa);
  // 如果计数大于1则减1然后返回
  if (kmem.ref_page[index] > 1) {
    // 执行计数减1
    desc(pa);
    return ;
  }
  // 如果最后一个使用该页面的也被释放了,则释放该页面
  if (kmem.ref_page[index] == 1) {
    desc(pa);
  }
  ......
}

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
### 回答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的内部机制和操作系统的基本理论。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值