XV6 Lab4: Copy-on-write Fork

Lab4:Copy-on-write Fork

本 lab 的任务是实现写时复制 fork。

参考文章:Lab6: Copy-on-Write Fork for xv6 详解

阅读指路:
kernel/vm.c:【核心数据结构是 pagetable_t,它实际上是一个指向 RISC-V 根页表页的指针;pagetable_t可以是内核页表,也可以是进程的页表。核心函数是 walk 和 mappages,前者通过虚拟地址得到 PTE,后者将虚拟地址映射到物理地址】
kernel/sysproc.c

There is a saying in computer systems that any systems problem can be solved with a level of indirection.

  • fork机制存在的问题:

The fork() system call in xv6 copies all of the parent process’s user-space memory into the child. If the parent is large, copying can take a long time. Worse, the work is often largely wasted; for example, a fork() followed by exec() in the child will cause the child to discard the copied memory, probably without ever using most of it.
众所周知,有关存储的存取和复制等操作是计算机性能的一大瓶颈。
【只有当父进程和子进程共享同一个物理页并且有一方要向共享的物理页写入内容时,才真正需要分配新页并进行复制,这就是Copy-on-Write机制的思想】

  • 解决方案——COW机制详细介绍:

Copy-on-Write fork() 机制对每一个子进程创建一个页表,其中的页表项PTE指向父进程的物理页,对于所有父进程和子进程的页表项PTE都标记为不可写。

当任何其中一个进程尝试写入这些物理页,触发缺页异常。内核的缺页异常处理函数检测到这种情况,对于造成异常的进程,分配新的一页物理内存,复制原来物理页的内容到新的物理页,修改对应的页表项PTE使得该进程引用新的物理页,标记为可写。当缺页异常程序返回,用户态进程可以正常向新的物理页中写入内容(内核维护COW机制,应用程序感知不到这种异常的发生)。

Copy-on-Write fork()机制对于释放物理页的方式有所变化,给定的物理页可能同时被很多线程的页表所引用,当且仅当没有任何页表引用该物理页(即最后一个引用取消时),释放物理页内存。

合理的实现方法:

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

Hints:(细节处理的提示)

  • It may be useful to have a way to record, for each PTE, whether it is a COW mapping. You can use the RSW (reserved for software) bits in the RISC-V PTE for this.
    【设置页表项PTE中的标记位,标记当前物理页是COW机制映射的物理页】
  • Some helpful macros and definitions for page table flags are at the end of kernel/riscv.h.
    【kernel/riscv.h最后部分的定义有帮助】
  • If a COW page fault occurs and there’s no free memory, the process should be killed.
    【当COW写入的页错误触发时,如果没有空闲页可以分配,进程应该被 killl】

主要工作:

首先根据Hints,在 kernel/riscv.h 中设置新的PTE标记位,标记是否为COW机制的页面:

// [kernel/riscv.h]
#define PTE_V (1L << 0) // valid
#define PTE_R (1L << 1)
#define PTE_W (1L << 2)
#define PTE_X (1L << 3)
#define PTE_U (1L << 4) // 1 -> user can access

#define PTE_COW (1L << 8) // 标志位:标记一个页面是否为COW Fork页面

根据实现方法的第一条,修改 kernel/vm.c 中的 uvmcopy() 函数():

// Given a parent process's page table, copy
// its memory into a child's page table.
// Copies both the page table and the physical memory.
// returns 0 on success, -1 on failure.
// frees any allocated pages on failure.
// 【Modify: map the parent's physical pages into the child, instead of allocating new pages. 
// Clear PTE_W in the PTEs of both child and parent.】
int
uvmcopy(pagetable_t old, pagetable_t new, uint64 sz) //typedef uint64 *pagetable_t; (512 PTEs)
{
  pte_t *pte; //uint64
  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) // invalid
      panic("uvmcopy: page not present");

    pa = PTE2PA(*pte);  // 地址翻译:页表项到物理地址
    
    // COW机制:
    // 不再为子进程重新分配内存,只进行标志位操作
    // 将父进程和子进程的PTE权限改为不可写,并设置该页为COW Fork物理页 
    *pte = (*pte & ~PTE_W) | PTE_COW;
    flags = PTE_FLAGS(*pte);
    
    // if((mem = kalloc()) == 0) // 分配内存
    // goto err;
    // memmove(mem, (char*)pa, PGSIZE);  // 复制页pa到新分配的页mem

    if(mappages(new, i, PGSIZE, pa, flags) != 0){   // Create PTEs 
      goto err;
    }

    if(add_ref_count((void*)pa) != 0){   
    // 成功添加页表项后, 调用kalloc.c中的函数, 令该物理页的引用数++
      goto err;
    }
  }
  return 0;

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

根据实现方法的第三条,在 kenel/kalloc.c 中添加物理页的引用计数以及相关机制:

  1. 加入数据结构维护每个物理页的引用数(注意需要定义新的锁进行互斥性保护)
// [COW fork]
struct ref_mem{
  struct spinlock lock;
  uint64 count[PHYSTOP / PGSIZE]; 
  // count:计数器数组, 每个物理页需要一个计数器 (数组长度 = 地址空间大小/页大小 = 最大页数)
} ref;
  1. kinit() 中加入初始化锁
void
kinit()
{
  initlock(&kmem.lock, "kmem");
  initlock(&ref.lock, "ref"); //[initlock]
  freerange(end, (void*)PHYSTOP);
}
  1. kalloc() 函数修改,新分配的物理页引用计数=1
// Allocate one 4096-byte page of physical memory.
// Returns a pointer that the kernel can use.
// Returns 0 if the memory cannot be allocated.
void *
kalloc(void)
{
  struct run *r;

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

  if(r){  //成功分配, 指向该页的进程数设为初值1
    acquire(&ref.lock);
    ref.count[(uint64)r / PGSIZE] = 1;
    release(&ref.lock);
  }

  if(r)
    memset((char*)r, 5, PGSIZE); // fill with junk
  return (void*)r;
}
  1. kfree() 函数修改,当且仅当物理页没有被引用时才真正释放
void
kfree(void *pa)
{
  struct run *r;

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

//
  acquire(&ref.lock);
  ref.count[(uint64)pa / PGSIZE]--; // 指向该物理页的进程数--
  if(ref.count[(uint64)pa / PGSIZE] != 0){ 
  // 【只有当没有进程引用该物理页才继续执行kfree真正释放内存】
    release(&ref.lock);
    return;
  }
  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);
}
  1. 添加函数接口,分别实现 引用数++获取当前引用数
int
add_ref_count(void* pa){
  if(((uint64)pa % PGSIZE)){
    printf("Invalid address: Cannot divided by page size.\n");
    return -1;
  }
  if((char*)pa < end || (uint64)pa >= PHYSTOP){
    printf("Invalid address: out of range.\n");
    return -1;
  }
  acquire(&ref.lock);
  ref.count[(uint64)pa / PGSIZE]++;
  release(&ref.lock);
  return 0;
}

int get_ref_count(void* pa) {  // 获取内存的引用计数
  return ref.count[(uint64)pa / PGSIZE];
}
  1. freerange() 函数中隐藏了难以发现的可能的错误:

发现 kinit()—freerange()—kfree() 的调用关系,需要做特殊处理:在freerange()中设所有物理页引用数=1,在kfree()中减为0(否则若一开始引用数为0,在kfree中0–会变为很大的正数,造成难以发现的错误,无法释放物理页)

void
freerange(void *pa_start, void *pa_end) // 以页为单位, 释放某个区域的内存
{
  char *p;
  p = (char*)PGROUNDUP((uint64)pa_start);
  for(; p + PGSIZE <= (char*)pa_end; p += PGSIZE){
    // 注意:初始化kinit()调用freerange(),
    // 对于所有页的count值设为1; 后续在kfree中减为0, 可以正常进行释放
    ref.count[(uint64)p / PGSIZE] = 1;
    kfree(p);
  }
}

以上我们已经实现了 uvmcopy()复制父进程物理页方法的修改kalloc.c中的COW支持
下面根据实现方法第二条,修改 usertrap() 对于页错误的情况给出处理步骤:

//
// handle an interrupt, exception, or system call from user space.
// called from trampoline.S
//
//【Modify: 当COW出现缺页异常时,使用kalloc()分配一个新页面,
// 将旧页面复制到新页面,然后将新页面设置到PTE中并设置PTE_W位】
void
usertrap(void)
{
	......
	else if(r_scause() == 13 || r_scause() == 15){
    uint64 va = r_stval();	// 【获取出现页错误的虚拟地址】
    if(va >= p->sz 
    || cow_judge(p->pagetable, va) != 1 
    || cow_alloc(p->pagetable, va) == 0)
      p->killed = 1;
  }
  ......
  usertrapret();
}

kernel/trap.c 中定义 cow_judge() 函数判断一个页面是否为COW页面:

// cow_judge 判断一个页面是否为COW页面(是,返回1;不是,返回0;错误,返回-1)
int cow_judge(pagetable_t pagetable, uint64 va) {
  if(va > MAXVA)	// 虚拟地址大于进程可占有的最大虚拟地址,错误。
    return -1;
  pte_t* pte;
  if((pte = walk(pagetable, va, 0)) == 0){	// 不存在此页表,错误
  /* 
  pte_t*  walk(pagetable_t pagetable, uint64 va, int alloc):
  Return the address of the PTE in page table pagetable 
  that corresponds to virtual address va. If alloc != 0, 
  create any required page-table pages.*/
    return -1;
  }
  if((*pte & PTE_V) == 0) // invalid page
    return -1;
    
  if(*pte & PTE_COW)  // 是COW Fork页面返回1,否则返回0
    return 1;
  return 0;
}

kernel/trap.c 中定义 cow_alloc() 函数分配新的物理页:

// cow_alloc copy-on-write分配器
void* cow_alloc(pagetable_t pagetable, uint64 va) {
  
  va = PGROUNDDOWN(va);
  
  if(va % PGSIZE != 0)
    return 0;

  uint64 pa = walkaddr(pagetable, va);  // 获取对应的物理地址
  if(pa == 0)
    return 0;

  pte_t* pte = walk(pagetable, va, 0);  // 获取对应的PTE

  int count = get_ref_count((void*)pa);
  if(count == 1) {  // 唯一进程引用当前页,无需重新分配,直接设置为可写并取消COW Fork标志位
    *pte = (*pte & ~PTE_COW) | PTE_W;
    return (void*)pa;
  } 
  else {	// 需要重新分配物理页
   
    char* new_pa;
    if((new_pa = kalloc()) == 0){ // 分配新的一个物理页
      return 0;
    }
    memmove(new_pa, (char*)pa, PGSIZE);
    // 【注意清除有效位PTE_V,否则在mappagges中会判定为remap】:
    *pte = (*pte) & ~PTE_V;

    if(mappages(pagetable, va, PGSIZE, (uint64)new_pa, (PTE_FLAGS(*pte) & ~PTE_COW) | PTE_W) != 0) {
       // 如果添加映射失败,恢复以前状态
       kfree(new_pa);
      *pte = (*pte) | PTE_V; 
      return 0;
    }

    kfree((void*)PGROUNDDOWN(pa)); // 最后一步:减小pa物理页的引用数(或直接释放物理页)
    return (void*)new_pa;
  }
}

最后不要忘了,根据实现方法第四步,需要修改 kernel/vm.ccopyout() 函数的复制机制:

// Copy from kernel to user.
// Copy len bytes from src to virtual address dstva in a given page table.
// Return 0 on success, -1 on error.
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); // 转换为物理地址

    //【COW Fork页面没有可写权限,分配新的物理页进行复制】
    if(cow_judge(pagetable, va0) == 1) {
      pa0 = (uint64)cow_alloc(pagetable, va0);
    }

    if(pa0 == 0)
      return -1;


    n = PGSIZE - (dstva - va0); // n = 页面大小-偏移量
    if(n > len)
      n = len;  // 一次复制搞定
    memmove((void *)(pa0 + (dstva - va0)), src, n); // 内存复制(注意第一个参数已转换为物理地址)

    len -= n; // 每次复制n字节
    src += n; // 新的复制起始点
    dstva = va0 + PGSIZE; // 虚拟地址偏移更新
  }
  return 0;
}

PS:【以上新定义的函数都需要在 kernel/def.h 中声明,这样其他用户程序只需要 #include"def.h" 就可以调用这些函数】

  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 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、付费专栏及课程。

余额充值