Lab6 写时复制
预备知识
写时复制(COW)
核心思想就是等到修改数据时才真正分配内存空间来拷贝资源。比如说,有多个请求同时请求相同资源(如内存或磁盘上的数据存储),它们首先会共同使用相同的资源,但被要求只读,直到某个请求试图修改资源时,系统才会真正复制一份副本给该请求者,而其他请求者所见到的资源仍然保持不变。这过程对其他的调用者都是透明的。
fork()中的写时复制
xv6 中的 fork() 系统调用会将父进程的用户空间内存全部复制到子进程中。如果父进程较大,复制过程可能需要较长时间。而且可能造成大量浪费;比如在fork()后子进程首先调用 exec(),就会导致子进程丢弃复制的内存,而其中大部分可能从未被使用。
而COW fork() 仅为子进程创建一个和父进程一样的页表和一个唯一的进程标识号,其中的 PTE 指向父进程的物理页面。在这种情况下,需要确保每个物理页面在最后一个指向它的 PTE 引用消失时才能被释放。实现这一点的方法是为每个物理页面维护一个“引用计数”,记录引用该页面的用户页面表的数量。COW fork() 将父进程和子进程中的所有用户 PTE 标记为不可写。当任一进程尝试写入这些 COW 页面时,CPU 将触发页面故障。内核的页面故障处理程序检测到此情况后,检测该页面的引用计数,当引用计数小于等于1时,不会有竞态条件,直接返回该物理页面,如果大于1,则将该物理页的引用计数减1,然后返回一个新的物理页,将原始页面复制到新页面,并修改故障进程中相关的 PTE 以指向新页面,此时 PTE 被标记为可写。当页面故障处理程序返回时,用户进程即可写入其页面副本。
Implement copy-on write
在Xv6内核中实现COW fork()功能。
添加COW标志
首先根据提示为PTE添加新的标记为,PTE中的低8,9,10位是保留位。
#define PTE_COW (1L << 8) //1 -> cow PTE
添加引用计数
记录每个物理页面被多少个进程引用,并修改相关的逻辑。
新增相关的数据结构和宏
//kernel/kalloc.c
int page_ref[ (PHYSTOP-KERNBASE) / PGSIZE]; //记录所有物理页面引用计数的数组
#define PA2PGID(pa) (( (pa)-KERNBASE )/PGSIZE) //物理地址转记录数组下标
#define PA2PGREF(pa) (page_ref[PA2PGID((uint64)pa)]) //获取物理地址对应的物理页的引用计数
struct spinlock pgreflock; //防止page_ref竞态条件的自旋锁
初始化锁
//kernel/kalloc.c
void
kinit()
{
initlock(&kmem.lock, "kmem");
initlock(&pgreflock, "page_ref");
freerange(end, (void*)PHYSTOP);
}
修改kalloc,在这里初始化引用计数为1
//kernel/kalloc.c: kalloc
if(r){
memset((char*)r, 5, PGSIZE); // fill with junk
PA2PGREF(r) = 1;//这里不用加锁,因为还没有kalloc函数还没有返回,不会有进程来使用
}
//在defs.h中声明
void pgrefadd(void *);
实现引用计数+1
//kernel/kalloc.c
//将物理地址对应的页面引用计数+1
void
pgrefadd( void* pa){
acquire(&pgreflock);
PA2PGREF(pa)++;
release(&pgreflock);
}
根据引用计数来返回页面的函数
//kernel/kalloc.c
//根据页面的引用计数返回
void*
kcow_pgref( void* pa){
acquire(&pgreflock);
if( PA2PGREF(pa) <= 1){
release(&pgreflock);
return pa;
}
void* newpa = kalloc();
if( newpa == 0 ){
release(&pgreflock);
printf("内存不足");
return 0;
}
memmove( newpa, pa, PGSIZE );
PA2PGREF(pa)--;
release(&pgreflock);
return newpa;
}
//kernel/defs.h
void* kcow_pgref(void *);
修改kfree,只有当引用计数为0时才释放物理页面
void
kfree(void *pa)
{
struct run *r;
if(((uint64)pa % PGSIZE) != 0 || (char*)pa < end || (uint64)pa >= PHYSTOP)
panic("kfree");
//释放时要加锁
acquire(&pgreflock);
//这里如果是等于0会导致Xv6启动时卡死
//估计是因为初始模拟物理页分配给硬件时没有设置COW位默认为0
//因此在kfree时会小于0
PA2PGREF(pa)--;
if( PA2PGREF(pa) <= 0 ){
// 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);
}
release(&pgreflock);
}
修改父子进程复制
原来父进程执行fork()时会调用uvmcopy()复制完整的物理内存给子进程。现在需要将复制功能取消。首先将可写页面的W清除,并将这些页的COW位置1,只读页的COW还是0。这样的好处在于触发写时复制时还是可以只复制一部分物理页,同时也可以分辨出哪些是可写的(COW为1),只读的(COW为0)。
//kernel/vm.c
uvmcopy(pagetable_t old, pagetable_t new, uint64 sz)
{
pte_t *pte;
uint64 pa, i;
uint flags;
//char *mem;
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");
pa = PTE2PA(*pte);
//为W位为1的PTE的COW位置1,并清除其W
//W为0的PTE不会进行操作
if( *pte & PTE_W)
*pte = (*pte & ~PTE_W) | PTE_COW;
flags = PTE_FLAGS(*pte);
//子进程的虚拟地址直接映射到父进程的物理页面
if( mappages(new, i, PGSIZE, (uint64)pa, flags) != 0)
goto err;
pgrefadd((void*)pa);
// if((mem = kalloc()) == 0)
// goto err;
// memmove(mem, (char*)pa, PGSIZE);
// if(mappages(new, i, PGSIZE, (uint64)mem, flags) != 0){
// kfree(mem);
// goto err;
// }
}
return 0;
err:
uvmunmap(new, 0, i / PGSIZE, 1);
return -1;
}
实现写时复制
在usertrap中检测是否是COW机制导致的trap,如果是则执行写时复制
//kernel/trap.c:usertrap
else if((which_dev = devintr()) != 0){
// ok
} else if( (r_scause()==13 || r_scause()==15) && iscow( r_stval() ) ){
if(cowcopy(r_stval()) == -1)
p->killed = 1;
}
检测是否是COW引发的trap
//kernel/vm.c
#inldue "spinlock.h"
#include "proc.h"
int
iscow( uint64 va){
pte_t* pte;
struct proc* p = myproc();
if( va < 0 || va > p->sz)//不加这个过不了usertests
return 0;
pte = walk(p->pagetable,va, 0);
if(pte == 0)
return 0;
if(*pte & PTE_COW)
return 1;
else
return 0;
}
具体的写时复制实现
int
cowcopy( uint64 va){
pte_t* pte;
struct proc* p = myproc();
if(( pte = walk(p->pagetable, va, 0)) == 0)
panic("cowcopy:walk");
uint64 pa = PTE2PA(*pte);
uint64 newpa = (uint64)kcow_pgref( (void*)(pa));
if(newpa == 0)
return -1;
//建立新的映射
//恢复W位,清除COW位
uint64 flags = (PTE_FLAGS(*pte) | PTE_W) & ~PTE_COW;
//取消旧的映射
uvmunmap(p->pagetable, PGROUNDDOWN(va), 1, 0);
if(mappages(p->pagetable, va, 1, newpa, flags) == -1 )
panic("cowcopy:mappages");
return 0;
}
最后修改copyout,为其添加检测机制
while(len > 0){
//检查是否是COW页
if(iscow(dstva))
cowcopy(dstva);
va0 = PGROUNDDOWN(dstva);
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;
}
执行测试
$ cowtest
simple: ok
simple: ok
three: ok
three: ok
three: ok
file: ok
ALL COW TESTS PASSED
$usertests
...
test forktest: OK
test bigdir: OK
ALL TESTS PASSED

浙公网安备 33010602011771号