Lab6 写时复制

预备知识

写时复制(COW)

核心思想就是等到修改数据时才真正分配内存空间来拷贝资源。比如说,有多个请求同时请求相同资源(如内存或磁盘上的数据存储),它们首先会共同使用相同的资源,但被要求只读,直到某个请求试图修改资源时,系统才会真正复制一份副本给该请求者,而其他请求者所见到的资源仍然保持不变。这过程对其他的调用者都是透明的。

fork()中的写时复制

xv6 中的 fork() 系统调用会将父进程的用户空间内存全部复制到子进程中。如果父进程较大,复制过程可能需要较长时间。而且可能造成大量浪费;比如在fork()后子进程首先调用 exec(),就会导致子进程丢弃复制的内存,而其中大部分可能从未被使用。
而COW fork() 仅为子进程创建一个和父进程一样的页表和一个唯一的进程标识号,其中的 PTE 指向父进程的物理页面。在这种情况下,需要确保每个物理页面在最后一个指向它的 PTE 引用消失时才能被释放。实现这一点的方法是为每个物理页面维护一个“引用计数”,记录引用该页面的用户页面表的数量。COW fork() 将父进程和子进程中的所有用户 PTE 标记为不可写。当任一进程尝试写入这些 COW 页面时,CPU 将触发页面故障。内核的页面故障处理程序检测到此情况后,检测该页面的引用计数,当引用计数小于等于1时,不会有竞态条件,直接返回该物理页面,如果大于1,则将该物理页的引用计数减1,然后返回一个新的物理页,将原始页面复制到新页面,并修改故障进程中相关的 PTE 以指向新页面,此时 PTE 被标记为可写。当页面故障处理程序返回时,用户进程即可写入其页面副本。

Implement copy-on write

在Xv6内核中实现COW fork()功能。

添加COW标志

首先根据提示为PTE添加新的标记为,PTE中的低8,9,10位是保留位。

#define PTE_COW (1L << 8) //1 -> cow PTE

添加引用计数

记录每个物理页面被多少个进程引用,并修改相关的逻辑。
新增相关的数据结构和宏

//kernel/kalloc.c
int page_ref[ (PHYSTOP-KERNBASE) / PGSIZE]; //记录所有物理页面引用计数的数组
#define PA2PGID(pa) (( (pa)-KERNBASE )/PGSIZE) //物理地址转记录数组下标
#define PA2PGREF(pa) (page_ref[PA2PGID((uint64)pa)])   //获取物理地址对应的物理页的引用计数
struct spinlock pgreflock;                     //防止page_ref竞态条件的自旋锁

初始化锁

//kernel/kalloc.c
void
kinit()
{
  initlock(&kmem.lock, "kmem");
  initlock(&pgreflock, "page_ref");
  freerange(end, (void*)PHYSTOP);
}

修改kalloc,在这里初始化引用计数为1

//kernel/kalloc.c: kalloc
  if(r){
    memset((char*)r, 5, PGSIZE); // fill with junk
    PA2PGREF(r) = 1;//这里不用加锁,因为还没有kalloc函数还没有返回,不会有进程来使用
  }
//在defs.h中声明
void            pgrefadd(void *);

实现引用计数+1

//kernel/kalloc.c
//将物理地址对应的页面引用计数+1
void 
pgrefadd( void* pa){
  acquire(&pgreflock);
  PA2PGREF(pa)++;
  release(&pgreflock);
}

根据引用计数来返回页面的函数

//kernel/kalloc.c
//根据页面的引用计数返回
void*
kcow_pgref( void* pa){
  acquire(&pgreflock);
  if( PA2PGREF(pa) <= 1){
    release(&pgreflock);
    return pa;
  }

  void* newpa = kalloc();
  if( newpa == 0 ){
    release(&pgreflock);
    printf("内存不足");
    return 0;
  }

  memmove( newpa, pa, PGSIZE );

  PA2PGREF(pa)--;
  release(&pgreflock);
  return newpa;
}
//kernel/defs.h
void*            kcow_pgref(void *);

修改kfree,只有当引用计数为0时才释放物理页面

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

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

  //释放时要加锁
  acquire(&pgreflock);
  //这里如果是等于0会导致Xv6启动时卡死
  //估计是因为初始模拟物理页分配给硬件时没有设置COW位默认为0
  //因此在kfree时会小于0
  PA2PGREF(pa)--;
  if( PA2PGREF(pa) <= 0 ){

      // 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);
  }
  release(&pgreflock);

}

修改父子进程复制

原来父进程执行fork()时会调用uvmcopy()复制完整的物理内存给子进程。现在需要将复制功能取消。首先将可写页面的W清除,并将这些页的COW位置1,只读页的COW还是0。这样的好处在于触发写时复制时还是可以只复制一部分物理页,同时也可以分辨出哪些是可写的(COW为1),只读的(COW为0)。

//kernel/vm.c
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);

    //为W位为1的PTE的COW位置1,并清除其W
    //W为0的PTE不会进行操作
    if( *pte & PTE_W)
      *pte = (*pte & ~PTE_W) | PTE_COW;

    flags = PTE_FLAGS(*pte);

    //子进程的虚拟地址直接映射到父进程的物理页面
    if( mappages(new, i, PGSIZE, (uint64)pa, flags) != 0)
      goto err;
    
    pgrefadd((void*)pa);

    // if((mem = kalloc()) == 0)
    //   goto err;
    // memmove(mem, (char*)pa, PGSIZE);
    // if(mappages(new, i, PGSIZE, (uint64)mem, flags) != 0){
    //   kfree(mem);
    //   goto err;
    // }

  }
  return 0;

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

实现写时复制

在usertrap中检测是否是COW机制导致的trap,如果是则执行写时复制

//kernel/trap.c:usertrap
else if((which_dev = devintr()) != 0){
    // ok
  } else if( (r_scause()==13 || r_scause()==15) && iscow( r_stval() ) ){
    if(cowcopy(r_stval()) == -1)
      p->killed = 1;
  }

检测是否是COW引发的trap

//kernel/vm.c
#inldue "spinlock.h"
#include "proc.h"
int 
iscow( uint64 va){
  pte_t* pte;
  struct proc* p = myproc();

  if( va < 0 || va > p->sz)//不加这个过不了usertests
  return 0;
  
  pte = walk(p->pagetable,va, 0);
  if(pte == 0)
    return 0;
  if(*pte & PTE_COW)
    return 1;
  else
    return 0;
}

具体的写时复制实现

int
cowcopy( uint64 va){
  pte_t* pte;
  struct proc* p = myproc();

  if(( pte = walk(p->pagetable, va, 0)) == 0)
    panic("cowcopy:walk");
  
  uint64 pa = PTE2PA(*pte);
  uint64 newpa = (uint64)kcow_pgref( (void*)(pa)); 

  if(newpa == 0)
    return -1;

  //建立新的映射
  //恢复W位,清除COW位
  uint64 flags = (PTE_FLAGS(*pte) | PTE_W) & ~PTE_COW;
  //取消旧的映射
  uvmunmap(p->pagetable, PGROUNDDOWN(va), 1, 0);
  if(mappages(p->pagetable, va, 1, newpa, flags) == -1 )
    panic("cowcopy:mappages");
    
  return 0;

}

最后修改copyout,为其添加检测机制

  while(len > 0){
    //检查是否是COW页
    if(iscow(dstva))
      cowcopy(dstva);

    va0 = PGROUNDDOWN(dstva);
    pa0 = walkaddr(pagetable, va0);
    if(pa0 == 0)
      return -1;
    n = PGSIZE - (dstva - va0);
    if(n > len)
      n = len;
    memmove((void *)(pa0 + (dstva - va0)), src, n);

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

执行测试

$ cowtest
simple: ok
simple: ok
three: ok
three: ok
three: ok
file: ok
ALL COW TESTS PASSED

$usertests
...
test forktest: OK
test bigdir: OK
ALL TESTS PASSED
posted @ 2025-07-19 11:17  名字好难想zzz  阅读(20)  评论(0)    收藏  举报