人生像是在跑马拉松,能够完赛的都是不断地坚持向前迈进;人生就是像在跑马拉松,不断调整步伐,把握好分分秒秒;人生还是像在跑马拉松,能力决定了能跑短程、半程还是全程。人生其实就是一场马拉松,坚持不懈,珍惜时间。
分类: LINUX
2017-06-18 15:57:06
缺页异常是很常见的现象,但是其来源有两种,一种是真实的异常,这是由于内存访问的地址未分配并未映射而产生的访问了非法地址的情况;另外一种是虚拟内存已经分配出去了,但是实际上的物理内存并未映射分配而产生的缺页异常。这里主要分析后者,这是与内存管理相关的,前者是代码逻辑的问题。
根据惯例,先来了解一下异常。除了异常,还有中断,这二者通常是一起的。据《深入理解Linux内核》的描述,中断通常分为同步中断和异步中断,而二者定义:同步中断是当指令执行时由CPU控制单元产生的,之所以称为同步,是因为只有在一条指令终止执行后CPU才会发出中断;异步中断是由其他硬件设备依照CPU时钟信号随机产生的。而在Intel微处理器手册中,将同步和异步中断分别称之为异常和中断。
其中中断分为:可屏蔽中断,通过INTR引脚获取信号;不可屏蔽中断,通过NMI引脚获取信号。而异常是由处理器检测出来的,分为:错误、陷阱和中止。
具体异常分类:
1) 错误异常上报于指令引起异常前,上报的同时保存环境,以便于恢复执行环境;80386认为故障是可排除的,进入故障处理程序,所保存的断点CS及EIP的值指向引起故障的指令。故障排除后,执行IRET返回到引起故障的程序继续执行,引起故障的地方继续执行,重新执行不需要操作系统额外参与。接下来详细分析的缺页异常就属于错误异常。
2) 陷阱异常是在引起异常的指令后,把异常情况通知系统。进入异常处理程序时,所保存的断点CS及EIP的值指向引起陷阱的指令的下一条要执行的指令。下一条要执行的指令并非下一条指令。因此不能够反推。常见的陷阱异常有软中断指令和单步异常。(注:软中断有时候又被称之为编程异常)
3) 中止异常不能够确定引起该异常的指令也不能恢复引起异常的该程序正常执行。中止被用来上报服务错误,如硬件错误或系统表中出现非法或不一致值。
但是无论是中断还是异常,Intel通过8bit的位于[0,255]范围内的无符号整数为之一一编码标识,该标识称之为向量。
向量范围 |
向量说明 |
|
0-19 |
0x0-0x13 |
非屏蔽中断和异常 |
20-31 |
0x14-0x1f |
Intel保留 |
32-47 |
0x20-0x2f |
可屏蔽外设中断 |
48-127 |
0x30-0x7f |
外部中断 |
128 |
0x80 |
用于系统调用的可编程异常 |
129-238 |
0x81-0xee |
外部中断 |
239 |
0xef |
本地APIC时钟中断 |
240 |
0xf0 |
本地APIC高温中断 |
241-250 |
0xf0-x0fa |
由Linux留作将来使用 |
251-253 |
0xfb-0xfd |
处理器间中断 |
254 |
0xfe |
本地APIC错误中断 |
255 |
0xff |
本地APIC伪中断 |
中断异常发生后的系统运行流程大致如下:
产生中断异常后,CPU从中断控制器中取得中断向量,接着根据中断向量从IDT表中找到相应表项。继而根据表项的设置进入到服务程序的入口,执行中断异常处理函数。
首先研究其初始化部分,异常中断是伴随着保护模式开启而同步设置的。
该段代码的上下文就不详细分析了。主要看一下lidt指令的操作。lidt指令用于将一个表示描述表大小的16bit数据以及32bit的线性地址数据从指定空间中加载到IDTR寄存器中。注意这里用的是线性地址,此时已经是使能保护模式。
而idt_descr的定义:
这是一个类似这样的结构:
struct idt_descr{
short cnt;
void *idt_table;
short reserve;
}
其中首个word字长的数据表示中断向量表项数量,而long字长的数据表示中断向量表地址,这是一个线性地址,最后的word字长的数据只是用来做数据对齐的。至于idt_table则是一个全局的数组定义。
该中断向量表在开启保护模式的时候,中断处理函数统一设置成ignore_int。具体实现:
这里也不具体分析了。专注于缺页异常,那么接下来分析一下缺页异常初始化的地方。缺页异常初始化函数为early_trap_init(),在setup_arch()中调用。
前二者set_intr_gate_ist()和set_system_intr_gate_ist()分别设置了调试和断点的中断处理,set_intr_gate()正好设置了缺页异常处理,最后通过load_idt()刷新中断向量表。
set_intr_gate()是一个宏定义。
它包含了两个动作,_set_gate()是用于设置中断向量,_trace_set_gate()的实现和_set_gate()一致,也是写中断向量,但是它写的是一个中断跟踪向量表trace_idt_table,写入处理函数为trace_page_fault(),用于中断向量跟踪用的。
具体分析一下_set_gate()的实现。
该函数先是pack_gate()打包中断向量描述符,然后通过write_idt_entry()写入到中断向量表中。而write_trace_idt_entry()则是将相同的中断向量描述符写入到trace_idt_table中,它和_trace_set_gate()都是写入到trace_idt_table中,可能是先在当前优先将正确的中断处理使能之后,再将调试跟踪的进行设置,该点后面有空再深入。
接下来看一下pack_gate()的实现:
在保护模式下,中断向量表项由8字节主城,其中的每个表项称之为一个门描述符,“门”的含义是当中断发生时必须先通过这些门,然后才能进入相应的处理程序。所以该函数起名为pack_gate(),“门”打包,其具体填充的数据如图。
对于x86处理器,Intel把中断描述符(即“门”)分三类:任务门、中断门、陷阱门,而Linux则分成五类:
中断门:Intel的中断门,DPL = 0,描述中断处理程序,通过set_intr_gate宏设置;
系统门:Intel的陷阱门,DPL = 3,用于系统调用,通过set_system_gate宏设置;
系统中断门:Intel的中断门,DPL = 3,用于向量3的异常处理,通过set_system_intr_gate宏设置;
陷阱门:Intel陷阱门,DPL = 0,大部分的异常处理,通过set_trap_gate宏设置;
任务门:Intel任务门,DPL = 0,对”Double fault“异常处理,通过set_task_gate宏设置;
打包好“门”后,通过write_idt_entry()写入到中断向量表中。
而native_write_idt_entry()实现更为简单,直接将中断描述符拷贝到向量表中。
至此,中断异常的处理函数设置完毕。