# COW
cow
就是 copy on write
的简称,当进程通过 fork 创建子进程时候,因为子进程和父进程的数据还有文件描述符等数据结构都是一样的,所以需要把父进程的所有数据拷贝到子进程,就必须申请和父进程相同的内存。但是比如说 sh
进程,它首先 fork 生成了一个子进程,子进程接着去执行 exec,exec 会清除进程数据然后 load 新的数据和指令。原来 fork 操作的数据拷贝就显得浪费了,所以就有了 cow 技术。
当 fork 的时候并不执行新的内存申请,而是页表项映射到 parent 的实地址中。这样我们就省下了申请新的空间并拷贝数据的开销,但是这样也有一个问题,那就是如果子进程虽然由父进程 fork 得到,但是他们之间应该存在数据隔离,如果修改了父进程的数据子进程不应该察觉到。因此我们需要把父进程和子进程的页表项的 flag 同时设定为只读。
当子进程或父进程需要修改这些 cow 内存页那么就会触发 page fault,然后 trap 会重新分配一个空闲页,并把数据拷贝进去。这样就完成了 cow。
这里还有一个问题,我们应该如何回收这些 cow 页。xv6 给出的办法是维护一个数组,内容为内存页的 reference 个数。但 ref 为 0 时候就可以回收这个 cow 页。
# riscv.h 添加宏
#define PTE_COW (1L << 8) //cow flag,用于表示该页表项为 cow 页表项 | |
#define PA2PGREF_ID(pa) (((pa) - KERNBASE)/PGSIZE) // 该 physical address 对应 ref 数组的索引 | |
#define REF_MAX PA2PGREF_ID(PHYSTOP) //ref 数据长度 |
# kalloc.c 代码
struct page_ref { //ref 数组元素数据结构 | |
int cnt; | |
struct spinlock lock; | |
}; | |
struct page_ref page_ref_list[REF_MAX]; //ref 数组 | |
int | |
ref_page(uint64 pa, int i){ // 修改 ref, 并返回修改后的 ref 数 | |
acquire(&page_ref_list[PA2PGREF_ID((uint64)pa)].lock); | |
page_ref_list[PA2PGREF_ID((uint64)pa)].cnt += i; | |
release(&page_ref_list[PA2PGREF_ID((uint64)pa)].lock); | |
return page_ref_list[PA2PGREF_ID((uint64)pa)].cnt; | |
} | |
void | |
kinit() | |
{ | |
for(int i=0; i<REF_MAX; ++i){ // 初始化 ref 为 1, 这里初始化为 0 会报错可能 xv6 内核还可以通过其他方式分配内存? | |
initlock(&(page_ref_list[i].lock), "kpage_ref"); | |
page_ref_list[i].cnt =1; | |
} | |
... | |
} | |
void | |
kfree(void *pa) | |
{ | |
... | |
int ref = ref_page((uint64)pa, -1); | |
if(ref > 0){ | |
return; | |
} | |
... | |
} | |
void * | |
kalloc(void) | |
{ | |
... | |
if(r){ | |
memset((char*)r, 5, PGSIZE); // fill with junk | |
acquire(&page_ref_list[PA2PGREF_ID((uint64)r)].lock); | |
page_ref_list[PA2PGREF_ID((uint64)r)].cnt =1; // 分配的时候设置为 1 | |
release(&page_ref_list[PA2PGREF_ID((uint64)r)].lock); | |
} | |
... | |
} |
# trap.c 代码
void | |
usertrap(void) | |
{ | |
... | |
}else if(r_scause() == 13 || r_scause()==15){ | |
//cow | |
uint64 addr = r_stval(); | |
if(cow_check(p->pagetable, addr)){ | |
if(cow(addr)==0){ | |
p->killed = 1; | |
} | |
}else{ | |
p->killed = 1; | |
} | |
}else if((which_dev = devintr()) != 0){ | |
// ok | |
} else { | |
printf("usertrap(): unexpected scause %p pid=%d\n", r_scause(), p->pid); | |
printf(" sepc=%p stval=%p\n", r_sepc(), r_stval()); | |
p->killed = 1; | |
} | |
} | |
extern pte_t* walk(pagetable_t pagetable, uint64 va, int alloc); | |
uint64 cow(uint64 addr){ | |
pte_t* pte; | |
uint64 va = PGROUNDDOWN(addr); | |
struct proc* p = myproc(); | |
pte = walk(p->pagetable, addr, 0); | |
uint64 pa = PTE2PA((uint64)*pte); | |
char* mem = kalloc(); | |
if(mem == 0){ | |
return 0; | |
}else{ | |
memset(mem, 0, PGSIZE); | |
memmove((void*)mem, (void*)pa, PGSIZE); | |
*pte = *pte & ~PTE_V; // 申请该页表项无效,防止 remap | |
uint64 flag = PTE_FLAGS(*pte); | |
flag = flag | PTE_W; | |
flag = flag & (~PTE_COW); | |
if(mappages(p->pagetable, va ,PGSIZE , (uint64)mem, flag)!=0){ | |
kfree((void*)mem); | |
return 0; | |
} | |
} | |
kfree((void*)PGROUNDDOWN(pa)); | |
/* panic("cow"); */ | |
return (uint64)mem; | |
} | |
int cow_check(pagetable_t pagetable, uint64 va){ // 检查是否为有效 cow | |
if (va > MAXVA) { | |
return 0; | |
} | |
pte_t *pte = walk(pagetable, va, 0); | |
if (pte == 0) { | |
return 0; | |
} | |
if (( (*pte) & PTE_V) == 0) { | |
return 0; | |
} | |
return ((*pte) & (PTE_COW)); | |
} |
# vm.c 代码
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"); | |
*pte = (*pte|PTE_COW) & ~(PTE_W); // 只是将该页表项设置为 cow 页表项,并且设置不可写。 | |
pa = PTE2PA(*pte); | |
ref_page((uint64)pa, 1); | |
flags = PTE_FLAGS(*pte); | |
/* if((mem = kalloc()) == ) */ | |
/* goto err; */ | |
/* memmove(mem, (char*)pa, PGSIZE); */ | |
if(mappages(new, i, PGSIZE, (uint64)pa, flags) != 0){ | |
/* kfree(mem); */ | |
goto err; | |
} | |
} | |
return 0; | |
err: | |
uvmunmap(new, 0, i / PGSIZE, 1); | |
return -1; | |
} | |
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)){ // | |
/* panic("copy"); */ | |
pa0 = cow(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; | |
} |
这里 copyout 需要修改,但用户的虚地址指向的是 cow 地址,说明这个地址不能写,那么为什么这里不能让 trap 来处理呢?因为我们只是修改了 usertrap 函数来处理用户态的缺页中断,而这里 copy_out 是发生在内核态的,而 kerneltrap 并没有处理缺页中断的函数,因此需要在 copyout
这里手动进行修改。
至此,整个实验已经全部结束了,难度真的高。。。
# 结果
# 参考资料
MIT 6.s081 xv6-lab6-cow - 大尾巴羊的文章 - 知乎 https://zhuanlan.zhihu.com/p/429821940