在Lab中使用的JOS内核实现使用了微内核的设计理念,即内核仅包含最基本的服务,如最低级的硬件驱动程序、通信、内存管理等。其他操作系统服务,如文件系统、网络协议等,运行在用户空间,与内核分离。对于用户空间页错误page fault
的处理,也允许用户通过自定义的方式来创建错误处理函数。在实验的评估环节有两个样例,用来检查内核这一feature
的实现情况:
faultalloc.c
// test user-level fault handler -- alloc pages to fix faults
#include <inc/lib.h>
void
handler(struct UTrapframe *utf)
{
int r;
void *addr = (void*)utf->utf_fault_va;
cprintf("fault %x\n", addr);
if ((r = sys_page_alloc(0, ROUNDDOWN(addr, PGSIZE),
PTE_P|PTE_U|PTE_W)) < 0)
panic("allocating at %x in page fault handler: %e", addr, r);
snprintf((char*) addr, 100, "this string was faulted in at %x", addr);
}
void
umain(int argc, char **argv)
{
set_pgfault_handler(handler);
cprintf("%s\n", (char*)0xDeadBeef);
cprintf("%s\n", (char*)0xCafeBffe);
}
faultallocbad.c
// test user-level fault handler -- alloc pages to fix faults
// doesn't work because we sys_cputs instead of cprintf (exercise: why?)
#include <inc/lib.h>
void
handler(struct UTrapframe *utf)
{
int r;
void *addr = (void*)utf->utf_fault_va;
cprintf("fault %x\n", addr);
if ((r = sys_page_alloc(0, ROUNDDOWN(addr, PGSIZE),
PTE_P|PTE_U|PTE_W)) < 0)
panic("allocating at %x in page fault handler: %e", addr, r);
snprintf((char*) addr, 100, "this string was faulted in at %x", addr);
}
void
umain(int argc, char **argv)
{
set_pgfault_handler(handler);
sys_cputs((char*)0xDEADBEEF, 4);
}
最大的区别是前者使用了cprintf
来输出一串字符,而后者直接使用系统调用sys_cputs
进行字符输出。在大致实现上,cprintf
也是对系统调用sys_cputs
的封装,但执行结果上二者却不一样。
其中,前者正确进入了对应的处理函数,而后者的代码中即便注册了对应的处理函数,但没有进入过处理函数,而是内核直接assert
用户访问了没有映射的地址。
页错误:用户先访问 vs. 内核先访问
首先观察sys_cputs
的执行过程,可以发现在内核准备打印之前,会先检查内存的合法性。
static void
sys_cputs(const char *s, size_t len) {
// Check that the user has permission to read memory [s, s+len).
// Destroy the environment if not.
// LAB 3: Your code here.
user_mem_assert(curenv, s, len, PTE_P);
// Print the string supplied by the user.
cprintf("%.*s", len, s);
}
faultallocbad
就是在这个assert的位置出错。
[00000000] new env 00001000
[00001000] user_mem_check assertion failure for va deadbeef
[00001000] free env 00001000
看看cprintf
的流程,可以表述为以下的函数调用链:
cprintf > vcprintf > vprintfmt > sys_cputs
有一个细节,在这个格式化输出的过程中,所有字符并不会直接通过系统调用进行输出,而是存进一个缓冲区,处理好格式,等待缓冲区满后才会调用系统调用:
static void
putch(int ch, struct printbuf *b)
{
b->buf[b->idx++] = ch;
if (b->idx == 256-1) {
sys_cputs(b->buf, b->idx);
b->idx = 0;
}
b->cnt++;
}
可以看到,只有当buffer的256字节容量满后,才会调用系统调用,这之前都是发生在用户空间的读取操作。
读取的具体过程如下:
// vprintfmt.c
...
// string
case 's':
if ((p = va_arg(ap, char *)) == NULL)
p = "(null)";
if (width > 0 && padc != '-')
for (width -= strnlen(p, precision); width > 0; width--)
putch(padc, putdat);
// p的解引用获取 %s 代表的字符串中的字符
// 读取不存在的页 发生用户空间page fault
for (; (ch = *p++) != '\0' && (precision < 0 || --precision >= 0); width--)
...
结论
在faultallocbad.c
中,未经封装的系统调用会直接访问到page fault的页,导致内核态先访问到缺页,用户态缺页处理函数无法生效;在faultalloc.c
中,调用cprintf
会先读取目标地址的字符串进入缓冲区,处理后再进行输出,缺页先由用户态的读取行为访问到,用户态缺页处理函数正常生效,而后的系统调用访问到的是用户处理后的缺页情况,此时行为恢复正常。