MIT 6.S081 lab5:Copy-on-Write | 写时复制
1. 实验背景与原理
在原始的 xv6 中,fork() 系统调用通过
uvmcopy()
分配物理内存并直接拷贝父进程的所有内容。对于大型进程,这种全量拷贝极其耗时且浪费内存。
Copy-on-Write (COW) 的核心思想是:延迟分配。 * fork 时:父子进程共享相同的物理页,将这些页设置为只读,并在 PTE 中标记为 COW 页。 * 写操作时:当任何一个进程尝试写入该页,硬件触发 Store Page Fault。 * 内核处理:内核捕获异常,发现是 COW 页,此时才分配新物理页并执行拷贝,更新页表为可写。
2. 核心实现步骤
2.1 物理页引用计数
(kalloc.c)
由于多个进程共享同一物理页,只有当最后一个引用该页的进程释放它时,物理页才能被真正收回。
我们需要在 kernel/kalloc.c 中维护一个引用计数数组:
1 | |
2.2 修改 uvmcopy 实现共享映射 (vm.c)
在 kernel/riscv.h
中定义自定义标志位:#define PTE_COW (1L << 8) // 使用 PTE 的保留位
修改 uvmcopy,使其不再分配内存,而是建立只读映射并标记
COW: 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29int 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) continue;
if((*pte & PTE_V) == 0) continue;
pa = PTE2PA(*pte);
flags = PTE_FLAGS(*pte);
// 如果原页是可写的,则将其标记为 COW 并设为只读
if(flags & PTE_W) {
flags |= PTE_COW;
flags &= ~PTE_W;
*pte = PA2PTE(pa) | flags;
}
// 映射到子进程,不拷贝物理内容
if(mappages(new, i, PGSIZE, pa, flags) != 0) goto err;
add_ref(pa); // 增加该物理页的引用计数
}
return 0;
err:
uvmunmap(new, 0, i / PGSIZE, 1);
return -1;
}
2.3 处理缺页异常(trap.c, vm.c)
当硬件抛出 scause == 15 (Store/AMO Page Fault)
时,说明进程尝试写入一个只读页。我们需要判断这是否是一个 COW
页并进行处理。
在 kernel/vm.c 中实现异常处理函数: 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36int handle_cow(pagetable_t pagetable, uint64 va) {
if(va >= MAXVA) return -1;
va = PGROUNDDOWN(va);
pte_t *pte = walk(pagetable, va, 0);
if(pte == 0 || !(*pte & PTE_V) || !(*pte & PTE_COW) || !(*pte & PTE_U))
return -1;
uint64 pa = PTE2PA(*pte);
// 检查引用计数
acquire(&pageref.lock);
int count = pageref.count[pa / PGSIZE];
release(&pageref.lock);
if(count > 1) {
// 存在共享,执行实际分配与拷贝
char *mem = kalloc();
if(mem == 0) return -1;
memmove(mem, (char*)pa, PGSIZE);
uint flags = PTE_FLAGS(*pte);
flags &= ~PTE_COW;
flags |= PTE_W; // 变为可写
// 更新页表映射到新物理地址
*pte = PA2PTE((uint64)mem) | flags;
kfree((void*)pa); // 减少旧物理页的引用
} else {
// 如果引用计数已经是 1,说明该页已归当前进程独占,直接“转正”
*pte |= PTE_W;
*pte &= ~PTE_COW;
}
return 0;
}
在 kernel/trap.c 的 usertrap() 中拦截异常:
1
2
3
4
5
6
7
8
9
10
11
12
13void usertrap(void) {
uint64 scause = r_scause();
// ...
if(scause == 8) { // 系统调用
// ...
} else if(scause == 15) { // 关键:写操作导致的缺页中断
if(handle_cow(p->pagetable, r_stval()) < 0)
p->killed = 1;
} else {
// 其他异常处理
}
// ...
}
2.4 兼容 copyout
系统调用
内核向用户地址空间写数据(例如 read
系统调用将文件内容存入用户
Buffer)时,是在内核态执行的,不会触发用户态的
usertrap。我们需要在 kernel/vm.c 的
copyout 中手动处理 COW 逻辑: 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21int copyout(pagetable_t pagetable, uint64 dstva, char *src, uint64 len) {
uint64 n, va0, pa0;
while(len > 0){
va0 = PGROUNDDOWN(dstva);
// 必须在写之前检查并处理可能存在的 COW 页
if(handle_cow(pagetable, va0) < 0)
return -1;
pa0 = walkaddr(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;
}