目录
Unix 提供
fork()
系统调用作为其主要的进程创建原语。该fork()
系统调用将调用进程的地址空间(父)创建一个新的进程(孩子)。xv6 Unix通过将父页面中的所有数据复制到为子分配的新页面来实现
fork()
。这基本上与dumbfork()
所采用的方法相同。将父级地址空间复制到子级是该fork()
操作中最昂贵的部分。然而,在子进程中,
fork()
调用后exec()
几乎立即紧随其后,这会用新程序替换子进程的内存。这就是 shell 通常所做的。在这种情况下,复制父进程的地址空间所花费的时间在很大程度上被浪费了,因为子进程在调用exec()之前很少使用内存
.出于这个原因,后来的 Unix 版本利用虚拟内存硬件来允许父 进程和子进程共享映射到各自地址空间的内存,直到其中一个进程实际修改它。这种技术称为写时复制。为此,在
fork()
内核上将从父到子复制地址空间映射 而不是映射页面的内容,同时将现在共享的页面标记为只读。当两个进程之一尝试写入这些共享页面之一时,该进程会出现页面错误。在这一点上,Unix 内核意识到该页面实际上是一个“虚拟”或“写时复制”副本,因此它为故障进程创建了一个新的、私有的、可写的页面副本。这样,单个页面的内容在实际写入之前不会被实际复制。这种优化使得子进程中的fork()
后跟exec()
便宜得多:子进程在调用exec()
之前可能只需要复制一页(其堆栈的当前页)。
用户级别错误页面处理
写时复制是用户级页面错误处理的用途之一。
设置地址空间是很常见的,以便页面错误指示何时需要执行某些操作。例如,大多数 Unix 内核最初只映射新进程堆栈区域中的单个页面,然后随着进程的堆栈消耗增加并导致尚未映射的堆栈地址上的页面错误,“按需”分配和映射额外的堆栈页面。典型的 Unix 内核必须跟踪在进程空间的每个区域发生页面错误时要采取的操作。例如,堆栈区域中的错误通常会分配和映射物理内存的新页面。程序的 BSS 区域中的错误通常会分配一个新页面,用零填充它并映射它。在具有按需分页可执行文件的系统中,这是内核需要跟踪的大量信息。与采用传统的 Unix 方法不同,您将决定如何处理用户空间中的每个页面错误,其中错误的破坏性较小。这种设计的额外好处是允许程序在定义其内存区域时具有很大的灵活性;稍后您将使用用户级页面错误处理来映射和访问基于磁盘的文件系统上的文件。
设置页面错误处理程序
sys_env_set_pgfault_upcall:为每个进程设置一个页面错误处理程序入口点
// Set the page fault upcall for 'envid' by modifying the corresponding struct // Env's 'env_pgfault_upcall' field. When 'envid' causes a page fault, the // kernel will push a fault record onto the exception stack, then branch to // 'func'. // // Returns 0 on success, < 0 on error. Errors are: // -E_BAD_ENV if environment envid doesn't currently exist, // or the caller doesn't have permission to change envid. static int sys_env_set_pgfault_upcall(envid_t envid, void *func) { // LAB 4: Your code here. struct Env* env_store; if(envid2env(envid,&env_store,1)<0) return -E_BAD_ENV; env_store->env_pgfault_upcall=func; return 0; // panic("sys_env_set_pgfault_upcall not implemented"); }
用户环境中的正常和异常堆栈
在正常执行过程中,JOS用户环境将在运行正常的用户堆栈:它的ESP寄存器开始在指向
USTACKTOP,
且堆栈数据之间是推动在页面上驻留USTACKTOP-PGSIZE
和USTACKTOP-1之间
。然而,当在用户模式下发生页面错误时,内核将重新启动用户环境,在用户异常堆栈不同的堆栈上运行指定的用户级页面错误处理程序。本质上,我们将让 JOS 内核实现进程自动“堆栈切换”,这与 x86处理器 在从用户模式转换到内核模式时已经代表 JOS 实现堆栈切换非常相似!JOS 用户异常栈也是一页大小,其顶部定义为虚拟地址
UXSTACKTOP
,因此用户异常栈的有效字节为UXSTACKTOP-PGSIZE
到UXSTACKTOP-1之间
。在这个异常堆栈上运行时,用户级缺页处理程序可以使用 JOS 的常规系统调用来映射新页面或调整映射,以修复最初导致页面错误的任何问题。然后,用户级页面错误处理程序通过汇编语言存根返回原始堆栈上的错误代码。每个想要支持用户级页面错误处理的进程都需要使用A 部分介绍的系统调用
sys_page_alloc()
为其自己的异常堆栈分配内存。
调用用户页面错误处理程序
您现在需要更改kern/trap.c 中的页面错误处理代码 以处理来自用户模式的页面错误,如下所示。我们将故障时用户环境的状态称为trap--time状态。
如果没有注册页面错误处理程序,JOS 内核会像以前一样用消息破坏用户环境。否则,内核会在异常堆栈上设置一个陷阱帧,看起来像
struct UTrapframe
来自inc/trap.h:内核然后安排用户环境恢复执行,页面错误处理程序运行在具有这个堆栈帧的异常堆栈上(如何做到?)。该fault_va是导致页面错误的虚拟地址。
如果在发生异常时用户环境已经在用户异常堆栈(
tf->tf_esp
在UXSTACKTOP-PGSIZE
和UXSTACKTOP-1)
上运行,则页面错误处理程序本身已出错。在这种情况下,您应该在当前的下方tf->tf_esp
而不是在UXSTACKTOP
处开始新的堆栈帧 。您应该首先推送一个空的(四个字节),然后推送一个struct UTrapframe。
struct UTrapframe的定义如下:
struct UTrapframe { /* information about the fault */ uint32_t utf_fault_va; /* va for T_PGFLT, 0 otherwise */发生页面错误的虚拟地址 uint32_t utf_err;//error code /* trap-time return state */ struct PushRegs utf_regs; uintptr_t utf_eip; uint32_t utf_eflags; /* the trap-time stack to return to */ uintptr_t utf_esp; } __attribute__((packed));
与内核态的TrapFrame相比,缺少了es,ds,cs,ss等段,因为无论是从用户栈到用户异常栈,还是从用户异常栈到用户栈的切换,都是一个用户进程,因此不涉及到段的切换。TrapFrame作为记录进程完整状态存在,而UTrapeFrame只是在处理用户定义错误时使用。
Page_fault_hanlder:将页面错误分派到用户处理模式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. if((tf->tf_cs&3)==0)//cpl panic("page fault happens in kernel mode!adress:%d",fault_va); // LAB 3: Your code here. // We've already handled kernel-mode exceptions, so if we get here, // the page fault happened in user mode. // Call the environment's page fault upcall, if one exists. Set up a // page fault stack frame on the user exception stack (below // UXSTACKTOP), then branch to curenv->env_pgfault_upcall. // // The page fault upcall might cause another page fault, in which case // we branch to the page fault upcall recursively, pushing another // page fault stack frame on top of the user exception stack. // // It is convenient for our code which returns from a page fault // (lib/pfentry.S) to have one word of scratch space at the top of the // trap-time stack; it allows us to more easily restore the eip/esp. In // the non-recursive case, we don't have to worry about this because // the top of the regular user stack is free. In the recursive case, // this means we have to leave an extra word between the current top of // the exception stack and the new stack frame because the exception // stack _is_ the trap-time stack. // // If there's no page fault upcall, the environment didn't allocate a // page for its exception stack or can't write to it, or the exception // stack overflows, then destroy the environment that caused the fault. // Note that the grade script assumes you will first check for the page // fault upcall and print the "user fault va" message below if there is // none. The remaining three checks can be combined into a single test. // // Hints: // user_mem_assert() and env_run() are useful here. // To change what the user environment runs, modify 'curenv->env_tf' // (the 'tf' variable points at 'curenv->env_tf'). // LAB 4: Your code here. // Destroy the environment that caused the fault. else{ struct UTrapframe* u_tf; if(curenv->env_pgfault_upcall){ //No正常用户堆栈,在之前的基础上继续建立栈帧 if(tf->tf_esp>=UXSTACKTOP-PGSIZE&&tf->tf_esp<=UXSTACKTOP-1){ u_tf=(struct UTrapframe*)(tf->tf_esp-sizeof(struct UTrapframe)-4);//32bit } else{//正常用户堆栈,从异常栈顶部开始建立 u_tf=(struct UTrapframe*)(UXSTACKTOP-sizeof(struct UTrapframe)); } user_mem_assert(curenv,(void*) u_tf,sizeof(struct UTrapframe),PTE_P|PTE_W);//检查这段内存的权限 //设置u_tf u_tf->utf_eflags=tf->tf_eflags; u_tf->utf_regs=tf->tf_regs; u_tf->utf_err=tf->tf_trapno; u_tf->utf_fault_va=fault_va; //保存现场,便于返回 u_tf->utf_eip=tf->tf_eip; u_tf->utf_esp=tf->tf_esp; //当前进程的入口转为页面错误处理入口 tf->tf_eip=(uintptr_t)(curenv->env_pgfault_upcall);//jump to //当前进程的栈顶设置为异常栈帧开始 tf->tf_esp=(uintptr_t)(u_tf);// //重新运行当前进程 env_run(curenv);// } else{ cprintf("[%08x] user fault va %08x ip %08x\n", curenv->env_id, fault_va, tf->tf_eip); print_trapframe(tf); env_destroy(curenv); } } }
若是异常栈的大小超过PGSIZE,那么栈向下生长,下面区域的权限为不可用,可以通过user_mem_assert来检查权限,若是超过,直接destroy。
用户模式页面错误入口点
首先看一下用户环境中.set_pgfault_handler的设置:
extern void _pgfault_upcall(void);
注册了一个_pgfault_upcall函数,是PEntry.s中定义的页面错误入口点。
void (*_pgfault_handler)(struct UTrapframe *utf);
_pgfault_handler指向C程序中页面错误处理函数。
当我们没有设置页面处理函数时,_pgfault_handler为0,因为还没有设置异常堆栈以及设置内核调用_pgfault_upcall。
void set_pgfault_handler(void (*handler)(struct UTrapframe *utf)) { int r; if (_pgfault_handler == 0) {//第一次设置 if((r=sys_page_alloc(sys_getenvid(),(void*)(UXSTACKTOP-PGSIZE),PTE_SYSCALL))<0)//配置异常堆栈 panic("exception stack failed:%e",r); sys_env_set_pgfault_upcall(sys_getenvid(),(void*) _pgfault_upcall);//页面错误处理入口 } // Save handler pointer for assembly to call. _pgfault_handler = handler; }
然后来看一下汇编中所有页面处理程序入口_pgfault_upcall(lib/pfentry.s)
的实现。首先调用C中的page_fault_handler,完成后切换到用户栈出错的地方继续执行。不能使用jmp指令:所有的寄存器状态要恢复到发生异常前的状态。
也不能够使用ret指令:
ret
会修改esp
(ret
相当于pop %eip
,会自动将esp+0x4
)。在恢复
eflags
后不能使用任何add, sub
指令,防止对标志位发生修改。因此:把
eip
送入用户正常栈的栈顶,然后在恢复esp
到旧esp-0x4
后调用ret
将它pop
出来。首先看一下异常栈的布局:
|------------------------------| | | * 0x30(%esp) |------------------------------| utf_esp * | | * 0x2C(%esp) ----> |------------------------------| utf_eflags * | | * 0x28(%esp) ----> |------------------------------| utf_eip * | edi | * | esi | * | ebp | * | oesp | * | ebx | * | edx | * | ecx | * | eax | * 0x08(%esp) ----> |------------------------------| utf_regs * | | * 0x04(%esp) ----> |------------------------------| utf_error * | | * %esp------------>+------------------------------+utf_fault_va
因此需要的恢复操作:
1. 从
0x28(%esp)
取出故障时eip
到临时寄存器%eax(因为之前设置了eip为tf的eip)。
2. 0x30(%esp)
处的值减0x04(故障时esp-0x04,这里存的是正常用户栈的esp
)。
3.
从0x30(%esp)
取出故障时esp-0x04
到%ebx。
4.
在故障时esp-0x04
位置写入故障时eip
:即在正常用户栈的栈顶上方写入故障时eip。
5.
按顺序恢复utf_regs, utf_eflags, utf_esp
到各寄存器(此时utf_esp
的值为故障时esp-0x04
)。6.
ret
5555,写了这么多,且很绕,其实目的就是将eip放到发生页面错误的esp下方,当恢复到utf_esp时,它会跳到正常栈esp-4的位置处,然后我们pop掉,那么esp,eip都回到了原始的状态。。。。(不知道这么理解对不对,但是看了很多博客都把这个过程讲的不太清楚, MIT-JOS系列9:多任务处理(二)_快乐咸鱼每一天-CSDN博客 这篇文章讲的比较清晰:)
因此_pgfault_upcall的实现如下:
.text .globl _pgfault_upcall _pgfault_upcall: // 自定义的页面错误处理程序 pushl %esp // function argument: pointer to UTF movl _pgfault_handler, %eax call *%eax addl $4, %esp // pop function argument //eip放到正常栈esp-4处 movl 0x28(%esp), %eax subl $4, 0x30(%esp) movl 0x30(%esp), %ebx movl %eax, (%ebx) // Restore the trap-time registers. After you do this, you // can no longer modify any general-purpose registers. // LAB 4: Your code here. addl $0x8, %esp popal // 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 $4,%esp // Switch back to the adjusted trap-time stack. // LAB 4: Your code here. popfl // Return to re-execute the instruction that faulted. // LAB 4: Your code here. popl %esp ret
实现写时复制fork( )
在上一节中已经说过,fork是页面错误处理的应用之一。fork()的基本控制流程
如下:
- 父进程安装
pgfault()
为 C 级页面错误处理程序,使用set_pgfault_handler()
实现的函数。- 父进程调用
sys_exofork()
以创建子环境。- 对于每一个UTOP下方可写或写入时复制页面,父进程调用
duppage
,这应该写入时复制的页面映射到子进程的地址空间,然后重新映射写入时复制页面的到自己的地址空间。注意:这里的排序(先将子进程的页面标记,然后再标记父进程页面)很重要!
然而,异常堆栈不会以这种方式重新映射。相反,需要在子级中为异常堆栈分配一个新页面。由于页面错误处理程序将进行实际的复制,并且页面错误处理程序在异常堆栈上运行,因此不能将异常堆栈设为写时复制:谁来复制它?
fork()
还需要处理存在但不可写或写时复制的页面。
- 父级将子级的用户页面错误入口点设置为看起来像它自己的。
- 孩子现在准备好运行了,所以父母将它标记为可运行。
每次其中一个进程写入从未写入的写时复制页面时,都会发生页面错误。这是用户页面错误处理程序的控制流:
- 内核将页面错误传到
_pgfault_upcall
,调用fork()
的pgfault()
处理程序。pgfault()
检查错误是写入(FEC_WR
在错误代码中检查)并且页面的 PTE 标记为PTE_COW
。pgfault()
分配一个映射在临时位置的新页面,并将故障页面的内容复制到其中。然后故障处理程序将新页面映射到具有读/写权限的适当地址,代替旧的只读映射。pgfault():
static void pgfault(struct UTrapframe *utf) { void *addr = (void *) utf->utf_fault_va;//出错的页面地址 uint32_t err = utf->utf_err; int r; // Check that the faulting access was (1) a write, and (2) to a // copy-on-write page. If not, panic. // Hint: // Use the read-only page table mappings at uvpt // (see <inc/memlayout.h>). if((err&FEC_WR)==0)//是写入错误吗 panic("page fault isnt caused by write !"); if((uvpt[PGNUM(addr)]&(PTE_COW|PTE_W))==0)//cow/w错误 panic("page fault isnt caused by COW !%x\n",addr); // set_pgfault_handler() // LAB 4: Your code here. // Allocate a new page, map it at a temporary location (PFTEMP), // copy the data from the old page to the new page, then move the new // page to the old page's address. // Hint: // You should make three system calls. envid_t envid=sys_getenvid(); if((r=sys_page_alloc(envid,PFTEMP,PTE_P|PTE_U|PTE_W))<0)//为临时页面分配物理内存 panic("error when page alloc:%e\n!",r); addr=ROUNDDOWN(addr,PGSIZE); memcpy((void*)PFTEMP,(const void*)addr,PGSIZE);//将出错的页面copy到临时页面 if((r=sys_page_map(envid,(void*)PFTEMP,envid,addr,PTE_P|PTE_W|PTE_U))<0)//将临时页面与原本页面指向同一个物理内存 panic("page map failed:%e\n!",r); if((r=sys_page_unmap(envid,(void*)PFTEMP))<0)//取消临时页面的映射,那么现在原本的COW页面指向了修改后的物理页面 panic("page unmap failed:%e\n!",r); // LAB 4: Your code here. // panic("pgfault not implemented"); }
duppage():将进程的第pn页映射到目的进程的相同地址。
static int duppage(envid_t envid, unsigned pn) { int r; envid_t pid=sys_getenvid(); uintptr_t va=(uintptr_t)pn*PGSIZE; if((uvpt[pn]&PTE_W)||(uvpt[pn]&PTE_COW)){//有PTE_W/PTE_COW限制的需要标记,并且当前进程需要再次映射 if((r=sys_page_map(pid,(void*)va,envid,(void*)va,PTE_COW|PTE_P|PTE_U))<0){ panic("copy fault:%e\n!",r); return r; } if((r=sys_page_map(pid,(void*)va,pid,(void*)va,PTE_COW|PTE_P|PTE_U))<0){ panic("copy fault:%e\n!",r); return r; } } else{ if((r=sys_page_map(pid,(void*)va,envid,(void*)va,PTE_P|PTE_U))<0) panic("copy fault:%e\n!",r); return r; } // LAB 4: Your code here. // panic("duppage not implemented"); return 0; }
Why do we need to mark ours copy-on-write again if it was already copy-on-write at the beginning of this function?
fork()
:终于写到这里了,查了两天的bug~~
envid_t fork(void) { // LAB 4: Your code here. set_pgfault_handler(pgfault);//设置页面错误处理程序 envid_t cid=sys_exofork(); envid_t pid=sys_getenvid(); int r; if(cid<0){ panic("env error! pid need >=0!\n"); return cid; } if(cid==0){ //子进程中,那么将thisenv设置为子进程 thisenv=&envs[ENVX(pid)]; return 0; } //将父进程的地址空间copy给子进程 for( uintptr_t i=0;i<USTACKTOP;i+=PGSIZE){ if(!(uvpd[i>>PDXSHIFT]&PTE_P)||!(uvpt[PGNUM(i)]&PTE_P)) continue; if((r=duppage(cid,i/PGSIZE))<0){ panic("map pages failed!\n"); return r; } } //子进程异常栈 if((r=sys_page_alloc(cid,(void*)(UXSTACKTOP-PGSIZE),PTE_P|PTE_U|PTE_W))<0){ panic("page alloc failed!\n"); return r; } extern void _pgfault_upcall(void); //子进程设置页面错误处理 if((r=sys_env_set_pgfault_upcall(cid,(void*) _pgfault_upcall))<0) return r; //设置子进程状态 if((r=sys_env_set_status(cid,ENV_RUNNABLE))<0){ panic("status changes failed!\n"); return r; } // panic("fork not implemented"); return cid; }
这里有个bug找了很久,把pid和cid两行写反了,导致在子进程赋给thisenv的是父进程的id,然后make stressched一直不通过,然后一直以为是RR调度写的有问题,后来才发现这里有问题,当然也有对操作系统了解不深的原因在。
以上就是LAB4PartB的内容,关于UVPT查找的内容,争取补上!