Page-fault exceptions
当CPU无法将虚拟地址转换成物理地址时,生成一个page-fault excecption,称为缺页异常。RISC-V中存在三种类型的缺页异常:load(load指令)/store(store指令)/instruction(指令所在的虚拟地址无法转换成物理地址)。scause寄存器中保存了异常类型,stval寄存器保存无法转换的虚拟地址。
Copy-on-write fork的基本方式是父进程和子进程初始共享相同的物理页,并将PTE设置为只读的,当子进程或父进程执行store指令对物理页进行写操作是,RISC-V抛出一个缺页异常,为发生异常的虚拟地址对应的物理页创建一个副本,并将PTE映射到该副本中,修改操作类型为可读可写,之后再对副本进行写操作。由于子进程在fork后通常会执行exec系统调用,因此在exec时子进程会产生缺页异常,内核处理这些异常并创建对应的副本,而不需要复制所有的物理页。
另一种处理缺页异常的机制称为lazy allocation,主要分为两步。第一步,在用户进程调用sbrk以扩充地址空间时,内核仅仅改变其虚拟地址空间,在页表中将这些新的虚拟地址对应的PTE置为无效,而不需要真正分配物理页。第二步,在进程访问这些新的虚拟地址产生缺页异常时,内核分配物理页并将其映射到页表中。这样做的好处是可以节省内存分配。
另一种处理缺页异常的机制称为paging from disk。当用户进程需要更多内存,而RAM中目前没有更多的可用页面进行分配时,内核从当前的内存中选择一些页面,将其调出内存,暂时保存在磁盘上,并将对应的PTE设置为无效。当进程对这些页面进行读写访问时,由于PTE无效,会产生缺页异常,内核对访问的虚拟地址进行检查,如果这一虚拟地址所在的页面当前保存在磁盘上,则选择内存中的某些页面调出内存,将需要的物理页调回内存,更新PTE并返回到用户进程继续执行。这种机制在应用程序的局部性较好时具有比较高的效率。
Eliminate allocation from sbrk()
修改系统调用sbrk(n),当n>0时,sbrk会为进程分配n个字节,然后返回新分配区域的地址。在lazy allocation中,只有内存真正被使用时才会分配物理页,因此新的sbrk(n)只增加进程的虚拟内存大小,而不会影响其实际映射的物理页,并且返回新增加区域的起始虚拟地址。
uint64
sys_sbrk(void)
{
int addr;
int n;
struct proc *p = myproc();
if(argint(0, &n) < 0)
return -1;
addr = p->sz;
if (n < 0) {
uvmdealloc(p->pagetable, p->sz, p->sz + n);
}
p->sz += n;
return addr;
}
lazy allocation
修改trap.c的代码,当用户空间发生缺页中断时,为进程新分配一个物理页并映射到发生缺页的虚拟地址处,然后返回用户代码继续执行。
缺页中断对应scause寄存器的值为13或15,分别代表读/写,stval寄存器中保存造成缺页中断的虚拟地址va。当va超出了当前进程的内存大小时,说明出现了非法的地址访问,直接kill整个进程。
在fork中,为子进程创建页表时,通过uvmcopy将父进程的用户地址空间的内存拷贝到子进程的用户地址空间,而在lazy allocation中,如果父进程页表中的某一项PTE不存在或者该PTE对应的物理页不存在,说明在父进程中没有对该页面的访问,因此在对子进程的页表初始化时也不需要分配这些表项,而是等到真正访问时再进行分配。
在为进程新分配物理页失败时,说明当前系统的物理内存已经被占满,直接kill进程并释放对应的物理内存。
在xv6中,栈的最低一页作为guardpage,用于检测用户栈分配时出现stack overflow错误,对于guard page中的地址,不应该读写,也不应该为其分配物理页,如果出现缺页的位置在guard page中,说明出现了栈溢出,应该kill掉整个进程,根据xv6进程地址空间的排布,对应的判断条件为PGROUNDDOWN(va) == r_sp()
。
完整的缺页中断处理代码如下:
void
usertrap(void)
{
//......
if(r_scause() == 8){
// system call
} else if (r_scause() == 13 || r_scause() == 15) {
// Kill a process if it page-faults on a virtual memory address higher than any allocated with sbrk()
// virtual address that caused the page fault
uint64 va = r_stval();
if (va < p->sz && PGROUNDDOWN(va) != r_sp() &&
((pte = walk(p->pagetable, va, 0)) == 0 || (*pte & PTE_V) == 0)) {
char *mem = kalloc();
if (mem == 0) {
printf("out of memory\n");
p->killed = 1;
} else {
memset(mem, 0, PGSIZE);
if (mappages(p->pagetable, PGROUNDDOWN(va), PGSIZE, (uint64)mem, PTE_W|PTE_X|PTE_R|PTE_U) != 0) {
printf("mappage failed when handling page fault\n");
kfree(mem);
uvmdealloc(p->pagetable, PGROUNDDOWN(va), PGSIZE);
}
}
} else {
printf("illegal va: %p", va);
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;
}
} else if((which_dev = devintr()) != 0){
// ok
} 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();
}
当进程将虚拟地址作为参数传递给系统调用以供内核读写(copyin©out)时,如果该虚拟地址刚刚通过sbrk分配,但是并没有映射到某个物理页,此时并不会触发缺页中断,而是会引起读写错误,对于这种情况应该首先检测虚拟地址是否存在对应的物理页面,如果对应的页面不存在,则先进行物理页面的分配,然后再进行读写,对应的代码如下:
// Copy from kernel to user.
// Copy len bytes from src to virtual address dstva in a given page table.
// Return 0 on success, -1 on error.
int
copyout(pagetable_t pagetable, uint64 dstva, char *src, uint64 len)
{
uint64 n, va0, pa0;
pte_t *pte;
struct proc *p = myproc();
if (dstva < p->sz && PGROUNDDOWN(dstva) != r_sp() && (((pte = walk(p->pagetable, dstva, 0))==0) || ((*pte & PTE_V)==0))) {
if (dstva >= p->sz || PGROUNDDOWN(dstva) == r_sp()) {
printf("illegal va in copyin\n");
p->killed = 1;
exit(-1);
}
char *mem = kalloc();
if (mem == 0) {
printf("out of memory when copyin\n");
p->killed = 1;
exit(-1);
} else {
memset(mem, 0, PGSIZE);
if (mappages(p->pagetable, PGROUNDDOWN(dstva), PGSIZE, (uint64)mem, PTE_W|PTE_X|PTE_R|PTE_U) != 0) {
printf("mappage failed when handling copyin page fault\n");
kfree(mem);
uvmdealloc(p->pagetable, PGROUNDDOWN(dstva), PGSIZE);
}
}
}
while(len > 0){
va0 = PGROUNDDOWN(dstva);
pa0 = walkaddr(pagetable, va0);
if(pa0 == 0)
return -1;
n = PGSIZE - (dstva - va0);
if(n > len)
n = len;
memmove((void *)(pa0 + (dstva - va0)), src, n);
len -= n;
src += n;
dstva = va0 + PGSIZE;
}
return 0;
}
// Copy from user to kernel.
// Copy len bytes to dst from virtual address srcva in a given page table.
// Return 0 on success, -1 on error.
int
copyin(pagetable_t pagetable, char *dst, uint64 srcva, uint64 len)
{
uint64 n, va0, pa0;
pte_t *pte;
struct proc *p = myproc();
if (srcva < p->sz && PGROUNDDOWN(srcva) != r_sp() && ((pte = walk(p->pagetable, srcva, 0)) == 0 || (*pte & PTE_V) == 0)) {
char *mem = kalloc();
if (mem == 0) {
printf("out of memory when copyin\n");
p->killed = 1;
} else {
memset(mem, 0, PGSIZE);
if (mappages(p->pagetable, PGROUNDDOWN(srcva), PGSIZE, (uint64)mem, PTE_W|PTE_X|PTE_R|PTE_U) != 0) {
printf("mappage failed when handling copyin page fault\n");
kfree(mem);
uvmdealloc(p->pagetable, PGROUNDDOWN(srcva), PGSIZE);
}
}
}
while(len > 0){
va0 = PGROUNDDOWN(srcva);
pa0 = walkaddr(pagetable, va0);
if(pa0 == 0)
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;
}
完成以上修改后,顺利通过lazytests和usertests。