一、什么是中断
1、概述
中断(interrupt)是指在 CPU 正常运行期间, 由外部或内部事件引起的一种机制。 当中断发生时,CPU 会停止当前正在执行的程序,并转而执行触发该中断的中断处理程序。处理完中断处理程序后,CPU 会返回到中断发生的地方, 继续执行被中断的程序。中断机制允许 CPU 在实时响应外部或内部事件的同时,保持对其他任务的处理能力。
中断的流程图如下:
2、中断的分类
中断通常被定义为一个事件,该事件改变处理器执行的指令顺序。这样的事件与 CPU 芯片内外部硬件电路产生的电信号相对应。
中断通常分为同步(synchronous)中断和异步(asynchronous)中断:
- 同步中断是当指令执行时由 CPU 控制单元产生的,之所以称为同步,是因为只有在一条指令终止执行后CPU才会发出中断;
- 异步中断是由其他硬件设备依照 CPU 时钟信号随机产生的。
在 Intel 微处理器手册中,把同步和异步中断分别称为异常(exception)和中断(interrupt)我们也采用这种分类,当然有时我们也用术语“中断信号”指这两种类型(同步及异步)。
二、中断和异常
1、中断和异常
Intel 文档把中断和异常分为以下几类:
- 中断:
- 可屏蔽中断(maskabie interrupt)
I/O 设备发出的所有中断请求(IRQ)都产生可屏蔽中断。可屏蔽中断可以处于两种状态:屏蔽的(masked)或非屏蔽的(unmasked),一个屏蔽的中断只要还是屏蔽的,控制单元就忽略它。要根据中断允许标志的设置来判断 CPU 是否能响应中断请求。 - 非屏蔽中断(nonmakable interrupt)
只有几个危急事件(如硬件故障)才引起非屏蔽中断。非屏蔽中断总是由 CPU 辨认。不受中断允许标志的影响,不能用软件进行屏蔽。
可屏蔽的中断可以被阻塞,使用 x86_64 的指令sti
和cli
。这两个指令修改了在中断寄存器中的 IF 标识位。sti
指令设置 IF 标识,cli
指令清除这个标识。不可屏蔽的中断总是被报告。通常,任何硬件上的失败都映射为不可屏蔽中断。我们可以在 Linux 内核代码中找到这两个指令的使用:
- 可屏蔽中断(maskabie interrupt)
static inline void native_irq_disable(void)
{
asm volatile("cli": : :"memory");
}
static inline void native_irq_enable(void)
{
asm volatile("sti": : :"memory");
}
- 异常:
- 处理器探测异常(processor-detected exception)
当 CPU 执行指令时探测到的一个反常条件所产生的异常。可以进一步分为三组,这取决于 CPU 控制单元产生异常时保存在内核态堆栈 eip 寄存器中的值。- 故障(fault)
通常可以纠正:一旦纠正,程序就可以在不失连贯性的情况下重新开始。保存在 eip 中的值是引起故障的指令地址。因此,当异常处理程序终止时,那条指令会被重新执行。 - 陷阱(trap)
在陷阱指令执行后立即报告;内核把控制权返回给程序后就可以继续它的执行而不失连贯性。保存在 eip 中的值是一个随后要执行的指令地址。只有当没有必要重新执行已终止的指令时,才触发陷阱。陷阱的主要用途是为了调试程序。在这种情况下,中断信号的作用是通知调试程序一条特殊指令已被执行(例如到了一个程序内的断点)。一旦用户检查到调试程序所提供的数据,它就可能要求被调试程序从下一条指令重新开始执行。 - 异常中止(abort)
发生一个严重的错误:控制单元出了问题,不能在 eip 寄存器中保存引起异常的指令所在的确切位置。异常中止用于报告严重的错误,如硬件故障或系统表中无效的值或不一致的值。由控制单元发送的这个中断信号是紧急信号,用来把控制权切换到相应的异常中止处理程序,这个异常中止处理程序除了强制受影响的进程终止外,没有别的选择。
- 故障(fault)
- 编程异常(programmed exception)
在编程者发出请求时发生。是由int
或int3
指令触发的,当into
(检查溢出)和bound
(检查地址出界)指令检查的条件不为真时,也引起编程异常。控制单元把编程异常作为陷阱来处理。编程异常通常也叫做软中断(sofware interrupt)这样的异常有两种常用的用途:执行系统调用及给调试程序通报一个特定的事件。
- 处理器探测异常(processor-detected exception)
每个中断和异常是由 0~255 之间的一个数来标识。因为一些未知的原因,Intel 把这个 8 位的无符号整数叫做一个向量(vector)。非屏蔽中断的向量和异常的向量是固定的,而可屏蔽中断的向量可以通过对中断控制器的编程来改变。
中断和异常的区别:中断是由硬件引起的;异常则发生在编程失误而导致错误指令,或者在执行期间出现特殊情况必须要靠内核来处理的时候(比如缺页)。
2、中断的上下部
中断的执行需要快速响应, 但并不是所有中断都能迅速完成。 此外, Linux 中的中断不支持嵌套, 意味着在正式处理中断之前会屏蔽其他中断, 直到中断处理完成后再重新允许接收中断,如果中断处理时间过长, 将会引发问题。
这里以炒菜的过程中接电话进行举例:当你正在炒菜的时候,菜正在锅里翻炒着。 突然, 你的手机响起,打破了你正常的炒菜流程,接电话的时间很短并不会对炒菜产生很大的影响, 而接电话的时候可能就有问题了,因为菜可能会因为没来得及翻面而炒糊了。
为了让系统可以更好地处理中断事件, 提高实时性和响应能力, 将中断服务程序划分为上下文两部分:
-
上半部:上半部是中断处理函数的一部分,它主要处理一些紧急且需要快速响应的任务。 中断上文的特点是执行时间较短,旨在尽快完成对中断的处理。这些任务可能包括保存寄存器状态、更新计数器等, 以便在中断处理完成后能够正确地返回到中断前的执行位置。
上半部的执行是在中断上下文中进行的,它运行在中断服务例程(ISR)所在的内核线程上下文中,而不是用户进程的上下文中。因此,上半部的执行是在中断被触发时立即执行的,不会被其他中断打断。 -
下半部是中断处理函数的另一部分,它相对于上半部来说是延迟执行的。下半部的目的是在中断被触发后,尽快将一些不紧急或者耗时的处理工作延后执行,以减轻上半部的负担,从而使中断处理更加高效。
下半部的执行是在非中断上下文中进行的,它不会被其他中断打断,并且可以访问用户空间的内存。下半部的执行可以在任意时刻进行,但是需要注意的是,下半部执行的时间越长,会导致中断延迟更长,从而影响系统的响应性能。下半部一般包括以下几种形式:- 内核线程:创建一个新的内核线程来执行一些独立于中断的任务。
- 任务队列:将需要执行的任务放入任务队列中,由内核调度器来选择适当的时机执行。
- 工作队列:类似于任务队列,但是工作队列可以绑定到某个 CPU,以提高处理效率。
3、异常
80x86 微处理器发布了大约 20 种不同的异常(依赖于体系结构)。内核必须为每种常提供一个专门的异常处理程序。对于某些异常,CPU 控制单元在开始执行异常处理程序前会产生一个硬件出错码(hardware error code),并且压入内核态堆栈。
下面的列表给出了在 80x86 处理器中可以找到的异常的向量、名字、类型及其简单描述。更多的信息可以在 Intel 的技术文挡中找到。
- 0:“Divide error”(故障)
当一个程序试图执行整数被 0 除操作时产生。 - 1:“Debug”(陷阱或故障)
产生于:- 设置 eflags 的 TF 标志时(对于实现调试程序的单步执行是相当有用的);
- 一条指令或操作数的地址落在一个活动 debug 寄存器的范围之内。
- 2:未用
为非屏蔽中断保留(利用 NMI 引脚的那些中断)。 - 3:“Breakpoint”(陷阱)
由int3
(断点)指令(通常由 debugger 插入)引起。 - 4:“Overflow”(陷阱)
当 eflags 的 OF(overflow)标志被设置时,into
(检查溢出)指令被执行。 - 5