mit6s081 lab6: copy of write fork
mit6s081 lab6: copy of write fork
写时复制fork的实现包含一些要点:当fork创建子进程的时候,为子进程分配页表,但是此时父进程和子进程的页表项都设置不可写,并分配一个权限位表示是否位cow页面,然后子进程的页表项都映射到父进程的物理页面中,当子进程中需要对物理页面进行修改的时候,这个时候,会触发中断,中断处理程序,会为子进程分配真正的物理页面,同时修改子进程的页表项为可写,并将其映射到刚分配的物理页面中,中断返回的时候,子进程可用修改读写页表项指向的物理内存的内容。
这个过程中,还涉及到物理页面的释放,由于父子进程可能会共享物理地址,所以需要对物理页面的引用量进行计数,只有当物理页面的引用量为0的时候,才能真正地释放物理地址。
题干描述:你的任务是在 xv6 内核中实现写时复制fork。如果你修改后的内核成功执行了 cowtest 和 usertests 程序,你就完成了。
实现步骤:
- 修改uvmcopy()以将父进程的物理页面映射到子进程的页面,而不是为子进程分配新物理页面。同时在子进程和父进程的PTE中清除PTE_W;
- 修改usertrap()以识别页面错误,当需要写时复制的页面发生错误的时候,使用kalloc()为其分配一个新页面,并将旧页面复制到新页面中,并设置到pte,修改pte的映射地址为新页面,同时PTE_W位设置为1;
- 确保在对每个物理页面的最后一个PTE引用消失的时候释放物理页面。所以需要为每一个物理页面设置一个计数器。分配物理页面kalloc的时候将页面的引用计数设置为1。但fork导致父子进程共享页面的时候,增加该物理页面的计数,进程消亡的时候,减少页面的计数,只有当页面的引用计数为零的时候,kfree()才真正地释放物理页面。可用采用一个固定大小的数组来保存每个物理页面的引用数;
- 修改copyout(),由于该函数把内核物理地址中的内容复制到用户物理地址中,对于COW页面,需要为该用户分配新物理页面,并建立映射关系和设置权限位,将内核物理地址的内容写入到新分配的物理页面中。
官方提示:
- 本实验不基于lazy allocation实验
- 对于每一个pte,可以启用保留位来查看该页面是否为COW页面,用RISC-V中的RSW位
- 要同时通过cowtest和usertest
- 页表标志的一些宏定义位于kernel/riscv.h的末尾
- 如果发生COW页面错误并且没有可用的内存,则应该终止进程
具体实现:
设置PTE_COW标志位,用于标记该页面是否为COW页面。
// kernel/riscv.h
#define PTE_COW (1L << 8) // 将PTE中倒数第8位为COW标志位,这一位是软件保留位
在kalloc.c文件中设置用于对物理页面计数的全局变量mem_ref,由于mem_ref是全局变量,所以在mem_ref数据结构中加上自旋锁,避免不同的cpu对mem_ref同时修改导致计数出错。
struct {struct spinlock lock;int cnt[PHYSTOP/PGSIZE];
} mem_ref;
在kinit函数中初始化mem_ref的自旋锁;
void
kinit()
{initlock(&kmem.lock, "kmem");initlock(&mem_ref.lock, "mem_ref");freerange(end, (void*)PHYSTOP);
}
修改kfree()函数,只有当引用计数为0的时候,才真正释放物理页面,其他时候只需要把引用计数减一即可。
void
kfree(void *pa)
{struct run *r;if(((uint64)pa % PGSIZE) != 0 || (char*)pa < end || (uint64)pa >= PHYSTOP)panic("kfree");// 获得mem_ref中的自旋锁acquire(&mem_ref.lock);// 如果引用计数变为0,那么回收物理页面,否则不用管--mem_ref.cnt[(uint64)pa/PGSIZE];// 对mem_ref自旋锁的操作完毕,马上释放锁,避免其他需要该自旋锁的cpu一直等待release(&mem_ref.lock);if(mem_ref.cnt[(uint64)pa/PGSIZE]==0){// 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);}
}
kalloc.c文件中应该还需要一些功能函数
// 判断是否为合法的cow页面,如果是则返回0,
int cowpage(pagetable_t pagetable, uint64 va) {if(va >= MAXVA)return -1;pte_t* pte = walk(pagetable, va, 0);if(pte == 0)return -1;if((*pte & PTE_V) == 0)return -1;return (*pte & PTE_COW ? 0 : -1);
}// 为cow页面分配空闲物理页面
void* cowalloc(pagetable_t pagetable, uint64 va){if(va%PGSIZE!=0) return 0;uint64 pa = walkaddr(pagetable, (uint64)va); // 找到cow的物理页面;pte_t* pte = walk(pagetable, (uint64)va, 0); // 找到cow页面的pte;// 如果该pa物理页面的引用数只有一个,那么我们直接把其pte的PTE_W设置为1, 将PTE_COW设置为0acquire(&mem_ref.lock);uint64 mem_ref_cnt = mem_ref.cnt[(uint64)pa/PGSIZE];release(&mem_ref.lock);if(mem_ref_cnt==1){*pte = (*pte | PTE_W) & ~PTE_COW; // 设置权限位return (void*)pa;}else{uint64 new_pa;new_pa = (uint64)kalloc(); // 分配新物理页面// 将就物理页面的内容复制到新的物理页面memmove((void*)new_pa, (void*)pa, PGSIZE);// 为虚拟地址和新物理页面建立起映射*pte = *pte & ~PTE_V; // 在调用mappages前,将pte设置为无效,不然mappages函数会因为remap崩溃if(mappages(pagetable, (uint64)va, PGSIZE, (uint64)new_pa, (PTE_FLAGS(*pte)|PTE_W)&~PTE_COW)!=0){// 结果不等于0, 表明映射失败kfree((void*)new_pa); // 映射失败,收回刚刚分配的物理页面*pte = *pte | PTE_V;return 0;}// 映射成功kfree((void*)pa); // 对原物理页面引用数量减1;return (void*)new_pa;}
}// 返回现有pa对应物理页面的引用计数
int krefcnt(void* pa){return mem_ref.cnt[(uint64)pa/PGSIZE];
}// 增加内存的引用计数
int kaddrefcnt(void* pa){if(((uint64)pa%PGSIZE)!=0 || (char*)pa<end || (uint64)pa>PHYSTOP) return -1;acquire(&mem_ref.lock);++mem_ref.cnt[(uint64)pa/PGSIZE];release(&mem_ref.lock);return 0;
}
修改freerange()函数,因为kinit函数中调用freerange函数,freerange函数又会调用kfree函数,如果不对freerange函数做修改,那么引用计数会变为-1。
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.cnt[(uint64)p/PGSIZE]=1;kfree(p);
}
修改kalloc()函数,将对应物理页面的引用计数设置为1;
void *
kalloc(void)
{struct run *r;acquire(&kmem.lock);r = kmem.freelist;if(r)kmem.freelist = r->next;// 将mem_ref对应的计数设置为1;acquire(&mem_ref.lock);mem_ref.cnt[(uint64)r/PGSIZE]=1;release(&mem_ref.lock);release(&kmem.lock);if(r)memset((char*)r, 5, PGSIZE); // fill with junkreturn (void*)r;
}
fork函数中,将父进程的内存复制给子进程的函数是uvmcopy,所以还需要修改uvmcopy函数来满足COW fork。uvmcopy函数完成的工作就是为子进程和父进程建立起COW页面的映射,设置权限位,还要为物理页面进行引用计数。
int
uvmcopy(pagetable_t old, pagetable_t new, uint64 sz)
{pte_t *pte;uint64 pa, i;// uint flags;pte_t *new_pte;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);new_pte = walk(new, i, 1);*pte = (*pte & (~PTE_W)) | (PTE_COW); // 置于不可写,且设置PTE_COW*new_pte = *pte;kaaddrefcnt((void*)pa); //增加引用计数}return 0;
}
修改usertrap,当遇到页面load或store错误的时候,r_scause()会等于13或者15,这个时候需要检查是否为cow页面,如果是的话,需要为cow页面分配真实物理页面
void
usertrap(void)
{……} else if(r_scause()==13 || r_scause()==15){uint64 va = r_stval(); // 出错的虚拟地址if(va >= p->sz|| cowpage(p->pagetable, va) != 0|| cowalloc(p->pagetable, PGROUNDDOWN(va)) == 0)p->killed = 1;} else {……}……
}
copyout函数是将内核页面的内容复制到指定的用户页面中,需要写入到用户页面,如果遇到用户页面为cow,则需要为其分配空闲的物理页面
int
copyout(pagetable_t pagetable, uint64 dstva, char *src, uint64 len)
{// 从内核地址往用户地址写内容,如果用户页面为cow页面,则需要新分配物理页面// 然后将cow页面的内容写入到新物理页面中,设置好pte映射,再将内核内容写入到新物理页面中,uint64 n, va0, pa0;while(len > 0){va0 = PGROUNDDOWN(dstva);pa0 = walkaddr(pagetable, va0);// 如果va0对应着cow页面;if(cowpage(pagetable, va0) == 0){pa0 = (uint64)cowalloc(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;
}
总结:
cow页面的思想和lazy allocation很像,在真正需要写的时候,才为该虚拟地址分配可写的物理页面。