本文主要是介绍MIT 6s081 lab6:Copy-on-Write Fork for xv6,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!
lab6:Copy-on-write fork
作业地址:Lab: Copy-on-Write Fork for xv6 (mit.edu)
实现 fork 懒复制机制,在进程 fork 后,不立刻复制内存页,而是将虚拟地址指向与父进程相同的物理地址。在父子任意一方尝试对内存页进行修改时,才对内存页进行复制。 物理内存页必须保证在所有引用都消失后才能被释放,这里需要有引用计数机制。
一开始做的时候没有上锁,一直没通过全部测试。后来参考了下面这位大佬的博客后意识到了竞争问题,加入自旋锁后顺利通过测试。
[mit6.s081] 笔记 Lab6: Copy-on-write fork | fork 懒拷贝 | Miigon’s blog
fork时不立刻复制内存
uvmcopy(),在复制父进程的内存到子进程的时候,不立刻复制数据,而是建立指向原物理页的映射,并将父子两端的页表项都设置为不可写。
int 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");panic("uvmcopy: page not present");pa = PTE2PA(*pte);// 如果原来是可写的,那就要改成不可写,并且加上COW标志if(*pte & PTE_W){*pte &= ~PTE_W; // 去掉写flag*pte |= PTE_C; //标记为写时复制状态}flags = PTE_FLAGS(*pte);if(mappages(new, i, PGSIZE, (uint64)pa, flags) != 0){// kfree(mem);goto err;}// 添加这个物理地址的引用计数acquire(&pgreflock);modify(GET_IDX(pa), 1);release(&pgreflock); }return 0;err:uvmunmap(new, 0, i / PGSIZE, 1);return -1;
}
在riscv.h中加入对标志位PTE_C的定义:
#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_C (1L << 8) // copy on write
这样,fork 时就不会立刻复制内存,只会创建一个映射了。这时候如果尝试修改懒复制的页,会出现 page fault 被 usertrap() 捕获。接下来需要在 usertrap() 中捕捉这个 page fault,并在尝试修改页的时候,执行真正的复制操作。
捕获写操作并执行复制
在 usertrap() 中添加对 page fault 的检测,检测接收的页是否是一个懒复制页(有PTE_C标志,并且虚拟地址合法),如果此时物理页的引用次数大于1,那么需要进行复制,将旧物理页的引用次数减一,分配的新物理页的引用次数加一;若引用次数为1,直接返回该页。
void
usertrap(void)
{int which_dev = 0;if((r_sstatus() & SSTATUS_SPP) != 0)panic("usertrap: not from user mode");// send interrupts and exceptions to kerneltrap(),// since we're now in the kernel.w_stvec((uint64)kernelvec);struct proc *p = myproc();// save user program counter.p->trapframe->epc = r_sepc();if(r_scause() == 8){// system callif(p->killed)exit(-1);// sepc points to the ecall instruction,// but we want to return to the next instruction.p->trapframe->epc += 4;// an interrupt will change sstatus &c registers,// so don't enable until done with those registers.intr_on();syscall();} else if((which_dev = devintr()) != 0){// ok} else {} else if(r_scause() == 15){ // page fault// 12 : instruction page fault// 13 :load page fault// 15 :store/AMO page Faultuint64 va = r_stval(); // get the virtual address that caused the page fault.pte_t *pte = walk(p->pagetable, va, 0);uint flags = PTE_FLAGS(*pte);uint64 old_pa = PTE2PA(*pte);if(flags & PTE_C && uvmcheckcowpage(va)) // 是写时复制场景{acquire(&pgreflock);int count = modify(GET_IDX((old_pa)),0);release(&pgreflock);if(count > 1) { // 引用次数超过1char * pa = kalloc(); // alloc physial memory ,分配一页物理内存if(pa == 0){ // 申请失败p->killed = 1; // 杀死进程exit(-1);}else{memset(pa, 0, PGSIZE); // 记录之前指向的内存地址的值,然后拷贝过去memmove(pa, (void * )old_pa, PGSIZE); kfree((void*)old_pa); //对于旧的地址,直接尝试删除// 增加新地址的引用次数,在kalloc中完成uint flags_new = (flags | (PTE_W)) & (~PTE_C); // 把原来的flag,清空掉PTE_C标志,加上PTE_W标志*pte = PA2PTE(pa) | flags_new | PTE_V; //修改pte}}else{uint flags_new = (flags | (PTE_W)) & (~PTE_C); *pte = PA2PTE(old_pa) | flags_new | PTE_V;}}else{p->killed = 1;exit(-1);} }else {printf("r_scause() == %d\n", r_scause());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();
}
同时 copyout() 由于是软件访问页表,不会触发缺页异常,所以需要手动添加同样的监测代码,检测接收的页是否是一个懒复制页(有PTE_C标志,并且虚拟地址合法),如果此时物理页的引用次数大于1,那么需要进行复制,将旧物理页的引用次数减一,分配的新物理页的引用次数加一;若引用次数为1,直接返回该页。
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);if(pa0 == 0)return -1;// 这里可能会往一个cow page的页进行写操作,这时需要及时复制,和trap类似的操作pte_t *pte = walk(pagetable, va0, 0);if((*pte & PTE_U) == 0) return -1;uint flags = PTE_FLAGS(*pte);if((flags & PTE_C) && (flags & PTE_V) && uvmcheckcowpage(dstva)) // 是写时复制场景{acquire(&pgreflock);int count = modify(GET_IDX((pa0)),0);release(&pgreflock);if(count > 1) { // 引用次数超过1char * pa = kalloc(); // alloc physial memory ,分配一页物理内存if(pa == 0){ // 申请失败return -1;}else{memset(pa, 0, PGSIZE);// 记录之前指向的内存地址的值,然后拷贝过去uint64 old_pa = pa0;memmove(pa, (void * )old_pa, PGSIZE); kfree((void*)old_pa); //对于旧的地址,直接尝试删除uint flags_new = (flags | (PTE_W)) & (~PTE_C); // 把原来的flag,清空掉PTE_C标志,加上PTE_W标志*pte = PA2PTE(pa) | flags_new | PTE_V;pa0 = (uint64)pa; // 更新读写的位置}}else{ //此时引用次数为1uint flags_new = (flags | (PTE_W)) & (~PTE_C); *pte = PA2PTE(pa0) | flags_new | PTE_V;}} 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;
}
提供工具方法:uvmcheckcowpage
int uvmcheckcowpage(uint64 va) {struct proc *p = myproc();return va < p->sz;
}
到这里,就已经确定了大体的逻辑了:在 fork 的时候不复制数据只建立映射+标记,在进程尝试写入的时候进行实复制并重新映射为可写。
接下来,还需要做页的生命周期管理,确保在所有进程都不使用一个页时才将其释放
在原本的 xv6 实现中,一个物理页的生命周期内,可以支持以下操作:
- kalloc(): 分配物理页
- kfree(): 释放回收物理页
而在支持了懒分配后,由于一个物理页可能被多个进程(多个虚拟地址)引用,并且必须在最后一个引用消失后才可以释放回收该物理页,所以一个物理页的生命周期内,现在需要支持以下操作:
- kalloc,分配物理页时,引用次数加一
- fork,将旧的物理页的引用次数加一
- 写时复制场景usertrap\copyout:把旧的物理页的引用次数减一,新分配的物理页次数加一
- kfree:对物理页的引用次数减一,当计数为0,才真正释放物理页
记录物理页的引用次数
在kalloc.c中添加一个全局数组,用于记录物理页的引用次数,并提供Modify方法,用于修改引用计数数组
int rc_array[(PHYSTOP - KERNBASE) / PGSIZE];
struct spinlock pgreflock; // 用于 rc_array 数组的锁,防止竞态条件引起内存泄漏int modify(int x,int c)
{if(x < 1){return -1;}rc_array[x] += c;if(rc_array[x] < 0) rc_array[x] = 0;return rc_array[x];
}
修改kfree函数:引用次数为0才能删去
void
kfree(void *pa)
{struct run *r;if(((uint64)pa % PGSIZE) != 0 || (char*)pa < end || (uint64)pa >= PHYSTOP){panic("kfree");}// acquire(&pgreflock);// 添加对引用次数的判断,如果引用次数为1,才能删去acquire(&pgreflock);int count = modify(GET_IDX(pa), -1); // 把自己的引用计数减一,如果引用计数还是大于0,说明不能删除,则返回。release(&pgreflock);if(count <= 0){// Fill with junk to catch dangling refs.// 如果引用计数为-1,说明还未分配空间,那么往下执行,分配空间// 如果引用计数为0,说明空间已经不被使用了,释放空间memset(pa, 1, PGSIZE);r = (struct run*)pa;acquire(&kmem.lock);r->next = kmem.freelist;kmem.freelist = r;release(&kmem.lock);}}
修改Kalloc函数,分配物理页时将引用次数加一
void * // 添加对引用次数+1的功能
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// lab6 add if(r){acquire(&pgreflock);modify(GET_IDX(r), 1);release(&pgreflock);}return (void*)r;
}
在usertrap和copyout函数中,加入判断,当物理页的引用次数为1时,说明此时只有一个进程使用这个物理页,那么就不用进行复制,直接返回这个物理页即可
加锁防止竞争
在rc_array添加了自旋锁pgreflock,在使用modify方法时,都使用acquire("&pgreflock")
和release(&pgreflock)
来对操作进行保护,防止竞争导致的内存泄露。
如果出现了内存泄露问题,在运行Make grade后,系统会提示usertests调用前后的空闲页数量不同。
FAILED -- lost some free pages 32447 (out of 32448)
在以下四种场景调用Modify操作中加入锁进行保护。
- kalloc
- fork
- 写时复制场景usertrap\copyout
- kfree
提交
$ make qemu-gdb
(7.1s)
== Test simple == simple: OK
== Test three == three: OK
== Test file == file: OK
== Test usertests ==
$ make qemu-gdb
(78.3s)
== 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
这篇关于MIT 6s081 lab6:Copy-on-Write Fork for xv6的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!