只有一个task,实现copy-on-write fork
uvmcopy
在调用fork将父进程的页表复制给子进程时,❌(原版xv6)分配新的物理页
将同样的物理页映射到子进程的页表中,设置父进程和子进程对应的PTE_W为0,这样两个进程共享一个只读页
我们将RSW中的一位(这里选择#define PTE_COW (1L << 8))记为COW位,我们将这两个PTE的COW位设为1
c
// File: kernel/vm.c
int
uvmcopy(pagetable_t old, pagetable_t new, uint64 sz)
{
// ... (需注释掉部分代码)
// COW
if((*pte & PTE_W) != 0){
*pte = *pte & ~PTE_W; // set PTE_W to 0
*pte = *pte | PTE_COW; // set PTE_COW to 1
}
flags = PTE_FLAGS(*pte);
if(mappages(new, i, PGSIZE, pa, flags) != 0){
goto err;
}
cowcnt((void*)pa);
}
return 0;
}
vmfault
接下来,每当我们需要对COW标记的页执行写操作时
1.kalloc()分配一个新的物理页
2.将要执行写操作的页的物理内存复制到新的物理页上
3.修改flags位,设PTE_W = 1, PTE_COW = 0
4.将新的物理页映射到当前进程的页表中
c
// File: kernel/vm.c
uint64
vmfault(pagetable_t pagetable, uint64 va, int read)
{
// ...
// COW
if(iscow(pagetable, va)){
pte_t *pte = walk(pagetable, va, 0);
uint64 pa = PTE2PA(*pte);
uint flags = (PTE_FLAGS(*pte) | PTE_W) & ~PTE_COW;
mem = (uint64) kalloc();
if(mem == 0){
p->killed = -1;
return 0;
}
memmove((void*)mem, (void*)pa, PGSIZE);
kfree((void*)pa);
*pte = PA2PTE(mem) | flags;
}else{
if(ismapped(pagetable, va))
return 0;
mem = (uint64) kalloc();
if(mem == 0)
return 0;
memset((void *) mem, 0, PGSIZE);
if (mappages(p->pagetable, va, PGSIZE, mem, PTE_W|PTE_U|PTE_R) != 0) {
kfree((void *)mem);
return 0;
}
}
}
cowcnt
我们还需要对每个物理页记录其被进程页表引用的次数
c
// File: kernel/kalloc.c
#define MXPG (32*1024) // 128*1024*1024 / PGSIZE
struct {
struct spinlock lock;
struct run *freelist;
int cowcnt[MXPG];
} kmem;
对于物理地址为pa的物理页,cowcnt (pa - 0x80000000) / 4096 记录了当前物理页被引用的次数
每次调用kalloc时,当前物理页被引用的次数为1
c
// File: kernel/kalloc.c
void *
kalloc(void)
{
struct run *r;
acquire(&kmem.lock);
r = kmem.freelist;
if(r){
kmem.freelist = r->next;
// COW
int index = ((uint64)r - (uint64)end) / 4096;
kmem.cowcnt[index] = 1;
}
release(&kmem.lock);
if(r)
memset((char*)r, 5, PGSIZE); // fill with junk
return (void*)r;
}
每次调用kfree时,我们需要判断当前物理页的cowcnt
cowcnt > 1 说明有不止一个页调用它,我们cowcnt--后return (用kfree实现减少一次物理页引用次数的效果)
cowcnt = 1 说明这次kfree正是要释放这个物理页
cowcnt = 0 说明处于kinit内存初始化阶段,不要处理cowcnt
c
// File: kernel/kalloc.c
void
kfree(void *pa)
{
// ...
// COW
int index = ((uint64)pa - (uint64)end) / 4096;
if(kmem.cowcnt[index] > 0){
if(--kmem.cowcnt[index] > 0){
return;
}
}
}
我们还要实现一个函数,调用它时让pa对应物理页cowcnt + 1
每次通过uvmcopy()实现fork时,调用一次该函数(uvmcopy相关实现见上文代码)
c
// File: kernel/kalloc.c
// set cowcnt of physical page referring to pa
// n == 1 -> increment
void
cowcnt(void* pa){
acquire(&kmem.lock);
int index = ((uint64)pa - (uint64)end) / 4096;
kmem.cowcnt[index]++;
release(&kmem.lock);
}
others
还要注意,若在遇到COW页的缺页中断时发现内存不够,杀死当前进程p->killed = 1
这一点在vmfault和copyout中均要实现
分别负责usertrap和文件系统遇到COW时的处理
(vmfault相关实现见上文代码)
c
// File: kernel/vm.c
int
copyout(pagetable_t pagetable, uint64 dstva, char *src, uint64 len)
{
// ...
if((*pte & PTE_W) == 0){
if(*pte & PTE_COW){
if((pa0 = vmfault(pagetable, va0, 0)) == 0){
myproc()->killed = 1;
return -1;
}
}else{
return -1;
}
}
}
最后的通过截图是久违的AC!
