操作系统读者写者进程实现_MIT 6.828:实现操作系统 | Lab 4B:用户进程的缺页中断...

本文使用 Zhihu On VSCode 创作并发布

本文为本人实现6.828 Lab的笔记,Lab其他部分在专栏不定期更新,目录、环境搭建和各种说明请看第一篇:

MIT 6.828:实现操作系统 | Lab1:快来引导一个内核吧

请不要从中间乱入本系列文章,最好从头开始阅读。

本文md文档源码链接:AnBlogs

Lab 4B部分要求我们实现fork系统调用,本文只是前半部分,有关用户进程下的缺页中断Page Fault处理。这是实现fork非常重要的组件,我们想要实现Copy On Write,就不得不使用Page Fault机制。

中断流程总览

我们已经写了一些Page Fault处理流程,在函数page_fault_handler下。我们假设内核一定不会出现Page Fault,如果出现了,那就是内核有bug。本文完善用户态下的Page Fault处理。

和之前一样,任何中断都会进入trap函数,trap函数通过一次简单的switch,将中断分发到相应处理函数。对于Page Fault,也就是page_fault_handler。目前为止,内核将触发Page Fault的进程销毁,这当然不是我们想要的。

JOS在内核中做一些准备,然后以用户态进入在文件lib/pfentry.S中定义的流程。这个流程跳转到触发Page Fault的进程实现定义的一个处理函数,处理完成之后,再进行一些处理,返回到出发Page Fault的指令继续执行。JOS使用用户态流程处理Page Fault,其中需要内核参与的部分,通过System Call实现。

具体内核应做哪些准备,如何设置处理函数,如何返回触发中断的指令,就是本文接下来的内容。

指定处理函数

这个标题对应Exercise 811

用户进程通过函数set_pgfault_handler给自己设置Page Fault的处理函数,这个进程触发Page Fault之后,会由指定的函数处理。我们需要完善set_pgfault_handler

在写代码之前,首先看看指定处理函数的设计。

在结构体Env中,Lab 4新拉取的代码多了一个属性env_pgfault_upcall,用来指定一个入口。目前为止,我们总是将这个属性设置为函数_pgfault_upcall的地址,这个函数代表lib/pfentry.S定义的流程,故所有进程发生Page Fault之后,都跳转到这个流程。

pfentry.S的开头,这个流程马上跳转到了函数_pgfault_handler。这是个全局函数指针,不像env_pgfault_upcall总是接受一个固定的值,_pgfault_handler函数指针在函数set_pgfault_handler中设置为指定的值。这样一来,虽然env_pgfault_upcall的值总是固定,我们还是可以为进程定义不同的Page Fault处理函数。

有了这个理解,写代码就很容易了。全局函数指针_pgfault_handler没有设置初始值,则初始值是NULL,这让我们可以对第一次调用进行特殊处理。第一次调用set_pgfault_handler,要为中断时使用的栈分配空间。第一次调用之后,_pgfault_handler应设置为传入的函数指针,使得配置生效,并将当前进程的结构体通过系统调用sys_env_set_pgfault_upcall进行设置。具体代码如下:

void
set_pgfault_handler(void (*handler)(struct UTrapframe *utf))
{
	int r, ret;

	if (_pgfault_handler == 0) {
		// First time through!
		// allocate an exception stack
        ret = sys_page_alloc(thisenv->env_id, (void *)(UXSTACKTOP - PGSIZE), PTE_U | PTE_W);
        if (ret < 0) {
            panic("Allocate user exception stack failed!n");
        }
    }
    sys_env_set_pgfault_upcall(thisenv->env_id, _pgfault_upcall);

	// Save handler pointer for assembly to call.
	_pgfault_handler = handler;
}

系统调用sys_env_set_pgfault_upcall实现也很简单,通过envid将当前进程的Env结构体查出来,给相应env_pgfault_upcall属性赋值就可以。

static int
sys_env_set_pgfault_upcall(envid_t envid, void *func)
{
	int ret;

	struct Env *env;
	ret = envid2env(envid, &env, 1);
	if (ret < 0) {
	    return ret;
	}
	// set func
	env->env_pgfault_upcall = func;
	return 0;
}

调用处理函数并返回

本标题对应Exercise 9,主要代码写在文件kern/trap.c中的函数page_fault_handler

联系pfentry.S中的汇编代码,在切换至用户态代码之前,当前栈上应有一个UTrapframe结构体,栈顶esp可以直接作为结构体指针。进入处理函数_pgfault_handler之前,将当前栈顶地址压栈作为结构体指针传给处理函数。处理函数_pgfault_handler返回之后,栈回到传参动作pushl %esp之前的状态。后面的代码让处理器跳转回触发Page Fault的指令,恢复之前的执行状态。

我们需要给处理函数在栈上构造好一个结构体,并妥善维护栈结构,方便之后的恢复操作。

构造UTrapframe结构体

在构造结构体之前,我们需要选择栈。我们为用户态下的Page Fault中断专门准备了一个栈UXSTACKTOPPage Fault中断处理一定要在这个栈上进行。

Page Fault处理函数中发生Page Fault中断,应和其它情况进行不同处理,因为此时的栈已经是中断栈,不需要切换栈。联系之前处理内核中断,从用户态进入内核态需要切换栈和Page Directory,而内核中发生的中断不需要。

5bf055d8f84adf5e99be257ef40c802d.png
切换栈

判断是否为Page Fault处理函数引发的Page Fault,只需要看看进程栈顶位置就可以了。

struct UTrapframe *utf = NULL;
if (tf->tf_esp < UXSTACKTOP && tf->tf_esp >= UXSTACKTOP - PGSIZE) {
    utf = (struct UTrapframe *)(tf->tf_esp - sizeof(struct UTrapframe));
} else {
    // not recursive faults
    utf = (struct UTrapframe *)(UXSTACKTOP - sizeof(struct UTrapframe));
}

utf指针指向当前栈顶。将utf指针当做UTrapframe结构体进行赋值,就可以制造出想要的栈结构。当然,这个栈结构还不可以使用,之后要针对恢复执行修改这个结构。

一系列赋值:

// fault info
utf->utf_fault_va = fault_va;
utf->utf_err = tf->tf_err;
// return states
utf->utf_regs = tf->tf_regs;
utf->utf_regs = tf->tf_regs;
utf->utf_eip = tf->tf_eip;
utf->utf_eflags = tf->tf_eflags;
utf->utf_esp = tf->tf_esp;

装载后的到栈:

                    <-- UXSTACKTOP
trap-time esp
trap-time eflags
trap-time eip
trap-time eax       start of struct PushRegs
trap-time ecx
trap-time edx
trap-time ebx
trap-time esp
trap-time ebp
trap-time esi
trap-time edi       end of struct PushRegs
tf_err (error code)
fault_va            <-- %esp when handler is run

其中,trap-time应理解为中断时,这个栈保存了返回状态,方便之后的返回操作。

处理函数返回

上面的栈中保存了要返回到的状态,依次恢复寄存器,就可以恢复原先的执行状态。这里还是有困难。我们不能使用jmp进行跳转,因为jmp会占用一个寄存器,而跳转之前寄存器已经恢复,不能再更改。我们必须使用ret,而要返回到的地址没有直接存在于此时的栈上。这里主要解决运行ret的问题,主要代码写在pfentry.S后部分。

处理函数返回到pfentry.S的代码之后,处理器栈结构和上面代码块中的相同。在准备栈之前,可以先处理其它寄存器。

当前栈上前两个word不包含寄存器有关信息,故我们直接忽略:

addl $8, %esp

紧接着是一系列General Purpose寄存器,我们把从栈上取到寄存器里。在这之后,我们不能再操作这些寄存器。

popal

现在的栈顶是eip,我们暂时不处理,先处理下一个eflags。从这里开始,就不能再进行任何计算,否则会破坏刚刚恢复的寄存器eflags

addl $4, %esp
popf

紧接着恢复esp寄存器,在这之后,我们回到了中断发生之前使用的栈。

movl (%esp), %esp

我们在pfentry.S结尾执行ret,在这之前,必须让执行ret时的栈顶具有要跳转到的地址。这是关键所在。

我们手动为被中断的栈再压入一个4字节地址,从而使得上面最后一个代码块设置完esp后调用ret,可以跳转到这个地址。我们紧接着_pgfault_handler返回进行这个操作,避免破坏已经恢复的状态。

我们将中断栈上具有的esp值加上4字节,并以新值为地址,向那个地址写入要返回到的地址,也就是中断栈上的eip。我们将本标题下第一个代码块扩充成如下代码:

addl $8, %esp

movl 0x20(%esp), %eax
movl 0x28(%esp), %ebx
subl $4, %ebx
movl %ebx, 0x28(%esp)
movl %eax, (%ebx)

其中,十六进制数字0x用于通过当前栈顶,也就是中断栈,获得被中断进程的esp, eip,并把它们写到指定区域。

这样一来,切换回被中断栈之后,栈顶是被中断指令的地址。ret可以正确地令处理器跳转到那个指令。

这里还需要注意的是,对于在Page Fault处理函数中被中断的情况,和在其它地方被中断的情况不同。在处理函数中触发Page Fault不会切换栈,进入中断时,UTrapframe结构体直接压在原来栈上。如果像上面的代码块那样,直接在UTrapframe指定的esp上操作,就会覆盖到UTrapframe本身,从而导致错误。故在UTrapframe压栈时,必须预留4字节空间,避免这个问题。

之前的图应该改一下:

f5dbf5c75582a000149532783f709f3e.png
手动压返回地址

上面确定栈顶utf的代码应该做一些修改:

struct UTrapframe *utf = NULL;
if (tf->tf_esp < UXSTACKTOP && tf->tf_esp >= UXSTACKTOP - PGSIZE) {
    // must reserve 4 bytes for return address
    utf = (struct UTrapframe *)(tf->tf_esp - sizeof(struct UTrapframe) - 4);
} else {
    // not recursive faults
    utf = (struct UTrapframe *)(UXSTACKTOP - sizeof(struct UTrapframe));
}

完整代码

把以上代码整理到这里。

trap.c: page_fault_handler

void
page_fault_handler(struct Trapframe *tf)
{
	uint32_t fault_va;

	// Read processor's CR2 register to find the faulting address
	fault_va = rcr2();

	// Handle kernel-mode page faults.
    // LAB 3: Your code here.

    if ((tf->tf_cs & 3) != 3) {
        // tf comes from kernel mode
        cprintf("kernel fault va %08x ip %08xn",
                fault_va, tf->tf_eip);
	    panic("Page fault in kernel mode!n");
	}

	// LAB 4: Your code here.
	int ret;

    if (curenv->env_pgfault_upcall == NULL) {
        // no upcall
        page_fault_exit(fault_va, tf);
        return;
    }
    if (envtf->tf_esp > USTACKTOP && envtf->tf_esp <= UXSTACKTOP - PGSIZE) {
        // exception stack out of space
        page_fault_exit(fault_va, tf);
        return;
    }
    cprintf("user fault 0x%lx eip 0x%lx esp 0x%lxn", fault_va, tf->tf_eip, tf->tf_esp);

    // setup stack pointer
    struct UTrapframe *utf = NULL;
    if (tf->tf_esp < UXSTACKTOP && tf->tf_esp >= UXSTACKTOP - PGSIZE) {
        // must leave empty word for recursive faults
        utf = (struct UTrapframe *)(tf->tf_esp - sizeof(struct UTrapframe) - 4);
    } else {
        // not recursive faults
        utf = (struct UTrapframe *)(UXSTACKTOP - sizeof(struct UTrapframe));
    }
    // check user permissions
    user_mem_assert(curenv, utf, sizeof(struct UTrapframe), PTE_W);
    // pass struct UTrapframe as arguments
    // fault info
    utf->utf_fault_va = fault_va;
    utf->utf_err = tf->tf_err;
    // return states
    utf->utf_regs = tf->tf_regs;
    utf->utf_regs = tf->tf_regs;
    utf->utf_eip = tf->tf_eip;
    utf->utf_eflags = tf->tf_eflags;
    utf->utf_esp = tf->tf_esp;
    // new env run
    tf->tf_eip = (uintptr_t)curenv->env_pgfault_upcall;
    tf->tf_esp = (uintptr_t)utf;
    // run env
    env_run(curenv);
}

pfentry.S

.text
.globl _pgfault_upcall
_pgfault_upcall:
	// Call the C page fault handler.
	pushl %esp			// function argument: pointer to UTF
	movl _pgfault_handler, %eax
	call *%eax
	addl $4, %esp			// pop function argument
	
	addl $8, %esp

	movl 0x20(%esp), %eax
	movl 0x28(%esp), %ebx
    subl $4, %ebx
    movl %ebx, 0x28(%esp)
    movl %eax, (%ebx)

	popal

	addl $4, %esp
	popf

	movl (%esp), %esp

	ret

测试

和往常一样,这个Lab提供了一些测试。同样需要我们修改i386_init中的ENV_CREATE宏的参数,指定存在于user目录下的要运行的用户代码。

运行faultread,你应该看见进程触发了Page Fault中断,处理函数返回之后就正常退出了,应该输出exiting gracefully。如果没有输出正常退出有关信息,说明你的代码不能正确处理在正常用户态下发生Page Fault的情况。如果正确输出了,不能说明你的代码可以正确处理在Page Fault处理函数中触发Page Fault的情况。

为了验证你的代码确实能够正确处理一般用户态进入Page Fault处理函数的情况,可以再尝试运行faultdie。看看是否能够正常输出。

要验证你的代码是否能够处理Page Fault处理函数触发Page Fault的情况,你需要运行faultalloc, faultallocbad

在继续进行下面的Lab操作之前,务必保证以上测试全部通过。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值