前面的5个Lab,边抄边过的,虽然比较顺利,很快,觉得没啥意思,Lab6 打算自己安心做一做,不图结果和快了
问题描述:
fork()系统调用会把父进程的用户空间全拷贝到子进程,这种拷贝在父进程用户空间很大的时候,很耗费资源,而且还经常被浪费,比如fork后面紧跟exec,那么子进程会直接启用掉fork来的用户空间, 但是另一方面来讲,如果父 子进程都要写页面,那么复制是需要的
copy on write的目标,就是延迟分配,只在真的需要分配时,才放页表
COW 首先为子进程分配一个页表,但是页表里面的页表项(PTEs)是指向父进程的内存空间的,并且cow把该内存空间标为只读,当父子进程尝试写的时候,触发页错误,内核会处理这个异常,为写进程分配一个新页面,并把父进程的内容拷贝到该新页面,然后更新页表PTE,映射到新的内存,并把该页表项设置为可写,页错误hanlder返回以后,用户进程便能够写页表
cow的fork,会有一个比较麻烦的点,就是多个虚拟空间,会指向同一个物理空间,这样释放的时候,我们就得确认,最后一个指向物理空间的虚拟地址被释放以后,才能真正释放该物理空间
你必须通过usertests和cowtests
第一个测试就会失败,因为fork父进程先申请空闲内存的一半,子进程会再申请一半,内存不足,导致错误
hints
2. 修改usertrap来识别页错误,当出现cow页错误时,用kalloc分配内存,将老的页复制到新的页,并把新的页置为PTE_W
3. 当最后一个对物理空间的指针drop以后,释放该物理空间,要生成一个引用计数, 在kalloc时将该引用计数设置为1,每次释放虚拟页表时,将该引用减一,kfree只有在引用计数为0的时候,才真正把该页放入空闲页表中
4. 你可以把这些引用计数,放入一个数组中,但是你需要想办法解决如何在数组中查找,以及选择数组的大小 For example, 你可以用 物理地址/4096来作为数组的索引,并且给这个数组一个free list里面最高的物理页空间
5. 修改copyout,当其遇见一个COW页时
Lab 5的lazy allocation和COW看上去有点相似,但是你不该在lazy allocation的基础上改写,而应该重新写代码
对于每个PTE,都应该描述它是否是一个COW映射,你可以用PTE的RSW保留位来标注他
同时测试usertest和cowtest
riscv.h包含一些有用的宏和定义
如果COW也错误出现,而此时没有空闲内存,那么这个进程就该被kill
先把问题描述清楚
1.修改usertrap
对老的页面写失效,通过kalloc分配新的页面,如果无法分配则杀死进程,并通过walkaddr来找到原来的老物理页面地址,复制给新页面
} else if(r_scause() == 15) {
char* mem;
mem = kalloc();
if(mem == 0) {
p->killed = 1;
return 0;
}
uint64 va = PGROUNDDOWN(r_stval());
uint64 pa = walkaddr(p->pagetable, va);
pte_t* pte = walk(p->pagetable, va, 0);
if(pte & PTE_COW) {
memmove(mem, (char*)pa, PGSIZE);
//kfree((char*)pa);
uvmunmap(p->pagetable, va, 1, 1);
if(mappages(p->pagetable, va, PGSIZE, (uint64)mem, PTE_W|PTE_X|PTE_R|PTE_U) != 0){
kfree(mem);
p->killded = 1;
return 0;
}
} else {
kfree(mem);
p->killded = 1;
return 0;
}
}
2. 修改uvmcopy成只读~PTE_W,并且让子进程的虚拟地址映射也指向父进程的物理地址
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) & ~PTE_W) | PTE_COW ;
//if((mem = kalloc()) == 0)
// goto err;
//memmove(mem, (char*)pa, PGSIZE);
*pte = (*pte & ~PTE_W) | PTE_COW;
if(mappages(new, i, PGSIZE, (uint64)pa, flags) != 0){
//kfree(mem);
goto err;
}
}
return 0;
err:
uvmunmap(new, 0, i / PGSIZE, 1);
return -1;
}
3.在kalloc.c里面添加数组,用来记录指向某物理页的指针总数
extern int[] refNum = new int[PHYSTOP/PGSIZE];
4. 修改kalloc和kfree代码
void
kfree(void *pa)
{
struct run *r;
if(--refNum[(uint64)pa/PGSIZE] > 0) {
return;
}
if(((uint64)pa % PGSIZE) != 0 || (char*)pa < end || (uint64)pa >= PHYSTOP)
panic("kfree");
// 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);
}
// Allocate one 4096-byte page of physical memory.
// Returns a pointer that the kernel can use.
// Returns 0 if the memory cannot be allocated.
void *
kalloc(void)
{
struct run *r;
acquire(&kmem.lock);
r = kmem.freelist;
if(r)
kmem.freelist = r->next;
release(&kmem.lock);
if(r) {
refNum[(uint64)r/PGSIZE] += 1;
memset((char*)r, 5, PGSIZE); // fill with junk
}
return (void*)r;
}
5. riscv.h添加PTE_COW置位
#define PTE_U (1L << 4) // 1 -> user can access
#define PTE_COW (1L << 8)
6. 修改vm.c的copyout
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);
if (*pte & PTE_COW) {
char* mem;
mem = kalloc();
if(mem == 0) {
return -1;
}
//pa0 = walk(pagetable, va0, 1);
memmove(mem, (char*)pa0, PGSIZE);
if(mappages(pagetable, va0, PGSIZE, (uint64)mem, PTE_W|PTE_X|PTE_R|PTE_U) != 0){
kfree(mem);
return -1;
}
7. defs.h添加函数声明
pte_t* walk(pagetable_t, uint64, int);
总结
思考程序流程的时候,可以拟人化,这样你自己更容易接受