mit6.s081 lab6 实验记录

lab6: copy on write

本期实验实现了操作系统的写时复制功能(COW),cow是操作系统进程内存优化的一项机制。主要用于解决以下场景带来的问题:
假设父进程通过fork产生了一个子进程,子进程会复制父进程所有的内存地址,为了确保隔离,子进程会开辟新的物理内存,并将自己的页表建立映射。但往往子进程会调用exec用新的地址空间替换复制的得到的,因此子进程的页表和复制的物理内存全部会被丢弃。子进程复制父进程内存的过程中,如果父进程很大,将会产生一个很大的内存副本,占用内存资源,另外复制过程非常耗时,浪费了大量时间。
写时复制的机制是,在子进程复制父进程的内存时,将父进程的页表项改为只读,并新增一个COW标志,同时将子进程的页表映射到父进程同样的物理地址,权限均为只读。这样就可以避免子进程复制了大量的内存后直接丢弃造成的资源浪费。
假设子进程需要对复制的页面进行写操作,此时会触发一个page fault,内核的trap处理程序会捕获该错误,此时需要分配一个新的物理块,并并旧的内存数据复制进去,然后建立新的映射即可。

// cow page fault 的处理程序
int cowhander(pagetable_t pagetable, uint64 va)
{
  pte_t *pte = walk(pagetable, va, 0);
  if (*pte & PTE_COW)
  {
    uint64 old_pa = PTE2PA(*pte);     // 获取旧的物理地址
    *pte = (*pte | PTE_W) & ~PTE_COW; // 增加写标志 取消cow标志
    if (getparef(old_pa) == 1)        // 如果引用计数为1 那么不用再移动数据 只需要恢复写标志即可
    {
      return 0;
    }
    uint64 pa = (uint64)kalloc(); // 分配一个新的物理地址 同时pa的引用计数为1
    if (pa == 0)
    {
      return -2; // 说明当无空闲物理内存可用
    }
    memmove((void *)pa, (void *)old_pa, PGSIZE); // 复制数据
    int flag = PTE_FLAGS(*pte);
    *pte = PA2PTE(pa) | flag;
    kfree((void *)old_pa); // 尝试释放原物理内存
  }
  else
    return -1; // 说明该页面不是cow页面 本身就是一个只读的f
  return 0;
}

实验中的一些问题

1、考虑本身父进程本身就是只读的页面
在fork时,若父进程的某些页面本身就是只读的,对于这些页面,直接在对应的页块上建立映射即可,且页表项flags与父进程保持一致。注意不要添加cow标志,cow标志是为了在后续恢复子进程的可写权限时要用到的,如果本就不可写,那么就无需恢复。
2、为每个页块建立引用计数
由于进程可能多次fork,所以一个页块可能有多个引用,当释放物理块时,首先应该将引用计数减一,接着判断计数是否小于等于0,若是则说明该物理块已经可以释放了。在kalloc分配页块时,需要将引用计数设置初值1,以便与kfree统一起来。
3、引用计数的方式
在xv6系统中,可自由分配的物理内存大小为128MB,即从KERNELBASE到PHYSTOP区间内的地址可用。因此可以建立一个数组用来统计每个页块的引用次数。

#define PA2ID(p) ((p - KERNBASE) / PGSIZE) //获取块号P对应的数组下标
#define MAX_SIZE PA2ID(PHYSTOP) //计算数组的大小

struct
{
  struct spinlock lock; // 添加一个自旋锁
  int pageref[MAX_SIZE]; 
} refcnt;

#define GETPAGEREF(p) refcnt.pageref[PA2ID((uint64)p)] // 通过物理地址获得引用计数

另外,为了确保多个进程引用同一块物理内存,并同时对该进程的引用计数操作时能够正确统计数量,增加了一个自旋锁,每次对引用计数操作时需要先获得锁再进程操作。
4、考虑copyout将数据直接写入用户页面的情况
由于copyout是内核将内核数据写入用户页面,当发生page fault时,usertrap并不能捕获该异常。所以需要对copyout函数单独处理,事先判断一下用户页面是否是cow或者是否可写。

5、关于void *pa做函数的形参
因为void pa做形参是没有任何意义的,因此要想函数能够接受各种类型的数据,必须将形参设置为void *pa

uint64 pa = 0x....//这里pa保存一个地址数据
void fun(void *arg_pa){
//arg_pa的值仍然为pa的值,不过他的类型为void * 可以转为其他类型
}
fun((void *) pa); //调用

void *a,看起来是个指针,但可以接受任意类型的实参。实参的类型会变,但是实参的值拷贝到了a里面,若实参为一个地址,则可以通过a对改地址进行读写,需要进程类型转换,再解引用。若实参是一个变量,则a只是保存了该变量的值,并指向地址给该值的内存空间。
这也就是为什么kfree函数可以将pa直接转换成uint64数据的原因:

for (a = va; a < va + npages * PGSIZE; a += PGSIZE)
  {
    ....
    if (do_free)
    {
      uint64 pa = PTE2PA(*pte);
      kfree((void *)pa);
    }
    *pte = 0;
  }
//
void kfree(void *pa)
{
  struct run *r;

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

  acquire(&refcnt.lock); // 获取引用计数的自旋锁
  --GETPAGEREF(pa);      // 将引用计数减1 如果引用小于等于0 则说明可以释放物理内存了
  if (GETPAGEREF(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(&refcnt.lock);
}

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值