MIT xv6 2020系列实验:Lab6 Copy-on-Write Fork
实验六:fork懒更新页表。
这次优化的内容是针对fork时内存复制的优化。在fork后,子进程很可能只用到了父进程中内存资源的一小部分,但是却完整地拷贝了父进程的内存,除那一小部分,剩下的资源都被浪费掉了,凭空增加了开销。
这就很不合理,我们结合提示想想应该怎么减少不必要的开销。有了实验五懒更新的基础,实验六应该还是挺好做的,只需要将懒更新的思路应用到页表上,对于一个进程,在fork时我们将子进程页表的叶子节点与其共享,并将其修改为只读,当读取到只读页,内核陷入页表异常,我们就在页表异常处将该页对应的物理资源正式复制一份可写页表给子进程。
前置trick:pte中8~9位是保留位,这次把第8位作为fork懒复制的标志位,记为PTE_COW。
顺藤摸瓜,首先修改fork中的uvmcopy:
int
uvmcopy(pagetable_t old, pagetable_t new, uint64 sz)
{
pte_t *pte;
uint64 pa, i;
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 &= ~PTE_W;
*pte |= PTE_COW;
if(mappages(new, i, PGSIZE, (uint64) pa, PTE_FLAGS(*pte)) != 0){
goto err;
}
}
return 0;
err:
uvmunmap(new, 0, i / PGSIZE, 1);
return -1;
}
将物理页的属性修改为只读,标上懒更新标记。
copy有了,考虑释放进程时只读标记的变化,当物理页没有引用时,就应该删除他。
为此创建一个结构体记录每个物理页的引用:
struct {
struct spinlock lock;
int mem_map[(PHYSTOP - KERNBASE) >> PGSHIFT];
}kernel_tagger;
在映射与解映射时都对引用进行加减操作:
int
mappages(pagetable_t pagetable, uint64 va, uint64 size, uint64 pa, int perm)
{
uint64 a, last;
pte_t *pte;
a = PGROUNDDOWN(va);
last = PGROUNDDOWN(va + size - 1);
for(;;){
if((pte = walk(pagetable, a, 1)) == 0)
return -1;
if(*pte & PTE_V)
panic("remap");
if(perm & PTE_COW){
acquire(&kernel_tagger.lock);
if (kernel_tagger.mem_map[(pa - KERNBASE) >> PGSHIFT] == 0)
kernel_tagger.mem_map[(pa - KERNBASE) >> PGSHIFT] += 2;
else
kernel_tagger.mem_map[(pa - KERNBASE) >> PGSHIFT] += 1;
release(&kernel_tagger.lock);
}
*pte = PA2PTE(pa) | perm | PTE_V;
if(a == last)
break;
a += PGSIZE;
pa += PGSIZE;
}
return 0;
}
void
uvmunmap(pagetable_t pagetable, uint64 va, uint64 npages, int do_free)
{
uint64 a;
pte_t *pte;
if((va % PGSIZE) != 0)
panic("uvmunmap: not aligned");
for(a = va; a < va + npages*PGSIZE; a += PGSIZE){
if((pte = walk(pagetable, a, 0)) == 0)
panic("uvmunmap: walk");
if((*pte & PTE_V) == 0)
panic("uvmunmap: not mapped");
if(PTE_FLAGS(*pte) == PTE_V)
panic("uvmunmap: not a leaf");
uint64 pa = PTE2PA(*pte);
if(do_free){
if (*pte & PTE_COW) {
acquire(&kernel_tagger.lock);
if (--kernel_tagger.mem_map[(pa - KERNBASE) >> PGSHIFT] == 0)
kfree((void*)pa);
release(&kernel_tagger.lock);
} else
kfree((void*)pa);
}
*pte = 0;
}
}
在一个页面被复制且标记数为0时,我们才释放它,如果没有被复制过,就不考虑标记数。
接下来在trap中增加复制的部分:
...
} else if((which_dev = devintr()) != 0){
// ok
} else if (r_scause() == 15) {
if (uvmmapfork(p->pagetable, PGROUNDDOWN(r_stval())) != 0)
p->killed = 1;
} 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;
}
...
如果有页面异常,通过调用重分配函数给虚拟地址重新分配一个物理页表,拷贝出原物理页。
拷贝函数如下:
int
uvmmapfork(pagetable_t pagetable, uint64 va){
pte_t *pte;
if((va % PGSIZE) != 0)
panic("uvmmapfork: not aligned");
if(va >= MAXVA)
return -1;
if((pte = walk(pagetable, va, 0)) == 0)
panic("uvmmapfork: walk");
if((*pte & PTE_V) == 0)
panic("uvmmapfork: not mapped");
if((*pte & PTE_COW) == 0)
panic("uvmmapfork: not copy-on-write");
if(PTE_FLAGS(*pte) == PTE_V)
panic("uvmmapfork: not a leaf");
if((*pte & PTE_U) == 0)
return -1;
uint64 pa = PTE2PA(*pte);
acquire(&kernel_tagger.lock);
char *mem;
if (kernel_tagger.mem_map[(pa - KERNBASE) >> PGSHIFT] > 1) {
if((mem = kalloc()) == 0)
goto err;
memmove(mem, (void*)pa, PGSIZE);
} else if (kernel_tagger.mem_map[(pa - KERNBASE) >> PGSHIFT] == 1) {
mem = (char *)pa;
} else
goto err;
uint64 perm = PTE_FLAGS(*pte);
perm &= ~PTE_COW;
perm |= PTE_W;
*pte = PA2PTE(mem) | perm;
kernel_tagger.mem_map[(pa - KERNBASE) >> PGSHIFT]--;
release(&kernel_tagger.lock);
return 0;
err:
release(&kernel_tagger.lock);
return -1;
}
uvm的部分都完成了,还有copyout需要修改,这是一个大坑,因为是内核修改用户页,不会经过用户页表合法标记位的审查,所以这里对copyout的每一页都要进行地址重分配。
int
copyout(pagetable_t pagetable, uint64 dstva, char *src, uint64 len)
{
uint64 n, va0, pa0;
while(len > 0){
va0 = PGROUNDDOWN(dstva);
if (va0 > MAXVA)
return -1;
pte_t *pte = walk(pagetable, va0, 0);
if(pte == 0)
return -1;
if((*pte & PTE_V) == 0)
return -1;
if((*pte & PTE_COW) && (uvmmapfork(pagetable, va0) != 0))
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;
}
结束,编译!本次实验告一段落