MIT 6.828操作系统实验:为什么faultalloc.c的页错误可以被自定义的处理函数解决,而faultallocbad.c的被内核assert

文章讨论了在Lab环境中,JOS内核如何使用微内核设计,区分了两种处理用户空间页错误的方法:一种通过cprintf封装避免内核态直接访问,另一种直接使用sys_cputs导致内核先访问缺页。这展示了用户态处理和内核态介入在页错误处理中的重要性。
摘要由CSDN通过智能技术生成

在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会先读取目标地址的字符串进入缓冲区,处理后再进行输出,缺页先由用户态的读取行为访问到,用户态缺页处理函数正常生效,而后的系统调用访问到的是用户处理后的缺页情况,此时行为恢复正常。

  • 5
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值