图解计算机中断

作者:小牛呼噜噜 | https://xiaoniuhululu.com

计算机内功、源码解析、科技故事、项目实战、面试八股等更多硬核文章,首发于公众号「小牛呼噜噜

哈喽,大家好,我是呼噜噜,好久没有更新old linux了,在上一篇文章Linux0.12内核源码解读(7)-陷阱门初始化中,我们简要地提及了中断,但是中断机制在计算机世界里非常重要,处处都离不开中断,本文来详细聊聊计算机里的中断机制

现代计算机具有多任务处理的能力,可以同时运行着几十上百的任务,如今很难想象,当我们点击鼠标,需要等待计算机中的其他程序全部执行完毕

1956年,IBM 7049机器上首先使用了中断技术,提升了计算机具备应对处理突发事件的能力,并开始使用“中断”这一术语

中断,英文为Interrupt,即打断。当CPU在正常运行程序执行任务时,接收到硬件传过来的中断信号(interrupt request,IRQ),CPU会中断执行当前的工作任务(被打断),转而去处理其他任务,等处理完后再回来继续执行刚才被暂时中断的任务

常见的中断类型

外部中断和内部中断

广义上中断按照中断来源,可分为外部中断和内部中断

  • 外部中断

与CPU执行指令无关,中断信号来自CPU外部,一般指指由计算机外设发出的中断请求,如:键盘中断、打印机中断、定时器中断等。外部中断既有可屏蔽的中断也有不可屏蔽的中断,也是狭义上的中断(interrupt)

它不是由任何一条专门的指令造成的。比如硬盘,打印机,网络适配器,磁盘控制器等外部设备等硬件设备,通过向CPU上的引脚(NMI和**INTR)**发信号,并将异常号放在系统总线上,来触发中断。

中断是异步发生的(不同于同步:执行一条指令的结果),中断处理程序总是返回到当前指令的下一条指令

  • 内部中断

与CPU执行指令有关,中断信号来自CPU内部,一般指通过软件调用的中断,以及由执行指令过程中发生的错误所引起的中断,所以也称为异常(exception),如:trap指令、地址越界、算术溢出、虚存系统的缺页;

我们下文会具体讲讲x86下的异常,接下来还是会继续讲讲中断的其他分类

不可屏蔽中断和可屏蔽中断

中断按照是否可被屏蔽,可分为2类:不可屏蔽中断和可屏蔽中断

  • 不可屏蔽中断

不可屏蔽中断就是当不可屏蔽中断源一旦提出请求,表明问题非常严重或者系统发生了致命的错误,CPU必须立即无条件响应

另外不可屏蔽中断从源头还可以分为,既可由CPU内部产生,也可由外部NMI引脚产生,比如因运算出错(协处理器运算出错、除数为零、运算溢出、单步中断等)或 因硬件出错(如电源掉电,硬件线路故障等)所引起的中断

那什么是NMI引脚?

其实NMI和下面的INTR都是CPU上的引脚,INTR(Interrupt Require)表示可屏蔽中断请求NMI(Nonmaskable Interrupt)表示不可屏蔽中断请求,我们来看下8086CPU的引脚图:

NMI和INTR在上图左下角

所以不可屏蔽中断除了可由 CPU 内部产生,还可以由外部硬件的中断通过NMI这根信号线来通知CPU产生

  • 可屏蔽中断

可屏蔽中断就是当可屏蔽中断源提出请求CPU可以响应,也可以不响应;一般是由外部硬件的中断通过INTR这根信号线来通知CPU产生的,比如硬盘,打印机,网卡等外部设备产生中断,这类中断并不会影响计算机的正常运行。不像不可屏蔽中断,它是没有内部中断的,因为内部中断是不可屏蔽的中断

对于可屏蔽中断,除了受本身的屏蔽位的控制外,还都要受一个总的控制,即CPU标志寄存器中的中断允许标志位IF(Interrupt Flag)的控制,若IF位为1,可以得到CPU的响应,否则得不到响应。而不可屏蔽中断是不受中断标志位IF的影响,不管IF是什么,CPU都必须响应

随着保护模式的流行,Intel意识到使用中断来控制固件已不再是一种解决方案,引入系统管理模式SMM添加到CPU中,与正常中断相反,SMM是CPU的一种特殊模式;要想要输入SMM,必须生成一个系统管理中断SMI,其是在80386的更高版本中引入的,可以用于透明地转换硬件接口

随着奔腾系列的问世,英特尔推出了LAPIC(本地高级可编程中断控制器),INTR和NMI消失了,取而代之的是LINT0LINT1(本地中断),大家了解一下即可,本文的中断还是基于INTR和NMI

硬件中断和软件中断

根据中断源的不同,可以把中断分为硬件中断软件中断两大类

硬件中断是由硬件设备触发的中断,如时钟中断、串口接收中断、外部中断等。当硬件设备有数据或事件需要处理时,会向CPU发送一个中断请求,CPU在收到中断请求后,会立即暂停当前正在执行的任务,进入中断处理程序中处理中断请求。硬件中断具有实时性强、可靠性高、处理速度快等特点

软件中断不是由硬件设备触发的,而是由软件程序主动发起的,如系统调用、软中断、异常、键盘管理中断、显示器管理中断、打印机管理中断等;软件中断需要在程序中进行调用,其响应速度和实时性相对较差,但是具有灵活性和可控性高的特点

与之对应的还有软中断和硬中断

  1. 硬中断是由外部事件引起的因此具有随机性和突发性;硬中断是否可以嵌套的,是否有优先级,由硬件设计体系决定的
  2. 软中断是执行中断指令产生的,无面外部施加中断请求信号,因此中断的发生不是随机的而是由程序安排好的。软中断是一种推后执行的机制

操作系统为了提高中断的处理效率,一般当中断发生的时候,硬中断处理那些短时间,就可以完成的工作,而将那些比较耗时的任务,放到中断之后来完成,也就是软中断来完成

中断控制器

中断控制器是计算机系统中的一个重要组成部分,用于管理和控制中断请求。常见的中断控制器有Intel 8259A芯片,我们简单了解一下这个芯片:

Intel处理器允许256个中断,中断号的范围是0~2558259A负责提供其中的15个,但中断号并不固定,允许软件根据自己的需要灵活设置中断号,以防止发生冲突。该中断控制器芯片有自己的端口号,可以像访问其他外部设备一样用in和out指令来改变它的状态,包括各引脚的中断号。所以又被称为可编程中断控制器PIC

上图来源于百度百科

一个8259A芯片的组成可以分为5个主要的逻辑控件:中断屏蔽寄存器(IMR)、中断请求寄存器(IRR)、优先级仲裁单元(PR)、中断向量寄存器(ISR)和控制逻辑单元(Control Logic)

一个8259A芯片有IRQ0~IRQ7七个IRQ引脚,一个IRQ对应着一个中断号,一个中断号对应着一个中断向量,一个中断向量对应着一个中断处理子程序(ISR,Interrupt Service Routine)

8259A只适合单CPU的情况,为了充分挖掘SMP体系结构的并行性,能够把中断传递给系统中的每个CPU至关重要。Intel引入了一种名为I/O高级可编程控制器的新组件,来替代老式的8259A可编程中断控制器-高级可编程中断控制器(APIC),大家感兴趣地自行去了解一下

陷阱、故障和终止

我们再回到上文的异常这块,来了解一下X86下常见异常的类别:陷阱、故障和终止

  1. 陷阱trap:是有意的异常,一般用来在用户态和内核态之间提供系统调用接口,陷阱是同步异常,是执行一条指令的结果;陷阱程序总返回到当前指令的下一条指令,比如C语言中的printf函数,底层的实现中会有一条int 0x80指令,就是陷阱,即使用0x80号中断实现系统调用

  2. 故障fault:是由错误引起,但它可能被故障处理程序修正,故障是同步的,如果修正成功,将返回到当前正在执行的指令,CPU重新执这条指令,否则将终止故障程序。

    典型的一种故障,比如缺页异常:当程序试图访问已映射在虚拟地址空间中,但是并未被加载在物理内存中的一个分页时,由中央处理器的内存管理单元所发出的中断。但缺页异常是可以被修正的,有着专门的缺页处理程序,根据缺页中断的不同类型会进行不同的处理

  3. 终止abort:由不可恢复错误引起,会直接终止程序;终止是同步的,结束时不会返回任何指令即不会将控制返回给原程序

中断异常的优先级

本文到现在我们也介绍了许多中断和异常,他们之间也是有优先级的,我们这里Intel的开发手册为例

我们接下来看看操作系统是如何处理中断的?

中断向量表 IVT

不同的中断信号,需要用不同的中断处理程序来处理。当CPU检测到中断信号后,会根据中断信号的类型去查询“中断向量表”,以此来找到相应的中断处理程序在内存中的存放位置。

中断向量表就是存放中断号和中断处理函数入口地址的表,结构类似数组,我们这里以Linux0.12为例,来看看其是如何实现中断机制的:

实模式下,16位的中断机制依赖的是中断向量表(IVT,Interrupt Vector Table),中断向量表初始化在0x0000处,位置是固定的,IVT由 BIOS程序所使用,定义了256种中断的入口地址,包括16位段地址和16位段内偏移量,其中将0到31保留用于异常处理和不可屏蔽中断。

1
2
3
4
5
6
7
8
9
10
11
12
13
256种中断如下:

0-19的中断向量对应于异常和非屏蔽中断。
20-31Intel保留
32-127可屏蔽硬件中断
128用于系统调用的可编程异常
129-238可屏蔽硬件中断
239本地APIC时钟中断
240本地APIC高温中断
241-250由Linux留作将来使用
251-253处理器间中断
254本地APIC错误中断
255本地APIC伪中断(CPU屏蔽某个中断时产生的)

当中断发生时,处理器要么自发产生一个中断向量,要么从** int n**指令中得到中断向量,或者从外部的中断控制器接受一个中断向量。接着该向量作为索引访问中断向量表,寻找对应的中断处理程序入口地址(中断处理函数的地址为=中断向量表地址 + 4 * n),去执行程序

中断描述符表IDT

IDT,Interrupt Descriptor Table,即中断描述符表,和GDT类似,记录着0~255的中断号和调用函数之间的关系,与中段向量表有些相似,但要包含更多的信息。

其中每一个表项叫做中断描述符或门描述符(gate descriptor),的含义是指当中断发生时,必须先通过这些门,然后才能进入相应的处理程序

除了我们非常熟悉的中断描述符,IDT内还可以存放2种描述符:任务门描述符,陷阱门描述符

这些参数大家了解一下就行

  1. 中断门Interrupt Gate:中断门包含段选择符和中断或异常处理程序的段内偏移量。当控制权转移到一个适当的段时,处理器清IF标志,从而关闭将来会发生的可屏蔽中断,以避免嵌套中断的发生。中断门中的DPL(Descriptor Privilege Level)为0,因此,用户态的进程不能访问Intel的中断门。所有的中断处理程序都由中断门激活,并全部限制在内核态

什么叫中断嵌套?除了同种中断,linux任何一个新的硬中断都可以打断正在执行的中断,形如嵌套;软中断无法嵌套,但相同类型的软中断可以在不同CPU上并行执行

  1. 陷阱门Trap Gate:与中断门类似,其唯一的区别是,控制权传递到一个适当的段时处理器不修改IF标志,即不关中断;一般中断门用于处理中断,而陷阱门用来处理异常

  2. 任务门Task Gate:段选择符中存放的是任务状态段 TSS(Task State Segment)的选择子,当中断信号发生时,必须取代当前进程的那个进程的TSS选择符存放在任务门中

实模式下,16位的中断机制依赖的是中断向量表,中断向量表初始化在0x0000处,位置是固定的。为了让操作系统的代码中的逻辑地址和实际物理地址一致,操作系统启动时会把system模块搬到零地址处,这样中断向量表就会被覆盖

而在保护模式下,中断机制用的是中断描述符表IDT,位置是不固定的,设计操作系统时可以灵活设置,只需最后把其地址赋值给CPU中的IDTR寄存器。中断描述符表寄存器IDTR是一个48位的寄存器,其低16位保存中断描述符表的大小,高32位保存IDT的基址。

当中断发生时,CPU获取到中断向量后,通过IDTR的值,去查找IDT中断描述符表,得到相应的中断描述符,再根据中断描述符记录的信息来作权限判断,运行级别转换,最终调用相应的中断处理程序

IDT这个我们应该非常熟悉了,之前的文章中频繁出现,我们再来回顾一下IDT中的中断有哪些:

操作系统中的中断机制

通常在操作系统中,中断一般的处理流程如下:

  1. 外设 将中断信号发送给中断控制器8259A
  2. 8259A中优先级裁决器PR根据中断优先级,有序地将中断传递给 CPU
  3. CPU 中止执行当前程序流,将 CPU 所有寄存器的数值保存到栈中
  4. CPU 根据中断向量,从中断向量表IDT中查找中断处理程序的入口地址,继而执行中断处理程序(期间还要检查IDT表中门描述符的DPL,以保证当前程序有权限使用中断服务程序)
  5. CPU 恢复寄存器中的数值,返回原程序流停止位置继续执行

笔者再结合操作系统相关的知识,吐血画了张图,帮助大家更加直观地了解中断流程:

需要注意的是,中断前后,进程的上下文的保存与恢复,上图不是很详细,但这部分我们其实在前一篇文章Linux0.12内核源码解读(7)-陷阱门初始化介绍过:

linux调用中断函数的流程:

linux0.12对应上下文保存与恢复的源码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
.globl _divide_error,_debug,_nmi,_int3,_overflow,_bounds,_invalid_op
//.globl xx表示将符号标记为一个全局符号,以供其他文件访问!!!
.globl _double_fault,_coprocessor_segment_overrun
.globl _invalid_TSS,_segment_not_present,_stack_segment
.globl _general_protection,_coprocessor_error,_irq13,_reserved
.globl _alignment_check

_divide_error:
pushl $_do_divide_error # 首先把将要调用的函数地址入栈;_do_divide_error是C函数do_divide_error被编译后的名字
no_error_code:
# 保存被中断的进程的上下文
xchgl %eax,(%esp) #_do_divide_error的地址→eax,eax被交换入栈
pushl %ebx
pushl %ecx
pushl %edx
pushl %edi
pushl %esi
pushl %ebp
push %ds # 16 位的段寄存器入栈后也要占用 4 个字节
push %es
push %fs
pushl $0 # "error code",将数值 0 作为出错码入栈
lea 44(%esp),%edx # 取有效地址,即栈中原调用返回地址处的栈指针位置
pushl %edx # 并压入堆栈(即esp0 指针入栈)

# 所有段寄存器都设置为内核数据段选择符,设置好数据寻址的基址
movl $0x10,%edx # 初始化段寄存器ds、es和fs,加载内核数据段选择符
mov %dx,%ds
mov %dx,%es
mov %dx,%fs
call *%eax #* 号表示调用操作数指定地址处的函数,称为间接调用,这里就是执行C语言函数do_divide_error

# 恢复被中断进程的上下文
addl $8,%esp
pop %fs
pop %es
pop %ds
popl %ebp
popl %esi
popl %edi
popl %edx
popl %ecx
popl %ebx
popl %eax # 弹出原来eax中的内容
iret # 返回中断处理之前的程序,继续执行后续指令

全文完,感谢您的阅读,如果我的文章对你有所帮助的话,还请点个免费的,你的支持会激励我输出更高质量的文章,感谢!

计算机内功、源码解析、科技故事、项目实战、面试八股等更多硬核文章,首发于公众号「小牛呼噜噜」,我们下期再见!

参考资料:

英特尔® 64 位和 IA-32 架构开发人员手册:卷 3A-英特尔®

《Linux内核完全注释5.0》

https://www.codenong.com/40583848

https://zhuanlan.zhihu.com/p/651460336

维基百科、百度百科