Lab6: Copy-on-Write Fork for xv6
跟着提示一步一步来
(1). 在kernel/riscv.h中选取PTE中的保留位定义标记一个页面是否为COW Fork页面的标志位
// 记录应用了COW策略后fork的页面#define PTE_F (1L << 8)
(2). 在kalloc.c中进行如下修改
定义引用计数的全局变量
ref,其中包含了一个自旋锁和一个引用计数数组,由于ref是全局变量,会被自动初始化为全0。这里使用自旋锁是考虑到这种情况:进程P1和P2共用内存M,M引用计数为2,此时CPU1要执行
fork产生P1的子进程,CPU2要终止P2,那么假设两个CPU同时读取引用计数为2,执行完成后CPU1中保存的引用计数为3,CPU2保存的计数为1,那么后赋值的语句会覆盖掉先赋值的语句,从而产生错误
struct ref_stru {struct spinlock lock;int cnt[PHYSTOP / PGSIZE]; // 引用计数} ref;
- 在
kinit中初始化ref的自旋锁
voidkinit(){initlock(&kmem.lock, "kmem");initlock(&ref.lock, "ref");freerange(end, (void*)PHYSTOP);}
- 修改
kalloc和kfree函数,在kalloc中初始化内存引用计数为1,在kfree函数中对内存引用计数减1,如果引用计数为0时才真正删除
voidkfree(void *pa){struct run *r;if(((uint64)pa % PGSIZE) != 0 || (char*)pa < end || (uint64)pa >= PHYSTOP)panic("kfree");// 只有当引用计数为0了才回收空间// 否则只是将引用计数减1acquire(&ref.lock);if(--ref.cnt[(uint64)pa / PGSIZE] == 0) {release(&ref.lock);r = (struct run*)pa;// Fill with junk to catch dangling refs.memset(pa, 1, PGSIZE);acquire(&kmem.lock);r->next = kmem.freelist;kmem.freelist = r;release(&kmem.lock);} else {release(&ref.lock);}}void *kalloc(void){struct run *r;acquire(&kmem.lock);r = kmem.freelist;if(r) {kmem.freelist = r->next;acquire(&ref.lock);ref.cnt[(uint64)r / PGSIZE] = 1; // 将引用计数初始化为1release(&ref.lock);}release(&kmem.lock);if(r)memset((char*)r, 5, PGSIZE); // fill with junkreturn (void*)r;}
- 添加如下四个函数,详细说明已在注释中,这些函数中用到了
walk,记得在defs.h中添加声明,最后也需要将这些函数的声明添加到defs.h,在cowalloc中,读取内存引用计数,如果为1,说明只有当前进程引用了该物理内存(其他进程此前已经被分配到了其他物理页面),就只需要改变PTE使能PTE_W;否则就分配物理页面,并将原来的内存引用计数减1。该函数需要返回物理地址,这将在copyout中使用到。
/*** @brief cowpage 判断一个页面是否为COW页面* @param pagetable 指定查询的页表* @param va 虚拟地址* @return 0 是 -1 不是*/int cowpage(pagetable_t pagetable, uint64 va) {if(va >= MAXVA)return -1;pte_t* pte = walk(pagetable, va, 0);if(pte == 0)return -1;if((*pte & PTE_V) == 0)return -1;return (*pte & PTE_F ? 0 : -1);}/*** @brief cowalloc copy-on-write分配器* @param pagetable 指定页表* @param va 指定的虚拟地址,必须页面对齐* @return 分配后va对应的物理地址,如果返回0则分配失败*/void* cowalloc(pagetable_t pagetable, uint64 va) {if(va % PGSIZE != 0)return 0;uint64 pa = walkaddr(pagetable, va); // 获取对应的物理地址if(pa == 0)return 0;pte_t* pte = walk(pagetable, va, 0); // 获取对应的PTEif(krefcnt((char*)pa) == 1) {// 只剩一个进程对此物理地址存在引用// 则直接修改对应的PTE即可*pte |= PTE_W;*pte &= ~PTE_F;return (void*)pa;} else {// 多个进程对物理内存存在引用// 需要分配新的页面,并拷贝旧页面的内容char* mem = kalloc();if(mem == 0)return 0;// 复制旧页面内容到新页memmove(mem, (char*)pa, PGSIZE);// 清除PTE_V,否则在mappagges中会判定为remap*pte &= ~PTE_V;// 为新页面添加映射if(mappages(pagetable, va, PGSIZE, (uint64)mem, (PTE_FLAGS(*pte) | PTE_W) & ~PTE_F) != 0) {kfree(mem);*pte |= PTE_V;return 0;}// 将原来的物理内存引用计数减1kfree((char*)PGROUNDDOWN(pa));return mem;}}/*** @brief krefcnt 获取内存的引用计数* @param pa 指定的内存地址* @return 引用计数*/int krefcnt(void* pa) {return ref.cnt[(uint64)pa / PGSIZE];}/*** @brief kaddrefcnt 增加内存的引用计数* @param pa 指定的内存地址* @return 0:成功 -1:失败*/int kaddrefcnt(void* pa) {if(((uint64)pa % PGSIZE) != 0 || (char*)pa < end || (uint64)pa >= PHYSTOP)return -1;acquire(&ref.lock);++ref.cnt[(uint64)pa / PGSIZE];release(&ref.lock);return 0;}
- 修改
freerange
voidfreerange(void *pa_start, void *pa_end){char *p;p = (char*)PGROUNDUP((uint64)pa_start);for(; p + PGSIZE <= (char*)pa_end; p += PGSIZE) {// 在kfree中将会对cnt[]减1,这里要先设为1,否则就会减成负数ref.cnt[(uint64)p / PGSIZE] = 1;kfree(p);}}
(3). 修改uvmcopy,不为子进程分配内存,而是使父子进程共享内存,但禁用PTE_W,同时标记PTE_F,记得调用kaddrefcnt增加引用计数
intuvmcopy(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);flags = PTE_FLAGS(*pte);// 仅对可写页面设置COW标记if(flags & PTE_W) {// 禁用写并设置COW Fork标记flags = (flags | PTE_F) & ~PTE_W;*pte = PA2PTE(pa) | flags;}if(mappages(new, i, PGSIZE, pa, flags) != 0) {uvmunmap(new, 0, i / PGSIZE, 1);return -1;}// 增加内存的引用计数kaddrefcnt((char*)pa);}return 0;}
(4). 修改usertrap,处理页面错误
uint64 cause = r_scause();if(cause == 8) {...} else if((which_dev = devintr()) != 0){// ok} else if(cause == 13 || cause == 15) {uint64 fault_va = r_stval(); // 获取出错的虚拟地址if(fault_va >= p->sz|| cowpage(p->pagetable, fault_va) != 0|| cowalloc(p->pagetable, PGROUNDDOWN(fault_va)) == 0)p->killed = 1;} else {...}
(5). 在copyout中处理相同的情况,如果是COW页面,需要更换pa0指向的物理地址
while(len > 0){va0 = PGROUNDDOWN(dstva);pa0 = walkaddr(pagetable, va0);// 处理COW页面的情况if(cowpage(pagetable, va0) == 0) {// 更换目标物理地址pa0 = (uint64)cowalloc(pagetable, va0);}if(pa0 == 0)return -1;...}
