6.S081-2021-lab5 Copy-on-Write Fork
2022-04-20 09:28
543 查看
Copy-on-Write Fork
主要根据
hins来一步一步修改。cow的思想是在fork的时候,子进程与父进程共享物理页,当需要修改页面内容的时候才会真正分配自己的页表空间,也就是
lazy allocation
cow使得多个
va映射到了同一个
pa上,所以 free 的时候我们要特别小心,因为可能别的进程还依赖这个物理页,所以我们需要对每一个 pa 对应的物理页进行引用计数。
如何计数呢?虽然硬件为操作系统保留了三个比特位让其自由发挥,但很明显这点位置是不够应用计数的,只适合用其中一位来表示当前页是否是 cow 页。
但我们知道 xv6 能够使用的最大物理地址,以及页表大小。所以只需要定义一个大小为 PGPYHA / PGSIZE 的一维数组来记录每一页的引用数即可
// riscv.h #define PTE_COW (1L << 8) // copy on write flag
// kalloc.c int refcount[PHYSTOP/PGSIZE]; struct spinlock reflock;
kalloc 和 kfree 会导致引用计数的增加和减少
// kalloc.c void incref(uint64 pa) { int pn = pa/PGSIZE; acquire(&kmem.lock); if(pa>=PHYSTOP || refcount[pn]<1){ panic("incref"); } refcount[pn] += 1; release(&kmem.lock); } void * kalloc(void) { struct run *r; acquire(&kmem.lock); r = kmem.freelist; if(r){ kmem.freelist = r->next; int pn = (uint64)r / PGSIZE; acquire(&reflock); if(refcount[pn]!=0){ panic("kalloc ref"); } refcount[pn] = 1; release(&reflock); } release(&kmem.lock); if(r) memset((char*)r, 5, PGSIZE); // fill with junk return (void*)r; } void kfree(void *pa) { ... if(((uint64)pa % PGSIZE) != 0 || (char*)pa < end || (uint64)pa >= PHYSTOP) panic("kfree"); // 因为可能有多个进程引用了同一页面,所以这里要加锁 acquire(&kmem.lock); int pn = (uint64) pa / PGSIZE; if(refcount[pn]<1){ panic("kfree ref"); } refcount[pn]-=1; int tmp = refcount[pn]; release(&kmem.lock); // 计数不为0说明还有进程需要这一页,不能free if(tmp>0){ return; } // Fill with junk to catch dangling refs. ... }
同时我们需要在 kinit 中初始化引用计数数组为1。因为 freerange 会调用 kfree,会导致数组变为负数,抛出 painc 。
// kalloc.c void kinit() { initlock(&kmem.lock, "kmem"); char *p; p = (char*)PGROUNDUP((uint64)end); // 将引用计数数组初始化为1 for(; p + PGSIZE <= (char*)PHYSTOP; p += PGSIZE){ refcount[(uint64)p/ PGSIZE] = 1; } freerange(end, (void*)PHYSTOP); }
因为 fork 使用了 uvmcopy 所以我们要修改这个函数,让子进程的地址空间延迟分配,先与父进程共享相同的物理页
// 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); // 将父亲和孩子都置为写保护,并标识该页为cow *pte &= ~PTE_W; *pte |= PTE_COW; flags = PTE_FLAGS(*pte); //引用计数++ incref(pa); //直接把pa给孩子的页表项,也就是说现在父子的va都对应父亲的pa if(mappages(new, i, PGSIZE, pa, flags) != 0){ goto err; } } return 0; err: uvmunmap(new, 0, i / PGSIZE, 1); return -1; }
因为我们将PTE_W抹去了,所以当进程需要进行写操作的时候,会发生 page fault。这时候需要在 usertrap 中处理,为子进程分配新的物理地址。
// trap.c // 为cow page分配新页 int cowfault(pagetable_t pagetable, uint64 va) { if(va >= MAXVA){ return -1; } pte_t *pte; pte = walk(pagetable,va,0); if(pte == 0 ) return -1; if ((*pte & PTE_U) == 0 || (*pte & PTE_V) == 0 || (*pte & PTE_COW) == 0){ return -1; } uint64 pa1,pa2; pa1 = PTE2PA(*pte); pa2 = (uint64)kalloc(); if(pa2 == 0){ return -1; } memmove((char*)pa2,(char*)pa1,PGSIZE); // 只有当引用计数为0的时候才会真正free kfree((void*)pa1); uint flags = PTE_FLAGS(*pte); *pte = PA2PTE(pa2) | flags | PTE_W; *pte &= ~PTE_COW; return 0; } void usertrap(void) { ... else if((which_dev = devintr()) != 0){ // ok }else if(r_scause()==15 || r_scause()==13){ // 处理page fault if(cowfault(p->pagetable,r_stval())<0){ p->killed = 1; } } 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; } ... }
最后需要注意 copyout 将 kernel 的物理地址复制给用户进程,但没有校验 PTE_W 和 PTE_COW,所以有可能直接覆盖了写保护的COW页。 而这一操作不经过 MMU ,没办法引发 page fault ,从而在 usertrap 中处理。所以我们需要修改 copyout 函数,处理的办法跟在 usertrap 中一样,为用户进程分配一个新页,以供写入覆盖。
// vm.c 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 = walk(pagetable,va0,0); if(pte == 0 || (*pte & PTE_V )==0 || (*pte & PTE_U) ==0){ return -1; } // 如果写保护,并且是因为COW造成的 if((*pte & PTE_W) == 0 && (*pte && PTE_COW) == 1){ // 为当前虚拟地址分配一个新的物理地址以供写入 if(cowfault(pagetable,va0)<0){ return -1; } } pa0 = PTE2PA(*pte); 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; }
参考
相关文章推荐
- Linux写时拷贝技术(copy-on-write)及fork、vfork流程介绍
- babyos2(13)—— process page table,fork, COW(copy on write)
- XV6学习(9)Lab cow: Copy-on-write fork
- fork, vfork and write-on-copy
- Copy On Write和fork、vfork(很容易理解的图解)
- 【转】Copy-On-Write技术 [ linux fork进程时使用技术]
- fork(),vfork(),写时复制(Copy-On-Write, COW)
- fork的Copy-On-Write技术
- std string的内存共享和Copy-On-Write技术
- ceph存储 Linux写时拷贝技术(copy-on-write)
- 标准C++类string的Copy-On-Write技术(二)
- Linux写时拷贝技术(copy-on-write)
- “Copy-on-write”---->写时复制
- copy on write
- copy on write
- 标准C++类std::string的内存共享和Copy-On-Write技术(转)
- php引用传值与传值赋值的copy on write,简单底层原理
- 并发-Java中的Copy-On-Write容器
- 标准C++类std::string的内存共享和Copy-On-Write技术
- Java中的Copy-On-Write容器(一) --CopyOnWriteArrayList