本Lab为xv6添加一个copy on write的功能。
笔者用时约6h(太菜啦 不难但是细节多
xv6中原始fork
系统的实现是,当产生一个子进程时,直接把父进程页表中的每一页复制给子进程的页表,这样子做无疑有许多物理空间被浪费,因为并不是每一个空间都会在之后被修改。一个经典的思路就是copy on write,也就是一开始父子进程共享一块物理空间,当某一个物理空间需要被写的时候,再进行复制。
具体的做法就是,当父进程fork
出一个子进程的时候,关闭父进程所有页表项的写权限,然后进行复制,复制时父子进程映射到相同的物理地址空间。当代码对没有写权限的页表项进行写操作时,有两种情况,分为内核态与用户态,如果在用户态,则会产生page fault
;如果在内核态,则是在copyout
函数中;两种情况的处理方式相同,都是为错误的虚拟地址重新分配一页物理内存,具体细节见下文。
在写代码之前最好先把细节都想清楚,不然就会像笔者一样产生一堆bug >_<
由于fork
函数中会调用uvmcopy
函数对页表进行复制,故首先修改uvmcopy
函数,将父进程的物理页映射到子进程中,而不是分配新物理页,并清空父子进程对应PTE
中的PTE_W
标志。具体代码如下,其中的updateref
函数之后会讲。
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");
pa = PTE2PA(*pte);
// remove the PTE_W flag
flags = PTE_FLAGS(*pte);
if ((flags & PTE_W) != 0) {
flags ^= PTE_W;
*pte ^= PTE_W;
}
updateref((void*) pa, 1);
// map child's va to parent's pa
if(mappages(new, i, PGSIZE, pa, flags) != 0){
kfree((void*) pa);
goto err;
}
}
return 0;
err:
uvmunmap(new, 0, i / PGSIZE, 1);
return -1;
}
接下来需要修改usertrap
函数,以便在page fault
出现时进行处理。类似lazy lab,需要先判断是否为page fault,这里只需要处理scause
为15的情况(13对应load的page fault,15对应store的)。首先需要判断当前错误的虚拟地址对应的PTE是否是不可写状态(文档里说重新用一个位来标志当前页面是否为cow页,但是在这里我直接用是否不可写来判断了,好像不太严谨但是也能过测试),之后便是分配新的物理页,并复制旧物理页内容到新物理页中,把旧物理页映射删除(这里用uvmunmap
是有必要的,后面会讲)。
else if(r_scause() == 15) {
uint64 va = PGROUNDDOWN(r_stval());
pte_t* pte = walk(p->pagetable, va, 0);
uint64 flags = PTE_FLAGS(*pte);
if (((*pte) & PTE_W) == 0) {
uint64 pa = (uint64) kalloc();
if (pa == 0) {
p->killed = 1;
} else {
uint64 oldpa = walkaddr(p->pagetable, va);
memmove((void*) pa, (void*) oldpa, PGSIZE);
uvmunmap(p->pagetable, va, 1, 1);
mappages(p->pagetable, va, PGSIZE, pa, flags | PTE_W);
}
}
}
在copyout
函数中也差不多,处理内核可能发生的写失败问题(内核只有在这个函数中才会写用户空间),处理代码如下所示。
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;
// ======= mycode
pte_t* pte = walk(pagetable, va0, 0);
if (((*pte) & PTE_W) == 0) {
uint64 flags = PTE_FLAGS(*pte);
uint64 oldpa = pa0;
pa0 = (uint64) kalloc();
if (pa0 == 0) return -1;
memmove((void*) pa0, (void*) oldpa, PGSIZE);
uvmunmap(pagetable, va0, 1, 1);
mappages(pagetable, va0, PGSIZE, pa0, flags | PTE_W);
}
// =======
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;
}
最后还有一个重点,就是对于每一页物理页,我们不能在kfree
函数调用时就直接把它回收,因为它可能还被其他进程所共享。一种解决方案是,为每一页物理页保留引用该页面的进程数,在kalloc
函数中将该物理页的引用数置为1;在父进程fork
时也就是uvmcopy
函数中;增加物理页的引用计数(也就是之前说的updateref
函数);在kfree
中,减少该物理页的引用计数(如果为0则不减,一开始会在kinit
中调用kfree
进行初始化所以会为0),如果减少之后引用计数不为0,则不回收该物理页。
由于该引用计数数组需要被多个CPU共享,于是需要加个自旋锁,定义数据结构如下。其中,数组ref的大小其实只需要(PHYSTOP - end) / PGSIZE
。
struct {
struct spinlock lock;
int ref[(PHYSTOP-KERNBASE) / PGSIZE];
} kmemref;
然后定义两个函数,方便引用计数的读与修改。其中updateref
函数将物理页r
的引用计数增加k,getref
函数获取某个物理页的引用计数。
void
updateref(void* r, int k)
{
acquire(&kmemref.lock);
kmemref.ref[((uint64)r - KERNBASE) / PGSIZE] += k;
release(&kmemref.lock);
}
int
getref(void* r)
{
acquire(&kmemref.lock);
int res = kmemref.ref[((uint64)r - KERNBASE) / PGSIZE];
release(&kmemref.lock);
return res;
}
最后修改kalloc
和kfree
函数,如下所示。这里重新解释一下uvmunmap
调用的必要性,其实就是利用uvmunmap
会调用kfree
函数的特点,将对应物理页的引用计数减一,这样就很方便啦。
void
kfree(void *pa)
{
struct run *r;
if(((uint64)pa % PGSIZE) != 0 || (char*)pa < end || (uint64)pa >= PHYSTOP)
panic("kfree");
// mycode
if (getref(pa) > 0)
updateref(pa, -1);
if (getref(pa) > 0) return;
// mycode end
// Fill with junk to catch0 dangling refs.
memset(pa, 1, PGSIZE);
r = (struct run*)pa;
acquire(&kmem.lock);
r->next = kmem.freelist;
kmem.freelist = r;
release(&kmem.lock);
}
void *
kalloc(void)
{
struct run *r;
acquire(&kmem.lock);
r = kmem.freelist;
if(r) {
kmem.freelist = r->next;
// mycode
updateref((void*)r, 1);
}
release(&kmem.lock);
if(r) {
memset((char*)r, 5, PGSIZE); // fill with junk
}
return (void*)r;
}
不知道为啥把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");
pa = PTE2PA(*pte);
// remove the PTE_W flag
flags = PTE_FLAGS(*pte);
if ((flags & PTE_W) != 0) {
flags ^= PTE_W;
*pte ^= PTE_W;
}
// map child's va to parent's pa
if(mappages(new, i, PGSIZE, pa, flags) != 0){
// 删了这里
// kfree((void*) pa);
goto err;
}
// 改了这里
updateref((void*) pa, 1);
}
return 0;
err:
uvmunmap(new, 0, i / PGSIZE, 1);
return -1;
}