cow代码在这里。完成了lazy后,cow的实现就非常明了了……
经典写在前面。cow是copy-on-write的缩写(不是母牛┗|`O′|┛ 嗷~~),从字面上来看就是只在要写的时候复制内存。考虑这样一个情况:调用fork()
后,子进程是需要复制所有的父进程内存还是说当且仅当子进程或者父进程要写的时候才复制呢?答案显而易见了。这就是cow的核心思想。
按照惯例,这篇博客OS实验xv6 6.S081 开坑中给出了另一些有用的参考资料。
给出经典的官方指导书。
实验内容没啥可说的,就是在xv6中实现我们在写在前面里谈到的cow。
为了实现cow,MIT给出了一种方案:基于cow的fork()函数只在子进程中创建指向父进程物理页面的页表,而不创建真实的物理页面;在调用fork()函数后,子进程和父进程的pte(page table entry)均被置为不可写,并且予以一个COW标记,表示该pte是属于cow的,这样,当其中一个进程要写的时候,就会在trap.c
中捕捉到写错误同时发现va对应的pte是被COW标记的,就会对原物理页进行复制操作,并修改该pte映射的物理页为被复制的物理页。此外还需要注意一个细节:我们应该为每一块物理页面添加一个引用指针,用于记录它被进程引用的次数。当其引用次数为0的时候,我们就应该将其释放。这种情况对应着两个进程都复制了原物理页,那么原物理页就没有存在的必要了,调用kfree
释放即可。整个过程可以用下图描述。
下面,按照Hints,我们先更改fork
中调用的uvmcopy
函数,使其只复制页表而不复制物理页面,并且修改子进程和父进程的pte为不可写,并标记为PTE_COW
。代码如下:
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);
/** 将Parent和Child的PTE权限均改为不可写,且均为COW Page */
*pte = (*pte & ~PTE_W) | PTE_COW;
flags = PTE_FLAGS(*pte);
/**
* 不重新分配物理内存,指向pa即可
* flags = PTE_FLAGS(*pte);
* if((mem = kalloc()) == 0)
* goto err;
* memmove(mem, (char*)pa, PGSIZE); */
if(mappages(new, i, PGSIZE, (uint64)pa, flags) != 0){
/** kfree(mem); */
printf("uvmcopy():can not map page\n");
goto err;
}
addref("uvmcopy()",(void*)pa);
/** True */
/* printf("origin shoot: %p, new shoot: %p\n", walkaddr(old, i), walkaddr(new, i));
printf("origin perm: %x, new perm %x \n", PTE_FLAGS(*walk(old,i,0)), PTE_FLAGS(*walk(new,i,0)));
printf("origin perm & PTE_COW: %d, new perm & PTE_COW %d \n", PTE_FLAGS(*walk(old,i,0)) & PTE_COW, PTE_FLAGS(*walk(new,i,0)) & PTE_COW); */
}
return 0;
err:
uvmunmap(new, 0, i, 1);
return -1;
}
接着,我们需要处理trap.c
中的内容以处理PTE_COW
的写错误。这里写错误的标记为r_scause() == 15
。在if(pa)
后面,便是复制页面的操作了,这里要记住va映射到的被复制的页面的权限应该是可写且非PTE_COW的。
else if (r_scause() == 15) {
pte_t* pte;
uint64 va = PGROUNDDOWN(r_stval());
if (va >= MAXVA){
printf("va is larger than MAXVA!\n");
p->killed = 1;
goto end;
}
if (va > p->sz){
printf("va is larger than sz!\n");
p->killed = 1;
goto end;
}
pte = walk(p->pagetable, va, 0);
if(pte == 0 || ((*pte) & PTE_COW) == 0 || ((*pte) & PTE_V) == 0 || ((*pte) & PTE_U)==0){
printf("usertrap: pte not exist or it's not cow page\n");
p->killed=1;
goto end;
}
//printf("------------------------------\n");
//printf("pte addr: %p, pte perm: %x\n",pte, PTE_FLAGS(*pte));
if(*pte & PTE_COW){
//printf("usertrap():got page COW faults at %p\n", va);
char *mem;
if((mem = kalloc()) == 0)
{
printf("usertrap(): memery alloc fault\n");
p->killed = 1;
goto end;
}
memset(mem, 0, PGSIZE);
uint64 pa = walkaddr(p->pagetable, va);
if(pa){
memmove(mem, (char*)pa, PGSIZE);
int perm = PTE_FLAGS(*pte);
perm |= PTE_W;
perm &= ~PTE_COW;
if(mappages(p->pagetable, va, PGSIZE, (uint64)mem, perm) != 0){
printf("usertrap(): can not map page\n");
kfree(mem);
p->killed = 1;
goto end;
}
/** mem处是新的页,添加一处引用,原来的物理地址减少一处引用 */
kfree((void*) pa);
}
else
{
printf("usertrap(): can not map va: %p \n", va);
p->killed = 1;
goto end;
}
}
else
{
printf("usertrap(): not caused by cow \n");
p->killed = 1;
goto end;
}
}
接下来,根据Hint:Next, ensure that each physical page is freed when the last PTE reference to it goes away (but not before!), perhaps by implementing reference counts in kalloc.c,我们要在kalloc.c
中实现页面引用。这里的实现方式有一点trick,经过计算,kalloc最多可分配32723的物理页面,因此,我直接开辟了一个32723大小的ref
数组,用以记录。相关代码如下:
#define NPAGE 32723
char reference[NPAGE];
int
getrefindex(void *pa){
int index = ((char*)pa - (char*)PGROUNDUP((uint64)end)) / PGSIZE;
return index;
}
int
getref(void *pa){
return reference[getrefindex(pa)];
}
void
addref(char *tip, void *pa){
reference[getrefindex(pa)]++;
}
void
subref(char *tip,void *pa){
int index = getrefindex(pa);
if(reference[index] == 0)
return;
reference[index]--;
}
之后我们在free_range()
中初始化reference
数组:
void
freerange(void *pa_start, void *pa_end)
{
char *p;
p = (char*)PGROUNDUP((uint64)pa_start);
printf("start ~ end:%p ~ %p\n", p, pa_end);
for(; p + PGSIZE <= (char*)pa_end; p += PGSIZE){
/** 初始化ref_count */
reference[getrefindex(p)] = 0;
kfree(p);
}
}
在调用kalloc
后,物理页应该被引用1次:
void *
kalloc(void)
{
struct run *r;
acquire(&kmem.lock);
r = kmem.freelist;
if(r)
kmem.freelist = r->next;
release(&kmem.lock);
/** implementation of ref count */
/** r is the start of physical page */
if(r){
memset((char*)r, 5, PGSIZE); // fill with junk
int index = getrefindex((void *)r);
reference[index] = 1;
}
/** r出去后会被修改 */
return (void*)r;
}
接下来是重点,reference
的减少一定要在kfree
中进行,因为还有许多程序要用到kfree
,如果在kfree
之外进行,那就需要修改非常多的地方,这要感谢我同学ljj的指点,可惜他不写博客。代码如下:
void
kfree(void *pa)
{
struct run *r;
if(((uint64)pa % PGSIZE) != 0 || (char*)pa < end || (uint64)pa >= PHYSTOP)
panic("kfree");
// printf("----------------\n");
// printf("r->ref_count befroe: %d\n",((struct run *)pa)->ref_count);
// Fill with junk to catch dangling refs.
/** implementation of ref count */
//int ref_count = ((struct run *)pa)->ref_count;
/**
*
* 大坑:一定要在kfree中减,因为其他很多程序也会调用kfree,如此一来,那些程序就无法kfree掉
* 鸣谢:ljj同学
* */
subref("kfree()", (void *) pa);
int ref_count = getref(pa);
if(ref_count == 0){
//printf("!\n");
memset(pa, 1, PGSIZE);
// printf("r->ref_count after: %d\n",((struct run *)pa)->ref_count);
// printf("----------------\n");
r = (struct run*)pa;
//r->ref_count = ref_count;
acquire(&kmem.lock);
r->next = kmem.freelist;
kmem.freelist = r;
release(&kmem.lock);
}
}
最后,根据Hint:Finally, modify copyout() to use the same scheme as page faults when it encounters a COW page,我们去修改copyout
。为什么要修改copyout
呢,个人认为是因为copyout
是将Kernel Page复制给User Page,而用户进程要求页面是可读写的,因此要做出修改。代码如下:
int
copyout(pagetable_t pagetable, uint64 dstva, char *src, uint64 len)
{
uint64 n, va0, pa0;
pte_t *pte;
while(len > 0){
va0 = PGROUNDDOWN(dstva);
if(va0 >= MAXVA){
//printf("copyout(): va is greater than MAXVA\n");
return -1;
}
pte = walk(pagetable, va0, 0);
if(*pte & PTE_COW){
//printf("copyout(): got page COW faults at %p\n", va0);
char *mem;
if((mem = kalloc()) == 0)
{
printf("copyout(): memery alloc fault\n");
return -1;
}
memset(mem, 0, sizeof(mem));
uint64 pa = walkaddr(pagetable, va0);
if(pa){
memmove(mem, (char*)pa, PGSIZE);
int perm = PTE_FLAGS(*pte);
perm |= PTE_W;
perm &= ~PTE_COW;
if(mappages(pagetable, va0, PGSIZE, (uint64)mem, perm) != 0){
printf("copyout(): can not map page\n");
kfree(mem);
return -1;
}
kfree((void*) 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;
}
至此,我们完成了cow,运行make grade
,测试成功!(PS:记得要在根目录下添加time.txt
文件,该文件标识了该实验你做了多少个小时,不添加这个文件的话不能拿满分┭┮﹏┭┮)
OK,起飞