MIT6.S081实验6学习记录
Lab 6 COW 实验要求
这个实验继承自实验五。在fork时,父进程会把页表复制给子进程,并且为子进程的页表分配内存,建立映射,属于一种eager allocation。 这个实验就是要修改为一种类似的lazy allocation。
首先是父进程在uvmcopy(将页表复制给子进程)时,不会立即分配物理内存,而是只把这个修改记录下来(设PTE_COW位,并设置PTE_W为不可写)。然后当子进程要写入对应的物理地址时,才触发一个缺页中断,检测当前是否是COW页,如果是,那就给他建立映射,并把PTE_COW位置零,让PTE_W置为可写。但是要注意,如果该进程不止一个子进程,如果在这时释放,就会让其他子进程无法共享到父进程的内存,所以需要添加一个值,每当子进程申请复制时,就加一,直到这个值为0,说明其他子进程都不再需要这段内存,此时再释放即可。
接下来按照实验的提示一步一步进行即可。
首先是修改 uvmcopy() ,需要做的就是不要立即分配子进程的内存(把映射map到物理内存),而是只修改PTE_W和PTE_COW,前者修改为不可写(父、子进程),后者将这一页改为COW页(子进程)。
int
uvmcopy(pagetable_t old, pagetable_t new, uint64 sz)
{
pte_t *pte;
uint64 pa, i;
uint64 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); //来自父进程的PTE标志
flags = PTE_FLAGS(*pte);
//不分配内存,而是设置标记
if (flags & PTE_W)
{
flags = (flags | PTE_COW) & (~PTE_W); //子进程PTE_W不可写,PTE_COW置1
*pte = PA2PTE(pa) | flags; //父进程PTE_W为不可写
}
increase_rc((void*)pa); //增加计数
//map this vitural address
if(mappages(new, i, PGSIZE, (uint64)pa, flags) != 0){
goto err;
}
}
return 0;
err:
uvmunmap(new, 0, i / PGSIZE, 1);
return -1;
}
接下来就是修改trap.c中的usertrap() 函数,利用trap机制来处理这个缺页中断(类似于上个实验)。也就是判断有缺页中断,就先判断一下当前地址是否有效(报错的虚拟地址超过最大的虚拟地址,或者访问的是每个进程的guard page),然后给有COW标志位的页表进行副本copy,然后设置为可写。
void
usertrap(void)
{
...
syscall();
}else if((r_scause() ==13 || r_scause() ==15)){
uint64 va = r_stval();
//保证当前页地址有效
if (va >= MAXVA ||
(va <= PGROUNDDOWN(p->trapframe->sp)
&& va >= PGROUNDDOWN(p->trapframe->sp) - PGSIZE))
{
p->killed = 1;
}else if(uvmcowcopy(p->pagetable, va) != 0){
//判断页是否是COW页,并且为其实际分配内存
p->killed = 1;
}
}
else if((which_dev = devintr()) != 0){
// ok
} else {
printf("usertrap(): unexpected scause %p pid=%d\n", r_scause(), p->pid);
printf(" sepc=%p stval=%p\n", r_sepc(), r_stval());
p->killed = 1;
}
if(p->killed)
exit(-1);
// give up the CPU if this is a timer interrupt.
if(which_dev == 2)
yield();
usertrapret();
}
接下来就是添加一个函数,根据上面的描述,这个函数的作用就是,识别当前页表是不是PTE_COW页,如果是,就将其分配一个副本,这个副本要是可写的,然后映射到实际物理内存,让子进程能够访问到完成写任务。然后把这个页表的PTE_COW置零,并且使用 kfree() 来减少计数以识别最后一个子进程也不再需要这个页表了,然后彻底释放。
int
uvmcowcopy(pagetable_t pagetable, uint64 va)
{
uint64 pa;
pte_t *pte;
uint flags;
if (va >= MAXVA) return -1;
va = PGROUNDDOWN(va);
pte = walk(pagetable, va, 0);
if (pte == 0) return -1;
pa = PTE2PA(*pte);
if (pa == 0) return -1;
flags = PTE_FLAGS(*pte);
if (flags & PTE_COW) //判断是否是COW页
{
char *ka = kalloc();
if (ka == 0) return -1;
memmove(ka, (char*)pa, PGSIZE);
kfree((void*)pa);
flags = (flags & ~PTE_COW) | PTE_W; //将flag的COW为置0,并且设置为可写
*pte = PA2PTE((uint64)ka) | flags; //修改页表的pte
return 0;
}
return 0;
}
写到这里,trap处理的部分就结束了,接下来需要修改kalloc.c部分,主要是能够让内存分配或者释放时,更新计数。首先定义一个结构体,为了避免访问出现竞态,定义一个锁(仿照kmem写)。
## kernel/kalloc.c
#define PA2IDX(pa) (((uint64)pa) >> 12)
struct
{
struct spinlock lock;
int count[PGROUNDUP(PHYSTOP) / PGSIZE];
}mem_ref;
//增加计数
void
increase_rc(void *pa)
{
acquire(&mem_ref.lock);
mem_ref.count[PA2IDX(pa)]++;
release(&mem_ref.lock);
}
//减少计数
void
decrease_rc(void *pa)
{
acquire(&mem_ref.lock);
mem_ref.count[PA2IDX(pa)]--;
release(&mem_ref.lock);
}
//获取计数
int
get_rc(void *pa)
{
acquire(&mem_ref.lock);
int rc = mem_ref.count[PA2IDX(pa)];
release(&mem_ref.lock);
return rc;
}
//初始化
void
kinit()
{
initlock(&mem_ref.lock, "mem_ref");
acquire(&kmem.lock);
//将计数都初始化为0
for (int i = 0; i < PGROUNDUP(PHYSTOP) / PGSIZE; i++)
mem_ref.count[i] = 0;
release(&kmem.lock);
initlock(&kmem.lock, "kmem");
freerange(end, (void*)PHYSTOP);
}
注意这里计数不要一开始全部设置为1,因为有些页可能并没有被用到也被设置为1,所以要在freerange时增加,freerange函数作用是清理这段范围的内存,腾出空间给当前进程使用。
void
freerange(void *pa_start, void *pa_end)
{
char *p;
p = (char*)PGROUNDUP((uint64)pa_start);
for(; p + PGSIZE <= (char*)pa_end; p += PGSIZE){
increase_rc((void*)p); //在这里增加一个计数
kfree(p);
}
}
在释放页面时,需要考虑这个界面的使用计数是否为0,因此,每次访问kfree时,就将这个计数减一,如果为0,就直接彻底free掉就行。
void
kfree(void *pa)
{
struct run *r;
if(((uint64)pa % PGSIZE) != 0 || (char*)pa < end || (uint64)pa >= PHYSTOP)
panic("kfree");
decrease_rc(pa); //每调用一次就减少一次计数
if (get_rc(pa) > 0) //如果计数没有到0,还是返回,不会销毁
return;
memset(pa, 1, PGSIZE);
r = (struct run*)pa;
acquire(&kmem.lock);
r->next = kmem.freelist;
kmem.freelist = r;
release(&kmem.lock);
}
每当给COW页分配内存时,就需要增加一个计数。
void *
kalloc(void)
{
struct run *r;
acquire(&kmem.lock);
r = kmem.freelist;
if(r){
kmem.freelist = r->next;
}
release(&kmem.lock);
if(r){
memset((char*)r, 5, PGSIZE); // fill with junk
increase_rc((void*)r); //增加一个计数
}
return (void*)r;
}
最后需要在copyout 函数也做一个判断,是否当前页表为COW页。
int
copyout(pagetable_t pagetable, uint64 dstva, char *src, uint64 len)
{
uint64 n, va0, pa0;
while(len > 0){
va0 = PGROUNDDOWN(dstva);
if(uvmcowcopy(pagetable, va0) != 0) //在这里判断,如果是就分配,不是就return
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;
}
然后用脚本测试一下。
jimmy@ubuntu:~/xv6-test/xv6-labs-2020$ ./grade-lab-cow
riscv64-linux-gnu-gcc -Wall -Werror -O -fno-omit-frame-pointer -ggdb -DSOL_COW -DLAB_COW -MD -mcmodel=medany -ffreestanding -fno-common -nostdlib -mno-relax -I. -fno-stack-protector -fno-pie -no-pie -c -o kernel/trap.o kernel/trap.c
riscv64-linux-gnu-ld -z max-page-size=4096 -T kernel/kernel.ld -o kernel/kernel kernel/entry.o kernel/start.o kernel/console.o kernel/printf.o kernel/uart.o kernel/kalloc.o kernel/spinlock.o kernel/string.o kernel/main.o kernel/vm.o kernel/proc.o kernel/swtch.o kernel/trampoline.o kernel/trap.o kernel/syscall.o kernel/sysproc.o kernel/bio.o kernel/fs.o kernel/log.o kernel/sleeplock.o kernel/file.o kernel/pipe.o kernel/exec.o kernel/sysfile.o kernel/kernelvec.o kernel/plic.o kernel/virtio_disk.o
riscv64-linux-gnu-ld: warning: cannot find entry symbol _entry; defaulting to 0000000080000000
riscv64-linux-gnu-objdump -S kernel/kernel > kernel/kernel.asm
riscv64-linux-gnu-objdump -t kernel/kernel | sed '1,/SYMBOL TABLE/d; s/ .* / /; /^$/d' > kernel/kernel.sym
== Test running cowtest == (7.7s)
== Test simple ==
simple: OK
== Test three ==
three: OK
== Test file ==
file: OK
== Test usertests == (143.1s)
(Old xv6.out.usertests failure log removed)
== Test usertests: copyin ==
usertests: copyin: OK
== Test usertests: copyout ==
usertests: copyout: OK
== Test usertests: all tests ==
usertests: all tests: OK
== Test time ==
time: OK
Score: 110/110
总结
这个实验代码量不多,但是debug比较困难,尤其是一些初始化做不好就会报一堆错。
[1]. https://pdos.csail.mit.edu/6.S081/2020/labs/cow.html
[2]. https://zhuanlan.zhihu.com/p/301027032