xv6中的fork()系统调用将父进程的所有用户空间内存复制到子进程中。 如果父节点很大,复制可能需要很长时间。 更糟糕的是,这些工作往往大部分都被浪费了; 例如,子进程中的fork()后跟exec()将导致子进程丢弃复制的内存,可能根本不会使用其中的大部分内存。 另一方面,如果父母和孩子都使用一个页面,并且其中一个或双方都写了它,那么副本是非常必要的。
写时拷贝(COW) fork()的目标是推迟为子进程分配和复制物理内存页,直到实际需要拷贝(如果需要的话)。
COW fork()只为子进程创建一个可分页的页面,用户内存的pte指向父进程的物理页面。 COW fork()将所有父用户和子用户的pte都标记为不可写。 当任何一个进程尝试写这些COW页时,CPU将强制出现页错误。 内核页故障处理程序检测到这种情况,为故障进程分配物理内存页,将原始页复制到新页,并修改故障进程中的相关PTE以引用新页,这次PTE标记为可写。 当页面错误处理程序返回时,用户进程将能够编写页面的副本。
COW fork()使实现用户内存的物理页的释放变得有点棘手。 一个给定的物理页可能会被多个进程的页表引用,只有当最后一个引用消失时才会释放。
Implement copy-on write(hard)
要求:
您的任务是在xv6内核中实现写时复制的fork。 如果修改后的内核成功地执行了cowtest和usertests程序,那么就完成了。
为了帮助你测试你的实现,我们提供了一个名为cowtest的xv6程序(源代码在user/cowtest.c中)。 Cowtest运行各种测试,但即使第一个也会在未修改的xv6上失败。 因此,一开始,你会看到:
$ cowtest
simple: fork() failed
$
“simple”测试分配一半以上的可用物理内存,然后fork()。 fork失败是因为没有足够的空闲物理内存给子进程提供父进程内存的完整副本。
当你完成时,你的内核应该通过所有的测试在cowtest和用户测试。 那就是:
$ cowtest
simple: ok
simple: ok
three: zombie!
ok
three: zombie!
ok
three: zombie!
ok
file: ok
ALL COW TESTS PASSED
$ usertests
…
ALL TESTS PASSED
$
这是一个合理的计划:
修改uvmcopy()将父节点的物理页映射到子节点,而不是分配新的页。 在父、子pte中均清除PTE_W。
修改usertrap()以识别页面错误。 当COW页面出现页面错误时,使用kalloc()分配一个新页面,将旧页面复制到新页面,并使用PTE_W set将新页面安装到PTE中。
确保当最后一个PTE引用消失时释放每个物理页,而不是在它之前。 这样做的一种好方法是,对于每个物理页面,保持引用该页的用户页面表的数量的“引用计数”。 当kalloc()分配一个页面时,将它的引用计数设置为1。 当fork导致子进程共享页时,增加页的引用计数;当任何进程将页从其页表中删除时,减少页的计数。 Kfree()只在页面的引用计数为零时才会将其放回空闲列表。 将这些计数保存在固定大小的整数数组中是可以的。 您必须制定出如何为数组建立索引以及如何选择其大小的方案。 例如,您可以用页面的物理地址除以4096为数组建立索引,并为数组指定若干元素,这些元素等于kalloc.c中kinit()放置在空闲列表中的任何页面的最高物理地址。
修改copyout()以在遇到COW页面时使用与页面错误相同的方案。
提示:
lazy page allocation实验可能使您熟悉了许多与“写时复制”相关的xv6内核代码。 但是,您不应该将此实验建立在lazy allocation解决方案的基础上; 相反,请按照上面的指示从一个新的xv6版本开始。
对于每个PTE,有一种记录它是否是COW映射的方法可能是有用的。 您可以使用RISC-V PTE中的RSW(reserved for software)位。
Usertests会探索cowtest无法测试的场景,所以不要忘记检查所有的测试是否都通过了。
一些有用的宏和页表标记的定义在kernel/riscv.h的末尾。
如果发生COW页面错误,并且没有空闲内存,则应该终止进程。
代码:
/*
vm.c
*/
int refNum[(PHYSTOP-KERNBASE)/PGSIZE];//新增refNum引用计数
/*
vm.c
*/
#define PTE_COW (1L << 8) // 用保留位 新增PTE_COW标志位
/*
vm.c
让进程fork时,不赋物理页,而是child进程页表指向parent进程的物理页,
但标记要将parent和child的PTE都清除PTE_W标志位,并添加COW标志位,表明两个PTE指向一个物理页。
*/
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 & ~PTE_W;//清除PTE_W
*pte = *pte | PTE_COW;//添加COW标志位
flags = PTE_FLAGS(*pte);
//if((mem = kalloc()) == 0)
//goto err;
//memmove(mem, (char*)pa, PGSIZE);
if(mappages(new, i, PGSIZE, pa, flags) != 0){
//kfree(mem);
goto err;
}
}
return 0;
err:
uvmunmap(new, 0, i / PGSIZE, 1);
return -1;
}
/*
vm.c
修改kernel/vm.c的mappages(),在页表与物理页绑定时,增加refNum对应元素计数。
*/
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");
*pte = PA2PTE(pa) | perm | PTE_V;
if (pa>=KERNBASE){
refNum[(pa - KERNBASE)/PGSIZE] += 1;//增加refNum对应元素计数
}
if(a == last)
break;
a += PGSIZE;
pa += PGSIZE;
}
return 0;
}
/*
vm.c
修改kernel/vm.c的uvmunmap(),在页表与物理页解绑时,减少refNum对应元素计数,当refNum==1时,释放内存。
*/
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(pa>=KERNBASE){
refNum[(pa-KERNBASE)/PGSIZE]-=1;//减少refNum对应元素计数
}
if(do_free){
if(refNum[(pa-KERNBASE)/PGSIZE]==1){
kfree((void*)pa);//释放内存
}
}
*pte = 0;
}
}
/*
trap.c
修改kernel/trap.c的usertrap(),引入refNum,
在发生page fault时,若该虚拟地址关联的PTE,表明关联的物理页是一个COW页,则新申请一个物理页,
让此虚拟地址指向新物理页,并修改refNum计数。
当COW页面出现页面错误时,使用kalloc()分配一个新页面,将旧页面复制到新页面,并使用PTE_W set将新页面安装到PTE中。
*/
else if(r_scause() == 13 || r_scause() == 15){//若该虚拟地址关联的PTE,表明关联的物理页是一个COW页,则新申请一个物理页,让此虚拟地址指向新物理页,并修改refNum计数。
pte_t *pte;
uint64 addr = r_stval();
addr = PGROUNDDOWN(addr);
if((pte = walk(p->pagetable, addr, 0)) == 0 || !(*pte & PTE_COW)){
p->killed = 1;
} else {
char *mem;
uint64 pa = PTE2PA(*pte);
uint flags;
if(refNum[(pa - KERNBASE)/PGSIZE] == 2){
*pte = *pte | PTE_W;
*pte = *pte & ~PTE_COW;
}else{
if((mem=kalloc())==0){
p->killed=1;
}else{
refNum[(pa - KERNBASE)/PGSIZE]-=1;
memmove(mem,(char*)pa,PGSIZE);
*pte = *pte | PTE_W;
*pte = *pte & ~PTE_COW;
flags= PTE_FLAGS(*pte);
*pte = PA2PTE((uint64)mem) | flags;
refNum[((uint64)mem - KERNBASE)/PGSIZE]+=1;
}
}
}
}
/*
vm.c
类似usertrap,copyout也要增加页面错误处理,因为copyout是在内核中调用的,缺页不会进入usertrap
*/
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;
pte_t *pte;
if((pte = walk(pagetable, va0, 0)) == 0){
return -1;
}
if(*pte & PTE_COW){
char *mem;
uint flags;
if(refNum[(pa0 - KERNBASE)/PGSIZE] == 2){
*pte = *pte | PTE_W;
*pte = *pte & ~PTE_COW;
}else{
if((mem=kalloc())==0){
return -1;
}else{
refNum[(pa0 - KERNBASE)/PGSIZE]-=1;
memmove(mem,(char*)pa0,PGSIZE);
*pte = *pte | PTE_W;
*pte = *pte & ~PTE_COW;
flags= PTE_FLAGS(*pte);
*pte = PA2PTE((uint64)mem) | flags;
refNum[((uint64)mem - KERNBASE)/PGSIZE]+=1;
pa0 = (uint64)mem;
}
}
}
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;
}
结果: