MIT6.828 Lab5 Copy-on-Write Fork for xv6
TOC
实验内容
- 背景:
- 在 xv6 中,fork()系统调用会将父进程的所有用户空间内存复制到子进程中。如果父进程很大,复制操作可能会花费很长时间。
- 在子进程中,fork()通常会紧接着 exec(),这会丢弃复制的内存,通常不会使用大部分内存。
- 解决方案:
- 延迟分配
- 写时复制
- COW fork() 为子进程创建一个页表,用户内存的 PTE 指向父进程的物理页。COW fork() 将父进程和子进程中的所有用户 PTE 标记为只读。当任一进程尝试写入这些 COW 页面时,CPU 将引发页面故障。内核页面故障处理程序检测到这种情况,为故障进程分配一页物理内存,将原始页面复制到新页面,并在故障进程的相关 PTE 中修改为指向新页面,这次 PTE 标记为可写。当页面故障处理程序返回时,用户进程将能够写入其页面的副本。
- 具体任务
- 在 xv6 内核中实现copy-on-write fork。如果你修改后的内核能够成功执行 cowtest和usertests -q程序,那么你就完成了任务。
实现逻辑
修改内容
kernel/riscv.h
中添加PTE_COW
标志位
#define PTE_COW (1L << 8) //自定义COW标志位 lab5
kernel/kalloc.c
添加页映射计数数组、相关函数(修改时使用锁)
// lab5
struct {
struct spinlock lock; // 引用计数专用锁
int count[(PHYSTOP - KERNBASE) / PGSIZE]; // 引用计数数组
}kref;
// 增加引用计数
void kref_incr(uint64 pa) {
acquire(&kref.lock);
int idx = (pa - KERNBASE) / PGSIZE;
kref.count[idx]++;
release(&kref.lock);
}
// 减少引用计数
void kref_decr(uint64 pa) {
int idx = (pa - KERNBASE) / PGSIZE;
acquire(&kref.lock);
if (kref.count[idx] <= 0) panic("kref_decr: invalid refcount");
kref.count[idx]--;
release(&kref.lock);
}
// 查询引用次数
int kref_count(uint64 pa) {
acquire(&kref.lock);
int idx = (pa - KERNBASE) / PGSIZE;
release(&kref.lock);
return kref.count[idx];
}
kernel/kalloc.c
中修改初始化、分配页和释放页函数
void
kinit()
{
// lab5 初始化计数数组
for (int i = 0; i < (PHYSTOP - KERNBASE) / PGSIZE; i++)
kref.count[i] = 0;
initlock(&kref.lock, "kref");
initlock(&kmem.lock, "kmem");
freerange(end, (void*)PHYSTOP);
}
void
kfree(void *pa)
{
struct run *r;
if(((uint64)pa % PGSIZE) != 0 || (char*)pa < end || (uint64)pa >= PHYSTOP)
panic("kfree");
// lab5
acquire(&kmem.lock);
if ( kref_count((uint64)pa) > 1) {
kref_decr((uint64)pa); // 引用计数减 1
release(&kmem.lock);
return;
}
acquire(&kref.lock);
kref.count[((uint64)pa - KERNBASE) / PGSIZE] = 0; // 清零后释放
release(&kref.lock);
// 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 *
kalloc(void)
{
struct run *r;
acquire(&kmem.lock);
r = kmem.freelist;
if(r){
kmem.freelist = r->next;
// lab5
acquire(&kref.lock);
kref.count[((uint64)r - KERNBASE) / PGSIZE] = 1; // 初始化为 1
release(&kref.lock);
}
release(&kmem.lock);
if(r)
memset((char*)r, 5, PGSIZE); // fill with junk
return (void*)r;
}
- 只有当页的映射数为1时才能释放
- 分配页时增加数组计数
kernel/vm.c
中添加写时分配的实现形式
int cow_alloc(pagetable_t pagetable, uint64 va) {
va = PGROUNDDOWN(va); // 对齐到页边界
// 检查va大小,在walk之前,否则无法应对恶意输入地址
if(va >= MAXVA)
return -1;
pte_t *pte = walk(pagetable, va, 0);
if (!pte || !(*pte & PTE_V) || !(*pte & PTE_COW)) {
return -1; // 非法地址或非 COW 页
}
uint64 pa = PTE2PA(*pte); // 原物理地址
// 如果引用计数为 1,直接恢复写权限
if (kref_count(pa) == 1) {
*pte |= PTE_W;
*pte &= ~PTE_COW;
sfence_vma(); // 刷新 TLB
return 0;
}
// 分配新页并复制数据
char *new_pa = kalloc();
if (!new_pa) return -1; // 内存不足
memmove(new_pa, (char*)pa, PGSIZE); // 复制原页内容
// 更新页表项:新页可写,清除 COW 标志
uint64 flags = (PTE_FLAGS(*pte) | PTE_W ) & ~PTE_COW;
*pte = PA2PTE(new_pa) | flags;
kfree((void*)pa); // 减少原页的引用计数
sfence_vma(); // 刷新 TLB
return 0;
}
kernel/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");
// lab5 只考虑可写页面标记cow,否则会使不可写的页在cow处理后变为可写!
if(*pte & ~PTE_W){
*pte = (*pte & ~PTE_W) | PTE_COW; // 清除父进程的写权限,标记为 COW 页
}
pa = PTE2PA(*pte); // 获取物理地址
flags = PTE_FLAGS(*pte);
kref_incr(pa); //增加引用计数
// 子进程页表继承只读 + COW 标志,映射到同一物理页
if(mappages(new, i, PGSIZE, (uint64)pa, flags) != 0){
kref_decr(pa);
goto err;
}
}
return 0;
err:
uvmunmap(new, 0, i / PGSIZE, 1);
return -1;
}
int
copyout(pagetable_t pagetable, uint64 dstva, char *src, uint64 len)
{
uint64 n, va0, pa0;
pte_t *pte;
while(len > 0){
va0 = PGROUNDDOWN(dstva);
if(va0 >= MAXVA)
return -1;
pte = walk(pagetable, va0, 0);
// lab5去掉pte_w检查,进行常规检查
if(pte == 0 || (*pte & PTE_V) == 0 || (*pte & PTE_U) == 0 )
return -1;
// 禁止写入代码页(PTE_X)
if (*pte & PTE_X) {
return -1;
}
// lab5
if(*pte & PTE_COW){
// cow页
uint64 pa = PTE2PA(*pte); // 对应的物理页地址
if(kref_count(pa) > 1){
// 该物理页被多次映射,需重新分配
if( cow_alloc(pagetable, va0) < 0 ){
return -1;
}
// 获取新的pte
pte = walk(pagetable, va0, 0);
if( pte == 0 || (*pte & PTE_V) == 0 || (*pte & PTE_U) == 0 || (*pte & PTE_W) == 0){
return -1;
}
sfence_vma(); // 刷新TLB
}
else{
// 该物理页只有一次映射
*pte |= PTE_W;
*pte &= ~PTE_COW;
sfence_vma(); // 刷新TLB
}
} else {
// 非 COW 页:检查是否可写
if ((*pte & PTE_W) == 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;
}
kernel/trap.c
中添加陷入处理
void
usertrap(void)
{
int which_dev = 0;
if((r_sstatus() & SSTATUS_SPP) != 0)
panic("usertrap: not from user mode");
// send interrupts and exceptions to kerneltrap(),
// since we're now in the kernel.
w_stvec((uint64)kernelvec);
struct proc *p = myproc();
// save user program counter.
p->trapframe->epc = r_sepc();
if(r_scause() == 8){
// system call
if(killed(p))
exit(-1);
// sepc points to the ecall instruction,
// but we want to return to the next instruction.
p->trapframe->epc += 4;
// an interrupt will change sepc, scause, and sstatus,
// so enable only now that we're done with those registers.
intr_on();
syscall();
} else if( r_scause() == 15 ){ //lab5 页写入错误 // 写时错误
uint64 va = r_stval(); // 获取触发错误的虚拟地址
// 检查虚拟地址是否合法(在用户地址空间内)
if (va >= p->sz || va < PGSIZE) {
printf("usertrap: invalid va %p\n", va);
setkilled(p);
}
// 处理 COW 错误(分配新页并更新页表)
if (cow_alloc(p->pagetable, va) < 0) { // 调用 COW 处理函数
printf("usertrap: cow_alloc failed\n");
setkilled(p);
}
} else if((which_dev = devintr()) != 0){
// ok
} else {
printf("usertrap(): unexpected scause %p pid=%d\n", r_scause(), p->pid);
printf(" sepc=%p stval=%p\n", r_sepc(), r_stval());
setkilled(p);
}
if(killed(p))
exit(-1);
// give up the CPU if this is a timer interrupt.
if(which_dev == 2)
yield();
usertrapret();
}
测试结果
$cowtest
simple: ok
simple: ok
three: ok
three: ok
three: ok
file: ok
ALL COW TESTS PASSED
$usertests -q
...
test sbrklast: OK
test sbrk8000: OK
test badarg: OK
ALL TESTS PASSED