cow的目标就是延迟分配,并且直到必须要复制的时候才会分配物理内存
如果能够通过cowtest
和usertest
,则通过这个lab
修改uvmcopy
使其不是分配一个物理页面,而是将父进程的物理页面映射到子进程的页表中
将父进程和子进程的PTE中的PTE_W都清空
补充:加入cow标识:#define PTE_C (1L<<8)
修改usertrap
使其能够识别出页错误
当一个页错误发生在cow的page上时
保证每个物理页面都是在完全没有进程引用的时候再被free,不可以提前
一个好的实现方法是未每个物理界面都维护一个引用count
你可以将引用计数记录在一个固定大小的数组中
你需要想出一个映射的策略,以及决定它的size
你可以将一个物理地址除以4096来得出索引的下标
并且通过kinit能够给出的最大的物理地址得到最大的数组大小(size):12810241024/4096
// the kernel expects there to be RAM
// for use by the kernel and user pages
// from physical address 0x80000000 to PHYSTOP.
#define KERNBASE 0x80000000L
#define PHYSTOP (KERNBASE + 128*1024*1024)
修改copyout
,让它在遇到cow的page时,使用和页错误相同的策略
按照实验文档推荐的路线来即可,但是还是有一些小坑的
这一部分实验文档没有给出非常具体的指导没有直接把饭喂到我这种菜鸡嘴里,所以有许多具体实现的方式
我是将这一部分的代码全部放在了kalloc.c
文件中。可以想一下,我们什么时候会用到这个页引用计数呢?
前2点已经足够让我们把相关的定义放到kalloc.c
文件里,这里用到了一些宏,主要是为了后面使用其他方便
这里我们的count数组的大小,是由PHYSTOP和KERNBASE计算出来的,一个是可以分配的物理内存的最大值,一个是最小值。因此将它们相减,再除以页面的大小,就可以得到页数,也就是数组的大小。
通过PA2INDEX可以快速得出当前地址位于数组的哪个下标
下面的四个宏分别是求出这个地址对应的页面的引用计数值,以及初始化,减1和加1的操作
在对这个数组操作时,要用lock将其夹住
// KERNBASE 不是 end
#define PA2INDEX(pa) ((((uint64)pa) - KERNBASE) / PGSIZE)
struct {
struct spinlock lock;
int count[PA2INDEX(PHYSTOP)];
} ref_count;
#define PA2REFCOUNT(pa) (ref_count.count[PA2INDEX(pa)])
#define PAINITRC(pa) (ref_count.count[PA2INDEX(pa)] = 1)
#define PADEC(pa) (ref_count.count[PA2INDEX(pa)]--)
#define PAINC(pa) (ref_count.count[PA2INDEX(pa)]++)
接下来分别在kinit
,kfree
和kalloc
时将引用计数的逻辑加入
kinit比较简单,初始化这个锁就行了
void kinit() {
initlock(&kmem.lock, "kmem");
// 初始化计数数组的锁
initlock(&ref_count.lock, "ref_count");
freerange(end, (void *)PHYSTOP);
}
kfree只在这个页面引用计数为0时才真的free它。按理说,应该是用==0去判断,可是这样的话xv6都启动不起来。找出问题了,因为最开始freerange的时候,引用计数没有值,你走来就给它减1,就是负数了,结果导致所有的页面都没有放到freelist中。
void kfree(void *pa) {
struct run *r;
if (((uint64)pa % PGSIZE) != 0 || (char *)pa < end || (uint64)pa >= PHYSTOP)
panic("kfree");
acquire(&ref_count.lock);
PADEC(pa);
if (PA2REFCOUNT(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(&ref_count.lock);
}
kalloc只需要一行,将对应的值初始化为1即可
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
// 初始化这个物理地址的引用数
PAINITRC((char *)r);
}
return (void *)r;
}
同时,我们还需要两个函数,一个是在cow被写时用来分配一个物理页面并将原来的页面拷贝过去,一个是在fork的时候增加引用计数,分别叫做kcopy
和kinc
// 发生了对cow页面的写操作,必须要分配一个物理页面了
void *kcopy(void *pa) {
acquire(&ref_count.lock);
// 如果自己就是唯一的拥有者了,那么就不用申请页面,直接用就完事了
if (PA2REFCOUNT(pa) == 1) {
release(&ref_count.lock);
return pa;
}
void *npa = kalloc();
// 没有可用页面,返回0
if (npa == 0) {
release(&ref_count.lock);
return NULL;
}
// 将当前页面的计数减1,并复制新的页面
PADEC(pa);
memmove(npa, pa, PGSIZE);
release(&ref_count.lock);
return npa;
}
// 给某个页面增加一个计数
void kinc(void *pa) {
acquire(&ref_count.lock);
PAINC(pa);
release(&ref_count.lock);
}
fork时,不真正分配,只增加引用计数,并修改标志位
int 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);
flags = PTE_FLAGS(*pte);
// 如果这个页面可写,才需要设置成cow,因为后面会直接给其write的权利,所以如果只读,那就不用cow了
if (*pte & PTE_W) {
*pte |= PTE_C;
*pte &= ~PTE_W;
}
// 更新flags,下面mappages要用
flags = PTE_FLAGS(*pte);
// 将父进程的物理地址映射到子进程的页表上
if (mappages(new, i, PGSIZE, (uint64)pa, flags) != 0) {
goto err;
}
// 增加引用计数
kinc((void *)pa);
}
return 0;
err:
uvmunmap(new, 0, i / PGSIZE, 1);
return -1;
}
对页错误的trap进行捕获,然后排除以下情况
第2,3,4在xv6里面其实是有点重复的判断,但是小心点反正不会出bug
还有一个细节就是在uvmunmap的时候,dofree必须是0,因为我们在kcopy的时候已经给这个页面的引用减1了,如果dofree=1,待会还得减1,就会出bug
} else if ((which_dev = devintr()) != 0) {
// ok
} else if (r_scause() == 12 || r_scause() == 13 || r_scause() == 15) {
// 地址越界
if (r_stval() >= p->sz) {
p->killed = 1;
} else {
// 分配新的一页
pte_t *pte = walk(p->pagetable, r_stval(), 0);
// 不存在,或者不是cow页
if ((*pte & PTE_W) != 0 || ((*pte) & PTE_V) == 0 || ((*pte) & PTE_C) == 0) {
p->killed = 1;
} else {
void *pa = (void *)PTE2PA(*pte);
void *npa = kcopy(pa);
// 申请内存失败
if (npa == 0) {
p->killed = 1;
} else {
// 已经获得了一块属于自己的物理内存,将地址和标志位更新到页表中
int flag = PTE_FLAGS(*pte);
flag |= PTE_W;
flag &= ~PTE_C;
uvmunmap(p->pagetable, PGROUNDDOWN(r_stval()), 1, 0);
mappages(p->pagetable, PGROUNDDOWN(r_stval()), PGSIZE, (uint64)npa, flag);
}
}
}
}
整体逻辑和trap捕获差不多
int copyout(pagetable_t pagetable, uint64 dstva, char *src, uint64 len) {
uint64 n, va0, pa0;
while (len > 0) {
va0 = PGROUNDDOWN(dstva);
pa0 = walkaddr(pagetable, va0);
if (pa0 == 0)
return -1;
// 到了这里,肯定是一个合法的值了,但是不一定可以写啊
pte_t *pte = walk(pagetable, va0, 0);
// 如果是cow并且不可以写
if (*pte & PTE_C && !(*pte & PTE_W)) {
// 请求获得一块物理内存
void *npa = kcopy((void *)pa0);
if (npa == 0) {
return -1;
}
// 这个物理内存可用
int flag = PTE_FLAGS(*pte);
flag &= ~PTE_C;
flag |= PTE_W;
uvmunmap(pagetable, va0, 1, 0);
mappages(pagetable, va0, PGSIZE, (uint64)npa, flag);
pa0 = (uint64)npa;
}
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;
}