(本文部分参考了《Linux内核源代码情景分析》)
内核对页面异常处理时,是从 do_page_fault()开始的。本文主要介绍内核对页面异常的响应过程,也就是从发生异常至 CPU 到达do_page_fault()之间的那一段路程,以及从 do_page_fault()返回之后到 CPU 返回到用户空间这一段路程。
Linux 内核在初始化阶段完成了对页式虚存管理的初始化以后,便调用 trap_int()和 init_IRQ()两个函数进行中断机制的初始化。其中 trap_init()主要是对一些系统保留的中断向量的初始化,而 init_IRQ()则主要是用于外设的中断。在trap_init()中可清楚看到为页面异常设置的中断门指向程序入口 page_fault:
void __init trap_init(void)
{
...
set_trap_gate(12,&stack_segment);
set_trap_gate(13,&general_protection);
set_trap_gate(14,&page_fault);
set_trap_gate(15,&spurious_interrupt_bug);
...
}
当发生页面异常时,CPU 穿过中断门以后就直接到达了 page_fault()。 CPU 因异常而穿过中断门的过程,包括堆栈的变化,与因外设中断而引起的过程基本上是一样的。需要注意的是,异常在保护现场时还需要把出错代码压栈(但不是所有异常都产生出错代码)。代码如下:
/* 寻址的页不在内存,相应的页表项为空。或者违反了一种分页保护机制。 */
ENTRY(page_fault)
pushl $do_page_fault
jmp error_code
这里的跳转目标 error_code 就好像外设中断处理中的 common_interrupt 一样,是各种异常处理所共用的程序入口。而将服务程序 do_page_fault()的地址压入堆栈,则为具体的服务程序做好了准备。
程序入口 error_code 的代码如下:
error_code:
/* 保存高级C函数会用到的寄存器到栈中,请参见高级C函数的 */
pushl %ds
pushl %eax
xorl %eax, %eax
pushl %ebp
pushl %edi
pushl %esi
pushl %edx
decl %eax # eax = -1
pushl %ecx
pushl %ebx
cld /* cld指令清eflags方向标志DF,主要用于movs这样的指令。 */
执行到此处,我们把 CPU 此时的堆栈(左边)与 CPU 在外设中断时SAVE_ALL 以后的堆栈(右边)作一比较如下:
可是,下面会将堆栈中对应于 ORIG_EAX 位置上的内容转移到寄存器%esi 中,并将其替换成%eax 中的内容。这样一来,出错代码就到了%esi 中,而堆栈中的 ORIG_EAX 就变成了-1。同时,又以寄存器%ecx 的内容替换堆栈中 ES 处的函数指针,而把函数指针转移到寄存器%edi 中。在此之前已经将%es 的内容装入了%ecx,所以在此(movl %ecx, ES(%esp))以后,堆栈的内容于中断或系统调用时就完全一样了,只是 ORIG_EAX 的位置上为-1。这么一来,堆栈就调整好了。将来返回时在 RESTORE_ALL 中会把 ORIG_EAX 跳过去(中断一节中已经讲到)。代码如下:
movl %es, %ecx
movl ES(%esp), %edi /* 将esp+32处的高级C函数地址载入edi.*/
/**
* 将esp+36处的出错码复制到edx中,将将-1写到栈中。
* 请注意前面xorl %eax, %eax和decl %eax一句,它将eax中置-1
* 这样,如果是异常,这个位置就是-1,否则就表示是0x80系统调用。
*/
movl ORIG_EAX(%esp), %edx # get the error code
movl %eax, ORIG_EAX(%esp)
movl %ecx, ES(%esp)
/* 把用户数据段选择符复制以ds和es寄存器中。 */
movl $(__USER_DS), %ecx
movl %ecx, %ds
movl %ecx, %es
/**
* 把内核栈的当前栈顶复制到eax。这是一个内存单元的地址。
* 它的地址是最后被pushl到栈中的值的地址。通过这个地址就可以访问所有保存的寄存器了。
* 也就是pt_regs结构。
*/
movl %esp,%eax # pt_regs pointer
/**
* 调用高级C函数,被调用函数的参数是通过eax和edx传递。
* 请参见fastcall void do_page_fault(struct pt_regs *regs, unsigned long error_code)
* 其中的fastcall表示由寄存器传参,regs的值在eax,error_code的值在edx中。
*/
call *%edi
jmp ret_from_exception
对于不产生出错代码的异常在进入 error_code 之前会补上一个。请看,同一源文件( entry.S)中因协处理器( coprocessor)出错而导致的异常coprocessor_error:
ENTRY(coprocessor_error)
pushl $0
pushl $do_coprocessor_error
jmp error_code
这里多了一行“ pushl $0”,将0压入堆栈中与出错代码相应的地方,此后就都一样了。
从调用的函数,也就是 do_page_fault()返回以后, CPU 就转入 ret_from_exception。由于do_page_fault()的类型是 void,所以没有返回值。 ret_from_exception 的代码也在 entry.S 中:
ret_from_exception:
/**
* 在中断返回前,handle_IRQ_event调用local_irq_disable禁止了中断
* 所以在中断返回时,不用关中断,但是在异常返回时,需要将中断关闭。
*/
preempt_stop
如果没有软中断请求需要处理,就直接进入 ret_from_intr:
``ret_from_intr:
/**
把当前thread_info半截到ebp中。 */
GET_THREAD_INFO(%ebp)
/* 接下来判断EFLAGS和CS,确定是否运行在用户态,是否是VM模式。 */
movl EFLAGS(%esp), %eax # mix EFLAGS and CS
movb CS(%esp), %al
testl $(VM_MASK | 3), %eax
/**
* 如果是运行在内核态,并且不是VM模式,就跳到resume_kernel,
* 否则跳转到resume_userspace
*/
jz resume_kernel # returning to kernel or vm86-space
后面就与中断处理一致了。