Lab: Copy-on-Write Fork for xv6
实验目标
本实验你的目标是实现xv6上的 copy-on-write
实验实现
copy-on-write介绍
copy-on-write 是指当你创建子进程时,并不实际复制父进程的空间地址的内容到新的物理内存,而是将页表项改为只读,并在第一次父/子进程要改变页面内容时才进行复制,并将原来的页面解锁为可读写。这种做法节省了大量的物理内存,尤其是现在大部分程序在 fork() 和通常立马会接上 exce() 载入新的程序 。总体来说本实验的思路跟lazy alloc是比较类似的
hints
- 修改 uvmcopy() ,使子进程的页表映射到父进程的物理内存,并清除写权限
- 修改usertrap() , 识别cow 的页面错误, 为引起cow 页面错误的进程分配复制页表以及物理内存,将原页面写权限解锁
- 确保物理内存在没有引用的情况下才被释放。可以用一个数组保存引用计数,用 物理地址/PGSIZTE 来区分不同的物理页
- 修改 copyout() 来处里再内核中遇到 cow 页面错误的情况
- 使用标志位来记录PTE 是否为cow 页面,可使用RSW区域
- risc.h 底部有一些关于页表标识的宏定义
实现思路
-
新增数据结构/标志位保存必要信息
- 使用第pte第8位标记 cow 页面
/* riscv.h */ #define PTE_V (1L << 0) // valid #define PTE_R (1L << 1) #define PTE_W (1L << 2) #define PTE_X (1L << 3) #define PTE_U (1L << 4) // 1 -> user can access #define PTE_COW (1L << 8)
- 新增 mem_ref 记录某块物理内存引用计数,以及引用相关操作
/* kalloc.c */ struct mem_ref { struct spinlock lock; int cnt; }; struct mem_ref mem_ref[PHYSTOP/PGSIZE]; //初始化引用计数的锁 void kinit() { for(int i = 0; i < PHYSTOP/PGSIZE; ++i) initlock(&(mem_ref[i].lock), "kmem_ref"); initlock(&kmem.lock, "kmem"); freerange(end, (void*)PHYSTOP); } //初始化引用计数 void freerange(void *pa_start, void *pa_end) { char *p; p = (char*)PGROUNDUP((uint64)pa_start); for(; p + PGSIZE <= (char*)pa_end; p += PGSIZE) { mem_ref[(uint64)p/PGSIZE].cnt = 1; kfree(p); } } //引用计数为0时才释放物理内存 void kfree(void *pa) { struct run *r; if(((uint64)pa % PGSIZE) != 0 || (char*)pa < end || (uint64)pa >= PHYSTOP) panic("kfree"); uint64 pi = (uint64)pa/PGSIZE; acquire(&(mem_ref[pi].lock)); mem_ref[pi].cnt = mem_ref[pi].cnt - 1; if(mem_ref[pi].cnt > 0) { release(&(mem_ref[pi].lock)); return; } ...... } //分配物理内存时初始化引用计数为1 void * kalloc(void) { ...... if(r) { uint64 pi = (uint64)r/PGSIZE; acquire(&(mem_ref[pi].lock)); mem_ref[pi].cnt = 1; release(&(mem_ref[pi].lock)); kmem.freelist = r->next; } ...... } /* * 用于操作引用计数的辅助函数 */ //not thread safe int get_mem_ref(uint64 pa) { return mem_ref[(uint64)pa/PGSIZE].cnt; } int add_ref(uint64 pa) { if(((uint64)pa % PGSIZE) != 0 || (char*)pa < end || (uint64)pa >= PHYSTOP) return -1; acquire(&(mem_ref[pa/PGSIZE].lock)); mem_ref[(uint64)pa/PGSIZE].cnt = mem_ref[(uint64)pa/PGSIZE].cnt + 1; release(&(mem_ref[pa/PGSIZE].lock)); return 1; }
-
修改父子进程间页表复制,将父子页表都标志为 cow 页、撤销写权限,并映射到同一块物理内存,增加那块物理内存的引用。
/* vm.c */
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); // set the write flag fault
*pte = (*pte) | (PTE_COW);// set the cow flag true
flags = PTE_FLAGS(*pte);
//map the child pte to it's parent's phyical address
if(mappages(new, i, PGSIZE, (uint64)pa, flags) != 0){
goto err;
}
add_ref(pa);
}
return 0;
err:
uvmunmap(new, 0, i / PGSIZE, 1);
return -1;
}
- 做了cow 页面修改后,在用户态访问cow 页面会发生缺页中断,内核态则在copy_out中会写入cow 页面引起冲突。接下来就处理这两种情况。
/* vm.c */
/*
操作cow页面的辅助函数
*/
//检查页表项是否有效以及是否cow页面
int
cow_check(pagetable_t pagetable, uint64 va)
{
if(va > MAXVA)
return 0;
pte_t *pte = walk(pagetable, va, 0);
if(pte == 0)
return 0;
// printf("judge here 0\n");
if(((*pte) & (PTE_V)) == 0)
return 0;
// printf("judge here 1\n");
int ans = (*pte) & (PTE_COW);
return ans;
}
/*
为引发缺页中断的页表项分配物理内存以及映射
记得先将页表项的PTE_V标志位置0,否则会引发remap
旧页表项的标志位我们不和新页表项一起处理,当访问旧页表项时再回到这个函数统一处理,减少情况判断
*/
uint64
cow_copy(pagetable_t pagetable, uint64 va)
{
if(cow_check(pagetable, va) == 0)
return 0;
va = PGROUNDDOWN(va);
pte_t *pte = walk(pagetable, va, 0);
uint64 pa = PTE2PA(*pte);
if(get_mem_ref(pa) == 1)
{
*pte = (*pte) & (~PTE_COW);
*pte = (*pte) | (PTE_W);
return pa;
}
else
{
char *mem = kalloc();
if(mem == 0){
return 0;
}
memmove(mem, (char *)pa, PGSIZE);
*pte = (*pte) & (~PTE_V);
uint64 flag = PTE_FLAGS(*pte);
flag = flag | PTE_W;
flag = flag & (~PTE_COW);
if(mappages(pagetable, va, PGSIZE, (uint64)mem, flag) != 0)
{
kfree(mem);
return 0;
}
kfree((char*)PGROUNDDOWN(pa));
return (uint64)mem;
}
}
/* trap.c */
void
usertrap(void)
{
......
if(r_scause() == 8){
} else if(r_scause() == 13 || r_scause() == 15){
uint64 va = r_stval();
if(cow_copy(p->pagetable, va) == 0)
{
// panic("be kill\n");
p->killed = 1;
}
} else if((which_dev = devintr()) != 0){
}
......
usertrapret();
}
/* vm.c */
//若为cow页面则执行cow_copy操作
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(cow_check(pagetable, va0) != 0)
{
pa0 = cow_copy(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;
}