漫谈中断(一):PIC

这段时间想把Linux的网络栈给理顺一下,然后发现在读书的时候,很多书对于中断的产生都是很快的带过了,解释的并不是非常的清楚。虽然这个原因很好理解 —— 这些书的重点在于内核如何处理,但是我总觉得这样的话,我的知识的拼图就少了很重要的一块,所以就单独看了看(硬件)中断这一部分。看完之后发现其实内容也挺多的,所以在这里小小的总结一下,正因如此,这篇博客会更加注重硬件的层面,至于软件上如何进行处理,大家可以参照对应的内核书籍,这里就不多说了。

另外因为内容太多,所以这篇文章主要会借用x86/x64的架构来作为例子,arm的架构我们可以在后面的文章中再来讨论。

好了,闲话少说,我们这就开始!

1. 中断(Interrupt)

所谓中断,顾名思义,就是一种可以打断处理器当前运行的程序,转而去运行其他的程序的方式。当某些特定事件(event)被触发,如外部设备完成特定操作,或者执行了未知的指令,或者访问了无法访问的内存,处理器就会触发一个中断请求(IRQ, Interrupt Request),来对其进行处理。通过这种方式,我们可以快速的对这些事件进行响应。

中断理论上分为三类:外部中断(External Interrupt)软件中断(Software Interrupt)内部中断(Internal Interrupt)。不同的平台的分类经常有些许不同,但是大体都差不多,比如,intel就把它分类为:外部中断,可忽略的硬件中断,软件中断和异常。所以这里我们就不过多的对它们的叫法进行展开了,而是来依次看一看其内部的细节。

1.1. 外部中断

外部中断(External Interrupt),又叫硬件中断或者硬中断(Hardware Interrupt),是由外部硬件设备,以异步的方式,产生的中断。它一般通过触发CPU上对应的中断引脚来通知CPU对其进行处理,这就是中断请求(Interrupt Request,IRQ)。

中断请求其实非常简单,大体上都是用CPU上的中断引脚(Interrupt Input Pin)上的电平变化来表示的,无论是从低电平变为高电平或者从高电平变为低电平都可以。处理器会根据其内部时钟不停的对这个引脚的电平进行检查,如果发现电平变化了,就知道中断请求来了,于是就会跳转至中断处理例程(ISR, Interrupt Service Routine)进行处理。

这里我手上正好有一些小的模块,我们可以看到他们都有一个INT引脚,这个引脚就是用来发起中断请求的:

devices-and-interrupt-pins

看到这里,我们很容易联想到几个问题:

  • 如果有多个设备怎么办?处理器如何知道是哪个设备发出的请求的呢?
  • 处理器是如何找到设备对应的中断处理例程的呢?
  • 多个设备同时发起请求要如何解决呢?如果某个高优先级设备发起了中断,而处理器正在处理另一个低优先级设备的中断该怎么办呢?
  • 当中断处理结束之后,中断又是如何被清除的呢?

而这些问题,也是一个基本的中断的处理系统需要处理的核心问题,无论是那种指令集和架构,比如x86和arm。我们接下来在讨论完中断的类型之后,就来依次看一看这些问题。

1.2. 软件中断

软件中断(Software Interrupt),又叫自陷(Trap),顾名思义,是由软件,以同步的方式,产生的中断。它一般是通过调用特定的指令来触发的,比如x86的int指令,arm的swi指令。

它与外部中断有几点不同:

  1. 首先,它是由指令触发的,所以它的触发是同步的,也就是说,当我们调用了int指令之后,处理器就会立即跳转至中断处理例程进行处理,而不会等待CPU中断引脚的电平发生变化。
  2. 其次,它的中断处理例程一般是由OS来指定的,与外部设备没有关系,比如,int 3调用breakpoint中断处理例程,int 0x80调用系统调用中断处理例程(现在已经不用了),等等。
  3. 最后,它的中断向量号是由指令的操作数来指定的,所以不需要通过中断控制器来进行中断向量号的分配。(关于中断控制器和中断向量号,我们后面会讲到)

1.3. 内部中断

内部中断(Internal Interrupt),又叫异常(Exception),是由处理器,以同步的方式,产生的中断。它一般是由处理器内部的一些特殊的事件触发的,比如,除0错误,内存访问错误,系统调用,等等。

它和前两种中断也有些许的不同:

  1. 首先,它是由处理器内部的一些特殊的事件触发的,所以它的触发是同步的,也就是说,当处理器内部的某个事件发生时,处理器就会立即跳转至中断处理例程进行处理,而不会等待CPU中断引脚的电平发生变化。
  2. 其次,它的中断向量号都是处理器hardcode的,所以不需要通过中断控制器来进行中断向量号的分配。(关于中断控制器和中断向量号,我们后面会讲到)
  3. 和软件中断一样,它的中断处理例程也是一般由OS来指定的,与外部设备没有关系。

2. 多设备处理

由于处理器不知道有多少的设备会连接上,所以没有办法给每一个设备都预留一个中断用的引脚,而且这样也是对引脚的一种浪费,所以一般来说,CPU(核心)都只有一条中断请求引脚(INT/INTR)和一条中断确认引脚(INTA),而这也引入了我们的第一个麻烦 —— 我们要如何接收多设备的请求并且知道请求的来源呢?

2.1. 中断控制器(PIC)

首先,我们来解决发送中断请求的问题。我们先祭出计算机界的老套路,直接来不行,就加一层帮忙,而在中断处理中,这一层就是中断控制器(PIC,Programmable Interrupt Controller),它在主板上用来连接CPU和所有的外部设备,帮助CPU将并发的,带优先级的中断信号,转换成串行的中断信号,简化CPU的设计。

中断控制器出现的非常的早。早在1976年,Intel的MCS-85系列(8085)的CPU就用上了中断控制器 —— 8259,后来1983年,在8088上升级为了8259A,它和CPU的中断引脚直接相连,支持8个中断引脚连接8个设备,并且有8个数据引脚连接到总线,让处理器读取和写入中断相关的数据和配置,另外它还支持级联连接,有一个主8259A连接8个从8259A,这样最多可以连接64个外部设备,这对于当时的系统来说已经是非常强力了,不过一般根本用不了这么多,所以当时的主板上是一个主加一个从,一共可以连接15个外部设备,而这个设计也一直保留了下来,哪怕我们现在最新的intel cpu的设计中,也还是可以看到它的身影。

为了支持中断发送,PIC和CPU的两个中断引脚直接相连:

  • INTR:用于PIC发送中断给CPU
  • INTA:CPU接收到中断并且开始处理后,会激发该引脚(高电平转低电平),通知PIC中断已经接收,这样PIC就可以继续接收之后的中断请求,而不用担心请求过快而丢失了。

为了检查来自设备的中断,8259的IRQ0引脚连接着一个可编程时钟(Programmable Interval Timer,PIT),最开始是8253/8254,它最快的频率是1.1931816MHz,也就是说,它每隔838ns会检查一次INTR引脚的电平,如果发现电平变化了,就会将中断请求发送给CPU。当然现在的PIC连接的一般都是处理器的FSB时钟了,所以速度相比当时就会快非常多了,比如Intel i9-13900k的FSB是100MHz,也就是说,它每隔10ns就会检查一次中断引脚的电平了。

为了进行中断控制,PIC中还有三个非常重要的,可以被读写的,8-Bit寄存器:

  • Interrupt request regsiter (IRR):它存储着所有的当前已经触发的中断信号,一个bit一个信号,越高的bit,优先级也越高。
  • Interrupt service register (ISR):它存储着当前CPU上正在处理的中断信号。
  • Interrupt mask register (IMR):它存储着哪些中断信号被启用的状态,1代表启用,0代表禁用。被禁用的中断信号将不会被传递给CPU。

通过它们,PIC就可以来对中断触发进行控制了。

这里为了帮助大家更好的建立一个直观的印象,这是一个当时给Intel 8088 CPU使用的8259A:

(Source: Intel 8259 Wikipedia)

这是从当年Intel 8259A的datasheet中找出来的的框图,我们可以很清晰的看见其中断引脚和数据通道,和DIP28的封装:

(Source: Intel 8259A datasheet)

当然随着时间的发展,现在我们已经无法在主板上找到一个单独的中断控制器的芯片了,这个芯片已经被集成进了南桥,和(部分集成进了)后来的PCH(平台路径控制器,Platform Controller Hub)了。

2.2. 中断向量化

太好了!有了PIC之后,处理器就可以通过两根引脚来接收所有的设备中断了!但是……太惨了!只有两根功能如此简单引脚,我们怎么知道中断是谁发的啊?

当然,我们可以直接一把梭,大力出奇迹!在ISR(中断服务例程)里面一通轮询不就完了?这就是我们说的非向量化的中断处理流程了。在这种模式下,ISR基本逻辑如下:

1
2
3
4
5
6
7
8
9
if (is_interrupted(device_a)) {
call_service_routine_a();
}

if (is_interrupted(device_b)) {
call_service_routine_b();
}

// ...

这缺点不要太明显……这速度也太慢了,而且要怎么扩展啊?所以我们来重构吧!一般遇到这种代码,我们都会把他变成一个数组,通过一个下标来找到对应的函数直接调用,于是向量化中断(vectored interrupting)就出现了!

所以向量化中断主要做了两件事情:

  1. 把所有的中断服务例程放在一个结构体数组中,这就是中断描述符表(IDT,Interrupt Descriptor Table,或者叫中断向量表,IVT,Interrupt Vector Table)。
  2. 给每个设备或者中断会被赋予一个id,当中断被触发后,我们可以通过某种方式拿到这个id,将其作为中断向量表的下标,找到对应的ISR进行调用,这个id就是中断向量号(IVN,Interrupt Vector Number)。

可是,老问题又出现了,我们去哪里找中断描述符表和中断向量号呢?他们具体又长什么样子呢?

2.2.1. 中断描述符表(Interrupt Descriptor Table)

在x86/x64平台上,中断描述符表的地址被保存在一个叫IDTR(Interrupt Descriptor Table Register)的寄存器中,这个寄存器有48位,高32位是中断描述符表的起始地址(在x64中是64位),低16位是中断描述符表的大小。用C语言来表示的话,就是这样的:

1
2
3
4
struct idtr {
uint16_t limit;
uint32_t base; // uint64_t in x64
} __attribute__((packed));

当CPU收到中断时,会通过IDTR来找到中断描述符表,然后通过中断向量号来找到对应的中断服务例程。而由于IDTR的引入,我们可以把中断描述符表放在任意的内存地址中,而不是像之前那样只能放在内核空间的某个地方。这也让中断描述符表的使用变得更加灵活了。

在系统启动之后,中断描述符会被处理器默认设置为Base = 0x00000000,Limit = 0xFFFF,然后启动过程中,BIOS会重新对其进行一次映射,将BIOS的默认例程映射到中断描述符表中,接下来OS会再次对其进行初始化,将OS的ISR写入其中,当全部完成后,中断描述符表就被初始化完成了。

这里是一个x86平台的中断描述符表的示意图:

(Source: Real-Time Embedded Systems, Design Principles and Engineering Practices, Chapter 4.7 Case Study: x86)

2.2.2. 中断描述符(Interrupt Descriptor)

2.2.2.1. x86平台下的中断描述符

中断描述符表中的每一项都是一个中断描述符,它包含了中断服务例程的地址,以及一些其他的信息。x86平台下的中断描述符,大小为8个字节,用c语言来表示如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
struct idt_entry {
uint16_t base_lo; // 中断服务例程的低16位,task gate没有这个字段
uint16_t sel; // 段选择子
uint8_t always0; // 保留字段,必须为0
union {
uint8_t flags; // 中断描述符标志
struct {
uint8_t type : 3; // 中断门类型
uint8_t size : 1; // 中断门大小,0 = 16-bit, 1 = 32-bit
uint8_t descriptor_type : 1; // 描述符类型,0 = 系统描述符,1 = 代码或数据描述符,
// 对于中断描述符来说,这个字段必须为0
uint8_t dpl : 2; // 描述符特权级(Descriptor Privilege Level)
uint8_t p : 1; // Segment Present Flag
};
};
uint16_t base_hi; // 中断服务例程的高16位,task gate没有这个字段
} __attribute__((packed));

而根据不同的中断类型,Intel的手册中定义了三种不同中断描述符,分别是:中断门(Interrupt Gate)描述符,陷阱门(Trap Gate)描述符,任务门(Task Gate)描述符。它们的结构和行为也有着些许的区别:

类型 行为 中断门类型 中断门大小
中断门 中断门用于处理中断,触发时会清除IF标记来禁止中断 0b110 0b1
陷阱门 和中断门十分类似,但是不会清除IF标记来禁止中断 0b111 0b1
任务门 任务门用于处理任务切换 0b101 0b0

(Source: Intel® 64 and IA-32 Architectures Software Developer’s Manual Volume 3A: System Programming Guide, Part 1, Chapter 6.11 IDT Descriptors)

2.2.2.2. x64平台下的中断描述符

x64平台上会有些许不同,主要是地址的宽度变成了64位,所以中断描述符大小变成了16个字节,多了4个字节来存放高32位的地址,而且中断门类型和中断门大小的字段也变成了4位和1位,具体的结构如下:

(Source: Intel® 64 and IA-32 Architectures Software Developer’s Manual Volume 3A: System Programming Guide, Part 1, Chapter 6.14.2 64-Bit Mode Stack Frame)

2.2.2.3. 中断特权级(DPL)

中断描述符中还需要注意的是中断的特权级(DPL),它的值可以是0、1、2、3,分别对应着Ring 0、Ring 1、Ring 2、Ring 3。这个特权级只对软件中断有效,比如int调用,CPU在接收到软件中断时,会对其进行检查:

  • 如果当前的特权级(CPL)比DPL高,也就是说现在的程序没有权限调用这个中断服务例程,这时候就会触发一个异常,异常号为13(#GP,General Protection)。
  • 如果当前的特权级(CPL)比DPL低,也就是说权限没有问题,但是上下文有问题,这时候CPU会进行调用栈切换,找到一个合适的栈来执行中断服务例程。
  • 如果当前的特权级(CPL)和DPL相等,也就是说权限没有问题,上下文也没有问题,CPU就会直接执行中断服务例程。

当然,每个中断描述符也有自己的优先级,x86平台上,Intel定义的优先级如下:

(Source: Intel® 64 and IA-32 Architectures Software Developer’s Manual Volume 3A: System Programming Guide, Part 1, Chapter 6.9 PRIORITY AMONG SIMULTANEOUS EXCEPTIONS AND INTERRUPTS)

2.2.3. 中断向量号(Interrupt Vector Number)

有了中断描述符表,我们就可以把中断服务例程放在任意的内存地址中了,但是我们还需要一个东西来告诉CPU,中断服务例程(ISR)在哪里,这个东西就是中断向量号(IVN)。

中断向量号是一个8位的无符号整数,它的值是0-255,从上面的中断向量表的图中,我们可以看到,在x86系统中,中断向量号主要分为两个区域:

  • 0-31:这些是CPU保留的向量号,用于CPU内部的异常处理,这些中断的向量号都是每个系统中硬编码的,x86和arm平台上的中断向量号是不一样的。
  • 32-255:这些是外部中断可用的向量号,可以由BIOS或者OS来进行配置,提供每个设备对应的中断描述符。而当外部中断发生的时候,我们也需要获取对应的中断向量号,然后进行调用。

可是中断向量号要如何获取呢?别着急,我们不是刚刚提到了中断控制器(PIC)吗?还记得它有8个引脚连着总线吗,在CPU中断被触发之后,CPU就会自动的通过总线去问PIC拿IVN(中断向量号),这样问题就解决啦!

ARM的平台上也是非常的类似,有机会我们后面再聊。

3. 中断嵌套(Interrupt Nesting)和中断服务例程

之前我们提到一个问题,如果CPU在处理某个中断时,更高优先级的中断出现了怎么办?我们有两个选择:

  1. 我们可以在处理中断的过程中,全程禁止中断,这样就不会有更高优先级的中断进入了。
  2. 我们可以将当前的中断中断下来,然后去处理新的中断,这就是中断嵌套了。

方法一很简单,但是如果某些中断处理时间过长,就会导致系统性能猛烈下降,因为系统无法及时的处理更高优先级的中断,比如,键盘鼠标无法响应,这样就会导致系统的卡顿。所以很多时候,OS会选择方案二,也就是中断嵌套。

中断嵌套中需要注意的点就是如何安全的保存和恢复当前程序的上下文,而这没有什么魔法,无论是什么平台,解决方法都类似,而且简单粗暴 —— 如果有必要就禁止中断。

  1. 第一步,在中断发生后,CPU进入ISR之前,除了上面我们提到的一些权限检查的操作,为了保存上下文,CPU主要做了两件事情:1)(可选)禁止中断,避免其他的中断在上下文未保存完成前进入;2)将当前的指令寄存器(x86上的CS, EIP和arm上的R15)和极少数特别的状态寄存器(如x86上的EFLAGS和arm上的CPSR)保存到栈上(intel)或者其他寄存器(arm)上。
  2. 第二步,CPU将进入ISR(中断服务例程),此时中断服务例程将继续CPU未完成的动作,保存更多会被影响的寄存器到栈上,然后恢复中断,这也会使系统允许中断嵌套。
  3. 第三步,终于,我们可以开始执行ISR的主体逻辑了,在电平触发的中断中,这里也会通知设备中断完成,让其将中断线上的电平修改回去。
  4. 第四步,ISR主体执行完毕后,ISR需要再次停止中断,把之前保存的寄存器从栈上恢复出来,然后将控制返还给CPU(比如,调用x86下的iret指令)。
  5. 第五步,CPU会将保存的指令寄存器和状态寄存器恢复回来,并重新启用中断。

到此,中断服务例程调用完毕。整个过程中我们可以看到CPU是如何和ISR协同完成这个任务的,而也正因为如此,ISR的设计依赖于CPU的Contact,这也导致了第二步到第四步成为了所有ISR的模板,而它们也有了自己特有的名字:第二步叫做SOI(Start Of Interrupt),第四步叫做EOI(End Of Interrupt)。

对更多细节感兴趣的朋友,可以移步Intel的官方文档:Intel® 64 and IA-32 Architectures Software Developer’s Manual Volume 3A: System Programming Guide, Part 1, Chapter 6.12, 6.14

另外,我们应该保证中断服务例程足够的小,尽快的退出,因为即便是最低优先级的中断,也比用户态最高优先级的优先级高,如果中断无法结束,用户态的程序将永远得不到机会处理,比如,x11或者app可能完全无法绘制屏幕,用户态的网络程序无法响应网络请求等等。有兴趣的朋友可以搜索一下“中断风暴”,这里就不过多的展开了。

4. 中断链(Interrupt Chainning)和中断级联(Interrupt Cascading)

由于中断服务例程可以被覆盖,所以在使用中断的时候,我们通过使用新的例程覆盖老的例程的方法,来得到两种新的玩法:中断链和中断级联。这两个玩法的概念都很好理解,唯一需要注意的事情是,EOI最多只能被调用一次:

  • 中断链:在上层的中断服务例程的最后,跳转到下层的中断服务例程(避免重复的EOI调用),一般用于OS或设备的ISR调用被覆盖掉的默认ISR。

    (Source: Real-Time Embedded Systems, Design Principles and Engineering Practices, Chapter 4.5.3 ISR Chaining)

  • 中断级联:在上层的中断服务例程中,通过if else来进行判断,并跳转到下层的中断服务例程,但是只有最底层的ISR的EOI会被调用。

    (Source: Real-Time Embedded Systems, Design Principles and Engineering Practices, Chapter 4.5.4 ISR Cascading)

不过,这些在linux系统中都没有被使用,所以了解了就可以了,我们就不再过多的讨论了。

5. 中断信号

虽然中断信号听起来很简单,但是里面其实也有一些小的有意思的点,所以在我们做最后的总结之前,我们还是来仔细看看中断信号吧!

一般来说,中断信号分为两类:电平触发(也叫水平触发,level-sensitive)中断和边缘触发(edge-triggered)中断。它们的特点总结如下:

中断信号类型 触发条件 优点 缺点
电平触发 外部设备的中断线上的电平一直保持到中断服务例程处理完毕 中断服务例程可以在中断发生后,不需要立即处理,可以等到合适的时候再处理,而不会丢失中断 中断服务例程必须要通知设备取消中断,否则中断线上的电平将一直保持,这样会导致中断服务例程被重复调用,另外如果有设备出错无法取消中断,就会引起中断死循环
边缘触发 外部设备的中断线上的电平只有在发生变化的时候才会触发中断 可以减少设备出错带来的影响,因为中断线的电平不会一直保持 中断服务例程必须要立即处理,否则会导致中断丢失

这里选择哪种类型的中断信号,是可以由设备本身来决定的,所以中断系统必须要能够兼容两种类型的中断信号。而我们知道外部设备都是通过PIC来连接CPU的,所以这也就意味着中断着PIC必须能够支持这两种信号,并且能够处理两个问题:

  1. 电平触发时,设备出错,无法取消中断,导致中断死循环
  2. 边缘触发,无法马上处理而导致的中断丢失

当然解决方法并不难,PIC中的IRR(Interrupt Request Register)会保存中断线上的电平,所以可以充当这个缓存寄存器,然后在中断服务例程中,通过读取缓冲寄存器来判断中断是否已经被处理,如果没有被处理,就再次触发中断,直到中断被处理为止。这样就能够解决这两个问题了。

6. NMI(Non-Maskable Interrupt)

NMI是一种特殊的中断,顾名思义,它的特点是不会被屏蔽。NMI一般是通过硬件电路来触发的,与INTR引脚(通过PIC进行屏蔽)不同,CPU上会有一个专门用于处理NMI请求的引脚,绕过PIC直接与触发NMI的设备相连,所以也相对不可控。另外NMI中断的优先级也相对较高,比如,比所有通过INTR来的外部硬件中断都要高。也正因为如此,NMI一般用于处理一些比较严重的错误,比如,CPU温度过高,供电出错,内存出错等等。

在x86上,NMI的中断向量号是hardcode的2,除了硬件触发以外,我们还可以通过调用int 2来进行软件触发,但是这种方式不会修改NMI引脚电平的状态,而且一般不会被使用。

关于所有中断的优先级,可以参照上面中断向量表中的图表,这里就不重复了。

7. x86中断处理流程小结

好的,到此我们对x86下参与中断的各个部件的工作原理都有了一个比较大概的认识了,那么我们就来总结一下x86中断处理流程吧!

首先,在x86系统上我们有4中不同的中断源,它们的中断流程类似,唯一不同的是中断向量号的获取方式:

  1. 外部中断:通过PIC来触发,中断向量号是通过PIC的ISR(Interrupt Service Register)来获取的
  2. NMI:通过硬件电路来触发,中断向量号是2,也可以由int 2来触发,通过操作数来获取
  3. 软件中断:通过int指令来触发,中断向量号是通过操作数来获取的
  4. 内部中断:所有的中断向量号都是hardcode在CPU中的,无需额外获取

然后,我们假设系统中某个设备A连接到了PIC的IRQ5上,内核已经初始化完成,系统运行在保护模式下,然后发生了中断,那么其整体中断的流程大致如下(软件部分这里不会过多涉及,主要是关注在硬件的部分):

  1. 设备发起中断:设备A发起中断,将PIC上IRQ5引脚的电平拉高。
  2. PIC响应中断:PIC根据FSB的时钟对所有引脚的电平进行检测,发现IRQ5的电平变高后,会将其保存到IRR寄存器中,如果当前IMR寄存器没有禁用该引脚,没有更高优先级的中断(引脚号更高),且当前CPU没有处理中断,那么PIC就会将ISR寄存器的第5位设置为1,然后将CPU的INTR引脚拉高,通知CPU有新的中断发生。
  3. CPU响应中断:CPU收到INTR引脚的电平变化后,会等待当前正在执行的指令的完成,然后停止执行,准备中断处理。这个时候,CPU上CS:IP保存的地址是下一条指令的地址。
  4. CPU获取中断向量号:CPU通过系统总线向PIC获取存储在ISR寄存器中的中断向量号。
  5. CPU通知PIC中断响应:CPU通过INTA引脚通知PIC中断已经被响应,PIC会将IRR寄存器和ISR寄存器的第5位清零。
  6. PIC取消中断信号:PIC恢复INTR引脚的电平,这样CPU就可以继续接收新的中断了,这是为了支持中断嵌套。
  7. CPU读取中断描述符:CPU从IDTR寄存器中获取中断描述符表的地址,然后根据中断向量号来获取中断描述符,并通过GDTR寄存器找到GDT的基址和段描述符,最后结合IDT中的CS:IP,计算出ISR的最终地址。
  8. CPU进行特权级检查:CPU根据中断描述符中的特权级(DPL)来检查当前的特权级(CPL)是否满足中断描述符的要求,如果不满足,就会触发一个特权级异常并结束中断。
  9. CPU暂停接收中断信号:CPU根据当前中断描述符的类型,有选择的修改IF标记,暂停中断信号的接收,这是为了能安全的保存上下文。
  10. CPU保存当前的上下文:这里的上下文并不是完整的上下文,只是保存了当前的CS,EIP和EFLAGS寄存器。如果在步骤8中发现需要进行栈切换,那么这里还会保存SS和ESP寄存器。
  11. CPU跳转至中断服务例程:根据步骤7中计算出的ISR地址,CPU更新指令寄存器,跳转到中断服务例程。
  12. 中断服务例程开始(SOI):开始阶段,中断服务例程会做两件事情,一个保存当前的进程更加完整的上下文,另一个是如果允许中断嵌套,则会开始恢复中断,但是不同的系统做的事情可能略有不同。
  13. 中断服务例程执行:这里就是执行中断服务例程的主逻辑了,另外,在linux内核中,为了保证中断响应速度,硬中断处理的代码会尽量的短,核心逻辑会被移入软中断中来实现,这里就不多展开了。
  14. 中断服务例程结束(EOI):结束阶段,中断服务例程会做两件事情,首先再次禁止中断,保证上下文安全的恢复,然后从堆栈中恢复当前进程的上下文。
  15. CPU恢复上下文:中断服务例程的最后会调用iret指令,将控制流交还给CPU,CPU会从堆栈中恢复CS、EIP、EFLAGS寄存器,然后重置IF标记,恢复中断信号的接收。
  16. 结束:中断处理结束,此时CS和EIP指向的是中断前CPU执行的下一条指令,于是CPU可以恢复指令执行。

这就是x86系统下,中断的整个执行流程了。

8. 小结

好了,到此基本的x86中断处理流程就总结完毕了。当然随着计算机的发展,现代的x86系统已经基本不用8259的中断方式了,但是它的思想和类似的使用方法却是非常常见的,比如ARM的AIC,Intel的APIC Bypass模式,和Intel APIC中的Local APIC对中断的处理。不过这篇文章就先记录到这里,后面有时间再继续补充吧。

9. 参考资料


同系列文章:
原创文章,转载请标明出处:Soul Orbit
本文链接地址:漫谈中断(一):PIC