实验完善代码 LAB2-4下载链接 提取码:79t8
1、Part B : 写时复制
在 Part A 中最后一个实验中,实现了初步的fork()
功能。Unix 提供系统调用函数 fork()
作为创建进程的原语,它将父进程的地址空间拷贝到子进程。 xv86 Unix 的fork()
实现是:为子进程分配新的页面,并把父进程页面中的所有数据复制到子进程,数据拷贝是fork()
中代价最大的操作。
大多数时候,子进程在被fork()
出来之后会调用 exec()
,它将子进程的内存完全替换为新的程序,这时候子进程仅在调用exec()
之前可能会使用 继承于父进程的部分数据,其余的大部分数据都白白复制了。
因此,之后的Unix 利用虚拟内存的优势,允许子进程和父进程共享内存,直到其中任何一个进程修改内存中的数据,这种技术被称为写时复制。为了实现这个功能,在函数fork()
中内核只将父进程的地址空间复制给子进程,并不复制空间中的内容,同时将共享页面标记为只读。当其中任何一个进程想要修改共享内存中的数据的时候,进程将触发一个 page fault。这时,内核意识到这个页面是一个虚拟的或者写时复制的副本,然后给触发异常的进程分配一个私有的可写页面并复制原页面中的数据。这样,只有在其中一个进程企图改变页面中的内容的时候才会执行真正的复制,这样如果 fork()
之后 子进程立刻执行exec()
的话,代价小了很多。
接下来的实验中,将在 Part A 的基础上在用户的lib
中完善 fork()
使其支持写时复制。在用户空间实现fork()
和写时复制使得内核更简单不容易出错;同时也支持用户进程定义自己的fork()
函数的实现。
2、用户态的 page fault 处理
用户级别下的 fork()
和写时复制 需要知道何时在共享页面的时候发生了 page fault ,因为这个page fault 只是用户态出现 page fault 众多情况中的一种,我们需要进行区分。
内核为用户态 page fault 执行不同的处理方法。例如,大部分Unix 内核只为每个进程分配一个页面作为栈空间,当这个栈空间使用完了并触发 page fault ,内核才会继续为其分配新的栈空间页面。当用户空间不同区域发生错误时候,Unix 内核必须追踪其对应的错误,采取恰当的措施。下面列举了不同情况的错误下应该采取的措施:
- 栈空间发生 page fault ,内核将为用户进程分配一个新的栈空间页面;
- BSS 区发生 page fault ,内核将分配并映射一个新的物理页面,并将改页面初始化为0;
- 在按需分配系统中,如果 text 区域发生 page fault ,内核将从磁盘读取相应的二进制文件重新映射。
(1) 设置 page fault 处理函数
为了处理 page fault ,用户环境下将需要调用系统函数 sys_env_set_pgfault_upcall
向JOS 内核注册一个页面错误处理程序入口,并向env 数据结构增加 env_pgfault_upcall结构 记录这个信息。
- Exercise 8
static int sys_env_set_pgfault_upcall(envid_t envid, void *func)
{
// LAB 4: Your code here.
struct Env * env = NULL;
if(envid2env(envid,&env,1) < 0)
return -E_BAD_ENV;
env->env_pgfault_upcall = func;
return 0;
//panic("sys_env_set_pgfault_upcall not implemented");
}
(2) 用户环境的正常栈和异常栈
正常运行状态下,JOS 系统中,进程运行在正常的用户栈上:其 ESP 寄存器指向 USTACKTOP,压栈时数据被压入到 USTACKTOP-1 到 USTACKTOP - PGSIZE 之间的区域。当用户态下发生页面错误的时候,JOS 系统将栈从正常用户栈切换到用户异常栈(原理与中断/异常切换到内核态差不多),这里的用户异常栈 类似于 TSS。这个过程的切换是由JOS 内核代表用户进程自动完成的,类似于x 86 处理器在异常/中断的时候 主动实现用户堆栈到内核堆栈的切换一样。
JOS 用户异常栈的大小为一个页面,其栈顶为 UXSTACKTOP, 当运行在异常栈的时候,用户页面错误处理程序能通过JOS 的系统调用分配一个新的页面或调整地址映射来修复页面错误异常。处理完成后回到导致错误的地方继续执行。每个支持用户级别页面错误处理函数的进程将使用函数 sys_page_alloc
为用户异常栈分配内存。
(3) 调用用户页面错误处理程序
我们需要修改 kern/trap.c
中用户态页面错误处理代码。如果没有注册页面错误处理函数,JOS 在发生用户态页面错误的时候会直接销毁用户环境。否则,内核应该在用户异常栈上设置 struct UTrapframe
结构,这个结构在文件inc/trap.h 中(类似于中断/异常发生时候的压栈),异常处理完之后恢复用户进程,异常的处理在异常栈上执行它的页面错误处理函数。
如果用户态页面错误发生的时候,已经运行在用户异常栈上,则说明用户的页面错误处理函数本身出现了故障。这时新栈应该从当前的 tf->tf_esp
(指向哪里? 指向上一个UTrapframe 结构)开始,而不是 UXSTACKTOP
,压入 struct UTrapframe 结构进入新栈之前,应该先压入一个32位的空字。判断 tf->tf_esp
是不是已经指向 用户异常栈,可以判断其所指的范围是不是在 UXSTACKTOP-PGSIZE and UXSTACKTOP-1 之间。
总结下用户态异常处理的全过程:
-
发生异常前,用户已经使用函数
sys_env_set_pgfault_upcall
向JOS 系统注册了异常发生对应的处理函数,并为这个异常处理分配一个异常栈; -
用户态页面错误,进行正常的中断处理程序,切换到内核态,进入trap() ;
-
根据中断号,判断这个错误是页面错误,调用
page_fault_handler()
; -
检查
ts_cs
,判断是否是用户态发生的错误,如果是用户态产生的错误,进行下面的步骤; -
判断产生错误的进程,有咩有注册相应的处理函数,如果没有的话直接销毁这个进程,如果有的话进行下一步;
-
准备转向用户态异常处理,分为以下几个步骤:
①、检查tf->tf_esp
判断是否正处于异常栈中,如果处于异常栈,说明这个错误是在用户态异常处理函数中产生的,在这种情况下,应该递归地解决错误,首先在tf->tf_esp
所指向的地方新建一个栈,压栈前检查下是否存在越界访问,然后压入一个32位的字,接下来压入 UTrapframe;
②、如果当前进程并不处于异常栈,则将UXSTACKTOP
视为栈顶,压入 struct UTrapframe 结构;
③、设置当前用户栈指针tf->tf_esp
指向异常栈顶;
④、设置当前用户下一条指令执行用户异常助理函数env_pgfault_upcall
-
异常处理结束,恢复运行。
-
Exercise 9
void
page_fault_handler(struct Trapframe *tf)
{
uint32_t fault_va;
// Read processor's CR2 register to find the faulting address
fault_va = rcr2();
if((tf->tf_cs & 0x11) == 0)
{
panic("page_fault in kernel mode, fault address %d\n", fault_va);
}
// LAB 4: Your code here.