中断通常被定义为一个事件,该事件改变处理器执行的指令顺序。这样的事件与CPU芯片内外部硬件电路产生的电信号相对应。
中断通常分为同步中断和异步中断:
(1) 同步中断是当指令执行时由CPU控制单元产生的,之所以称为同步,是因为只有在一条指令终止执行后CPU才会发出中断。
(2) 异步中断是由其它硬件设备依照CPU时钟信号随机产生的。
在Intel微处理器手册中,把同步和异步中断分别称为异常和中断。
我们也采用这种分类,当然有时我们也用术语“中断信号”指这两种类型。
中断是由间隔定时器和I/O设备产生的,例如,用户的一次按键会引起一个中断。
另一方面,异常是由程序的错误产生的,或者是由内核必须处理的异常条件产生的。第一种情况下,内核通过发送一个每个Unixt程序员都熟悉的信号来处理异常。第二种情况下,内核执行恢复异常需要的所有步骤,例如缺页,或对内核服务的一个请求。
我们在下一节描述引入信号的动机,以此开始进行学习。然后,说明由I/O设备产生的著名IRQ如何引起中断,我们将详细讨论80x86微处理器如何在硬件级处理中断和异常。接下来,我们将在”初始化中断描述表”一节阐明Linux如何初始化Intel中断结构必需的所有数据结构。剩余的3节描述Linux如何在软件级处理中断信号。
继续进行学习之前,需要值得注意的是:我们在本章仅涉及对所有PC都通用的“经典”中断,而不涉及一些体系的非标准中断。
中断信号的作用
顾名思义,中断信号提供了一种特殊的方式,使处理器转而去运行正常控制流之外的代码。当一个中断信号达到时,CPU必须停止它当前正在做的事情,并且切换到一个新的活动。为了做到这一点,就要在内核态堆栈保存程序计数器的当前值,并把与中断类型相关的一个地址放进程序计数器。
在本章,有些事情会使你想起在前一章描述的上下文切换,这发生在内核用一个进程替换另一进程时,但是,中断处理与进程切换有一个明显的差异:由中断或异常处理程序执行的代码不是一个进程。更确切地说,它是一个内核控制路径,代表中断发生时正在运行的进程执行。作为一个内核控制路径,中断处理程序比一个进程要轻。
中断处理是由内核执行的最敏感的任务之一,因为它必须满足下列约束:
(1) 当内核正打算去完成一些别的事情时,中断随时会到来。因此,内核的目标就是让中断尽可能快地处理完,尽其所能把更多的处理向后推迟。例如,假设一个数据已到达了网线,当硬件中断内核时,内核只简单地标志数据到来了,让处理器恢复到它以前运行的状态,其余的处理稍后再进行。因此,内核响应中断后需要进程的操作分为两部分:关键而紧急的部分,内核立即执行;其余推迟的部分,内核随后执行,
(2) 因为中断随时会到来,所以内核可能正在处理其中的一个中断时,另一个中断又发生了。应该尽可能多地允许这种情况发生,因为这能维持更多的I/O设备处于忙状态。因此,中断处理程序必须编写成使相应的内核控制路径能以嵌套的方式执行。当最后一个内核控制路径终止时,内核必须能恢复被中断进程的执行,或者,如果中断信号已导致了重新调度,内核能切换到另外的进程。
(3) 尽管内核在处理前一个中断时可以接受一个新的中断,但在内核代码中还是存在一些临界区,在临界区中,中断必须被禁止。必须尽可能地限制这样的临界区,因为根据以前的要求,内核,尤其是中断处理程序,应该在大部分时间内开中断的方式运行。
中断和异常
Intel文档把中断和异常分为以下几类:
(1) 中断
可屏蔽中断
I/O设备发出的所有中断请求都产生可屏蔽中断。可屏蔽中断可以处于两种状态;屏蔽的或非屏蔽的;一个屏蔽的中断只要还是屏蔽的,控制单元就忽略它。
非屏蔽中断
只有几个危急事件才引起非屏蔽中断。非屏蔽中断总是由CPU辨认。
(2) 异常:
处理器探测异常
当CPU执行指令时探测到的一个反常条件所产生的异常。可以进一步分为三组,这取决于CPU控制单元产生异常时保存在内核态堆栈eip寄存器中的值。
故障
通常可以纠正;一但纠正,程序就可以在不失连贯性的情况下重俗人开始。保存在eip中的值是引起故障的指令地址。因此,当异常处理程序终止时,那条指令会被重俗人执行。
陷阱
在陷阱指令执行后立即报告:内核把控制权返回给程序后就可以继续它的执行而不失连续性。保存在eip中的值是一个随后要执行的指令地址。只有当没有必要重新执行已终止的指令时,才触发陷阱。陷阱的主要用途是为了调试程序。在这种情况下,中断信号的作用是通知调试程序一条特殊指令已被执行。一旦用户检查到调试程序所提供的数据,它就可能要求被调试程序从下一条指令重新开始执行。
异常中止
发生一个严重的错误:控制单元出了问题,不能在eip寄存器中引起异常的指令所在的确切位置。异常中止用于报告严重的错误,如硬件故障或系统表中无效的值或不一致的值。由控制单元发送的这个中断信号是紧急信号,用来把控制权切换到相应的异常中止处理程序,这个异常中止处理程度除了强制爱影响的进程终止外,没有别的选择。
编程异常
在编程者发出请求时发生。是由int或int3指令触发的;当into和bound指令检查的条件不为真是,也引起编程异常。控制单元把编程异常作为陷阱来处理。编程异常也叫做软中断。这样的异常有两种常用的用途:执行系统调用及给调试程序通报一个特定的事件。
每个中断和异常是由0~255之间的一个数来标识。因为一些未知的原因,Intel把这个8位的无符号整数叫做一个向量。非屏蔽中断的向量和异常的向量是固定的,而可屏蔽中断的向量叫做一个向量。非屏蔽中断的向量和异常的向量是固定的,而可屏蔽中断的向量可以通过对中断控制器的编程来改变。
IRQ和中断
每个能够发出中断请求的硬件设备控制器都有一条名为IRQ的输出线。所有现有的IRQ线都与一个名为可编程中断控制器的硬件电路的输入引脚相连。可编程中断控制器执行下列动作:
(1) 监视IRQ线,检查产生的信号。如果有条或两条以上的IRQ线上产生信号,就选择引脚编号较小的IRQ线。
(2) 如果一个引发信号出现在IRQ线上:
a. 把接收到的引发信号转换成对应的向量。
b. 把这个向量存放在中断控制器的一个I/O端口,从而允许CPU通过数据总线读此向量。
c. 把引发信号发送到处理器的INTR引脚,即产生一个中断。
d. 等待,直到CPU通过把这个中断信号写进可编程中断控制器的一个I/O端口来确认它;当这种情况发生时,清INTR线。
(3) 返回到第一步。
IRQ线是从0开始顺序编号的,因此,第一条IRQ线通常表示成IRQ0。与IRQn关联的Intel的缺省量是n+32。如前所述,通过向中断控制器端口发布合适的指令,就可以修改IRQ和向量之间的映射。
可以有选择地禁止每条IRQ线。因此,可以对PIC编程从而禁止IRQ,也就是说,可以告诉PIC停止对给定的IRQ线发布中断,或者激活它们。禁止的中断是丢失不了的,它们一旦被激活,PIC就又把他们发送到CPU。这个特点被大多数中断处理程序使用,因为这允许中断处理程序逐次地处理同一类型的IRQ.
有选择地激活/禁止IRQ线不同于可屏蔽中断的全局屏蔽/非屏蔽。当eflags寄存器的IF标志被清0时,由PIC发布的每个可屏蔽中断都由CPU暂时忽略。cli和sti汇编指令分别清除和设置该标志。
传统的PIC是由两片8259A风格的外部芯片以“级联”的方式连接在一起的。每个芯片可以处理多达8个不同的IRQ输入线。因为从PIC的INT输出线连接到主PIC的IRQ2引脚,因此,可用IRQ线的个数限制为15.
高级可编程中断控制器
以前的描述仅涉及为单处理器系统设计的PIC。如果系统只有一个单独的CPU,那么主PIC的输出线以直截了当的方式连接到CPU的INTR引脚。然而,如果系统中包含两个或多个CPU,那么这种方式不再有效,因而需要更复杂的PIC.
为了充分发挥SMP体系结构的并行性,能够把中断传递给系统中的每个CPU至关重要。基于此理由,Intel从Pentium III开始引入了一种名为I/O高级可编程控制器的新组件,用以代替老式的8259A可编程中断控制器。新近的主板为了支持以前的操作系统都包括两种芯片。此外,80x86微处理器当前所有的CPU都包含有一个本地APIC中断保留的两条额外的IRQ线LINT0和LINT1。所有本地APIC都连接到一个外部I/O APIC,形成一个多APIC的系统。
图4-1以示意图的方式显示了一个多APIC系统的结构。一条APIC总线把“前端”I/O APIC连接到本地APIC。来自设备的IRQ线连接到I/O APIC,因此,相对于本地APIC,I/O APIC起路由器的作用。在Pentium III和早期处理器的母板止,APIC总线是一个串行三线总线,从Pentium 4开始,APIC总线通过系统总线来实现。不过,因为APIC总线及其信息对软件是不可见的,因此,我们不做进一步详细讨论。
I/O APIC的组成为:一级24条IRQ线、一张24项的中断重定向表、可编程寄存器,以及通过APIC总线发送和接收APIC信息的一个信息单元。与8259A的IRQ引脚不同,中断优先级并不与引脚号相关联:中断重定向表中的每一项都可以被单独编程以指明中断向量和优先级,目标处理器及选择处理器的方式。重定向表给一个或多个本地APIC单元。
来自外部硬件设备的中断请求以两种方式在可用CPU之间分发:
静态分发
IRQ信号传递给重定表相应项中所列出的本地APIC。中断立即传递给一个特定的CPU,或一组CPU,或所有CPU。
动态分发
如果处理器正在执行最低优先级的进程,IRQ信号就传递给这种处理器的本地APIC每个本地APIC都有一个可编程任务优先级寄存器,TPR用来计算当前运行进程的优先级。Intel希望在操作系统内核中通过每次进程切换对这个寄存器进行修改。
如果两个或多个CPU共享最低优先级,就利用仲裁技术在这些CPU之间分配负荷。在本地APIC的仲裁优先级寄存器中,给每个CPU都分配一个0~15范围内的值。
每当中断传递给一个CPU时,其相应的仲裁优先级就自动置为0,而其它每个CPU的仲裁优先级都增加1.当仲裁优先级寄存器大于15时,就把它置为获胜CPU的前一个仲裁优先级加1.因此,中断以轮转方式在CPU之间分发,且具有相同的相同的任务优先级。
除了在处理器之间分发中断外,多APIC系统还允许CPU产生处理器间中断。当一个CPU希望把中断发给另一个CPU时,它就在自己本地APIC的中断指令寄存器中存放这个中断向量和目标本地APIC的标识符。然后,通过APIC总线向目标本地APIC发送一条消息,从而向自己的CPU发出一个相应的中断。
处理器间中断是SMP体系结构到关重要的组成部分,并由Linux有效地用来在CPU之间交换信息。
目前大部分单处理器系统都包含一个I/O APIC芯片,可以用以下两种方式对这种芯片进行配置:
(1) 作为一种标准8259A方式的外部PIC连接到CPU。本地APIC被禁止,两条LINT0和LINT1本地IRQ线分别配置为INTR和NMI引脚。
(2) 作为一种标准外部I/O APIC.本地APIC被激活,且所有的外部中断都通过I/O APIC接收。
异常
80x86微处理器发布了大约20种不同的异常。内核必须为每种异常提供一个专门的异常处理程序。对于某此异常,CPU控制单元在开始执行异常处理程序前会产生一个硬件出错码,并且压入内核态堆栈。
下面的列表给出了在80x86处理器中可以找到的异常的向量、名字、类型及其简单描述。更多的信息可以在Intel的技术文档中找到。
0 – “Divide error” 故障
当一个程序试图执行整数被0除操作时产生。
1 – “Debug” (陷阱或故障)
产生于:(1)设置eflags的TF标志时,(2)一条指令或操作数的地址落在一个活动debug寄存器的范围之内。
2 – 未用
为非屏蔽中断保留
3 – “Breakpoint” 陷阱
由int3 指令引起。
4 – “overflow” 陷阱
当eflags的OF标志被设置时,into指令被执行。
5 – “Bounds check” 故障
对于有效地址范围之外的操作数,bound指令被执行。
6 – “Invalid opcode” (故障)
CPU执行单元检测到一个无效的操作友。
7 – “Device not availabe”(故障)
随着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留作将来开发。如表4-1所示,每个异常都由专门的异常处理程序来处理,它们通常把一个Unix信号发送到引起异常的进程。