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
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
36
37
38
struct {
struct spinlock lock;
int count[PHYSTOP / PGSIZE];
} pageref;

void kinit() {
initlock(&pageref.lock, "pageref");
initlock(&kmem.lock, "kmem");
freerange(end, (void*)PHYSTOP);
}

// 增加引用计数的接口
void add_ref(uint64 pa) {
acquire(&pageref.lock);
if(pageref.count[pa / PGSIZE] < 1) panic("add_ref");
pageref.count[pa / PGSIZE]++;
release(&pageref.lock);
}

// 修改 kfree:只有计数归零才释放
void kfree(void *pa) {
struct run *r;
if(((uint64)pa % PGSIZE) != 0 || (char*)pa < end || (uint64)pa >= PHYSTOP)
panic("kfree");

acquire(&pageref.lock);
if(--pageref.count[(uint64)pa / PGSIZE] <= 0) {
release(&pageref.lock);
memset(pa, 1, PGSIZE);
r = (struct run*)pa;
acquire(&kmem.lock);
r->next = kmem.freelist;
kmem.freelist = r;
release(&kmem.lock);
} else {
release(&pageref.lock);
}
}

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
29
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) 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
36
int 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.cusertrap() 中拦截异常:

1
2
3
4
5
6
7
8
9
10
11
12
13
void 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.ccopyout 中手动处理 COW 逻辑:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
int 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;
}


MIT 6.S081 lab5:Copy-on-Write | 写时复制
https://acmicpc.top/2024/03/01/MIT-6.S081-lab06/
作者
江欣婷
发布于
2024年3月1日
许可协议