实验内容网址:https://xv6.dgs.zone/labs/requirements/lab5.html
本实验的代码分支:https://gitee.com/dragonlalala/xv6-labs-2020/tree/lazy3/
Eliminate allocation from sbrk()
关键点:p->sz的含义
思路:
sbrk(n)
系统调用将进程的内存大小增加n个字节,然后返回新分配区域的开始部分(即旧的大小)。新的sbrk(n)
应该只将进程的大小(myproc()->sz
)增加n,然后返回旧的大小。因此直接修改sys_sbrk()
函数,注释掉对growproc()函数的调用,直接修改p->sz。
uint64
sys_sbrk(void)
{
int addr;
int n;
if(argint(0, &n) < 0)
return -1;
addr = myproc()->sz;
// lab5-1
myproc()->sz += n; // increase size but not allocate memory
// if(growproc(n) < 0)
// return -1;
return addr;
}
进行 echo hi 测试,实验结果如图所示
Lazy allocation
这题才是本实验的主要部分,上面的实验在调用sbrk增加或者减少内存时并没有对物理内存进行操作,这会引起页面错误,本题就是为了解决内存增加时出现的页面错误。
思路&步骤
对于这部分的思路和步骤可以跟着提示进行展开。
- 在
kernel/trap.c
文件的usertrap()
函数中对r_scause()
为13或者15的情况进行处理,这两种情况是出现了页面错误。通过r_stval()
函数获得出现页面错误的虚拟地址。参考growproc()
函数中对kalloc()
和mappages()
的调用,为出现报错的虚拟地址申请1页物理地址(内存),是的,1页。物理内存申请成功后将虚拟地址映射到物理地址上。这段代码中有一点要注意的是如果出现物理内存申请不成功 或者映射失败 的情况,需要将p->killed
置为1,标记当前进程后就需要立即去exit(-1)
了,所以下面的代码我添加了goto
来实现这个效果。如果没加goto
,则会导致物理内存申请不成功后反而去初始化物理内存,导致其他错误。
...
else if((which_dev = devintr()) != 0){
// ok
} else if(r_scause() == 13 || r_scause() == 15){
// 出现报错的虚拟地址
uint64 va = r_stval();
// 虚拟地址高于sbrk分配的虚拟地址则杀死进程
// 申请1页物理内存
char* pa = kalloc();
if(pa == 0){ // 申请不成功
printf("alloc physical memory failed");
p->killed = 1;
goto end;
}
// 初始化物理内存
memset(pa, 0, PGSIZE);
// 映射, 将出错的虚拟地址向下舍入到页面边界,因为va所在的这一页还没有对应的物理内存
if(mappages(p->pagetable, PGROUNDDOWN(va), PGSIZE, (uint64)pa, PTE_W|PTE_X|PTE_R|PTE_U)){
// 映射失败,释放物理内存
kfree(pa);
p->killed = 1;
goto end;
}
}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;
}
end:
if(p->killed){
exit(-1);
}
- 在 xv6 的原本实现中, 由于是 Eager Allocation, 所以不存在分配的虚拟内存不对应物理内存的情况, 因此在
uvmunmap()
函数中取消映射时对于虚拟地址对应的 PTE 若是无效的, 便会引发 panic. 而此处改为 Lazy allocation 后, 对于用户进程的虚拟空间, 便会出现有的虚拟内存并不对应实际的物理内存的情况, 即页表中的 PTE 是无效的. 因此此时便不能引发 panic, 而是选择跳过该 PTE.
void
uvmunmap(pagetable_t pagetable, uint64 va, uint64 npages, int do_free)
{
uint64 a;
pte_t *pte;
if((va % PGSIZE) != 0)
panic("uvmunmap: not aligned");
for(a = va; a < va + npages*PGSIZE; a += PGSIZE){
if((pte = walk(pagetable, a, 0)) == 0)
panic("uvmunmap: walk");
if((*pte & PTE_V) == 0) {
continue; // lab5-2
// panic("uvmunmap: not mapped"); // lab5-2
}
if(PTE_FLAGS(*pte) == PTE_V)
panic("uvmunmap: not a leaf");
if(do_free){
uint64 pa = PTE2PA(*pte);
kfree((void*)pa);
}
*pte = 0;
}
}
Lazytests and Usertests
思路:
- 处理
sbrk()
负数参数的情况 - 引发 page fault 的地址低于用户栈和高于分配地址的情况杀死进程
- 处理
fork()
时的父进程向子进程拷贝内存的情况 - 处理
read()/write()
使用未分配物理内存的情况 kalloc()
失败时杀死进程(上一部分实验已处理).
步骤&代码:
- 处理
kernel/sysproc.c
中sys_sbrk()
参数为负数的情况.
处理的方法可以直接参考原本调用的 growproc()
函数, 因为在 Lazy allocation 的情况下减少内存同样是将多余的内存进行释放, 此处便调用了 uvmdealloc()
函数.在sys_sbrk()
函数中进行如下修改。
这里可以做一个改进,就是内存增加或者减少时可以判断一下会不会超出界限。
对于n>0的情况,可以做addr+n>addr?
的判断,因为addr+n可能超出int的范围导致变成负数。
对于n<0的情况,可以做 n<0 && addr ``+`` n ``>=`` ``PGROUNDUP``(``p``->``trapframe``->``sp``)
的判断。这里要注意sp是用户栈的栈顶指针,栈顶指针的初始地址是PGSIZE,然后向下生长,不超过一个PGSIZE。所以PGROUNDUP``(``p``->``trapframe``->``sp)
就可以知道栈底的位置。
对于n==0的情况,要立即return -1;
uint64
sys_sbrk(void)
{
int addr;
int n;
if(argint(0, &n) < 0)
return -1;
struct proc* p = myproc();
// 保存原本旧的sz
addr = p->sz;
int sz = p->sz;
if(n > 0){
// 对于内存增加情况,直接增大p->sz
p->sz += n;
}else if(sz + n > 0){
// 对于内存减少的情况,需要解映射,同时修改解映射后的p->sz
sz = uvmdealloc(p->pagetable, sz, sz + n);
p->sz = sz;
}else{
return -1;
}
// 返回旧地址
return addr;
}
- 进一步修改
kernel/vm.c
中的uvmunmap()
函数, 将pte==0
的情况由引发panic
改为 continue 跳过.
之所以这样操作, 原因在于 uvmunmap()
获取 pte 调用的 walk()
函数.walk()
用于根据虚拟地址 va 得到其相应页表中对应的 PTE. 而 xv6 的页表结构是有三级页目录, 因此可能在 L2 或 L1 的 PTE 可能就是无效的了, 此时便未分配低级的页目录, 自然也就未分配 va 对应的物理内存, 此时会根据 alloc 参数选择是否构建新的页目录, 但在 uvmunmap()
调用的 walk()
的 alloc 参数为 0, 因为取消映射页表时不应该再新建页目录了. 此时 walk()
便返回 0.
由于 Lazy allocation, 自然很可能出现虚拟地址对应的 L2 或 L1 的 PTE 就是未分配(无效)的情况, 所以此时不应该引发 panic, 同样应该跳过处理.
简而言之, 对于 pte==0
的情况, 一般是 sbrk()
申请了较大内存, L2 或 L1 中的 PTE 就未分配, 致使 L0 页目录就不存在, 虚拟地址对应的 PTE 也就不存在; 而对于之前提到的 (*pte&PTE_V)==0
的情况, 一般是sbrk()
申请的内存不是特别大, 当前分配的 L0 页目录还有效, 但虚拟地址对应 L0 中的 PTE 无效. 这两种情况都是 Lazy Allocation 未实际分配内存所产生的情况, 因此在取消映射时都应该跳过而非 panic.
void
uvmunmap(pagetable_t pagetable, uint64 va, uint64 npages, int do_free)
{
uint64 a;
pte_t *pte;
if((va % PGSIZE) != 0)
panic("uvmunmap: not aligned");
for(a = va; a < va + npages*PGSIZE; a += PGSIZE){
if((pte = walk(pagetable, a, 0)) == 0)
continue;
// panic("uvmunmap: walk");
if((*pte & PTE_V) == 0){
// panic("uvmunmap: not mapped");
continue;
}
//continue;
if(PTE_FLAGS(*pte) == PTE_V)
panic("uvmunmap: not a leaf");
if(do_free){
uint64 pa = PTE2PA(*pte);
kfree((void*)pa);
}
*pte = 0;
}
}
- 处理
kernel/proc.c
的fork()
函数中父进程向子进程拷贝时的 Lazy allocation 情况.
容易分析得到, fork()
是通过uvmcopy()
来进行父进程页表即用户空间向子进程拷贝的. 而对于 uvmcopy()
的处理和uvmunmap()
是一致的, 只需要将 PTE 不存在和无效的两种情况由引发 panic 改为 continue 跳过即可.
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){
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 / PGSIZE, 1);
return -1;
}
- 处理 page fault 的虚拟地址超过
p->sz
或低于用户栈的情况
超过p->sz
即报错的虚拟地址大于p->sz
。
低于p->sz
,笔者认为,报错的虚拟地址不应该低于栈底的位置(栈是向下生长的,不超过1页)。
还有要注意的是如果过程中有不成立的情况要立即将killed
标志位置1并及时退出。
void
usertrap(void)
{
int which_dev = 0;
if((r_sstatus() & SSTATUS_SPP) != 0)
panic("usertrap: not from user mode");
// send interrupts and exceptions to kerneltrap(),
// since we're now in the kernel.
w_stvec((uint64)kernelvec);
struct proc *p = myproc();
// save user program counter.
p->trapframe->epc = r_sepc();
if(r_scause() == 8){
// system call
// printf("here r_scause=8\n");
// printf("p-killed:%d\n",p->killed);
if(p->killed)
exit(-1);
// sepc points to the ecall instruction,
// but we want to return to the next instruction.
p->trapframe->epc += 4;
// an interrupt will change sstatus &c registers,
// so don't enable until done with those registers.
intr_on();
syscall();
} else if((which_dev = devintr()) != 0){
// ok
} else if(r_scause() == 13 || r_scause() == 15){
// 出现报错的虚拟地址
uint64 va = r_stval();
// 虚拟地址高于sbrk分配的虚拟地址则杀死进程
if(va < PGROUNDUP(p->trapframe->sp) || va > p->sz){
p->killed = 1;
goto end;
}else{
// 申请1页物理内存
char* pa = kalloc();
if(pa == 0){ // 申请不成功
printf("alloc physical memory failed");
p->killed = 1;
goto end;
}
// 初始化物理内存
memset(pa, 0, PGSIZE);
// 映射, 将出错的虚拟地址向下舍入到页面边界,因为va所在的这一页还没有对应的物理内存
if(mappages(p->pagetable, PGROUNDDOWN(va), PGSIZE, (uint64)pa, PTE_W|PTE_X|PTE_R|PTE_U)){
// 映射失败,释放物理内存
kfree(pa);
p->killed = 1;
goto end;
}
}
}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;
}
end:
if(p->killed){
exit(-1);
}
// give up the CPU if this is a timer interrupt.
if(which_dev == 2)
yield();
usertrapret();
}
- 处理进程从
sbrk()
向系统调用(如read
或write
)传递有效地址,但尚未分配该地址的内存。可以从argaddr()
函数入手。判断传入进来的用户的虚拟地址是否存在对应的pte,如果没有则为他申请物理内存并进行映射。
在这里walkaddr()
如果虚拟地址有对应的pte,则返回具体的pte,如果没有则返回0,一开始我是判断walkaddr函数的返回值非零,则return -1。但是这样会导致系统崩溃,也很难查找问题,即使加入了backtrace
也找不到问题的所在。如果返回值非零说明这个虚拟地址对应的pte是存在的,则正常返回0就行了。
int
argaddr(int n, uint64 *ip)
{
*ip = argraw(n);
// 获取当前的进程
struct proc *p = myproc();
uint64 va = *ip;
// 检查传递进来的虚拟地址是否有效
if(walkaddr(p->pagetable, va) == 0){
// 检查虚拟地址是否高于sbrk分配的虚拟地址或者低于栈顶指针
if(va < PGROUNDUP(p->trapframe->sp) || va >= p->sz){
return -1;
}
// 申请1页物理内存
char* pa = kalloc();
if(pa == 0){ // 申请不成功
return -1;
}
// 初始化物理内存
memset(pa, 0, PGSIZE);
// 映射, 将虚拟地址向下舍入到页面边界,因为va所在的这一页还没有对应的物理内存
if(mappages(p->pagetable, PGROUNDDOWN(va), PGSIZE, (uint64)pa, PTE_W|PTE_X|PTE_R|PTE_U)){
// 映射失败,释放物理内存
kfree(pa);
return -1;
}
}
return 0;
}