int
uvmcopy(pagetable_t old, pagetable_t new, uint64 sz)
{
pte_t *pte;
uint64 pa, i;
uint flags;
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为不可写,且为COW页
*pte = ((*pte) & ~PTE_W) | PTE_C;
flags = PTE_FLAGS(*pte);
pa = PTE2PA(*pte);
// 不为子进程分配内存,指向pa,页表属性设置为flags即可
if(mappages(new, i, PGSIZE, pa, flags) != 0) {
printf("uvmcopy failed \n");
goto err;
}
kreferCount((void*)pa,1); //该内存页的引用数加1(这里后面会提到)
}
return 0;
err:
panic("uvmcopy error");
uvmunmap(new,0,i / PGSIZE,1);
return -1;
}
//内核可用内存起始位置(做了对齐处理)
#define kstart PGROUNDUP((uint64)end)
//利用物理地址p求数组的下标数
#define N(p) (((PGROUNDUP((uint64)p)-(uint64)kstart) >> 12))
//用于存储引用值的内存段结束的位置
#define kend (uint64)kstart+N(PHYSTOP)
struct {
struct spinlock lock;
struct run *freelist;
//add
struct spinlock reflock; //维护计数数组的自旋锁
char *paref; //映射的用于计数的数组(起始位置kstart)
} kmem;
inline void
acquire_refcnt()
{
acquire(&kmem.reflock);
}
inline void
release_refcnt()
{
release(&kmem.reflock);
}
void
kinit()
{
initlock(&kmem.lock, "kmem");
initlock(&kmem.reflock,"reflock");
kmem.paref = (char*)kstart; //paref映射的用于计数的数组(起始位置kstart)
freerange((void*)kend, (void*)PHYSTOP); //初始化空闲列表
}
void
freerange(void *pa_start, void *pa_end)
{
char *p;
p = (char*)PGROUNDUP((uint64)pa_start);
for(; p + PGSIZE <= (char*)pa_end; p += PGSIZE){
acquire(&kmem.reflock);
*(kmem.paref+N(p)) = 1; //初始化为1,因为后面有kfree减1
release(&kmem.reflock);
kfree(p);
}
}
//pa为物理内存地址
//flag为指示标志,>0为+1,<0为-1
void
kreferCount(void *pa,int flag)
{
acquire(&kmem.reflock);
if(flag > 0){ //当前页映射加1
*(kmem.paref+N((uint64)pa)) += 1;
}
else if(flag < 0){ //当前页映射减1
*(kmem.paref+N((uint64)pa)) -= 1;
}
release(&kmem.reflock);
}
void
kfree(void *pa)
{
struct run *r;
//保证释放的物理内存是对齐的(4k)
if(((uint64)pa % PGSIZE) != 0 || (char*)pa < end || (uint64)pa >= PHYSTOP)
panic("kfree");
kreferCount(pa,-1); //减少一个引用,-1
if(*(kmem.paref+N(pa)) != 0){
return;
}
// 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);
}
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
kreferCount((void*)r,1); //映射该页,引用计数+1
}
return (void*)r;
}
我们已实现对fork映射机制修改,那么当页异常发生,要对子进程重新开辟内存具体应该怎么做?增加如下两个函数,会在后面使用到
#define PTE_C (1L << 8) // copy pte
/*判断是否为未分配内存COW页*/
//PTE_C标志用于区分该页是否是没有分配独立内存的cow页
//PTE_C与PTE_W标志一定为相反
int
uncopied_cow(pagetable_t pgtbl, uint64 va){
if(va >= MAXVA)
return -1;
pte_t* pte = walk(pgtbl, va, 0);
if(pte == 0) // 如果这个页不存在
return -2;
if((*pte & PTE_V) == 0)
return -3;
if((*pte & PTE_U) == 0)
return -4;
return ((*pte) & PTE_C); // 有 PTE_C 的代表还没复制过,并且是 cow 页
}
/*给合法的cow页分配内存*/
int
cowalloc(pagetable_t pgtbl, uint64 va){
pte_t* pte = walk(pgtbl, va, 0);
uint64 perm = PTE_FLAGS(*pte);
if(pte == 0) return -1;
uint64 prev_sta = PTE2PA(*pte); // 这里的 prev_sta 就是这个页帧原来使用的父进程的页表
// 这里写 sta 是因为这个地址是和页帧对齐的(page-aligned)
// 所以写个 sta 表示一个页帧的开始
uint64 newpage = (uint64)kalloc();
if(!newpage){
return -1;
}
uint64 va_sta = PGROUNDDOWN(va); // 当前页帧
perm &= (~PTE_C); // 复制之后就不是合法的 COW 页了
perm |= PTE_W; // 复制之后就可以写了
memmove((void*)newpage, (void*)prev_sta, PGSIZE); // 把父进程页帧的数据复制一遍
uvmunmap(pgtbl, va_sta, 1, 1); // 然后取消对父进程页帧的映射
if(mappages(pgtbl, va_sta, PGSIZE, (uint64)newpage, perm) < 0){
kfree((void*)newpage);
return -1;
}
return 0;
}
根据tips,我们要在trap.c中的usertrap中拦截写页异常,那么利用什么标志呢?
根据riscv手册(riscv-privileged),查到当scause寄存器为15时,为写页异常
在usertrap函数中添加
else if(r_scause() == 15) { // 缺页错误
if(uncopied_cow(p->pagetable,r_stval()) > 0){
if(r_stval() < PGSIZE) //对0起始地址等低地址直接写,那么直接退出
p->killed = 1;
if(cowalloc(p->pagetable,r_stval()) < 0)
p->killed = 1;
}
由于有些访问COW页的操作不是来自用户空间的,那么也需要对vm.c中的copyout函数进行修改(tips中也提到了这一点)
int
copyout(pagetable_t pagetable, uint64 dstva, char *src, uint64 len)
{
//此处发生于内核空间的复制,发生页异常时不会引起usertrap
uint64 n, va0, pa0;
while(len > 0){
va0 = PGROUNDDOWN(dstva); //目标虚拟地址的页首地址
int res = uncopied_cow(pagetable,va0);
if(res > 0){
if(cowalloc(pagetable,va0) != 0)
goto err;
}
else if(res < 0){
// printf(" %d \n",res);
goto err;
}
pa0 = walkaddr(pagetable, va0); //获取虚拟页对应的物理页
if(pa0 == 0)
goto err;
n = PGSIZE - (dstva - va0); //该页的剩余偏移量
if(n > len)
n = len;
memmove((void *)(pa0 + (dstva - va0)), src, n); //直接从物理地址copy到src
len -= n;
src += n;
dstva = va0 + PGSIZE; //翻页
}
return 0;
err:
return -1;
}
在开辟计数数组那一块,纠结了一下是直接申请数组还是在end处开始直接维护一段连续内存。主要是不太明白直接在内核代码中申请数组,那么这个数组会被储存到哪里,之后复习lab book后发现,这个数组会被储存在kernel data中去,而end也会随之增加。
usertests中增加了一个难缠的测试函数textwrite,这个也是去年lab没有的,困扰了我很久,然后发现其实这个函数是新增了一个对cow的bug的检查
子进程copy完父进程的页表后,会将每一页的pte的pte_W置0,pte_C置1,我们就可以通过判断pte_W和pte_C判断该页是不是cow页
那么随之也会出现一个bug,每个用户进程的低地址段都会用于储存代码(即text区域),根据book描述,这一段本来也没有pte_W标志。那么如果我们对其进行COW操作就会引发一系列的错误
所以在usertrap中对这一bug直接拦截
if(r_stval() < PGSIZE)
p->killed = 1;