There is a saying in computer systems that any systems problem can be solved with a level of indirection.
任何计算机系统的问题都可以通过增加一个中间层解决。
COW的思想就是fork时child的PTE指向parent的Physical Page,这样一个PP两个进程共同使用。等到真正要使用时,再拷贝一份出来,避免冲突。需要注意,fork可能在fork,于是一份物理页面可能不止两个进程共享,于是我们需要为每个物理页面额外维护一个引用计数。
S1 引用计数的设计
直接用一个大数组,将物理页的页号当作索引,计算使用次数。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| struct memcnt{ struct spinlock lock; int cnt; }; struct memcnt p_cnt[PHYSTOP/PGSIZE]; void increase_cnt(uint64 pa){ int pn = PA2PN(pa); acquire(&p_cnt[pn].lock); p_cnt[pn].cnt += 1; release(&p_cnt[pn].lock); } void set_cnt(uint64 pa){ int pn = PA2PN(pa); acquire(&p_cnt[pn].lock); p_cnt[pn].cnt = 1; release(&p_cnt[pn].lock); }
|
这里为什么要加锁?我也是看了其他人的博客才知道,应该是有冲突的场景。
然后第一次分配页面时,引用计数初始化为1,归还页面时只减少引用计数,只有引用计数为0时才真正归还页面。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49
| void kinit(){ for(int i=0; i<PHYSTOP/4096;++i){ initlock(&p_cnt[i].lock, "memref"); p_cnt[i].cnt=0; } initlock(&kmem.lock, "kmem"); freerange(end, (void*)PHYSTOP); } void * kalloc(void){ struct run *r;
acquire(&kmem.lock); r = kmem.freelist; if(r) kmem.freelist = r->next; release(&kmem.lock);
if(r){ memset((char*)r, 5, PGSIZE); set_cnt((uint64)r); } return (void*)r; } void kfree(void *pa){ struct run *r;
if(((uint64)pa % PGSIZE) != 0 || (char*)pa < end || (uint64)pa >= PHYSTOP) panic("kfree");
int pn = PA2PN((uint64)pa); acquire(&p_cnt[pn].lock); p_cnt[pn].cnt -= 1; if(p_cnt[pn].cnt>0){ release(&p_cnt[pn].lock); return; } release(&p_cnt[pn].lock); memset(pa, 1, PGSIZE);
r = (struct run*)pa;
acquire(&kmem.lock); r->next = kmem.freelist; kmem.freelist = r; release(&kmem.lock);
}
|
S2 修改fork时的行为
fork不再单独拿出一个新物理页,而是指向parent的pa。注意要修改pte的标识位,去掉可写,增加cow标志。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
| 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_COW; flags = PTE_FLAGS(*pte); if(mappages(new, i, PGSIZE, pa, flags) != 0){ goto err; } increase_cnt(pa); } return 0;
err: uvmunmap(new, 0, i / PGSIZE, 1); return -1; }
|
S3 应对PageFault
首先要检查出错的va的合法性,然后要检查cow标识位,再就是分配新物理页,复制内容,然后替换原来的pte。这里有一点需要注意,减少引用计数时用的是kfree函数。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40
| void usertrap(void){ ... else if (r_scause()==13 || r_scause() ==15){ uint64 va = r_stval(); pte_t *pte; uint64 pa; do{ if (va >= MAXVA || va >p->sz){ p->killed = 1; break; } if((pte = walk(p->pagetable, va, 0))==0){ p->killed = 1; break; } if((*pte & PTE_COW) == 0){ p->killed = 1; break; } pa = PTE2PA(*pte); char* pg; if((pg = kalloc()) == 0){ p->killed = 1; break; } memmove(pg, (char*)pa, PGSIZE); uint flags = PTE_FLAGS(*pte); flags = (flags & ~PTE_COW) | PTE_W; *pte = PA2PTE(pg) | flags; kfree((void*)pa); } while (0); } .... }
|
其实这么设计还是有一个缺陷:当最后一个cow页的page fault时,它可以不用复制原来的物理页,而是直接复用这个页面,因为没有其他进程与它共享了。
S4 应对copyout
copyout的行为和中断处理几乎一样。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49
| int copyout(pagetable_t pagetable, uint64 dstva, char *src, uint64 len){ uint64 n, va0, pa0;
while(len > 0){ va0 = PGROUNDDOWN(dstva); if(va0 >= MAXVA) return -1; pte_t *pte; if((pte = walk(pagetable, va0, 0))==0){ return -1; } if((*pte & PTE_V) == 0){ return -1; } if((*pte & PTE_U) == 0){ return -1; } if((*pte & PTE_COW)){ uint64 pa = PTE2PA(*pte); char* pg; if((pg = kalloc()) == 0){ return -1; } memmove(pg, (char*)pa, PGSIZE); uint flags = PTE_FLAGS(*pte); flags = (flags & ~PTE_COW) | PTE_W; *pte = PA2PTE(pg) | flags; kfree((void*)pa); } pa0 = PTE2PA(*pte); 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标志位与计算页号的宏定义
1 2 3
| #define PTE_COW (1L << 8)
#define PA2PN(pa) (((uint64)pa) >> 12)
|