概述
缺页异常是很常见的现象,但是其来源有两种,一种是真实的异常,这是由于内存访问的地址未分配并未映射而产生的访问了非法地址的情况;另外一种是虚拟内存已经分配出去了,但是实际上的物理内存并未映射分配而产生的缺页异常。这里主要分析后者,这是与内存管理相关的,前者是代码逻辑的问题。
根据惯例,先来了解一下异常。除了异常,还有中断,这二者通常是一起的。据《深入理解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表中找到相应表项。继而根据表项的设置进入到服务程序的入口,执行中断异常处理函数。
首先研究其初始化部分,异常中断是伴随着保护模式开启而同步设置的。【file:/arch/x86/kernel/head_32.s】
is486:
movl $0x50022,%ecx # set AM, WP, NE and MP
movl %cr0,%eax
andl $0x80000011,%eax # Save PG,PE,ET
orl %ecx,%eax
movl %eax,%cr0
lgdt early_gdt_descr
lidt idt_descr
ljmp $(__KERNEL_CS),$1f
该段代码的上下文就不详细分析了。主要看一下lidt指令的操作。lidt指令用于将一个表示描述表大小的16bit数据以及32bit的线性地址数据从指定空间中加载到IDTR寄存器中。注意这里用的是线性地址,此时已经是使能保护模式。
而idt_descr的定义:【file:/arch/x86/kernel/head_32.s】
idt_descr:
.word IDT_ENTRIES*8-1 # idt contains 256 entries
.long idt_table
# boot GDT descriptor (later on used by CPU#0):
.word 0 # 32 bit align gdt_desc.address
这是一个类似这样的结构:
struct
idt_descr{
short cnt;
void
*idt_table;
short reserve;
}
其中首个word字长的数据表示中断向量表项数量,而long字长的数据表示中断向量表地址,这是一个线性地址,最后的word字长的数据只是用来做数据对齐的。至于idt_table则是一个全局的数组定义。【file:/arch/x86/kernel/traps.c】
/* Must be page-aligned because the real IDT is used in a fixmap. */
gate_desc idt_table[NR_VECTORS] __page_aligned_bss;
该中断向量表在开启保护模式的时候,中断处理函数统一设置成ignore_int。具体实现:【file:/arch/x86/kernel/head_32.s】
__INIT
setup_once:
/*
* Set up a idt with 256 entries pointing to ignore_int,
* interrupt gates. It doesn't actually load idt - that needs
* to be done on each CPU. Interrupts are enabled elsewhere,
* when we can be relatively sure everything is ok.
*/
movl $idt_table,%edi
movl $early_idt_handlers,%eax
movl $NUM_EXCEPTION_VECTORS,%ecx
1:
movl %eax,(%edi)
movl %eax,4(%edi)
/* interrupt gate, dpl=0, present */
movl $(0x8E000000 + __KERNEL_CS),2(%edi)
addl $9,%eax
addl $8,%edi
loop 1b
movl $256 - NUM_EXCEPTION_VECTORS,%ecx
movl $ignore_int,%edx
movl $(__KERNEL_CS << 16),%eax
movw %dx,%ax /* selector = 0x0010 = cs */
movw $0x8E00,%dx /* interrupt gate - dpl=0, present */
2:
movl %eax,(%edi)
movl %edx,4(%edi)
addl $8,%edi
loop 2b
这里也不具体分析了。专注于缺页异常,那么接下来分析一下缺页异常初始化的地方。缺页异常初始化函数为early_trap_init(),在setup_arch()中调用。【file:/arch/x86/kernel/traps.c】
/* Set of traps needed for early debugging. */
void __init early_trap_init(void)
{
set_intr_gate_ist(X86_TRAP_DB, &debug, DEBUG_STACK);
/* int3 can be called from all */
set_system_intr_gate_ist(X86_TRAP_BP, &int3, DEBUG_STACK);
#ifdef CONFIG_X86_32
set_intr_gate(X86_TRAP_PF, page_fault);
#endif
load_idt(&idt_descr);
}
前二者set_intr_gate_ist()和set_system_intr_gate_ist()分别设置了调试和断点的中断处理,set_intr_gate()正好设置了缺页异常处理,最后通过load_idt()刷新中断向量表。
set_intr_gate()是一个宏定义。【file:/arch/x86/include/asm/desc.h】
/*
* This needs to use 'idt_table' rather than 'idt', and
* thus use the _nonmapped_ version of the IDT, as the
* Pentium F0 0F bugfix can have resulted in the mapped
* IDT being write-protected.
*/
#define set_intr_gate(n, addr)
do {
BUG_ON((unsigned)n > 0xFF);
_set_gate(n, GATE_INTERRUPT, (void *)addr, 0, 0,
__KERNEL_CS);
_trace_set_gate(n, GATE_INTERRUPT, (void *)trace_##addr,
0, 0, __KERNEL_CS);
} while (0)
它包含了两个动作,_set_gate()是用于设置中断向量,_trace_set_gate()的实现和_set_gate()一致,也是写中断向量,但是它写的是一个中断跟踪向量表trace_idt_table,写入处理函数为trace_page_fault(),用于中断向量跟踪用的。
具体分析一下_set_gate()的实现。【file:/arch/x86/include/asm/desc.h】
static inline void _set_gate(int gate, unsigned type, void *addr,
unsigned dpl, unsigned ist, unsigned seg)
{
gate_desc s;
pack_gate(&s, type, (unsigned long)addr, dpl, ist, seg);
/*
* does not need to be atomic because it is only done once at
* setup time
*/
write_idt_entry(idt_table, gate, &s);
write_trace_idt_entry(gate, &s);
}
该函数先是pack_gate()打包中断向量描述符,然后通过write_idt_entry()写入到中断向量表中。而write_trace_idt_entry()则是将相同的中断向量描述符写入到trace_idt_table中,它和_trace_set_gate()都是写入到trace_idt_table中,可能是先在当前优先将正确的中断处理使能之后,再将调试跟踪的进行设置,该点后面有空再深入。
接下来看一下pack_gate()的实现:【file:/arch/x86/include/asm/desc.h】
static inline void pack_gate(gate_desc *gate, unsigned char type,
unsigned long base, unsigned dpl, unsigned flags,
unsigned short seg)
{
gate->a = (seg << 16) | (base & 0xffff);
gate->b = (base & 0xffff0000) | (((0x80 | type | (dpl << 5)) & 0xff) << 8);
}
在保护模式下,中断向量表项由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()写入到中断向量表中。【file:/arch/x86/include/asm/desc.h】
#define write_idt_entry(dt, entry, g) native_write_idt_entry(dt, entry, g)
而native_write_idt_entry()实现更为简单,直接将中断描述符拷贝到向量表中。【file:/arch/x86/include/asm/desc.h】
static inline void native_write_idt_entry(gate_desc *idt, int entry, const gate_desc *gate)
{
memcpy(&idt[entry], gate, sizeof(*gate));
}
至此,中断异常的处理函数设置完毕。
最后
以上就是外向冰棍为你收集整理的Linux内存数据异常,Linux-3.14.12内存管理笔记【缺页异常处理(1)】的全部内容,希望文章能够帮你解决Linux内存数据异常,Linux-3.14.12内存管理笔记【缺页异常处理(1)】所遇到的程序开发问题。
如果觉得靠谱客网站的内容还不错,欢迎将靠谱客网站推荐给程序员好友。
发表评论 取消回复