实验完善代码 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.
if(curenv->env_pgfault_upcall != NULL)
{
struct UTrapframe * utf;
if(UXSTACKTOP - PGSIZE <= tf->tf_esp && tf->tf_esp < UXSTACKTOP)
{
utf = (struct UTrapframe *)(tf->tf_esp - sizeof(struct UTrapframe) -4);
}
else {
utf = (struct UTrapframe *)(UXSTACKTOP- sizeof(struct UTrapframe) );
}
user_mem_assert(curenv,(void*) utf,sizeof (struct UTrapframe),PTE_U|PTE_W);
utf->utf_fault_va = fault_va;
utf->utf_err = tf->tf_err;
utf->utf_regs = tf->tf_regs;
utf->utf_eip = tf->tf_eip;
utf->utf_eflags = tf->tf_eflags;
utf->utf_esp = tf->tf_esp;
curenv->env_tf.tf_eip = (uintptr_t) curenv->env_pgfault_upcall;
curenv->env_tf.tf_esp = (uintptr_t) utf;
env_run(curenv);
}
// Destroy the environment that caused the fault.
cprintf("[%08x] user fault va %08x ip %08x\n",
curenv->env_id, fault_va, tf->tf_eip);
print_trapframe(tf);
env_destroy(curenv);
}
(4) 知识点小结
到目前为止我们接触了3种不同的栈:内核栈、用户运行栈和用户异常处理栈(用户态异常处理的时候使用)。
- 内核栈:运行内核相关程序的时候使用,大小是固定的,8页,内核栈与内核栈之间的 GAP 也是 8页;
- 用户运行栈:运行用户正常程序的时候使用,载入ELF文件的时候分配的,只有一个页面,边运行边分配;
- 用户异常栈:当用户栈页面用完的时候,会触发缺页错误,用户态缺页错误处理的时候会用这个栈。
仔细看源代码会发现UTrapframe 结构 和 Trapframe 结构的区别,UTrapframe 结构中没有 段寄存器,这是因为 无特权级转换的时,ret 指令只是 pop eip ;不同特权级返回的时,ret 指令会 pop ip 和 pop cs 。用户态缺页处理没有特权级的转换。
- Trapframe : 用于保存进程的完成信息,在中断后被中断的程序自动保存下来,并且作为数据结构在操作系统内的函数中传递,这个结构保存在TSS 中;
- UTrapframe : 为了有效地组织错误发生时,用户进程的状态,这个结构会保存在用户异常栈上,没有被操作系统内部使用。
(5) 用户页面错误处理程序入口
接下来需要在 lib/pfentry.S
中编写汇编代码_pgfault_upcall
来实现用户页面错误出现之后程序跳转到相应的错误处理函数处(这里的错误处理函数需要我们 调用 sys_env_set_pgfault_upcall 来注册,见Exercise 8)
- Exercise 10
当程序返回到_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
// LAB 4: Your code here.
movl 0x30(%esp),%eax;
subl $0x4, %eax;
movl %eax,0x30(%esp);
movl 0x28(%esp),%ebx;
movl %ebx,(%eax);
// LAB 4: Your code here.
// Restore eflags from the stack. After you do this, you can
// no longer use arithmetic operations or anything else that
// modifies eflags.
// LAB 4: Your code here.
addl $0x8,%esp;
popal;
// Switch back to the adjusted trap-time stack.
// LAB 4: Your code here.
addl $0x4,%esp;
popfl; //
// Return to re-execute the instruction that faulted.
// LAB 4: Your code here.
pop %esp;
ret;
这部分代码之后,栈上的布局如图所示:
- Exercise 11
void set_pgfault_handler(void (*handler)(struct UTrapframe *utf))
{
int r;
if (_pgfault_handler == 0) {
if((r = sys_page_alloc(0,(void *)(UXSTACKTOP - PGSIZE),PTE_U|PTE_P|PTE_W)) < 0)
panic("sys_page_alloc err.\n");
sys_env_set_pgfault_upcall(0,_pgfault_upcall);
}
_pgfault_handler = handler;
}
-
make grade
结果
3、写时复制 fork
接下来实现用户空间的 写时复制 fork
。在lib/fork.c
中提供了 fork
的基本框架,像 dumbfork()
, fork
应该新建一个进程,然后将父进程的地址空间映射关系复制到子进程。fork
的基本控制流如下所示:
- 父进程调用
set_pgfault_handler()
函数用户态缺页异常注册回调函数; - 父进程调用
sys_exofork()
函数创建新的进程; - 对于每一个位于
UTOP
下的可写/写时复制的页面,父进程调用duppage
将其映射到子进程的地址空间(目前只是映射而已);然后重新映射写时复制页面到自己的地址空间。
① 注意这里的顺序很重要,先在子进程空间标志一个写时复制页面然后再在父进程中标记。为什么?duppage
将每个页面都设置为只读,对于可能会修改的页面设置为写时复制。原理是:当企图写一个写时复制的只读页面,会触发页面错误,然后将错误页面拷贝成自己私有而不能直接修改的共享页面。
② 用户异常栈不以上面的方式进行复制,子进程需要自行分配一个新的页面作为自己的用户异常栈。 - 父进程为子进程设置页面错误处理程序入口;
- 父进程标记子进程的状态为 可运行。
当进程企图写一个写时复制的只读页面,会触发页面错误,这个错误处理流程如下:
-
内核将错误传递到
_pgfault_upcall
, 接下来会调用fork()
中的pgfault()
设置的页面错误处理函数; -
pgfault()
检查引起错误的原因是不是由于写一个写时复制的页面,如果不是则panic; -
如果是的,
pgfault()
函数新建一个页面并将发生页面错误的数据复制到新的页面,赋予其读写权限,然后修改映射。以上实现的用户级别的 带有写时复制功能的 lib/fork.c ,需要向进程页表咨询页面的操作权限(例如获取出错页面是是否被标记为写时复制),这也是内核将用户环境页表映射到 UVPT 的用处。当用户进程启动的时候,
lib/entry.S
中导出 UVPT 为 uvpt;uvpd = (UVPT+(UVPT>>12)*4) ; -
uvpt : uvpt[n] 为页表表项第n 个虚拟页面的PTE。 对虚拟地址 va ,其 PTE 为uvpt[PGNUM(VA)];
-
uvpd : uvpd[n] 为页目录中的第 n 项;
线性地址转换为物理地址的过程 pd = lcr3(); pt = (pd+4PDX(va)); page = (pt+4PTX(va));
- Exercise 12
- (1)完善fork()
envid_t fork(void)
{
// LAB 4: Your code here.//set_pgfault_handler(void (*handler)(struct UTrapframe *utf))
set_pgfault_handler(pgfault);
envid_t envid;
uintptr_t addr;
int r;
envid = sys_exofork();
if (envid < 0)
panic("sys_exofork: %e", envid);
if (envid == 0) {
// We're the child.
thisenv = &envs[ENVX(sys_getenvid())];
return 0;
}
for (addr = UTEXT; addr < USTACKTOP; addr += PGSIZE)
{
if((uvpd[PDX(addr)] & PTE_P) && (uvpt[PGNUM(addr)] & (PTE_P | PTE_U)))
duppage(envid, PGNUM(addr));
}
// Also copy the stack we are currently running on.
if((r = sys_page_alloc(envid,(void *)(UXSTACKTOP - PGSIZE),PTE_U | PTE_W |PTE_P))<0)
panic("fork sys_page_alloc: %e error", r);
extern void _pgfault_upcall(void);
sys_env_set_pgfault_upcall(envid, _pgfault_upcall);
// Start the child environment running
if ((r = sys_env_set_status(envid, ENV_RUNNABLE)) < 0)
panic("sys_env_set_status: %e", r);
return envid;
//panic("fork not implemented");
}
- (2)完善duppage()
static int duppage(envid_t envid, unsigned pn)
{
int r,perm = PTE_U | PTE_P;
// LAB 4: Your code here.
uintptr_t va = pn * PGSIZE;
if((uvpt[pn] & PTE_W) || (uvpt[pn] & PTE_COW))
perm |= PTE_COW;
// This is NOT what you should do in your fork.
if ((r = sys_page_map(0, (void*)va, envid, (void*)va, perm)) < 0)
panic("sys_page_map: %e", r);
if ((r = sys_page_map(0, (void*)va, 0, (void*)va, perm)) < 0)
panic("sys_page_map: %e", r);
//panic("duppage not implemented");
return 0;
}
- (3)完善pgfault()
static void
pgfault(struct UTrapframe *utf)
{
void *addr = (void *) utf->utf_fault_va;
uint32_t err = utf->utf_err;
int r;
// LAB 4: Your code here.
if((err & FEC_WR) == 0)
panic("pgfault : not a write error\n");
if((uvpt[PGNUM(addr)] & PTE_COW) == 0)
panic("pgfault : not a write error\n");
// LAB 4: Your code here.
if((r = sys_page_alloc(0, (void *)PFTEMP, PTE_P|PTE_U|PTE_W)) < 0)
panic("sys_page_alloc: %e", r);
addr = ROUNDDOWN(addr,PGSIZE);
memmove((void *)PFTEMP, addr, PGSIZE);
if ((r = sys_page_map(0, (void *)PFTEMP, 0, (void*)addr, PTE_P|PTE_U|PTE_W)) < 0)
panic("sys_page_map: %e", r);
if ((r = sys_page_unmap(0, PFTEMP)) < 0)
panic("sys_page_unmap: %e", r);
//panic("pgfault not implemented");
}
- (4)
make grade
自此,Part B 全部完成!