Lab6 Cow
实现懒拷贝,即推迟到子进程实际需要物理内存拷贝时再进行分配和复制物理内存页面。但是这样对于释放用户内存的物理页面变得更加棘手,所以我们将引入引用计数机制。
Implement copy-on write (hard)
fork不立即复制内存
修改uvmcopy
将父进程的物理页映射到子进程的,而不是立即分配新页,同时,清除父子进程的写权限并设置COW权限。
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 = *pte | PTE_COW; // 设置COW权限
flags = PTE_FLAGS(*pte);
if (mappages(new, i, PGSIZE, (uint64)pa, flags) != 0) // 直接将父进程的物理页映射到子进程中
{
goto err;
}
incr((void *)pa); // 增加一次对该物理页的引用计数
}
return 0;
err:
uvmunmap(new, 0, i / PGSIZE, 1);
return -1;
}
捕获页面错误并执行实复制
修改usertarp
使其能能够识别页面错误,当COW页出现页错误时,我们才使用kalloc
分配一个新页,并将旧页复制到新页,然后将这个新页添加到PTE
中并设置写权限。
void usertrap(void)
{
int which_dev = 0;
......
else if ((which_dev = devintr()) != 0)
{
// ok
}
else if (r_scause() == 15 || r_scause() == 13) //处理页错误
{
uint64 va = r_stval(); // 获取虚拟地址
if (is_cow_fault(p->pagetable, va)) // 识别是否是由于 COW引起的页错误
{
if (cow_alloc(p->pagetable, va) < 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;
}
}
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;
}
......
usertrapret();
}
int is_cow_fault(pagetable_t pagetable, uint64 va)
{
if (va >= MAXVA) // 边界情况
return 0;
va = PGROUNDDOWN(va); // 向下去整
pte_t *pte = walk(pagetable, va, 0); // 获取pte 进行合法型检测以及识别 COW 错误
if (pte == 0)
return 0;
if ((*pte & PTE_V) == 0)
return 0;
if ((*pte & PTE_U) == 0)
return 0;
if (*pte & PTE_COW)
{
return 1;
}
return 0;
}
int cow_alloc(pagetable_t pagetable, uint64 va)
{
va = PGROUNDDOWN(va); // 向下取整
pte_t *pte = walk(pagetable, va, 0); // 获取pte
uint64 pa = PTE2PA(*pte); // 获取物理地址
int flag = PTE_FLAGS(*pte); // 获取权限
char *mem = kalloc(); // 分配物理内存
if (mem == 0)
{
return -1;
}
memmove(mem, (char *)pa, PGSIZE); // 将原来的物理页中的内容拷贝到当前页中
uvmunmap(pagetable, va, 1, 1); // 清除原有映射 因为原有映射的权限存在问题
flag &= ~(PTE_COW); // 清除 COW 权限
flag |= PTE_W; // 设置写权限
if (mappages(pagetable, va, PGSIZE, (uint64)mem, flag) < 0)
{
kfree(mem);
return -1;
} // 实分配 同时进行映射
return 0;
}
修改引用计数及释放内存
为什么说懒分配会导致释放物理页变成棘手呢?这是为了确保每个物理页在最后一个PTE对它的引用撤销时被释放,而不是在此之前。所以 我们将引入引用计数机制,即当kalloc()
分配页时,将页的引用计数设置为1。当fork
导致子进程共享页面时,增加页的引用计数;每当任何进程从其页表中删除页面时,减少页的引用计数。kfree()
只应在引用计数为零时将页面放回空闲列表。
struct
{
struct spinlock lock;
struct run *freelist;
char *ref_page; // 引用计数
int pagecount; // 页数
char *end_; // 将整个内核空间向上扩大一部分
} kmem;
int page_cnt(void *pa_start, void *pa_end) // 获取当前所有物理页的页数
{
char *p;
int cnt = 0;
p = (char *)PGROUNDUP((uint64)pa_start);
for (; p + PGSIZE <= (char *)pa_end; p += PGSIZE)
cnt++;
return cnt;
}
void kinit()
{
initlock(&kmem.lock, "kmem");
kmem.pagecount = page_cnt(end, (void *)PHYSTOP);
kmem.ref_page = end;
for (int i = 0; i < kmem.pagecount; i++)
{
kmem.ref_page[i] = 0;
} // 初始化引用计数
kmem.end_ = kmem.ref_page + kmem.pagecount; // 扩大内核空间
freerange(kmem.end_, (void *)PHYSTOP);
}
int page_index(uint64 pa) // 获取页标
{
pa = PGROUNDDOWN(pa);
int res = (pa - (uint64)kmem.end_) / PGSIZE;
if (res < 0 || res >= kmem.pagecount)
{
panic("page_index illegal");
}
return res;
}
void incr(void *pa) // 原子性增加引用计数
{
int index = page_index((uint64)pa);
acquire(&kmem.lock);
kmem.ref_page[index]++;
release(&kmem.lock);
}
void desc(void *pa) // 原子性减少引用计数
{
int index = page_index((uint64)pa);
acquire(&kmem.lock);
kmem.ref_page[index]--;
release(&kmem.lock);
}
void kfree(void *pa)
{
int index = page_index((uint64)pa);
if (kmem.ref_page[index] > 1)
{
desc(pa);
return;
}
if (kmem.ref_page[index] == 1)
{
desc(pa);
} // 减少引用计数直至为0
struct run *r;
......
}
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); // fill with junk
incr(r); // 引用计数置为1
}
return (void *)r;
}
修改copyout()
在遇到COW
页面时使用与页面错误相同的方案。
int copyout(pagetable_t pagetable, uint64 dstva, char *src, uint64 len)
{
uint64 n, va0, pa0;
while (len > 0)
{
va0 = PGROUNDDOWN(dstva);
if (is_cow_fault(pagetable, va0))
{
if (cow_alloc(pagetable, va0) < 0)
{
printf("copyout: cow_alloc filed");
return -1;
}
}
......
}
return 0;
}
上述代码已经讲原理说明,具体细节自行补充。