0%

xv6-2020-lab5解析

先导知识

我们之前在使用fork的时候,是直接复制的物理内存。然而这么做其实是非常浪费的,因为在实际中发现,fork完之后立马执行exec的概率很大,而exec是去加载特定的镜像,所以复制这个动作完全是浪费的。

除了fork这个系统调用以外,还有一些可以利用懒复制的应用。比如sbrk,它的作用是开辟内存空间,但是如果应用要了就开辟空间,而应用要是最后没去使用就很浪费;所以比较好的做法是在页表中给它新增映射关系,但是并不分配真正的物理内存;还有一个就是可以不全加载一个进程的所有内容到内存中,而是真正要被用到的时候才加载进来。

Eliminate allocation from sbrk()(简单)

这个就是把分配物理内存的代码移除,但是proc的size记得要加。还有一个比较容易忽视的点是,当缩小的时候,页表的内存空间必须马上回收(这个其实是第三个小实验的内容….)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
uint64
sys_sbrk(void)
{
int addr;
int n;
struct proc *p;
p = myproc();

if (argint(0, &n) < 0)
return -1;
addr = p->sz;
// if(growproc(n) < 0)
// return -1;
if (n < 0)
p->sz = uvmdealloc(p->pagetable, p->sz, p->sz + n);
else
p->sz += n;
return addr;
}

Lazy allocation(中等)

修改在usertrap.c的代码,能够处理缺页中断,并且返回用户程序继续执行。修改其它的代码,能够让echo hi成功运行。

然后是一些Hints:

  • 你可以通过r_scause()是否等于13或者15来判断是否是缺页中断。
  • r_stval() 返回的是stval的值,也即造成这次缺页中断的虚拟地址的值。
  • 从vm.c中抄uvmalloc(),你还需要运行kalloc()mappages()
  • 使用PGROUNDDOWN(va)来确定虚拟地址所在页。
  • uvmunmap()会导致panic,如果一些页面没有被映射,让它不会panic。
  • 如果系统崩溃了,在kernel.asm中查看sepc
  • 使用在lab3写的vmprint来帮助调试。

照着上面的代码来写其实非常简单:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
if(r_scause() == 13 || r_scause() == 15){
// 缺页中断在这里进行处理
uint64 va = r_stval();
uint64 down = PGROUNDDOWN(va);
void * pa = kalloc();
if(pa == 0){
// 分配物理页失败
panic("kalloc!");
return;
}
memset(pa,0,PGSIZE);
if (mappages(p->pagetable, down, PGSIZE, (uint64)pa, PTE_W | PTE_X | PTE_R | PTE_U) != 0)
{
kfree(pa);
return;
}
}

就是找到引发这个缺页中断的虚拟地址,然后利用kalloc找到分配物理的页,利用mappages把它们之间的映射安装到对应的页表中。

最后不要忘了修改uvmunmap,因为它会导致panic,我们这里直接把panic给注释掉,然后加上continue!!因为原来的panic可以理解为return,导致不会进行之后的判断了,而我们把panic注释掉之后,后面的if会继续判断,就会有问题。

Lazytests and Usertests(中等)

xv6准备了测试程序,修改操作系统,使之能够通过 lazytestsusertests

hints:

  • 处理负数的sbrk参数(已经完成)。
  • 如果一个进程的虚拟地址超过了目前的p->sz,就认为错误。
  • 修改Fork的父进程向子进程拷贝的逻辑,让它能够正确处理。
  • 处理进程将有效地址从sbrk传递到系统调用(例如read或write),但尚未分配该地址的内存的情况。
  • 如果kalloc分配失败,那么kill掉对应的进程。
  • 处理在用户的栈之下的非法情况。

本质上其实就是对上面的那个函数加上一些条件判断,还需要对fork进行稍微的修改就可以了。这里附上一张虚拟内存的图:

image-20210413161832614

这里的key在于fork和read/write这俩。

fork跟踪一下可以发现它实际的实现函数是uvmcopy,所以进到对应的代码段里面,查看其中其实就是利用walk来找到对应的pte进行处理,找不到就会panic,所以应该把这些跳掉(注意下面的代码也不能执行,所以用continue

1
2
3
4
5
6
7
8
9
10
11
12
..........
char *mem;

for(i = 0; i < sz; i += PGSIZE){
if((pte = walk(old, i, 0)) == 0)
// panic("uvmcopy: pte should exist");
continue;
if((*pte & PTE_V) == 0)
// panic("uvmcopy: page not present");
continue;
pa = PTE2PA(*pte);
..........

对于read/write来说,其实就是对应的copyin和copyout,所以需要修改对应的函数。可以发现这里有好几处需要用到对应的函数,所以把之前的处理页错误的代码封装成一个函数进行处理:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
/**
* 处理页表的错误
* 发生错误返回-1,成功返回0
*/
int
handle_page_fault(uint64 va,struct proc*p){
if (r_scause() == 13 || r_scause() == 15){
// 缺页中断在这里进行处理
// printf("start handling page fault\n");
if (va >= p->sz)
{
// 虚拟地址不合法
// panic("virtural address is illegal : va >= p->sz");
return -1;
}
if(va < p->trapframe->sp){
// 虚拟地址不合法
// panic("virtural address is illegal : va < p->trapframe->sp");
return -1;
}

uint64 down = PGROUNDDOWN(va);
char *mem = kalloc();
if (mem == 0)
{
// 分配物理页失败
// panic("kalloc!");
return -1;
}
memset((void *)mem, 0, PGSIZE);
if (mappages(p->pagetable, down, PGSIZE, (uint64)mem, PTE_W | PTE_X | PTE_R | PTE_U) != 0)
{
printf("map fail!\n");
kfree(mem);
return -1;
}
return 0;
}else{
// 不需要处理,但是不应该调用这个函数,所以返回-1
return -1;
}
}

在对应的copyin和copyout中进行处理。后来发现还有copyinstr,也顺手改掉了。

1
2
3
4
5
6
7
8
9
10
11
.............
pa0 = walkaddr(pagetable, va0);
if (pa0 == 0)
{
if (handle_page_fault(va0, myproc()) == -1)
return -1;
else
pa0 = walkaddr(pagetable, va0);
}
n = PGSIZE - (srcva - va0);
.............

所有的这些过后,发现有一个小test没有通过:

1
2
3
4
5
6
== Test   usertests: sbrkarg ==
usertests: sbrkarg: FAIL
Failed sbrkarg

test sbrkarg: sbrkarg: write sbrk failed
FAILED

可以看到是sbrk这个函数的参数错误。

首先来分析一下调用路径:在usertests中利用write调用 -> 系统调用sys_write -> filewrite -> writei -> either_copyin -> copyin

在网上看到另外一种实现,就是可以发现所有的这三个copy函数其实都是pa0 = walkaddr(pagetable, va0);,那么直接修改walkaddr是不是也可以呢?就直接在里面进行修改。

总结

这部分首先留了一个问题,就是write的系统调用并没有完全通过。

然后是经过上一个lab的trap铺垫,发现其实按需分配物理页实现起来其实很简单。