Implement copy-on write
copy-on-write fork需要解决什么问题? xv6中的fork()调用需要拷贝所有父进程中的user space memory给子进程,如果user space memory中的空间很大,那么这个拷贝的过程需要消耗非常多的时间。而且这个拷贝的动作很多时候都是浪费的,因为在fork()后往往还会调用exec(),之前所拷贝的user space memory需要被丢弃掉。 为了解决以上问题,需要实现copy-on-write fork。在COW fork中将会推迟对物理内存的分配和拷贝操作,一开始只是创建一个pagetable给子进程,该pagetable指向父进程的物理页,COW fork会将父进程和子进程中的pte标记为不可写的,当其中的一个进程试图对物理页进行写操作室,CPU就会发生page fault,kernel中的page fault handler会对page fault进行处理,handler会为发生page fault的process分配一个物理页,并将原来的物理页中的内容拷贝到新的物理页,然后修改发生page fault的process中的pte指向新分配的物理页,并将pte标记为可写入的。物理页的删除需要通过一个引用计数来记录有多少个process正在共享当前的物理页,当引用数量为0时,才真正地释放该物理页。 为了实现cow fork需要对xv6进行以下修改: 1.修改uvmcopy()中的代码,复制页表时不再分配新的物理页,而是使子进程中的pte指向父进程的物理页,清除父进程和子进程中的pte中的PTE_W位。
int
uvmcopy(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);
// 关闭写入位
*pte &= (~PTE_W);
flags = PTE_FLAGS(*pte);
// new process指向old process的物理页
if(mappages(new, i, PGSIZE, (uint64)pa, flags) != 0){
goto err;
}
incref((void*)pa);
}
return 0;
err:
uvmunmap(new, 0, i / PGSIZE, 1);
return -1;
}
2.修改usertrap中的代码,增加对page fault的处理,在处理page fault时,只需要对写入发生的page fault进行处理,对应的scause为15。当发生page fault时应该使用kalloc进行物理内存的分配,并将之前物理页的内容拷贝到新的物理页上,然后对新物理页的pte设置PTE_W位。在cowfault_handler中实现处理page fault的逻辑,我们并不需要对所有的vm都进行处理,异常的vm包括大于MAXVA的,tampoline和trapframe,这些页面都没有设置PTE_U位,所以根据PTE_U位可以判断该页面是否是要处理的cow页。
int cowfault_handler(uint64 va) {
if (va >= MAXVA)
return -1;
pte_t* pte = walk(myproc()->pagetable, va, 0);
if (pte == 0 || (*pte & PTE_V) == 0 || (*pte & PTE_U) == 0)
return -1;
uint64 cow_pa = PTE2PA(*pte);
uint64 cow_flags = PTE_FLAGS(*pte);
void* mem = kalloc();
if (mem == 0) {
return -1;
}
memmove(mem, (void*)cow_pa, PGSIZE);
*pte = PA2PTE(mem) | cow_flags | PTE_W;
kfree((void*)cow_pa);
return 0;
}
void
usertrap(void)
{
....
else if (r_scause() == 15) {
if (cowfault_handler(r_stval()) != 0)
myproc()->killed = 1;
}
....
3.对物理页的引用计数进行正确的维护,当kalloc()时将新的物理的引用计数设为1,当进程调用fork()时对物理页的引用计数增加1,当进程中的pte不指向物理页时引用计数减1,当调用kfree()时只有引用计数为0时才将物理页放回free list。物理页的引用计数可以通过一个数组来维护,通过物理页的地址除以4096可以获取物理页对应的下标,通过这个下标可以获取到该物理页对应的引用计数。 首先在kalloc.c中对引用计数数组进行声明,数量为PYSTOP / PGSIZE
int refcount[PHYSTOP / PGSIZE];
对于kfree(),主要工作就是将对应物理页的引用计数减1,如果引用计数为0释放该物理页。由于refcount可能同时被多个进程同时使用,所以需要上锁
void
kfree(void *pa)
{
struct run *r;
if(((uint64)pa % PGSIZE) != 0 || (char*)pa < end || (uint64)pa >= PHYSTOP)
panic("kfree");
acquire(&kmem.lock);
int page_idx = (uint64)pa / PGSIZE;
int cur_ref = --refcount[page_idx];
release(&kmem.lock);
if (cur_ref > 0)
return;
// 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);
}
我们另外还需要增加一个对物理页增加引用计数的函数incref(),在xv6中唯一可能发生物理页引用计数的场景就是在fork()时调用uvmcopy(),所以在uvmcopy中对vm拷贝完后需要调用incref增加对应物理页的引用计数
void incref(void *pa) {
acquire(&kmem.lock);
int page_idx = (uint64)pa / PGSIZE;
refcount[page_idx]++;
release(&kmem.lock);
}
int
uvmcopy(pagetable_t old, pagetable_t new, uint64 sz)
{
pte_t *pte;
uint64 pa, i;
uint flags;
for(i = 0; i < sz; i += PGSIZE){
....
// new process指向old process的物理页
if(mappages(new, i, PGSIZE, (uint64)pa, flags) != 0){
goto err;
}
incref((void*)pa);
}
...
}
4.修改copyout()中的代码,如果发生对COW page的写入动作需要和处理page fault保持相同的动作。我们通过pte中的PTE_W位来识别该页是否为COW页,如果该页不是异常页且PTE_W位为0,那么该页是COW页。
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 || (*pte & PTE_V) == 0 || (*pte & PTE_U) == 0)
return -1;
// cow fault
if ((*pte & PTE_W) == 0) {
uint64 cow_pa = PTE2PA(*pte);
uint64 cow_flags = PTE_FLAGS(*pte);
void* mem = kalloc();
if (mem == 0) {
return -1;
}
memmove(mem, (void*)cow_pa, PGSIZE);
*pte = PA2PTE(mem) | cow_flags | PTE_W;
kfree((void*)cow_pa);
}
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;
}