文章目录
- 实现COW
- 一、问题
- 二、注意
- 三、实现COW
- 四、实验结果
实现COW
一、问题
准备:切换到 cow 分支
目前 xv6 的 fork
系统调用创建的子进程会赋值父进程所有的用户态内存,如果父进程比较大,那么这个复制过程会很耗时,而且一般通过 fork
创建的进程会紧接着执行 exec
系统调用,这将丢弃 fork
阶段拷贝的所有内存。
因此,我们需要实现一个具有cow功能的fork
,在创建子进程的时候仅创建一个页表,而实际的内容(PTE)仍然指向父进程。当进程需要写某些内容的时候,发生页错误,在页错误的处理函数中为进程分配一个新的页面,并拷贝该页面的内容(不管是父进程还是子进程,谁先触发谁拷贝)。
cow会使得物理页的回收变得麻烦,因为一个物理页可能被很多进程共享,只有最后一个引用该页面的进程释放该页面,该页面才能得到回收。
二、注意
- 修改
uvmcopy()
,将父进程的用户态空间物理页 map 到子进程,而不是为子进程分配新页面。清理所有设置了PTE_W
位的父子进程共享的页面的该标记位,因此父子进程都没有这些页面的写权限。 - 修改
usertrap()
,使其可以处理pagefault
,当一个cow页面
(被多个进程引用的页面)发生了write page-fault
,此时通过kalloc()
分配一个新的页面,并将旧页面的数据拷贝过去,然后设置新分配页面的PTE_W
bit位。如果write page fault
发生的页面是一个只读页面,那么该进程应该被kill
- 可以为每个用户态的页面设置一个引用计数,用于实现物理页的释放。可以设置一个固定大小的数组用来存储每个页的引用计数,数组的大小为物理内存的大小除以4k。通过每个页的高地址除以4KB来计算索引 (
kalloc() and kfree()
)。 - 修改
copyout()
以使用相同的算法来操作cow
页面(因为copyout
函数是在向用户页表中写入内容,因此会触发cow
页面的复制) - 可以使用
PTE
页表项中的RSW
位来记录当前页表项对应的页面是否为cow
页面 kernel/riscv.h
中定义了一些可能有用的宏和页表相关的flags- 如果一个进程的
cow
页面发生了页面错误,此时没有物理内存可以分配,此时该进程应该被kill
三、实现COW
思路:
- kernel/riscv.h:定义
PTE_RSW
位 - kernel/kalloc.c:添加一个全局数组以及锁,记录引用计数;
kalloc()
和kfree()
增加对于引用计数的增减操作 - kernel/vm.c:修改
uvmcopy()
实现子进程不拷贝父进程的用户态空间,而只是建立PTE
映射。此时需要将拷贝的原来置PTE_W
的页,清理该标志位,并设置PTE_RSW
位。 - kernel/vm.c:修改
copyout()
,在向一个cow
页(PTE_RSW = 1
)写入时,需要申请新的页面并复制内容,然后其余操作同uvmcopy
(此处也可以不修改copyout
函数,而是使用kerneltrap
的方式来实现,因为copyout
在向用户态的页表中写入内容时,如果不可写,也会出现page write fault
)。 - kernel/trap.c:
usertrap()
函数增加对于page fault
的处理,而page write fault
的异常值为15.
代码:
- kernel/riscv.h
#define PTE_RSW (1L << 8) // RSW
- kernel/defs.h
#include "memlayout.h"
extern int page_refcnt[PHYSTOP / PGSIZE];
/*
该锁保护的临界资源只有处于内核态的函数才能访问到,因此只有系统调用、trap路径可以访问
只要保证trap或者系统调用可能发生异常的地方之前释放锁就不会死锁
*/
extern struct spinlock ref_lock;
- kernel/main.c
initlock(&ref_lock, "ref_lock"); //初始化锁
procinit(); // process table
- kernel/kalloc.c
int page_refcnt[PHYSTOP / PGSIZE]; //定义引用数组,大小为物理内存总量除以每个page的大小
struct spinlock ref_lock; //用于保证并发
void *
kalloc(void)
{
struct run *r;
int index = -1;
// ...
if(r){
memset((char*)r, 5, PGSIZE); // fill with junk
// 新增代码
index = (uint64)r / PGSIZE;
if(r >= 0){
acquire(&ref_lock);
page_refcnt[index] = 1; //增加引用计数
release(&ref_lock);
}
//新增结束
}
return (void*)r;
}
void
kfree(void *pa)
{
struct run *r;
int refcnt = 0;
if(((uint64)pa % PGSIZE) != 0 || (char*)pa < end || (uint64)pa >= PHYSTOP)
panic("kfree");
r = (struct run*)pa;
// 新增代码
acquire(&ref_lock);
refcnt = (--page_refcnt[(uint64)r / PGSIZE]);
release(&ref_lock);
if(refcnt > 0){
return;
}
//新增结束
// Fill with junk to catch dangling refs.
}
- kernel/vm.c
#include "param.h"
#include "types.h"
#include "memlayout.h"
#include "elf.h"
#include "riscv.h"
#include "defs.h"
#include "fs.h"
#include "spinlock.h" // 引入头文件
#include "proc.h" // 引入头文件
int
uvmcopy(pagetable_t old, pagetable_t new, uint64 sz)
{
pte_t *pte;
uint64 pa, i;
uint flags;
//char *mem = 0;
// old 为父进程页表,new 为子进程页表
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");
// 如果是 PTE_W,清理该标记位,并设置 PTE_RSW
if((*pte & PTE_W) && (*pte & PTE_U)){
*pte &= ~PTE_W; //清理 PTE_W
*pte |= PTE_RSW;
}
pa = PTE2PA(*pte);
flags = PTE_FLAGS(*pte);
// 不需要分配内存
// if((mem = kalloc()) == 0)
// goto err;
acquire(&ref_lock);
page_refcnt[pa / PGSIZE]++;
release(&ref_lock);
//memmove(mem, (char*)pa, PGSIZE);
if(mappages(new, i, PGSIZE, pa, flags) != 0){
acquire(&ref_lock);
page_refcnt[pa / PGSIZE]--;
release(&ref_lock);
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 = 0;
while(len > 0){
va0 = PGROUNDDOWN(dstva);
pa0 = walkaddr(pagetable, va0);
if(pa0 == 0)
return -1;
if(va > myproc()->sz){
return -1;
}
// 新增
//pte = PA2PTE(pa0); 此处不能使用该宏,因为该宏将flags等都抹除了
pte = walk(pagetable, va0, 0);
if(*pte & PTE_RSW){
//如果是cow页面,分配一个新的页面
uint64 addr = (uint64)kalloc();
if(addr == 0){
// 分配失败,杀掉当前进程
myproc()->killed = 1;
} else {
// 复制旧页面的全部内容到新页面
memmove((void*)addr, (char*)pa0, PGSIZE);
uint flags = PTE_FLAGS(*pte);
// 解除旧页面和va0的映射,该函数内部会调用 kfree() 减少引用计数
uvmunmap(pagetable, va0, 1, 1);
// 设置PTE的标记位,重置其物理地址以及设置PTE_W标志位
*pte = (PA2PTE(addr) | flags | PTE_W);
*pte &= ~PTE_RSW; //清理 PTE_RSW 标志位
pa0 = (uint64)addr; //更正pa0指向新的页面
}
}
// 新增结束
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
int cowtrap(pagetable_t pagetable, uint64 va);
void
usertrap(void)
{
// ...
if(r_scause() == 8){
// ...
syscall();
} else if(r_scause() == 15){
//新增
uint64 va = r_stval();
if (va >= p->sz)
p->killed = 1;
if (cowtrap(p->pagetable, va) != 0)
p->killed = 1;
// 新增结束
} else if((which_dev = devintr()) != 0){
// ok
} else {
//...
}
// ...
}
int cowtrap(pagetable_t pagetable, uint64 va){
uint64 addr;
pte_t *pte = walk(pagetable, va, 0);
if (pte == 0)
return -1;
// check the PTE
if ((*pte & PTE_RSW) == 0 || (*pte & PTE_U) == 0 || (*pte & PTE_V) == 0) {
return -1;
}
if ((addr = (uint64)kalloc()) == 0) {
return -1;
}
// 获取旧页的物理地址
uint64 pa = PTE2PA(*pte);
// 拷贝旧页面内容到新页面
memmove((char*)addr, (char*)pa, PGSIZE);
//减少旧页面的引用计数
kfree((void*)pa);
uint flags = PTE_FLAGS(*pte);
// 重新设置pte页表项
*pte = (PA2PTE(addr) | flags | PTE_W);
// 取消当前进程pte的PTE_RSW标志位
*pte &= ~PTE_RSW;
return 0;
}
四、实验结果
- cowtest
- userstests -q