lab链接: https://pdos.csail.mit.edu/6.1810/2022/labs/cow.html
之所以写这篇总结,是因为这次的实验的难度确实困扰了笔者很久,所以我也想将做此实验的一些收获分享出来给大家
加上现在网上大多是21的lab经验贴,很少有22的,而22的usertest相对21又增加了一些难度,所以我也想将22中的一些问题分享出来
1.修改vm.c文件中的uvmcopy
(1)分析过程
- 要使得fork()中,父进程与子进程共享同一片存储区,主要就是要修改fork的页表分配方法
- 在原始的fork中,是先给子进程重新申请一块内存,并将父进程的页表copy给子进程,现在我们就是要对这一部分进行修改,要让父子进程共享一块内存
- 而这一部分的实现就是在uvmcopy函数中,修改代码如下
(2)代码修改
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;
}
2.修改kalloc.c文件,开辟一个内存区以储存每个物理页的引用数
(1)分析过程
- lab中的tips提到,我们需要去对每一页可用内存(end~PHYSTOP)去计数,只有当该页内存没有被任何一个进程映射,才可以用kfree去释放该页内存
- 如何去实现?tips中也给出了建议,可以去开辟一块数组,这个数组就会被存储在内核代码段中。我的做法是直接使用可用内存(end~PHYSTOP),在end起始位置开始去维护一段以进行存储数组
- 用于计数的内存开辟完成之后,就是修改kfree和kalloc,完成我们想要的逻辑,这部分相对好理解
(2)代码修改
- 在kalloc.c中添加一些对物理内存划分的宏定义
//内核可用内存起始位置(做了对齐处理)
#define kstart PGROUNDUP((uint64)end)
//利用物理地址p求数组的下标数
#define N(p) (((PGROUNDUP((uint64)p)-(uint64)kstart) >> 12))
//用于存储引用值的内存段结束的位置
#define kend (uint64)kstart+N(PHYSTOP)
- 修改结构体kmem,加入维护内存的自旋锁和数组声明
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);
}
- 修改kinit,修改初始化范围
void
kinit()
{
initlock(&kmem.lock, "kmem");
initlock(&kmem.reflock,"reflock");
kmem.paref = (char*)kstart; //paref映射的用于计数的数组(起始位置kstart)
freerange((void*)kend, (void*)PHYSTOP); //初始化空闲列表
}
- 在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){
acquire(&kmem.reflock);
*(kmem.paref+N(p)) = 1; //初始化为1,因为后面有kfree减1
release(&kmem.reflock);
kfree(p);
}
}
- 新增一个函数kreferCount(前面提到了),用于实现对某块内存的计数+1或-1
//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);
}
- 修改kfree函数,加入引用数判断,如果引用数大于0那么不对其做处理
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);
}
- 修改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
kreferCount((void*)r,1); //映射该页,引用计数+1
}
return (void*)r;
}
3.新增函数以实现COW恢复操作
(1)分析过程
我们已实现对fork映射机制修改,那么当页异常发生,要对子进程重新开辟内存具体应该怎么做?增加如下两个函数,会在后面使用到
(2)代码修改
- 根据tips,在riscv.h中增加pte中的cow标志
#define PTE_C (1L << 8) // copy pte
- 在vm.c中增加函数uncopied_cow,用于对写异常页的pte进行判断其是否合法(在defs.h中也要声明该函数)
/*判断是否为未分配内存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 页
}
- 在vm.c中增加函数cowalloc,用于进行COW具体操作(在defs.h中也要声明该函数)
/*给合法的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;
}
4.修改usertrap拦截页异常
(1)分析过程
根据tips,我们要在trap.c中的usertrap中拦截写页异常,那么利用什么标志呢?
根据riscv手册(riscv-privileged),查到当scause寄存器为15时,为写页异常
(2)代码修改
在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;
}
5.修改copyout
(1)分析过程
由于有些访问COW页的操作不是来自用户空间的,那么也需要对vm.c中的copyout函数进行修改(tips中也提到了这一点)
(2)代码修改
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;
}
6.结果验证
-
cowtest
-
usertests -q
7.遇到的一些问题
-
在开辟计数数组那一块,纠结了一下是直接申请数组还是在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;
8.参考文章
- https://ttzytt.com/2022/07/xv6_lab6_record/
- https://blog.csdn.net/lhwhit/article/details/120669827?ops_request_misc=%257B%2522request%255Fid%2522%253A%2522166591619116800180625307%2522%252C%2522scm%2522%253A%252220140713.130102334…%2522%257D&request_id=166591619116800180625307&biz_id=0&utm_medium=distribute.pc_search_result.none-task-blog-2allsobaiduend~default-1-120669827-null-null.142v56control_1,201v3control_2&utm_term=xv6%20cow&spm=1018.2226.3001.4187