MIT6.S081 ---- Lab cow
Lab cow
The problem
xv6 中的 fork
系统调用复制所有的父进程的用户空间内存到子进程。如果父进程用户空间内存很大,复制会很耗时。有时这个复制是不必要的,如果 fork
之后子进程调用 exec
,会释放掉复制的内存,可能大部分复制的内存都没有使用。如果父子进程共享一页,其中一个要写这个页,这才真正需要复制。
The Solution
COW fork 的目标是延迟为子进程分配物理页直到需要复制的时候。
COW fork 只为子进程创建一个页表,其对应用户内存的 PTEs 指向父进程的物理页。COW fork() 标记父子进程的所有的用户 PTEs 不可写。当其中一个进程想要写 COW pages时,CPU产生一个 page fault。内核的 page-fault handler 处理这种情况:
- 为错误进程分配一个物理页。
- 复制原指向的页的内容到新页。
- 修改 PTE,指向新页的物理地址,并标记为可写。
- 当 page fault handler 返回时,用户空间能够写复制的页。
释放物理页的时候需注意:一个物理页可能有多个进程页表引用,只有当引用数为 \(1\) 时才可以释放。
Implement copy-on write
vm.c
:
- 将父子进程的 PTE 标记为 COW page 和 不可写。
需要判断该页是否是 PTE_U,因为 trapframe 页在 fork 处理完用户页的映射后,会单独处理。且uvmcopy
的参数 \(sz\) 是进程用户空间内存的大小,不包含 trapframe。 - 父进程的物理页引用数加 \(1\) 。
- 子进程的 PTE 指向父进程的物理页(不为子进程分配物理页)。
// Given a parent process's page table, copy
// its memory into a child's page table.
// Copies only the page table and
// clear their PTE_W.
// returns 0 on success, -1 on failure.
// frees any allocated pages on failure.
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);
flags = PTE_FLAGS(*pte);
// trapframe(PTE_U=0) copied separately after call uvmcopy in fork()
// uvmcopy only pay attention to user memory(PTE_U set)
if ((flags & PTE_W) && (PTE_U | flags)) {
flags = (flags & (~PTE_W)) | PTE_RSW_COW;
*pte = ((*pte) & (~PTE_W)) | PTE_RSW_COW;
}
krefinc(pa);
if(mappages(new, i, PGSIZE, pa, flags) != 0){
goto err;
}
}
return 0;
err:
uvmunmap(new, 0, i / PGSIZE, 1);
return -1;
}
trap.c
:
cowpagefault()
处理写不可写的 COW page。- 通过
kalloc()
分配新的页。 - 重要优化:在复制之前可以判断该页的引用数,如果只有本进程在用,则可以直接设置该页可写。
- 将旧页的内容复制到新页上。
- 设置 PTE :可写,取消 COW page 标记,用新页的物理地址。
- 释放旧页(引用为 \(1\) 回收物理页,否则引用数减 \(1\))。
// only handler cow fault.
// write the COW page(PTE_RSW_COW) of PTE_W on 0.
int
cowpagefault(pagetable_t pagetable, uint64 va)
{
pte_t *pte;
uint64 oldpa, newpa;
// va >= MAXVA cause walk() panic
if (va >= MAXVA)
return -1;
if ((pte = walk(pagetable, va, 0)) == 0)
return -1;
if ((*pte & PTE_RSW_COW) == 0)
return -1;
if (*pte & PTE_W)
return -1;
// guard page, trampoline, trapframe
if ((*pte & PTE_U) == 0) {
printf("PTE_U=0\n");
return -1;
}
oldpa = PTE2PA(*pte);
// Important optimization: if a store page fault and
// the ref which pointed this phy-page is one, no copy.
if (krefnum(oldpa) == 1) {
*pte = (*pte | PTE_W) & (~PTE_RSW_COW);
return 0;
}
if ((newpa = (uint64)kalloc()) == 0) {
printf("cowpagefault: kalloc fault.\n");
return -1;
}
// copy the old page to the new page
memmove((void*)newpa, (void*)oldpa, PGSIZE);
*pte = (PA2PTE(newpa) | PTE_FLAGS(*pte) | PTE_W) & (~PTE_RSW_COW);
kfree((void*)oldpa);
return 0;
}
trap.c : usertrap()
:
完善 trap path。
else if (r_scause() == 15) {
if (cowpagefault(p->pagetable, r_stval()) < 0) {
p->killed = 1;
}
}
kalloc.c
:
- 释放物理页时判断引用计数,引用数为 \(1\) 时才可以真正释放物理页。
- 基于
kfree()
的设计,在空闲物理页链表的初始化freerange
中,必须先为引用计数设置为 \(1\) , 否则kfree()
会panic
。
// Free the page of physical memory pointed at by v,
// which normally should have been returned by a
// call to kalloc(). (The exception is when
// initializing the allocator; see kinit above.)
void
kfree(void *pa)
{
struct run *r;
if(((uint64)pa % PGSIZE) != 0 || (char*)pa < end || (uint64)pa >= PHYSTOP)
panic("kfree");
int phypageindex = PHYPAGEINDEX((uint64)pa);
uint16 refcnt;
acquire(&kmem.lock);
refcnt = kmem.ppagerefcnt[phypageindex];
if (refcnt < 1)
panic("kfree ref = 0.");
kmem.ppagerefcnt[phypageindex]--;
release(&kmem.lock);
if (refcnt > 1)
return;
// Fill with junk to catch dangling refs.
memset(pa, 1, PGSIZE);
r = (struct run*)pa;
acquire(&kmem.lock);
r->next = kmem.freelist;
kmem.freelist = r;
release(&kmem.lock);
}
void
krefinc(uint64 pa)
{
int phypageindex = PHYPAGEINDEX(pa);
acquire(&kmem.lock);
if (pa >= PHYSTOP || kmem.ppagerefcnt[phypageindex] < 1)
panic("krefinc pa invalid.");
kmem.ppagerefcnt[phypageindex]++;
release(&kmem.lock);
}
uint16
krefnum(uint64 pa)
{
int phypageindex = PHYPAGEINDEX(pa);
uint16 sz;
acquire(&kmem.lock);
sz = kmem.ppagerefcnt[phypageindex];
release(&kmem.lock);
return sz;
}
vm.c
:
copyout()
将内核中的数据复制到用户空间,获取虚拟地址 dstva
对应的物理地址的时候,并不是 MMU 在转换地址,而是内核通过 walkaddr
模拟硬件读取页表内容转换虚拟地址为物理地址,内核直接对物理地址进行操作,所以 copyout
时需要对 COW page 进行复制。否则会直接对不可写的 COW page 进行修改而不会出现 page-fault ,出现难以排查的 bug。
// Copy from kernel to user.
// Copy len bytes from src to virtual address dstva in a given page table.
// Return 0 on success, -1 on error.
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 & PTE_W) && (*pte & PTE_RSW_COW)) {
if (cowpagefault(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;
}