目录
- Task: Implement copy-on write
- step 1:对内存块进行引用计数
- step 2:`uvmcopy` 实现 fork 时将 parent 的物理页映射到 child 中
- step 3:在 `usertrap` 中增加对 page fault 的处理
- 执行测试
官方说明:Lab: Copy-on-Write Fork for xv6
这个实验是利用 page fault 实现操作系统的 Copy-On-Write 功能(COW)。
COW 的意思是说,当 fork 时,子进程不会完全拷贝父进程的整个内存空间,而是与父进程共享同一份物理内存,并在 page table 中记录映射关系,当任意一个进程需要写某块内存时,会发生 page fault,page fault handler 会真正分配出一块新的内存并重新建立映射,从而节省了大量的内存拷贝。
通过这个 lab,对 COW 有了更深的了解。
Task: Implement copy-on write
这个 task 的要求是实现 COW,并通过 cowtest
和 usertests
。
step 1:对内存块进行引用计数
由于多个进程可能会共享同一份物理内存,因此像 kfree()
这类函数不能随意释放一个物理内存,而是需要借助引用计数来决定能否真正释放物理内存,所以我们需要实现对每个物理内存块的引用计数。
在 kalloc.c
文件中,我们模仿 kmem 结构体写一个 kref,用来对每个内存块进行引用计数:
struct {struct spinlock lock;int counts[PHYSTOP / PGSIZE];
} kref;
在 kinit()
对 kref 进行初始化:
更改引用计数的函数:
// 增加对某个物理地址的引用计数
void
kref_inc(uint64 pa)
{acquire(&kref.lock);kref.counts[pa / PGSIZE]++;release(&kref.lock);
}// 减少对某个物理地址的引用计数
void
kref_dec(uint64 pa)
{acquire(&kref.lock);kref.counts[pa / PGSIZE]--;release(&kref.lock);
}// 获取某个物理地址的引用计数
int
kref_count(uint64 pa)
{int count;acquire(&kref.lock);count = kref.counts[pa / PGSIZE];release(&kref.lock);return count;
}
然后修改 kalloc()
,当新分配一个物理内存块时,其引用计数应当为 1:
然后是修改 kfree()
函数,当通过 kfree 想释放一个内存块时,需要检查这个内存块的引用计数,如果存在多个引用,只需要对引用计数 -1 即可,只有当没有更多进程引用这个内存块时,才能真正释放它:
step 2:uvmcopy
实现 fork 时将 parent 的物理页映射到 child 中
调用 fork 时,会使用 uvmcopy
来克隆一份 parent 的物理内存空间给 child,而我们为了实现 COW,在这里只需要共享同一个物理内存。
具体的做法就是,遍历 parent 的整个内存空间,将其物理地址封装成 PTE 放入 child 的 page table 中,同时需要将 parent 和 child 的 PTE 都改为只读的,这样之后任一进程在 write 时能够发生 page fault 并进而完成实际的内存分配。
除了 PTE 需要设置为只读的,还需要在 PTE 标记一下这是一个 COW page,从而与普通的只读 page 进行区分,为此,需要使用 PTE 中的预留位作为 COW 标志位:
修改 uvmcopy
:
这一块代码对原有拷贝整个内存空间的代码进行改造,实现遍历整个内存空间,并获取每块内存的 PTE 和物理地址,对 PTE 的 flags 做了修改后,再将其添加到 child 的 page table 中。另外注意,需要增加对物理内存的引用计数,因为这里每个内存增加了一个 child 进行对其的引用。
step 3:在 usertrap
中增加对 page fault 的处理
当一个被标为 COW 的只读 page 发生 page fault 时,我们需要对其进行 COW 的处理,为其分配一份新的物理内存,并修改 page table 中的映射,这样当 page fault 被处理并恢复正常流程后,就可以对内存进行 write 了。
首先我们需要一个函数来判断一个 page 是否为 COW page:
// 判断是否为 cow page
// 是的话返回 1,否则返回 0
int is_cowpage(pagetable_t pagetable, uint64 va){if (va >= MAXVA)return 0;pte_t *pte = walk(pagetable, va, 0);if(pte == 0)return 0;if ((*pte & PTE_V) == 0)return 0;return (*pte & PTE_COW) != 0;
}
另外我们将处理 COW 导致的 page fault 的逻辑封装为函数 handle_cow()
:
void* handle_cow(pagetable_t pagetable, uint64 va){va = PGROUNDDOWN(va);pte_t *pte = walk(pagetable, va, 0);if(pte == 0)return 0;uint64 pa = PTE2PA(*pte);if (pa == 0)return 0;// 根据 ref count 判断是否需要分配新的物理页if (kref_count(pa) == 1) {// 如果只存在一个引用,则直接修改 PTE 的 flags 并返回即可*pte |= PTE_W; // 增加 PTE 的写权限*pte &= ~PTE_COW; // 清除 PTE 的 COW 标志return (void*)pa;}// 如果存在多个引用,则需要分配新的物理页并拷贝旧页面内容char *mem = kalloc(); // 分配物理内存if (mem == 0) {return 0;}memmove(mem, (void*)pa, PGSIZE); // 拷贝旧页面内容// 修改 va 的 mapping*pte = PA2PTE(mem) | ((PTE_FLAGS(*pte) | PTE_W) & (~PTE_COW));// 将旧物理页的 ref count 减一kfree((void*)pa);return mem;
}
这里的易错点:
- 我们需要通过物理页的 ref count 来决定处理 COW page fault 时,是否需要分配一个新的物理页,因为当被标为 COW page 时,是两个及以上的进程来共享同一个物理 page,当随着有些进程因为 COW page fault 而创建了新的内存块而减少 ref count,直到当 ref count 为 1 时,发生 COW page fault 的进程就不再需要分配新的物理页,而是直接将之前的物理页恢复可写的 flag 即可。
- 修改 PTE 的 flags 时,注意不要出错
有了这两个函数,我们就可以在 usertrap()
中增加对 page fault 的处理逻辑:
在发生 page fault 时,会有三种可能:
- 访问了非法内存页
- 内存页合法,但是权限不合适(比如在不可执行的 page 上执行指令等)
- 在 COW 的只读 page 上执行 write 操作。这也是我们唯一能处理并恢复的 page fault。
完成以上修改后,这个实验就完成了。
执行测试
make qemu 后分别执行 cowtest
和 usertests
:
测试通过~