【实现】缺页异常处理
当启动分页机制以后,如果一条指令或数据的虚拟地址所对应的物理页框不在内存中或者访问的类型有错误(比如写一个只读页或用户态程序访问内核态的数据等),就会发生缺页异常。产生页面异常的原因主要有:
- 目标页面不存在(页表项全为0,即该线性地址与物理地址尚未建立映射或者已经撤销);
- 相应的物理页面不在内存中(页表项非空,但Present标志位=0,比如在swap分区或磁盘文件上),这将在下面介绍换页机制实现时进一步讲解如何处理;
- 访问权限不符合(此时页表项P标志=1,比如企图写只读页面).
当出现上面情况之一,那么就会产生页面page fault(#PF)异常。产生异常的线性地址存储在CR2中,并且将 #PF 的类型保存在 error code 中,比如 bit 0 表示是否 PTE_P为0,bit 1 表示是否 write 操作。
产生缺页异常后,CPU硬件和软件都会做一些事情来应对此事。首先缺页异常也是一种异常,所以针对一般异常的硬件处理操作是必须要做的,即CPU在当前内核栈保存当前被打断的程序现场,即依次压入当前被打断程序使用的eflags,cs,eip,errorCode;由于缺页异常的中断号是0xE, CPU把中断0xE服务例程的地址(vectors.S中的标号vector14处)加载到cs和eip寄存器中,开始执行中断服务例程。这时ucore开始处理异常中断,首先需要保存硬件没有保存的寄存器。在vectors.S中的标号vector14处先把中断号压入内核栈,然后再在trapentry.S中的标号__alltraps处把ds、es和其他通用寄存器都压栈。自此,被打断的程序现场被保存在内核栈中。
接下来,在trap.c的trap函数开始了中断服务例程的处理流程,大致调用关系为:
trap--> trap_dispatch-->pgfault_handler-->do_pgfault
下面需要具体分析一下do_pgfault函数。CPU把引起缺页异常的虚拟地址装到寄存器CR2中,并给出了出错码(tf->tf_err),指示引起缺页异常的存储器访问的类型。而中断服务例程会调用缺页异常处理函数do_pgfault进行具体处理。缺页异常处理是实现按需分页、swap in/out和写时复制的关键之处,后面的小节将分别展开讲述。
ucore中do_pgfault函数是完成缺页异常处理的主要函数,它根据从CPU的控制寄存器CR2中获取的缺页异常的虚拟地址以及根据 error code的错误类型来查找此虚拟地址是否在某个VMA的地址范围内以及是否满足正确的读写权限,如果在此范围内并且权限也正确,这认为这是一次合法访问,但没有建立虚实对应关系。所以需要分配一个空闲的内存页,并修改页表完成虚地址到物理地址的映射,刷新TLB,然后调用iret中断,返回到产生缺页异常的指令处重新执行此指令。如果该虚地址不再某VMA范围内,这认为是一次非法访问。
【注意】
地址空间的管理由虚存管理和页表管理两部分组成。 虚存管理限制了(程序)地址空间的范围以及权限,而页表维护的是实际使用的地址空间以及权限,后者不能比前者有更大的范围或者权限,因为前者是实际管理页表的。比如权限,虚存管理可以规定地址空间的某个范围是可写的,但是页表中却可以标记是read-only的(比如 copy-on-write 的实现),这种冲突可以被内核(通过硬件异常)轻易的捕获到,并进行相应的处理。反过来,如果页表权限比虚存规定的权限更大,内核是没有办法发现这种冲突的。由于虚存管理的存在,内核才能方便的实现更复杂和丰富的操作,比如 share memory、swap 等。 在后续的实验中还会遇到虚存管理只维护用户地址空间(也就是 [USERBASE, USERTOP) 区间)的情况,因为内核地址空间包括虚存和页表都是固定的。