MIT6.S081实验6学习记录

MIT6.S081实验6学习记录

Lab 6 COW 实验要求

这个实验继承自实验五。在fork时,父进程会把页表复制给子进程,并且为子进程的页表分配内存,建立映射,属于一种eager allocation。 这个实验就是要修改为一种类似的lazy allocation

首先是父进程在uvmcopy(将页表复制给子进程)时,不会立即分配物理内存,而是只把这个修改记录下来(设PTE_COW位,并设置PTE_W为不可写)。然后当子进程要写入对应的物理地址时,才触发一个缺页中断,检测当前是否是COW页,如果是,那就给他建立映射,并把PTE_COW位置零,让PTE_W置为可写。但是要注意,如果该进程不止一个子进程,如果在这时释放,就会让其他子进程无法共享到父进程的内存,所以需要添加一个值,每当子进程申请复制时,就加一,直到这个值为0,说明其他子进程都不再需要这段内存,此时再释放即可。

接下来按照实验的提示一步一步进行即可。

首先是修改 uvmcopy() ,需要做的就是不要立即分配子进程的内存(把映射map到物理内存),而是只修改PTE_W和PTE_COW,前者修改为不可写(父、子进程),后者将这一页改为COW页(子进程)。

int
uvmcopy(pagetable_t old, pagetable_t new, uint64 sz)
{
  pte_t *pte;
  uint64 pa, i;
  uint64 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标志

    flags = PTE_FLAGS(*pte);
    
    //不分配内存,而是设置标记
    if (flags & PTE_W)
    {
      flags = (flags | PTE_COW) & (~PTE_W); //子进程PTE_W不可写,PTE_COW置1
      *pte = PA2PTE(pa) | flags; //父进程PTE_W为不可写
    }

    increase_rc((void*)pa);  //增加计数
    
    //map this vitural address
    if(mappages(new, i, PGSIZE, (uint64)pa, flags) != 0){
      goto err;
    }
    
  }
  return 0;

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

接下来就是修改trap.c中的usertrap() 函数,利用trap机制来处理这个缺页中断(类似于上个实验)。也就是判断有缺页中断,就先判断一下当前地址是否有效(报错的虚拟地址超过最大的虚拟地址,或者访问的是每个进程的guard page),然后给有COW标志位的页表进行副本copy,然后设置为可写。

void
usertrap(void)
{
	...
    syscall();
  }else if((r_scause() ==13 || r_scause() ==15)){
    uint64 va = r_stval();
    //保证当前页地址有效
    if (va >= MAXVA || 
    (va <= PGROUNDDOWN(p->trapframe->sp) 
    && va >= PGROUNDDOWN(p->trapframe->sp) - PGSIZE)) 
    {
      p->killed = 1;
    }else if(uvmcowcopy(p->pagetable, va) != 0){
    //判断页是否是COW页,并且为其实际分配内存
      p->killed = 1;
    }
  }
  else if((which_dev = devintr()) != 0){
    // ok
  } 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;
  }

  if(p->killed)
    exit(-1);

  // give up the CPU if this is a timer interrupt.
  if(which_dev == 2)
    yield();

  usertrapret();
}

接下来就是添加一个函数,根据上面的描述,这个函数的作用就是,识别当前页表是不是PTE_COW页,如果是,就将其分配一个副本,这个副本要是可写的,然后映射到实际物理内存,让子进程能够访问到完成写任务。然后把这个页表的PTE_COW置零,并且使用 kfree() 来减少计数以识别最后一个子进程也不再需要这个页表了,然后彻底释放。

int
uvmcowcopy(pagetable_t pagetable, uint64 va)
{
  uint64 pa;
  pte_t *pte;
  uint flags;

  if (va >= MAXVA) return -1;

  va = PGROUNDDOWN(va);
  pte = walk(pagetable, va, 0);
  if (pte == 0) return -1;

  pa = PTE2PA(*pte);
  if (pa == 0) return -1;

  flags = PTE_FLAGS(*pte);

  if (flags & PTE_COW) //判断是否是COW页
  {
    char *ka = kalloc();
    if (ka == 0) return -1;
    memmove(ka, (char*)pa, PGSIZE);
    kfree((void*)pa);
    flags = (flags & ~PTE_COW) | PTE_W; //将flag的COW为置0,并且设置为可写
    *pte = PA2PTE((uint64)ka) | flags; //修改页表的pte
    return 0;
  }
  
  return 0;
}

写到这里,trap处理的部分就结束了,接下来需要修改kalloc.c部分,主要是能够让内存分配或者释放时,更新计数。首先定义一个结构体,为了避免访问出现竞态,定义一个锁(仿照kmem写)。

## kernel/kalloc.c
#define PA2IDX(pa) (((uint64)pa) >> 12)
struct
{
  struct spinlock lock;
  int count[PGROUNDUP(PHYSTOP) / PGSIZE];
}mem_ref;
//增加计数
void
increase_rc(void *pa)
{
  acquire(&mem_ref.lock);
  mem_ref.count[PA2IDX(pa)]++;
  release(&mem_ref.lock);
}
//减少计数
void
decrease_rc(void *pa)
{
  acquire(&mem_ref.lock);
  mem_ref.count[PA2IDX(pa)]--;
  release(&mem_ref.lock);
}
//获取计数
int
get_rc(void *pa)
{
  acquire(&mem_ref.lock);
  int rc = mem_ref.count[PA2IDX(pa)];
  release(&mem_ref.lock);
  return rc;
}
//初始化
void
kinit()
{
  initlock(&mem_ref.lock, "mem_ref");
  acquire(&kmem.lock);
  
  //将计数都初始化为0
  for (int i = 0; i < PGROUNDUP(PHYSTOP) / PGSIZE; i++)
    mem_ref.count[i] = 0;
  release(&kmem.lock);
  initlock(&kmem.lock, "kmem");
  freerange(end, (void*)PHYSTOP);
}

注意这里计数不要一开始全部设置为1,因为有些页可能并没有被用到也被设置为1,所以要在freerange时增加,freerange函数作用是清理这段范围的内存,腾出空间给当前进程使用。

void
freerange(void *pa_start, void *pa_end)
{
  char *p;
  p = (char*)PGROUNDUP((uint64)pa_start);
  for(; p + PGSIZE <= (char*)pa_end; p += PGSIZE){
    increase_rc((void*)p);   //在这里增加一个计数
    kfree(p);
  }
}

在释放页面时,需要考虑这个界面的使用计数是否为0,因此,每次访问kfree时,就将这个计数减一,如果为0,就直接彻底free掉就行。

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

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

  decrease_rc(pa);  //每调用一次就减少一次计数
  if (get_rc(pa) > 0) //如果计数没有到0,还是返回,不会销毁
    return;
  
  memset(pa, 1, PGSIZE);
  r = (struct run*)pa;
  acquire(&kmem.lock);
  r->next = kmem.freelist;
  kmem.freelist = r;
  release(&kmem.lock);
}

每当给COW页分配内存时,就需要增加一个计数。

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
    increase_rc((void*)r);  //增加一个计数
  }
    
  return (void*)r;
}

最后需要在copyout 函数也做一个判断,是否当前页表为COW页。

int
copyout(pagetable_t pagetable, uint64 dstva, char *src, uint64 len)
{
  uint64 n, va0, pa0;
  
  while(len > 0){
    va0 = PGROUNDDOWN(dstva);
    
    if(uvmcowcopy(pagetable, va0) != 0) //在这里判断,如果是就分配,不是就return
      return -1;
    
    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;
  }
  return 0;
}

然后用脚本测试一下。

jimmy@ubuntu:~/xv6-test/xv6-labs-2020$ ./grade-lab-cow 
riscv64-linux-gnu-gcc -Wall -Werror -O -fno-omit-frame-pointer -ggdb -DSOL_COW -DLAB_COW -MD -mcmodel=medany -ffreestanding -fno-common -nostdlib -mno-relax -I. -fno-stack-protector -fno-pie -no-pie   -c -o kernel/trap.o kernel/trap.c
riscv64-linux-gnu-ld -z max-page-size=4096 -T kernel/kernel.ld -o kernel/kernel kernel/entry.o kernel/start.o kernel/console.o kernel/printf.o kernel/uart.o kernel/kalloc.o kernel/spinlock.o kernel/string.o kernel/main.o kernel/vm.o kernel/proc.o kernel/swtch.o kernel/trampoline.o kernel/trap.o kernel/syscall.o kernel/sysproc.o kernel/bio.o kernel/fs.o kernel/log.o kernel/sleeplock.o kernel/file.o kernel/pipe.o kernel/exec.o kernel/sysfile.o kernel/kernelvec.o kernel/plic.o kernel/virtio_disk.o 
riscv64-linux-gnu-ld: warning: cannot find entry symbol _entry; defaulting to 0000000080000000
riscv64-linux-gnu-objdump -S kernel/kernel > kernel/kernel.asm
riscv64-linux-gnu-objdump -t kernel/kernel | sed '1,/SYMBOL TABLE/d; s/ .* / /; /^$/d' > kernel/kernel.sym
== Test running cowtest == (7.7s) 
== Test   simple == 
  simple: OK 
== Test   three == 
  three: OK 
== Test   file == 
  file: OK 
== Test usertests == (143.1s) 
    (Old xv6.out.usertests failure log removed)
== Test   usertests: copyin == 
  usertests: copyin: OK 
== Test   usertests: copyout == 
  usertests: copyout: OK 
== Test   usertests: all tests == 
  usertests: all tests: OK 
== Test time == 
time: OK 
Score: 110/110

总结

这个实验代码量不多,但是debug比较困难,尤其是一些初始化做不好就会报一堆错。
[1]. https://pdos.csail.mit.edu/6.S081/2020/labs/cow.html
[2]. https://zhuanlan.zhihu.com/p/301027032

  • 11
    点赞
  • 17
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值