前言
中断通常被定义为一个事件,该事件改变处理器执行的指令顺序。这样的事件与CPU芯片内外部硬件电路产生的电信号相对应。
中断通常分为同步(synchronous)中断和异步(asynchronous)中断:
- 同步中断是当指令执行时由CPU控制单元产生的,之所以称为同步,是因为只有在一条指令终止执行后CPU才会发出中断。
- 异步中断是由其他硬件设备依照CPU时钟信号随机产生的。
在Intel微处理器手册中,把同步和异步中断分别称为异常(exception)和中断(interrupt)
中断是由间隔定时器和I/O设备产生的,例如,用户的一次按键会引起一个中断。
异常是由程序的错误产生的,或者是由内核必须处理的异常条件产生的。第一种情况下,内核通过发送一个信号来处理异常,例如SIGTERM、SIGINT
等。第二种情况下,内核执行恢复异常需要的所有步骤,例如缺页,或对内核服务的一个请求。
中断信号的作用
中断信号提供了一种特殊的方式,使得处理器转而去运行正常控制流之外的代码。当一个中断信号到达时,CPU必须停止它当前所做的事情,并且切换到一个新的活动,为了做到这一点,就要在内核态堆栈保存程序计数器的当前值(即eip和cs寄存器的内容),并把与中断类型相关的一个地址放进程序计数器。
必须要声明的是,中断处理与进程切换有一个明显的差异:由中断或异常处理程序执行的代码不是一个进程。更确切地说,它是一个内核控制路径,代表中断发生时正在运行的进程执行,作为一个内核控制路径,中断处理程序比一个进程要“轻”(这是因为中断的上下文很少,建立或终止中断处理需要的事件很少)
中断处理是由内核执行的最敏感的任务之一,它必须满足以下约束
- 内核的目标就是让中断尽可能快的处理完,尽其所能把更多的处理向后推迟,例如,假设一个数据块已经到达了网线,当硬件中断内核时,内核只简单的标志数据到来了,让处理器恢复到它以前运行的状态,其余的处理稍后再进行(如把数据移入一个缓冲区,它的接收进程可以在缓冲区找到数据并恢复这个进程的执行)。因此,内核响应中断后需要进行的操作分为两部分:对于关键紧急的部分,内核立即执行;其余部分内核随后执行。
- 因为中断随时会到来,所以内核可能正在处理其中一个中断时,另一个中断又发生了,应该尽可能多的允许这种情况发生,因为这能维持更多的I/O设备处于忙状态。因此,中断处理程序必须编写成使相应的内核控制路径能以嵌套的方式执行。当最后一个内核控制路径终止时,内核必须能恢复被中断进程的执行,或者,如果中断信号已导致了重新调度,内核能切换到另外的进程。
- 尽管内核在处理前一个中断时可以接受一个新的中断,但在内核代码中还是存在一些临界区,在临界区中,中断必须被禁止。必须尽可能地限制这样的临界区,因为根据以前的要求,内核尤其是中断处理程序,应该在大部分时间内以开中断的方式运行。
中断和异常
中断
- 可屏蔽中断:I/O设备发出的所有中断请求都产生可屏蔽中断。可屏蔽中断可以处于两种状态:屏蔽的(masked)或非屏蔽的(unmasked),一个屏蔽的中断只要还是屏蔽的,控制单元就忽略它。
- 非屏蔽中断:只有几个危机事件(如硬件故障)才引起非屏蔽中断,非屏蔽中断总是由CPU辨认
异常
- 处理器探测异常:当CPU执行指令时探测到的一个反常条件所产生的异常。可以进一步分为三组。这取决于CPU控制单元产生异常时保存在内核态堆栈
eip
寄存器中的值- 故障(fault):通常可以纠正,一旦纠正,程序就可以在不失连贯性的情况下重新开始。保存在
eip
中的值是引起故障的指令地址,因此,当异常处理程序终止时,那条指令会被重新执行。 - 陷阱(trap):在陷阱指令执行后立即报告,内核把控制权返回给程序后就可以继续执行它的执行而不失连贯性,保存在
eip
中的值是一个随后要执行的指令地址。只有当没有必要重新执行已终止的指令时,才触发陷阱,陷阱的主要用途是为了调试程序。在这种情况下,中断信号的作用是通知调试程序一条特殊指令已被执行(例如到了一个程序的断点)。一旦用户检查到调试程序所提供的数据,他就可能要求被调试程序从下一条指令重新开始执行。 - 异常中止(abort):发生一个严重的错误,控制单元出了问题,不能在
eip
寄存器中保存引起异常的指令所在的确切位置。异常中止用于报告严重的错误,如硬件故障或系统表中无效的值或不一致的值。由控制单元发生的这个中断信号是紧急信号,用来把控制权切换到相应的异常中止处理程序,这个异常中止处理程序除了强制受影响的进程终止外,没有别的选择。
- 故障(fault):通常可以纠正,一旦纠正,程序就可以在不失连贯性的情况下重新开始。保存在
- 编程异常:在编程者发出请求时发生。是由
int
或int3
指令触发的,当into
(检查溢出)和bound
(检查越界)指令检查的条件不为真时,也引起编程异常。控制单元把编程异常作为陷阱来处理,编程异常通常也叫做软中断。这样的异常有两种常用的用途:执行系统调用及给调试程序通报一个特定的事件。
每个中断和异常是由0~255之间的一个数来标识,因为一些未知的原因,Intel把这个8位的无符号整数叫做一个向量(vector)。非屏蔽中断的向量和异常的向量是固定的。而可屏蔽中断的向量可以通过对中断控制器的编程来改变。
IRQ和中断
每个能够发出中断请求的硬件设备控制器都有一条名为IRQ(Interrupt ReQuest)的输出线,所有现有的IRQ线都与一个名为可编程中断控制器(Programmable Interrupt Controller,PIC)的硬件电路的输入引脚相连,可编程中断控制器执行下列动作
- 监视IRQ线,检查产生的信号(raised signal)。如果有条或两条以上的IRQ线上产生信号,就选择引脚编号较小的IRQ线,
- 如果一个引发信号出现在IRQ线上
- 把接收到的引发信号转换成对应的向量
- 把这个向量存放在中断控制器的一个I/O端口,从而允许CPU通过数据总线读此向量
- 把引发信号发送到处理器的INTR引脚,即产生一个中断
- 等待,直到CPU通过把这个中断信号写进可编程中断控制器的一个I/O端口来确认它,当这种情况发生时,清INTR线。
- 返回到监视IRQ线操作
IRQ线是从0开始顺序编号的,因此,第一条IRQ线通常表示为IRQ0,与IRQn关联的Intel的缺省向量是n+32。如前所述,通过向中断控制器端口发布合适的指令,就可以修改IRQ和向量之间的映射。
可以有选择地禁止每条IRQ线。因此,可以对PIC编程从而禁止IRQ,也就是说,可以告诉PIC停止对给定的IRQ线发布中断,或者激活它们。禁止的中断是丢失不了的,它们一旦被激活,PIC就又把它们发送到CPU。这个特点被大多数中断处理程序使用,因为这允许中断处理程序逐次地处理同一类型的IRQ。
高级可编程中断控制器
如果系统只有一个单独的CPU,那么主PIC的输出线可以直截了当的连接到CPU的INTR引脚。然而,如果系统中包含两个或多个CPU,那么这种方式不再有效,因而需要更复杂的PIC。
Intel从Pentiun III
开始引入了一种名为I/O高级可编程控制器(APIC)的新组件。此外,80x86
微处理器当前所有的CPU都含有一个本地APIC
。每个本地APIC
都有32位的寄存器、一个内部时钟、一个本地定时设备及为本地APIC
中断保留的两条额外的IRQ线LINR0
和LINT1
。所有本地APIC
都连接到一个外部I/O APIC
,形成一个多APIC
的系统。
异常
80x86
微处理器发布了大约20种不同的异常。内核必须为每种异常提供一个专门的异常处理程序。对于某些异常,CPU控制单元在开始执行异常处理程序前会产生一个硬件出错码,并且压入内核态堆栈。
在80x86
处理器中可以找到的异常的向量、名字、类型及其描述如下
- 0 “Divide error”(故障):当一个程序试图执行整数被0除操作时产生
- 1 “Debug”(陷阱或故障):设置
eflags
的TF标志时或一条指令或操作数的地址落在一个活动debug寄存器的范围之内时 - 2 未用 :为非屏蔽中断保留(利用NMI引脚的那些中断)
- 3 “Breakpoint” (陷阱):由
int3
(断点)指令引起 - 4 “Overflow”(陷阱):当
eflags
的OF标志被设置时,into
(检查溢出)指令被执行 - 5 “Bounds check”(故障):对于有效抵制范围之外的操作数,bound(检查地址边界)指令被执行
- 6 “Invalid opcode”(故障):CPU执行单元检测到一个无效的操作码
- 7 “Device not available”(故障):随着
cr0
的TS标志被设置,ESCAPE、MMX或XMM指令被执行 - 8 “Double fault”(异常中止):正常情况下,当CPU试图为前一个异常调用处理程序时,同时又检测到一个异常,两个异常能被串行地处理。然而,在少数情况下,处理器不能串行地处理它们,因而产生这种异常。
- 9 “Coprocessor segment overrun”(异常中止):因外部的数学斜处理器引起的问题
- 10 “Invalid TSS”(故障):CPU试图让一个上下文切换到有无效的TSS的进程
- 11 “Segment not present”(故障):引用一个不存在的内存段
- 12 “Stack segment fault”(故障):试图超过栈段界限的指令,或者由ss标识的段不在内存
- 13 “General protection”(故障):违反了
80x86
保护模式下的保护规则之一 - 14 “Page fault”(故障):寻址的页不在内存,相应的页表项为空,或者违反了一种分页保护机制
- 15 由Intel保留
- 16 “Floating point error”(故障):集成到CPU芯片中的浮点单元用信号通知一个错误情形,如数字溢出,或被0除
- 17 “Alignment check”(故障):操作数的地址没有被正确的对齐
- 18 “Machine check”(异常中止):机器检查机制检测到一个CPU错误或总线错误
- 19 “SIMD floating point exception”(故障):集成到CPU芯片中的SSE或SSE2单元对浮点操作用信号通知一个错误情形
- 20~31这些值由Intel留作将来开发
编号 | 异常 | 异常处理程序 | 信号 |
---|---|---|---|
0 | Divide error | divide_error() | SIGFPE |
1 | Debug | debug() | SIGTRAP |
2 | NMI | nmi() | None |
3 | Breakpoint | int3() | SIGTRAP |
4 | Overflow | overflow() | SIGSEGV |
5 | Bounds check | bounds() | SIGSEGV |
6 | Invalid opcode | invalid_op() | SIGILL |
7 | Device not available | device_not_available() | None |
8 | Double fault | doublefault_fn() | None |
9 | Coprocessor segment overrun | coprocessor_segment_overrun() | SIGFPE |
10 | Invalid TSS | invalid_tss() | SIGSEGV |
11 | Segment not present | segment_not_present() | SIGBUS |
12 | Stack exception | stack_segment() | SIGBUS |
13 | General protection | general_protection() | SIGSEGV |
14 | Page fault | page_fault() | SIGSEGV |
15 | Intel reserved | None | None |
16 | Floating point error | coprocessor_error() | SIGFPE |
17 | Alignment check | alignment_check() | SIGSEGV |
18 | Machine check | machine_check() | None |
19 | SIMD floating point | simd_coprocessor_error() | SIGFPE |
中断描述符表
中断描述符表(Interrupt Descriptor Table,IDT)是一个系统表,它与每一个中断或异常向量相联系,每一个向量在表中有相应的中断或异常处理程序的入口地址。内核在允许中断发生前,必须适当的初始化IDT
IDT包含三种类型的描述符,下图显示了每种描述符中的64位的含义,值得注意的是,在40~43位的Type字段的值表示描述符的类型。
- 任务门(task gate):当中断信号发生时,必须取代当前进程的那个进程的TSS选择符放在任务门中
- 中断门(interrupt gate):包含段选择符和中断或异常处理程序的段内偏移量。当控制权转移到一个适当的段时,处理器清IF标志,从而关闭将来会发生的可屏蔽中断
- 陷阱门(Trap gate):与中断门相似,只是控制权传递到一个适当的段时处理器不修改IF标志
异常处理
CPU产生的大部分异常都由Linux解释为出错条件,当其中一个异常发生时,内核就向引起异常的进程发送一个信号向它通知一个反常条件,例如,如果进程执行了一个被0除的操作,CPU就产生一个”Divide error”异常,并由相应的异常处理程序向当前进程发送一个SIGFPE信号,这个进程将采取若干必要的步骤来恢复或者中止运行。
异常处理程序有一个标准的结构,由以下三部分组成
- 在内核堆栈中保存大多数寄存器的内容
- 用高级的C函数处理异常
- 通过
ret_from_exception()
函数从异常程序退出
为了利用异常,必须对IDT进行适当的初始化,使得每个被确认的异常都有一个异常处理程序。trap_init()
函数的工作时将一些最终值(即处理异常的函数)插入到IDT的非屏蔽中断及异常表项中,这是由函数set_trap_gate()
、set_intr_gate()
、set_system_intr_gate()
和set_task_gate()
来完成的
中断处理
中断处理依赖于中断类型,目前主要有三种主要的中断类型。
- I/O中断:某些I/O设备需要关注,相应的中断处理程序必须查询设备以确定适当的操作过程
- 时钟中断:某些时钟产生一个中断,这种中断告诉内核一个固定的时间间隔已经过去
- 处理器中断:多处理器系统中一个CPU对另一个CPU发出一个中断
I/O中断处理
中断处理程序的灵活性是以两种不同的方式实现的
- IRQ共享:中断处理程序执行多个中断服务例程(interrupt service routine,ISR),每个ISR是一个与单独设备(共享IRQ线)相关的函数。因为不可能预先知道哪个特定的设备产生IRQ,因此,每个ISR都被执行,以验证它的设备是否需要关注,如果是,当设备产生中断时,就执行需要执行的所有操作。
- IRQ动态分配:一条IRQ线在可能的最后时刻才与一个设备驱动程序相关联,例如,软盘设备的IRQ线只有在用户访问软盘设备时才被分配。这样,即使几个硬件设备并不共享IRQ线,同一个IRQ向量也可以由这几个设备在不同时刻使用。
当一个中断发生时,并不是所有的操作都具有相同的急迫性。事实上,把所有的操作都放进中断处理程序本身并不合适,需要时间长的、非重要的操作应该推后,因为当一个中断处理程序正在运行时,相应的IRQ线上发出的信号就被暂时忽略。更重要的是,中断处理程序是代表进程执行的,它所代表的进程必须总处于TASK_RUNNING
状态,否则,就可能出现系统僵死情形。因此,中断处理程序不能执行任何阻塞过程。如磁盘I/O操作,因此Linux把紧随中断要执行的操作分为三类
- 紧急的(Critical):这样的操作诸如:对PIC应答中断,对PIC或设备控制器重编程,或者修改由设备和处理器同时访问的数据结构,这些都能被很快地执行,而之所以说它们是紧急的是因为他们必须被尽快的执行。紧急操作要在一个中断处理程序内立即执行,而且是在禁止可屏蔽中断的情况下。
- 非紧急的(Noncritical):这样的操作诸如修改那些只有处理器才会访问的数据结构,这些操作也要很快地完成,因此,它们由中断处理程序立即执行,但必须是在开中断的情况下。
- 非紧急可延迟的(Noncritical deferrable):这样的操作例如把缓冲区的内容拷贝到某个进程的地址空间。这些操作可能被延迟较长的时间间隔而不影响内核操作。
不管引起中断的电路种类如何,所有的I/O中断处理程序都执行四个相同的基本操作
- 在内核态堆栈中保存IRQ的值和寄存器的内容
- 为正在给IRQ线服务的PIC发送一个应答,这将允许PIC进一步发出中断
- 执行共享这个IRQ的所有设备的中断服务例程(ISR)
- 跳到
ret_from_intr()
的地址后终止
Linux中的中断向量
向量范围 | 用途 |
---|---|
0~19(0x0~0x13) | 非屏蔽中断和异常 |
20~31(0x14~0x1f) | Intel保留 |
32~127(0x20~0x7f) | 外部中断(IRQ) |
128(0x80) | 用于系统调用的可编程异常 |
129~238(0x81~0xee) | 外部中断(IRQ) |
239(0xef) | 本地APIC时钟中断 |
240(0xf0) | 本地APIC高温中断 |
241~250(0xf0~0xfa) | 由Linux留作将来使用 |
251~253(0xfb~0xff) | 处理器间中断 |
254(0xfe) | 本地APIC错误中断 |
255(0xff) | 本地APIC伪中断 |