这道题的要求很简单,将 sbrk()
中对增加内存的请求的处理改为直接增加 p->size
的大小即可。出于美观,在 kernel/proc.c: growproc()
中对 n > 0
的情况进行更改:
int
growproc(int n){
uint sz;
struct proc *p = myproc();
sz = p->sz;
if(n > 0){
sz += n;
// if((sz = uvmalloc(p->pagetable, sz, sz + n)) == 0) {
// return -1;
// }
} else if(n < 0){
sz = uvmdealloc(p->pagetable, sz, sz + n);
}
p->sz = sz;
return 0;
}
在上一道题中,我们将对进程增加内存的请求的处理进行了简单的修改,但显然这样的修改是不能正常工作的,我们并没有实际进行内存的分配。因此,在这道题中我们需要令我们的懒分配机制至少能在一定程度上工作起来(可以正常执行 echo hi
语句)。
由于在上一题中,我们对进程的请求仅仅只是将其内存大小增大至指定值,因此不难想象当应用程序实际访问该部分地址时会使得内核中断。因此,我们需要捕捉懒分配相应的中断,获取出错的地址,并为其分配内存。
第一步,我们需要识别由懒分配造成的中断。由于我们在增加进程内存大小时并没有实际分配内存,因此当进程尝试通过读/写指令访问这部分内存时会导致错误。查阅RISC-V手册可以发现由读写指令导致的中断发生时,scause
寄存器的值为 13
或 15
。因此,当 usertrap()
被调用时,通过 r_scause()
函数获取中断发生原因,如果返回值为 13
或 15
则说明中断可能是由于我们的懒分配机制而产生的。
第二步,我们确定中断产生的原因是读写指令后,由于产生中断的地址被存放在 stval
寄存器中,因此通过 r_stval()
函数我们可以定位产生错误的具体地址。此时我们再判断地址是否合法 (这里的判断可以留到下一道题中再完成)。如果地址合法,我们再通过 walk()
函数获取相应的 PTE,此时,若 PTE 不存在,或获取到的 PTE 中 PTE_V
位不为真,则说明产生错误的原因是懒分配机制。
最后一步,我们已经确认错误的产生原因是因为懒分配机制,我们需要做的便是将内存分配至对应的地址,处理完毕。具体操作如下:
在 kernel/vm.c
中实现页错误处理函数 int pagefaulthandler(uint64 va)
和内存分配函数 int truealloc(pagetable_t pagetable, uint64 va)
。pagefaulthandler()
接受一个类型为 uint64
的参数,表明页错误发生的地址,并通过 walk()
函数获取该地址相应的 PTE,若 PTE 为空或 PTE_V 非真则调用 truealloc()
进行内存分配;truealloc()
接收一个类型为 pagetable_t
的参数 pagetable
,表明当前进程的页表以及一个类型为 uint64
的参数,表明页错误发生的地址并通过 kalloc()
函数为对应地址进行内存分配,具体代码如下:
// Used when a lazy allocated page is used and a page fault is thrown.
int
truealloc(pagetable_t pagetable, uint64 va) {
uint64 begin = PGROUNDDOWN(va);
void *pa = kalloc();
if (pa == 0) {
return -1;
}
memset(pa, 0, PGSIZE);
if (mappages(pagetable, begin, PGSIZE, (uint64)pa, PTE_R | PTE_W | PTE_U)) {
kfree(pa);
return -1;
}
return 0;
}
int
pagefaulthandler(uint64 va) {
struct proc *p = myproc();
// trying to access address beyond allocation.
if (va >= p->sz) {
return -1;
}
// accessing address that is below user stack
if (PGROUNDDOWN(va) < p->trapframe->sp) {
return -1;
}
pte_t *pte = walk(p->pagetable, va, 0);
// lazy allocated page handler
if (!((!pte || (PTE_FLAGS(*pte) & PTE_V) == 0) && truealloc(p->pagetable, va) == 0)) {
return -1;
}
return 0;
}
最后将 pagefaulthandler()
的函数添加至 kernel/defs.h
中以便 kernel/traps.c
中的 usertrap()
调用。
完成页错误处理函数的实现后,我们便可以在 usertrap()
中将来自读写指令的错误交给 pagefaulthandler()
处理了:
void
usertrap(void)
{
// ...
if(r_scause() == 8){
// ...
} else if((which_dev = devintr()) != 0){
// ok
} else if (r_scause() == 13 || r_scause() == 15){
uint64 va = r_stval();
// printf("Page fault caught at %p\\n", va);
if (pagefaulthandler(va) < 0) {
p->killed = 1;
}
} else {
// ...
}
// ...
}
至此,我们就完成这一题了。
在上一题中,我们将懒分配机制进行了一定的完善,在这一题中,我们需要将其进一步地完善。题目给出了以下的要求:
1. 当 `sbrk()` 函数得到负的参数时,使其正常工作。
2. 如果一个进程尝试访问大于其通过 `sbrk()` 申请的地址时,将其杀死。
3. 正确地处理 `fork()` 中的内存拷贝。
4. 正确地处理当进程将一个通过 `sbrk()` 得到的合法地址作为参数调用系统调用而该地址未被分配内存的情况。
5. 处理在低于用户栈的地址上发生的错误。
其中第 1 点和第 5 点我们已经在上一题中实现了,第 2 点的处理方法相对简单,而第 5 点我们 PGROUNDDOWN(va)
和 p->trapframe->sp
的大小关系判断错误产生的地址是否位于用户栈之下——若是,则令内核将进程杀死。
而对于第 1 点,我们知道 sbrk()
是通过 growproc()
进行处理的;而当参数小于 0 时,growproc()
调用 uvmdealloc()
进行内存的释放,uvmdealloc()
进而调用 uvmunmap()
完成操作;因此,我们需要对 uvmunmap()
中可能与懒分配机制出现冲突的代码进行更改。
观察 uvmunmap()
的代码,我们不难发现当其调用 walk()
获取 PTE 时,若获取到的 PTE 尚未被分配或其 PTE_V
位不为真时会导致内核恐慌;然而当内存采用懒分配机制时,对未被实际分配的地址调用 walk()
得到这样的结果是符合预期的。因此,我们需要将相应的语句进行注释,同时跳过后面可能进行的物理释放步骤。