相关学习资料
《深入理解计算机系统(原书第2版)》.pdf http://zh.wikipedia.org/zh/%E4%B8%AD%E6%96%B7 独辟蹊径品内核:Linux内核源代码导读 李云华著 中文 PDF版 https://www.kernel.org/ http://blog.csdn.net/orange_os/article/details/7485069 http://blog.csdn.net/sunnybeike/article/details/6958473
目录
1. 从异常控制流开始说起 2. 中断类型 3. 中断的初始化 4. 异常控制类型
1. 从异常控制流开始说起
0x1: 异常控制流简介
从给处理器加电开始,知道断电为止,程序计数器假设一个值的序列:
A0, A1, ...., An-1
其中,每个Ak是某个相应的指令Ik的"地址"。每次从Ak到Ak+1的过渡称为控制转移(control transfer)。这样的控制转移序列叫作处理器的控制流(flow of control或control flow)
控制流可以大致分为:
1. "平滑的"序列(即顺序执行) 其中每个Ik和Ik+1在存储器中都是相邻的,CPU按照计数器进行逐条指令的依次执行 2. 非平滑控制流 Ik+1与Ik不相邻,是由诸如跳转、调用、和返回这样一些程序指令造成的。这样一些指令都是必要的机制,使得程序能够对由程序变量表示的内部状态中的变化做出反应。
但是系统也必须能够对系统状态变化做出反应,这些系统状态不是被内部程序变量捕获的,而且也不一定和程序的执行相关。比如:
但是系统也必须能够对系统状态变化做出反应,这些系统状态不是被内部程序变量捕获的,而且也不一定和程序的执行相关。比如: 1. 一个硬件定时器定期产生信息,这个事件必须得到处理。 2. 数据包到达网络适配器后,必须存放在存储器中。 3. 程序向磁盘请求数据,然后休眠,知道被通知数据已就绪。 4. 当子进程终止时,创造这些子进程的父进程必须得到通知。
现代系统通过使控制流发生突变来对这些情况做出反应(也就是异常控制流)。一般而言,我们把这些突变称为异常控制流(Exceptional Control Flow ECF)。异常控制流发生在计算机系统的各个层次。比如:
1. 在硬件层,硬件检测到事件时,会触发控制,使CPU突然转移到异常处理程序。 2. 在操作系统层,内核通过上下文切换将控制从一个用户进程转移到另一个用户进程(分时间片执行)。 3. 在应用层,一个进程可以发送信号到另一个进程,而接收者会将控制突然转移到它的一个信号处理程序。一个程序可以通过回避通常的栈规则,并执行到其他函数中任意位置的非本地跳转来对错误做出反应。
0x2: 异常控制流处理机制和中断技术的关系
在继续深入学习之前,我们必须先理清一个基本概念:
异常控制流是操作系统中的一种控制流处理机制,异常控制流处理机制被用于实现操作系统中的CPU处理流程切换的实现,而中断技术是实现这一技术的方法。即中断是一个技术,而异常控制流处理是一种机制
2. 中断类型
中断是现代操作系统的一项重要技术,利用中断技术可以极大地提高系统吞吐量。从本质上理解,中断是CPU提供的一项硬件机制,CPU可以根据中断号跳转到相应的中断处理例程上去
0x1: 中断硬件实现
在硬件实现上,中断可以是:
1. 包含控制线路的独立系统(本文重点学习的) 在IBM个人机上,广泛使用可编程中断控制器(Programmable Interrupt Controller,PIC)来负责中断响应和处理。PIC被连接在若干中断请求设备(各种外设)和处理器(CPU)的中断引脚之间,从而实现对处理器中断请求线路(多为一针或两针)的复用 2. 被整合进存储器子系统中 作为另一种中断实现的形式,即存储器子系统实现方式,可以将中断端口映射到存储器的地址空间,这样对特定存储器地址的访问实际上是中断请求
0x2: 中断分类
从实现机制上来分,中断可以分为以下2类:
1. 外部中断(包括可屏蔽、不可屏蔽中断) 外部中断是由外部设备引发的中断,而引发中断的设备被称为中断源,中断源大致可以分为以下几种 1) 定时器、计时器 2) 键盘 3) 内部实时时钟 4) 通用接口 5) PS/2鼠标 6) 协处理器 7) IDE/SATE硬盘 8) 串口 9) 并口 10) 软盘 ... 外部设备通过"可编程中断控制器(Programmable Interrupt Controller,PIC)"向CPU报告的中断,大致流程如下: 1) 外部设备通过中断请求线(IRQ)连接到一个"中断控制器"上 2) 当一个外部设备需要发出中断时,会驱动对应的中断请求线进入有信号状态 3) 中断控制器检测这个中断是否被屏蔽了(CPU的IF位被置1,则不屏蔽任何外部中断;CPU的IF位被置0,则屏蔽所有外部中断),如果没有被屏蔽就驱动CPU的"INTR中断请求线"进入信号状态 4) CPU随后就能检测到这个中断了(在每次CPU周期的下降沿检测一次中断) 5) 如果该中断被屏蔽(CPU中的中断屏蔽寄存器被选中),中断控制器中的寄存器中的某一位位将记录这一请求,等到中断被开启时再驱动CPU的"INTR中断请求线"进入信号状态 6) 之后CPU通过"中断应答"从中断控制器的数据线上读取中断号,并通过中断号获取中断向量 7) 如果多个设备(外设中断源)在同一时刻通过不同的中断请求线发出中断请求,中断控制器也会将这些请求记录在不同的位中 8) 如果这些中断都没有被屏蔽,则中断控制器根据优先级,依次执行优先级高的中断(IRQn的数字n越小优先级越大) /* 关于中断屏蔽,这里需要补充几点 关闭外部中断的方式有: 1. 通过cli指令把标志寄存器中的IF位清零,这样就关闭了"所有的"外部中断 2. 通过中断控制器中的中断屏蔽寄存器,屏蔽某一特定的IRQn,从而屏蔽该中断(只是屏蔽某个中断,不影响其他中断) 3. 在很多外设上,也设有控制寄存器,可以通过外设的中断控制器从数据源上关闭外设上的某个IRQn,从而屏蔽某个中断 */ 2. 内部中断 和外部中断相对的就是内部中断。从CPU的角度看,外部中断是一个异步事件,它可能在任何时候发送,而内部中断是一个同步事件,它是执行某条指令时产生的。 内部中断可以大致分为以下几种 1) 异常(faults) CPU在指令执行时产生的,异常是可以修复的。当异常发生时,压入堆栈的是产生异常的"那条指令",当CPU执行异常处理程序结束后,将"重新执行那一条指令"。 1.1) 缺页异常: 14: #PF 1.2) 保护错误(内存或其他保护检查): 13: #GP 1.3) 堆栈段错误(堆栈操作或者加载SS): 12: #SS 1.4) 段不存在(加载段寄存器后访问段): 11: #NP 1.5) 除法错误(DIV/IDIV指令): 0: #DE 1.6) 越界: 5: #BR 1.7) 无效操作码(无效操作指令): 6: #UD 1.8) 对齐校验(内存访问): 17: #AC 2) 陷阱(traps) 在CPU执行陷阱指令后,立刻通过中断描述表执行预定的陷阱处理例程。陷阱处理例程执行结束后,将返回陷阱指令的"下一条指令"继续执行。 2.1) 系统调用(system call) 系统调用是一种软中断,软中断是一条CPU指令,用以自陷一个中断。由于软中断指令通常要运行一个切换CPU至内核态(Kernel Mode/Ring 0)的子例程,它常被用作实现系统调用(System call) 这是最长使用到的中断,我们在编程中使用到的API最终都会通过系统调用这种内部中断来进行ring3到ring0的切换 2.2) 单步异常(调试异常): 1: #DB 用于单步执行、内存断点 2.3) INT3(断点异常): 3: #BP 2.4) 溢出(指令INT0): 4: #OF 3) 终止(aborts) 3.1) 双重错误(所有能产生异常、NMI、或者INTR的指令): 8: #DF /* 关于中断号(向量号)、中断向量表、中断描述符表的区别 1. 中断号(向量号) 中断号(向量号)是用来在中断描述符表中定位中断描述符的 2. 中断描述符表 保存中断描述符的一段连续内存(可以理解为一张表)。中断描述符是用来获取中断向量用的(知道了中断向量就知道中断服务程序的入口地址) 3. 中断向量表 保存中断向量的一段连续内存(可以理解为一张表)。中断向量代表着中断服务程序的入口地址 例如: INT 21H 21就是中断号 21H就就是一个中断描述符 21H*4 =84H 得到的就是中断向量 以84H为首地址(84H 85H 86H 87H)其中存放的就是中断服务程序的地址 */
3. 中断的初始化
我们知道,中断向量号是8位的,那么它一共有256项(0-255),也就意味着中断描述符表也是256项(其中记录着中断向量表的表项索引),同时中断向量表也是256项(其中记录着中断处理例程的入口地址)
对于不同的中断,在中断初始化和中断处理过程中,其处理方式是不一样的
1. 内部中断(0~31号、0x80作为中断号) 只要初始化: 1) 相关的中断向量表 2. 外部中断(0~255中的除了0~31号、0x80的其他中断号) 需要初始化: 1) 相关的中断向量表 2) 以及中断控制器(控制器负责优先级排队、屏蔽等工作)
0x1: 内部中断初始化
内部中断的初始化需要对0~31号和0x80号系统保留中断向量的初始化,这部分草走在trap_init()中完成
\linux-3.15.5\arch\x86\kernel\traps
void __init trap_init(void) { int i; #ifdef CONFIG_EISA void __iomem *p = early_ioremap(0x0FFFD9, 4); if (readl(p) == ‘E‘ + (‘I‘<<8) + (‘S‘<<16) + (‘A‘<<24)) EISA_bus = 1; early_iounmap(p, 4); #endif /* trap_init()主要是调用set_xxx_gate(中断向量, 中断处理函数) set_xxx_gate()就是按照中断门的格式填写中断向量表的 Intel x86支持4种"门描述符": 1) 调用门(call gate) 2) 陷阱门(trap gate) 3) 中断门(iinterrupt gate) 4) 任务门(task gate) */ set_intr_gate(X86_TRAP_DE, divide_error); set_intr_gate_ist(X86_TRAP_NMI, &nmi, NMI_STACK); /* int4 can be called from all */ set_system_intr_gate(X86_TRAP_OF, &overflow); set_intr_gate(X86_TRAP_BR, bounds); set_intr_gate(X86_TRAP_UD, invalid_op); set_intr_gate(X86_TRAP_NM, device_not_available); #ifdef CONFIG_X86_32 set_task_gate(X86_TRAP_DF, GDT_ENTRY_DOUBLEFAULT_TSS); #else set_intr_gate_ist(X86_TRAP_DF, &double_fault, DOUBLEFAULT_STACK); #endif set_intr_gate(X86_TRAP_OLD_MF, coprocessor_segment_overrun); set_intr_gate(X86_TRAP_TS, invalid_TSS); set_intr_gate(X86_TRAP_NP, segment_not_present); set_intr_gate_ist(X86_TRAP_SS, &stack_segment, STACKFAULT_STACK); set_intr_gate(X86_TRAP_GP, general_protection); set_intr_gate(X86_TRAP_SPURIOUS, spurious_interrupt_bug); set_intr_gate(X86_TRAP_MF, coprocessor_error); set_intr_gate(X86_TRAP_AC, alignment_check); #ifdef CONFIG_X86_MCE set_intr_gate_ist(X86_TRAP_MC, &machine_check, MCE_STACK); #endif set_intr_gate(X86_TRAP_XF, simd_coprocessor_error); /* Reserve all the builtin and the syscall vector: */ for (i = 0; i < FIRST_EXTERNAL_VECTOR; i++) set_bit(i, used_vectors); #ifdef CONFIG_IA32_EMULATION set_system_intr_gate(IA32_SYSCALL_VECTOR, ia32_syscall); set_bit(IA32_SYSCALL_VECTOR, used_vectors); #endif //设置系统调用中断 #ifdef CONFIG_X86_32 set_system_trap_gate(SYSCALL_VECTOR, &system_call); set_bit(SYSCALL_VECTOR, used_vectors); #endif /* * Set the IDT descriptor to a fixed read-only location, so that the * "sidt" instruction will not leak the location of the kernel, and * to defend the IDT against arbitrary memory write vulnerabilities. * It will be reloaded in cpu_init() */ __set_fixmap(FIX_RO_IDT, __pa_symbol(idt_table), PAGE_KERNEL_RO); idt_descr.address = fix_to_virt(FIX_RO_IDT); /* * Should be a barrier for any external CPU state: */ cpu_init(); x86_init.irqs.trap_init(); #ifdef CONFIG_X86_64 memcpy(&debug_idt_table, &idt_table, IDT_ENTRIES * 16); set_nmi_gate(X86_TRAP_DB, &debug); set_nmi_gate(X86_TRAP_BP, &int3); #endif }
0x2: 外部中断初始化
外部中断的初始化需要:
1. 对除了0~31、0x80中断号之外的其它中断向量 2. 中断控制器的初始化(相比内部中断初始化多了这一步)这两步操作都在在init_IRQ()中完成
\linux-3.15.5\arch\x86\kernel\i8259.c
void __init init_IRQ(void) { int i; /* * We probably need a better place for this, but it works for * now ... */ x86_add_irq_domains(); /* * On cpu 0, Assign IRQ0_VECTOR..IRQ15_VECTOR‘s to IRQ 0..15. * If these IRQ‘s are handled by legacy interrupt-controllers like PIC, * then this configuration will likely be static after the boot. If * these IRQ‘s are handled by more mordern controllers like IO-APIC, * then this vector space can be freed and re-used dynamically as the * irq‘s migrate etc. */ for (i = 0; i < legacy_pic->nr_legacy_irqs; i++) //对于单CPU结构, per_cpu(vector_irq, 0)[IRQ0_VECTOR + i] = i; //x86_init.irqs.intr_init()等价于调用:native_init_IRQ() x86_init.irqs.intr_init(); } void __init native_init_IRQ(void) { int i; /* Execute any quirks before the call gates are initialised: */ x86_init.irqs.pre_vector_init(); //调用 init_ISA_irqs apic_intr_init(); /* * Cover the whole vector space, no vector can escape * us. (some of these will be overridden and become * ‘special‘ SMP interrupts) */ /* interrupt数组,它保存的是每个中断服务程序的入口地址,它的定义是在\linux-3.15.5\arch\x86\kernel\entry_32.S中 */ for (i = FIRST_EXTERNAL_VECTOR; i < NR_VECTORS; i++) { //设置32~255号中断 /* IA32_SYSCALL_VECTOR could be used in trap_init already. */ if (!test_bit(i, used_vectors)) { //要除去0x80中断 set_intr_gate(i, interrupt[i-FIRST_EXTERNAL_VECTOR]); } } if (!acpi_ioapic && !of_ioapic) setup_irq(2, &irq2); #ifdef CONFIG_X86_32 /* * External FPU? Set up irq13 if so, for * original braindamaged IBM FERR coupling. */ if (boot_cpu_data.hard_math && !cpu_has_fpu) setup_irq(FPU_IRQ, &fpu_irq); irq_ctx_init(smp_processor_id()); #endif }
4. 异常控制类型
我们已经学习了中断有内部和外部中断,外部中断来自于硬件外设,内部中断又可以分为异常(faults)、陷阱(traps)、终止(aborts)。我们需要牢记的是,操作系统是不能执行命令的,整个机器中可以执行命令的只有是CPU,CPU通过硬件的方式提供中断机制,中断是所有异常处理的根本技术。不管是进程切换、硬件外设、除零异常、虚拟内存中的发生的缺页异常处理、SEH,归根结底,全部都要通过CPU的中断机制来实现。
异常控制流处理就是我们在中断的基础上,抽象出的一个理论性概念
异常(exception)就是控制流中的突变,用来响应处理器状态中的某些变化。在处理器中,状态被编码为不同的位和信号。状态变化称为事件(event)。
我们在学习异常处理流程的时候,要注意不要把异常和中断割裂成2个独立的概念去学习,相反,异常和中断说的都是一回事。只是一个从操作系统概念层面去阐述,一个从硬件技术原理角度去阐述
在任何情况下,当处理器检测到有事件(内部异常、外部异常)发生时,它就会通过一张叫做异常表(exception table)的跳转表(中断描述符表),进行一个间接过程调用(异常处理例程的调用)(通过中断向量表),到一个专门用来处理这类事件的操作系统子程序(异常处理程序 exception handler)。进行相应的异常处理
0x1: 异常的分类
异常可以分为四类:
1. 中断(interrupt) 来自I/O设备的信号,异步,总是返回到下一条指令 2. 陷阱(trap) 有意的异常,是执行一条指令的结果。陷阱最重要的用途是在用户程序和内核之间提供一个像过程一样的接口,叫系统调用 同步, 总是返回到下一条指令 3. 故障(fault) 潜在可恢复的错误。如果处理程序能够修正这个错误情况,它就将控制返回到引起故障的指令,从而重新执行它。斗则,处理程序返回到内核中的abort例程,abort例程会终止引起故障的应用程序。 同步,可能返回到当前指令 4. 终止(abort) 不可恢复的错误,同步,不会返回
Copyright (c) 2014 LittleHann All rights reserved
Linux中断技术、异常控制技术总结归类