xv6 6.S081 Lab4: lazy
lazy代码在这里。我去,lazy改好多文件啊。不过相比于buddy allocator,自认为lazy还是好多了😓。
写在前面
哈哈,老·写在前面了。在完成lazy的时候,一定要有Page的概念,说白了就是一个内存页,emmm,其实就是一片连续内存吧。另外,一定要理解lazy的思想:该分配Page时才分配,如果没有用到某一Page,我就不分配Page了
同样,可以参考我的开坑博客OS实验xv6 6.S081 开坑,这里给出了一些有用的参考资料,以防大家有不时之需。
实验介绍
官方指导书在这里,加收藏不迷路。
官方指导书给了四个小标题,事实上,我们只需要完成两个任务:
- 打印页表
- 实现Lazy Allocation
在开始本实验前,推荐阅读xv6 book的3.1和3.6节,不过没时间读也无所谓,我会对其进行介绍。
开始!
打印页表
打印内容什么的,我最喜欢了,递归就行了。
在这之前,我们需要知道xv6中的页表结构,它不是我们所学的一级或是二级页表,而是三级页表,这在xv6 book的3.1节中有所讲到,如下图所示:
我们的任务就是编写vmprint
函数来打印页表地址,再打印有效的pte(page table entry)。
官方给出了一些Hints:
Hint 1:可以用risc.h文件末尾定义的宏
Hint 2:可以看vm.c中的freewalk来获得灵感
Hint 3:在kernel/defs.h定义函数原型vmprint
,这样就可以在其他地方调用了
代码实现比较简单,如下:
int layer = 1;
void
vmprint(pagetable_t pagetable, int isinit){
if(isinit){
printf("page table %p\n", pagetable);
isinit = 0;
}
// there are 2^9 = 512 PTEs in a page table.
for(int i = 0; i < 512; i++){
pte_t pte = pagetable[i];
/* 如果该PTE是有效映射 */
if(pte & PTE_V){
/* 打印PTE信息 */
for (int i = 0; i < layer; i++)
{
/* if(i == layer - 1)
printf("..");
else
printf(".. "); */
printf(" ..");
}
printf("%d: pte %p pa %p\n",i, pte, PTE2PA(pte));
/* 这个PTE有孩子 */
if((pte & PTE_V) && (pte & (PTE_R|PTE_W|PTE_X)) == 0){
// this PTE points to a lower-level page table.
uint64 child = PTE2PA(pte);
layer++;
vmprint((pagetable_t)child, 0);
layer--;
}
}
}
}
实现Lazy Allocation
首先,我们要修改sbrk()
函数,使之能够支持lazy allocation;其次我们在trap.c
中完成lazy allocation
修改sbrk()
根据指导书的Hints:
- Your first task is to delete page allocation from the sbrk(n) system call implementation, which is the function sys_sbrk() in sysproc.c.
- so you should delete the call to growproc() (but you still need to increase the process’s size!).
好,找到sysproc.c
,删除sys_sbrk
中的growproc()
,并改为myproc()->sz += n
,这一步的操作是将进程理论大小提高,但暂时并不为其分配内存页面,这是为lazy allocation的实现作铺垫。
uint64
sys_sbrk(void)
{
int addr;
int n;
if(argint(0, &n) < 0)
return -1;
addr = myproc()->sz;
/* printf("old_addr: %d\n", addr); */
myproc()->sz += n;
/* printf("%d\n",myproc()->sz); */
/* if(growproc(n) < 0)
return -1; */
return addr;
}
做了这一步操作后,make qemu
后,执行一个echo hi
,你会看到系统崩溃了,显示Kerneltrap(内核陷入):
init: starting sh
$ echo hiusertrap(): unexpected scause 0x000000000000000f pid=3
sepc=0x0000000000001258 stval=0x0000000000004008
va=0x0000000000004000 pte=0x0000000000000000
panic: uvmunmap: not mapped
也许其中的sepc会不一样,但其他应该是差不多的。
MIT指导书告诉我们:
usertrap(): unexpected ...
是来自trap.c
中的,也就是内核碰到了一个它不能处理的异常,这里很明显就是缺页(page fault)异常,因为sbrk并没有真正分配Page页面。注意到stval = 0x4008
,什么是stval
呢?其实就是stop virtual address
,也就表明在虚拟地址为0x4008的地方发生了缺页,系统在这里停了下来。于是,接下来的任务就很明确了,我们要修改trap.c,使之能够处理缺页异常,也就是能够为其加载一个页面,这就是Lazy Allocation的实现了
实现Lazy Allocation
首先我们分析MIT官方的所有Hints:
- You can check whether a fault is a page fault by seeing if r_scause() is 13 or 15 in usertrap().
- Look at the arguments to the printf() in usertrap() that reports the page fault, in order to see how to find the virtual address that caused the page fault.
- Steal code from uvmalloc() in vm.c, which is what sbrk() calls (via growproc()). You’ll need to call kalloc() and mappages().
- Use PGROUNDDOWN(va) to round the faulting virtual address down to a page boundary.
- uvmunmap() will panic; modify it to not panic if some pages aren’t mapped.
- If the kernel crashes, look up sepc in kernel/kernel.asm
- Use your print function from above to print the content of a page table.
- If you see the error “incomplete type proc”, include “proc.h” (and “spinlock.h”).
- If all goes well, your lazy allocation code should result in echo hi working
逐条解读:
r_scause() == 13
或者r_scause() == 15
表明缺页异常;stval
就是导致缺页的虚拟地址;- 可以查看
vm.c
中的uvmalloc()
是如何分配页面的; - 用宏
PGROUNDDOWN(va)
让虚拟地址与页面向下对齐,例如:地址va=0x4008,PGROUNDDOWN(va)=0x4000; uvmunmap()
会panic,我们需要修改它,使得当某些页面没有分配时能够成功运行。这是因为由于lazy allocation的机制,有些页面我们并没有分配,但是这些页面并非没有分配的的,它们是虚拟存在的;- 内核崩了,可以看
kernel/kernel.asm
的信息; - 可以用我们写的
mvprint()
来debug; - 如果看到错误
incomplete type proc
,则说明需要导入proc.h
头文件; - 如果一切顺利,那么xv6就能够成功运行
echo hi
了;
这么多提示,那就先从trap.c
下手吧,借鉴一下uvmalloc()
代码,并修改trap.c
中的usertrap()
如下:
/**
* Page Fault
*/
else if (r_scause() == 13 || r_scause() == 15)
{
/* printf("****************************page fault!****************************\n");
printf("-------------------------before page table:\n");
vmprint(p->pagetable, 1);
printf("usertrap(): unexpected scause %p pid=%d\n", r_scause(), p->pid);
printf(" sepc=%p stval=%p\n", r_sepc(), r_stval());
*/
//char *mem;
uint64 va;
va = r_stval();
/* printf("size alloc:%d\n", p->sz);
printf("va:%d\n", va); */
if(va >= p->sz)
{
/* 虚拟地址大于分配的地址 */
printf("Virtual Address is greater than sbrk(n) \n");
p->killed = 1;
}
else {
/* 其他情况,懒加载一页 */
uint64 va_boundry = PGROUNDDOWN(va);
mem = kalloc();
if(mem != 0){
memset(mem, 0, PGSIZE);
//printf("%p\n", (uint64)mem);
//疑问:为何在mappages内对pa进行加减操作?
//回答:因为内存映射
if(mappages(p->pagetable, va_boundry, PGSIZE, (uint64)mem, PTE_W|PTE_X|PTE_R|PTE_U) != 0){
printf("Cannot allocate so much memory");
kfree(mem);
uvmdealloc(p->pagetable, va_boundry, p->sz);
p->killed = 1;
}
//printf("%p\n", (uint64)mem);
//printf("-------------------------after page table:\n");
//vmprint(p->pagetable, 1);
//printf("map:%p\n", walkaddr(p->pagetable, va));
}
else{
printf("Mem is 0 \n");
p->killed = 1;
}
}
} else {
printf("usertrap(): unexpected scause %p pid=%d\n", r_scause(), p->pid);
printf(" sepc=%p stval=%p\n", r_sepc(), r_stval());
p->killed = 1;
}
这里做了一个简单的判断if(va >= p->sz)
,虚拟地址总不能大于已分配的地址吧。
接下来,需要修改uvmunmap()
让它不要恐惧(panic):
void
uvmunmap(pagetable_t pagetable, uint64 va, uint64 size, int do_free)
{
uint64 a, last;
pte_t *pte;
uint64 pa;
a = PGROUNDDOWN(va);
last = PGROUNDDOWN(va + size - 1);
for(;;){
/**
* 移除Panic:当找不到相应pte时,我们应该继续查找
*/
pte = walk(pagetable, a, 0);
if(pte != 0 && PTE_FLAGS(*pte) == PTE_V)
panic("uvmunmap: not a leaf");
if(pte != 0 && do_free) {
/**
* 当且仅当PTE是有效映射时,才进行free操作
*/
if((*pte & PTE_V) != 0){
pa = PTE2PA(*pte);
kfree((void*)pa);
}
}
if(pte != 0)
*pte = 0;
if(a == last)
break;
a += PGSIZE;
pa += PGSIZE;
}
}
来对比一下原来的代码,我们删去了if((pte = walk(pagetable, a, 0)) == 0)
与if((*pte & PTE_V) == 0)
,并且当且仅当if((*pte & PTE_V) != 0)
才进行页的释放。这是因为还未分配上的页面的pte肯定是0,因此不用再对该页进行释放了。
if((pte = walk(pagetable, a, 0)) == 0)
panic("uvmunmap: walk");
if((*pte & PTE_V) == 0){
printf("va=%p pte=%p\n", a, *pte);
panic("uvmunmap: not mapped");
}
if(pte != 0 && PTE_FLAGS(*pte) == PTE_V)
panic("uvmunmap: not a leaf");
if(do_free){
pa = PTE2PA(*pte);
kfree((void*)pa);
完成这里后,运行应该就能够正常运行echo hi
了。
完善Lazy Allocation
最后,根据MIT官方指导书的说法,我们还需要进一步完善Lazy Allocator以通过usertests
测试。
同样是先分析Hints:
- Handle negative sbrk() arguments.
- Kill a process if it page-faults on a virtual memory address higher than any allocated with sbrk().
- Handle fork() correctly.
- Handle the case in which a process passes a valid address from sbrk() to a system call such as read or write, but the memory for that address has not yet been allocated.
- Handle out-of-memory correctly: if kalloc() fails in the page fault handler, kill the current process.
- Handle faults on the invalid page below the stack.
逐条解读:
sys_sbrk
需要处理传入的n为负数的情况;- 如果虚拟地址大于
sbrk
分配的空间,则终止进程,幸运的是,这一点我们已经在前面的代码中实现了; - 正确处理
fork()
; - 正确处理
copyout()
、copyin()
以及uvmcopy()
,它们在Lazy Allocation的情况下都会不同程度出现panic; - 正确处理内存耗尽的情况:例如
kalloc()
如果不能分配更多的内存了,就应该终止进程,事实上,这一点我们也已经通过if(mem != 0)
解决了; - 正确处理那些在用户栈之下的Page;
事实上,第1、2、5点我们很快就能解决。
其中,实现第1点的代码如下:
uint64
sys_sbrk(void)
{
int addr;
int n;
if(argint(0, &n) < 0)
return -1;
addr = myproc()->sz;
myproc()->sz += n;
/**
*
* Handle negtive sbrk
*/
if(n < 0){
printf("n:%d\n",n);
printf("oldsz:%d, newsz:%d\n ", addr, addr + n);
uvmdealloc(myproc()->pagetable, addr, addr + n);
}
return addr;
}
第2点我们已经解决了,第5点我们也已经解决了。
接下来我们先解决第6点。要想解决第6点,那么首先得明白什么是用户栈。xv6 book中讲到,xv6为每个进程都分配了一个虚拟空间,如下图所示。其中,stack即用户栈。
进程运行时的虚拟地址范围应该在stack到trapframe之间,即在stack和heap内,它不应该向下越过stack进入guard page,于是,只要我们知道stack的起始地址,我们就能在trap.c
中做出相应的处理。在exec()
中,我们可以找到方案:
// Allocate two pages at the next page boundary.
// Use the second as the user stack.
/**
* ---------- <-sp
* user satck
* ---------- <-stackbase
* gaurded page
* ----------
*/
sz = PGROUNDUP(sz);
if((sz = uvmalloc(pagetable, sz, sz + 2*PGSIZE)) == 0)
goto bad;
uvmclear(pagetable, sz-2*PGSIZE);
sp = sz;
stackbase = sp - PGSIZE;
// Push argument strings, prepare rest of stack in ustack.
for(argc = 0; argv[argc]; argc++) {
if(argc >= MAXARG)
goto bad;
sp -= strlen(argv[argc]) + 1;
sp -= sp % 16; // riscv sp must be 16-byte aligned
if(sp < stackbase)
goto bad;
if(copyout(pagetable, sp, argv[argc], strlen(argv[argc]) + 1) < 0)
goto bad;
ustack[argc] = sp;
}
ustack[argc] = 0;
/**
* Lazy alloc implementation
*/
//vmprint(pagetable);
if (p->pid == 1)
{
vmprint(pagetable, 1);
}
可以看到,stackbase
就是用户栈的首地址(见XV6官方注释与我的注释)。为了记录这个地址,我们需要为proc
添加一个字段userstack,然后在exec
中为其赋值:
/**
* 记录userstack的起始位置
*/
p->userstack = stackbase;
从而,我们就完成了第6点。接下来,我们解决第3点,因为在fork
的时候,由于我们为proc
新加了一个字段,因此,fork
之后的proc
也应该维护这个字段,于是修改fork
如下:
int
fork(void)
{
int i, pid;
struct proc *np;
struct proc *p = myproc();
/* printf("fork():\n");
vmprint(p->pagetable, 1); */
//printf("fork:basestack %p \n", p->userstack);
// Allocate process.
if((np = allocproc()) == 0){
return -1;
}
// Copy user memory from parent to child.
if(uvmcopy(p->pagetable, np->pagetable, p->sz) < 0){
freeproc(np);
release(&np->lock);
return -1;
}
/**
* 应该复制Userstack,维护一下
*/
np->userstack = p->userstack;
np->sz = p->sz;
np->parent = p;
// copy saved user registers.
*(np->tf) = *(p->tf);
// Cause fork to return 0 in the child.
np->tf->a0 = 0;
// increment reference counts on open file descriptors.
for(i = 0; i < NOFILE; i++)
if(p->ofile[i])
np->ofile[i] = filedup(p->ofile[i]);
np->cwd = idup(p->cwd);
safestrcpy(np->name, p->name, sizeof(p->name));
pid = np->pid;
np->state = RUNNABLE;
release(&np->lock);
return pid;
}
好了,接下来的任务便是完成第4点了。事实上,在copyout()
、copyin()
中我们需要修改的地方与trap.c
中需要完成的工作类似,只需要懒加载Page即可,由于有他们间有太多相似点,因此我在vm.c
中封装了一个lazyalloc
函数,如下所示:
int
lazyalloc(pagetable_t pagetable, uint64 va){
if(va >= myproc()->sz){
printf("lazyalloc: va is bigger than proc sz\n");
return -1;
}
if(va < myproc()->userstack){
printf("lazyalloc: va is enter guard page!\n");
return -1;
}
if(walkaddr(pagetable, va) != 0)
return 0;
char *mem;
mem = kalloc();
if(mem != 0){
memset(mem, 0, PGSIZE);
if(mappages(pagetable, va, PGSIZE, (uint64)mem, PTE_W|PTE_X|PTE_R|PTE_U) != 0){
printf("There is no page mapped");
kfree(mem);
uvmdealloc(pagetable, va, myproc()->sz);
return -1;
}
}
else{
/**
* Handle the kalloc is invalid
*/
printf("Mem is not enough \n");
return -1;
}
return 1;
}
然后实现copyout()
、copyin()
如下:
int
copyout(pagetable_t pagetable, uint64 dstva, char *src, uint64 len)
{
//copyout(pagetable, sp, (char *)ustack, (argc+1)*sizeof(uint64)
uint64 n, va0, pa0;
while(len > 0){
va0 = PGROUNDDOWN(dstva);
pa0 = walkaddr(pagetable, va0);
if(pa0 == 0){
/**
* 为va0分配一页
*/
//printf("*copyout*\n");
if(lazyalloc(pagetable, va0)){
//printf("copyout 1\n");
pa0 = walkaddr(pagetable, va0);
}
else
{
//printf("copyout 2\n");
return -1;
}
}
if(pa0 == 0){
return -1;
}
/**
* 这样就是在计算offset了
*/
uint64 offset = (dstva - va0);
n = PGSIZE - offset;
if(n > len)
n = len;
memmove((void *)(pa0 + offset), src, n);
len -= n;
src += n;
dstva = va0 + PGSIZE;
}
return 0;
}
int
copyin(pagetable_t pagetable, char *dst, uint64 srcva, uint64 len)
{
/**
* 系统调用时,os将用户态转化为内核态
*
*/
uint64 n, va0, pa0;
while(len > 0){
va0 = PGROUNDDOWN(srcva);
pa0 = walkaddr(pagetable, va0);
if(pa0 == 0){
/**
* 为va0分配一页
*/
//printf("copyin\n");
if(lazyalloc(pagetable, va0)){
pa0 = walkaddr(pagetable, va0);
}
else
{
return -1;
}
}
/**
* 分配了还是出错
* */
if(pa0 == 0){
return -1;
}
//return -1;
n = PGSIZE - (srcva - va0);
if(n > len)
n = len;
memmove(dst, (void *)(pa0 + (srcva - va0)), n);
len -= n;
dst += n;
srcva = va0 + PGSIZE;
}
return 0;
}
而对于uvmcopy()
,其与uvmunmap()
的处理方式又几乎一样,因为当uvmcopy不应该在复制未找到Page时panic,而应该直接跳过,这是Lazy Allocation机制导致的。代码修改如下:
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){
/**
* 父进程中的page table中有些项并未映射,直接跳过
*/
if((pte = walk(old, i, 0)) == 0){
continue;
/* panic("uvmcopy: pte should exist"); */
}
if((*pte & PTE_V) == 0){
continue;
/* panic("uvmcopy: page not present"); */
}
pa = PTE2PA(*pte);
flags = PTE_FLAGS(*pte);
if((mem = kalloc()) == 0)
goto err;
memmove(mem, (char*)pa, PGSIZE);
if(mappages(new, i, PGSIZE, (uint64)mem, flags) != 0){
kfree(mem);
goto err;
}
}
return 0;
err:
uvmunmap(new, 0, i, 1);
return -1;
}
接着,我们重写trap.c
,让它能够用上lazyalloc
函数:
/**
* Page Fault
*/
else if (r_scause() == 13 || r_scause() == 15)
{
uint64 va;
va = r_stval();
if(va >= p->sz)
{
/* 虚拟地址大于分配的地址 */
printf("Virtual Address is greater than sbrk(n) \n");
p->killed = 1;
}
else if (va < p->userstack)
{
printf("Guard page!");
p->killed = 1;
}
else {
/* 其他情况, */
uint64 va_boundry = PGROUNDDOWN(va);
if(lazyalloc(p->pagetable, va_boundry) < 0){
p->killed = 1;
}
}
} else {
printf("usertrap(): unexpected scause %p pid=%d\n", r_scause(), p->pid);
printf(" sepc=%p stval=%p\n", r_sepc(), r_stval());
p->killed = 1;
}
if(p->killed)
exit(-1);
// give up the CPU if this is a timer interrupt.
if(which_dev == 2)
yield();
usertrapret();
}
运行usertests,测试成功!