Page Fault异常处理

实现虚存管理的一个关键是page fault异常处理,其过程中主要涉及到函数 — do_pgfault的具体实现。比如,在程序的执行过程中由于某种原因(页框不存在/写只读页等)而使 CPU 无法最终访问到相应的物理内存单元,即无法完成从虚拟地址到物理地址映射时,CPU 会产生一次页访问异常,从而需要进行相应的页访问异常的中断服务例程。这个页访问异常处理的时机被操作系统充分利用来完成虚存管理,即实现“按需调页”/“页换入换出”处理的执行时机。当相关处理完成后,页访问异常服务例程会返回到产生异常的指令处重新执行,使得应用软件可以继续正常运行下去。

具体而言,当启动分页机制以后,如果一条指令或数据的虚拟地址所对应的物理页框不在内存中或者访问的类型有错误(比如写一个只读页或用户态程序访问内核态的数据等),就会发生页访问异常。产生页访问异常的原因主要有:

  • 目标页帧不存在(页表项全为0,即该线性地址与物理地址尚未建立映射或者已经撤销);
  • 相应的物理页帧不在内存中(页表项非空,但Present标志位=0,比如在swap分区或磁盘文件上),这在本次实验中会出现,我们将在下面介绍换页机制实现时进一步讲解如何处理;
  • 不满足访问权限(此时页表项P标志=1,但低权限的程序试图访问高权限的地址空间,或者有程序试图写只读页面).

当出现上面情况之一,那么就会产生页面page fault(#PF)异常。CPU会把产生异常的线性地址存储在CR2中,并且把表示页访问异常类型的值(简称页访问异常错误码,errorCode)保存在中断栈中。

[提示]页访问异常错误码有32位。位0为1表示对应物理页不存在;位1为1表示写异常(比如写了只读页;位2为1表示访问权限异常(比如用户态程序访问内核空间的数据)

[提示] CR2是页故障线性地址寄存器,保存最后一次出现页故障的全32位线性地址。CR2用于发生页异常时报告出错信息。当发生页异常时,处理器把引起页异常的线性地址保存在CR2中。操作系统中对应的中断服务例程可以检查CR2的内容,从而查出线性地址空间中的哪个页引起本次异常。

产生页访问异常后,CPU硬件和软件都会做一些事情来应对此事。首先页访问异常也是一种异常,所以针对一般异常的硬件处理操作是必须要做的,即CPU在当前内核栈保存当前被打断的程序现场,即依次压入当前被打断程序使用的EFLAGS,CS,EIP,errorCode;由于页访问异常的中断号是0xE,CPU把异常中断号0xE对应的中断服务例程的地址(vectors.S中的标号vector14处)加载到CS和EIP寄存器中,开始执行中断服务例程。这时ucore开始处理异常中断,首先需要保存硬件没有保存的寄存器。在vectors.S中的标号vector14处先把中断号压入内核栈,然后再在trapentry.S中的标号__alltraps处把DS、ES和其他通用寄存器都压栈。自此,被打断的程序执行现场(context)被保存在内核栈中。接下来,在trap.c的trap函数开始了中断服务例程的处理流程,大致调用关系为:

trap—> trap_dispatch—>pgfault_handler—>do_pgfault

下面需要具体分析一下do_pgfault函数。do_pgfault的调用关系如下图所示:

图 do_pgfault的调用关系图

image

产生页访问异常后,CPU把引起页访问异常的线性地址装到寄存器CR2中,并给出了出错码errorCode,说明了页访问异常的类型。ucore OS会把这个值保存在struct trapframe 中tf_err成员变量中。而中断服务例程会调用页访问异常处理函数do_pgfault进行具体处理。这里的页访问异常处理是实现按需分页、页换入换出机制的关键之处。

ucore中do_pgfault函数是完成页访问异常处理的主要函数,它根据从CPU的控制寄存器CR2中获取的页访问异常的物理地址以及根据errorCode的错误类型来查找此地址是否在某个VMA的地址范围内以及是否满足正确的读写权限,如果在此范围内并且权限也正确,这认为这是一次合法访问,但没有建立虚实对应关系。所以需要分配一个空闲的内存页,并修改页表完成虚地址到物理地址的映射,刷新TLB,然后调用iret中断,返回到产生页访问异常的指令处重新执行此指令。如果该虚地址不在某VMA范围内,则认为是一次非法访问。