哈喽,大家好,我是呼噜噜,好久没有更新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
消失了,取而代之的是LINT0
和LINT1
(本地中断),大家了解一下即可,本文的中断还是基于INTR和NMI
硬件中断和软件中断
根据中断源的不同,可以把中断分为硬件中断和软件中断两大类
硬件中断是由硬件设备触发的中断,如时钟中断、串口接收中断、外部中断等。当硬件设备有数据或事件需要处理时,会向CPU
发送一个中断请求,CPU
在收到中断请求后,会立即暂停当前正在执行的任务,进入中断处理程序中处理中断请求。硬件中断具有实时性强、可靠性高、处理速度快等特点
软件中断不是由硬件设备触发的,而是由软件程序主动发起的,如系统调用、软中断、异常、键盘管理中断、显示器管理中断、打印机管理中断等;软件中断需要在程序中进行调用,其响应速度和实时性相对较差,但是具有灵活性和可控性高的特点
与之对应的还有软中断和硬中断:
- 硬中断是由外部事件引起的因此具有随机性和突发性;硬中断是否可以嵌套的,是否有优先级,由硬件设计体系决定的
- 软中断是执行中断指令产生的,无面外部施加中断请求信号,因此中断的发生不是随机的而是由程序安排好的。软中断是一种推后执行的机制
操作系统为了提高中断的处理效率,一般当中断发生的时候,硬中断处理那些短时间,就可以完成的工作,而将那些比较耗时的任务,放到中断之后来完成,也就是软中断来完成
中断控制器
中断控制器是计算机系统中的一个重要组成部分,**用于管理和控制中断请求。**常见的中断控制器有Intel 8259A芯片,我们简单了解一下这个芯片:
Intel处理器允许256个中断,中断号的范围是0~255
,8259A
负责提供其中的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下常见异常的类别:陷阱、故障和终止
-
陷阱trap:是有意的异常,一般用来在用户态和内核态之间提供系统调用接口,陷阱是同步异常,是执行一条指令的结果;陷阱程序总返回到当前指令的下一条指令,比如C语言中的printf函数,底层的实现中会有一条int 0x80指令,就是陷阱,即使用0x80号中断实现系统调用
-
故障fault:是由错误引起,但它可能被故障处理程序修正,故障是同步的,如果修正成功,将返回到当前正在执行的指令,CPU重新执这条指令,否则将终止故障程序。
典型的一种故障,比如缺页异常:当程序试图访问已映射在虚拟地址空间中,但是并未被加载在物理内存中的一个分页时,由中央处理器的内存管理单元所发出的中断。但缺页异常是可以被修正的,有着专门的缺页处理程序,根据缺页中断的不同类型会进行不同的处理
- 终止abort:由不可恢复错误引起,会直接终止程序;终止是同步的,结束时不会返回任何指令即不会将控制返回给原程序
中断异常的优先级
本文到现在我们也介绍了许多中断和异常,他们之间也是有优先级的,我们这里Intel的开发手册为例
我们接下来看看操作系统是如何处理中断的?
中断向量表 IVT
不同的中断信号,需要用不同的中断处理程序来处理。当CPU检测到中断信号后,会根据中断信号的类型去查询“中断向量表”,以此来找到相应的中断处理程序在内存中的存放位置。
中断向量表就是存放中断号和中断处理函数入口地址的表,结构类似数组,我们这里以Linux0.12
为例,来看看其是如何实现中断机制的:
实模式下,16位的中断机制依赖的是中断向量表(IVT,Interrupt Vector Table),中断向量表初始化在0x0000
处,位置是固定的,IVT由 BIOS程序所使用,定义了256种中断的入口地址,包括16位段地址和16位段内偏移量,其中将0到31保留用于异常处理和不可屏蔽中断。
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种描述符:任务门描述符,陷阱门描述符
这些参数大家了解一下就行
- 中断门Interrupt Gate:中断门包含段选择符和中断或异常处理程序的段内偏移量。当控制权转移到一个适当的段时,处理器清IF标志,从而关闭将来会发生的可屏蔽中断,以避免嵌套中断的发生。中断门中的DPL(Descriptor Privilege Level)为0,因此,用户态的进程不能访问Intel的中断门。所有的中断处理程序都由中断门激活,并全部限制在内核态
什么叫中断嵌套?除了同种中断,linux任何一个新的硬中断都可以打断正在执行的中断,形如嵌套;软中断无法嵌套,但相同类型的软中断可以在不同CPU上并行执行
-
陷阱门Trap Gate:与中断门类似,其唯一的区别是,控制权传递到一个适当的段时处理器不修改IF标志,即不关中断;一般中断门用于处理中断,而陷阱门用来处理异常
-
任务门Task Gate:段选择符中存放的是任务状态段 TSS(Task State Segment)的选择子,当中断信号发生时,必须取代当前进程的那个进程的TSS选择符存放在任务门中
实模式下,16位的中断机制依赖的是中断向量表,中断向量表初始化在0x0000
处,位置是固定的。为了让操作系统的代码中的逻辑地址和实际物理地址一致,操作系统启动时会把system模块搬到零地址处,这样中断向量表就会被覆盖
而在保护模式下,中断机制用的是中断描述符表IDT
,位置是不固定的,设计操作系统时可以灵活设置,只需最后把其地址赋值给CPU中的IDTR寄存器。中断描述符表寄存器IDTR是一个48位的寄存器,其低16位保存中断描述符表的大小,高32位保存IDT的基址。
当中断发生时,CPU获取到中断向量后,通过IDTR
的值,去查找IDT中断描述符表
,得到相应的中断描述符,再根据中断描述符记录的信息来作权限判断,运行级别转换,最终调用相应的中断处理程序
IDT这个我们应该非常熟悉了,之前的文章中频繁出现,我们再来回顾一下IDT中的中断有哪些:
操作系统中的中断机制
通常在操作系统中,中断一般的处理流程如下:
- 外设 将中断信号发送给中断控制器
8259A
; 8259A
中优先级裁决器PR根据中断优先级,有序地将中断传递给 CPU- CPU 中止执行当前程序流,将 CPU 所有寄存器的数值保存到栈中
- CPU 根据中断向量,从中断向量表IDT中查找中断处理程序的入口地址,继而执行中断处理程序(期间还要检查IDT表中门描述符的
DPL
,以保证当前程序有权限使用中断服务程序) - CPU 恢复寄存器中的数值,返回原程序流停止位置继续执行
笔者再结合操作系统相关的知识,吐血画了张图,帮助大家更加直观地了解中断流程:
需要注意的是,中断前后,进程的上下文的保存与恢复,上图不是很详细,但这部分我们其实在前一篇文章Linux0.12内核源码解读(7)-陷阱门初始化介绍过:
linux调用中断函数的流程:
linux0.12对应上下文保存与恢复的源码:
.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 %ebxpushl %ecxpushl %edxpushl %edipushl %esipushl %ebppush %ds # 16 位的段寄存器入栈后也要占用 4 个字节push %espush %fspushl $0 # "error code",将数值 0 作为出错码入栈lea 44(%esp),%edx # 取有效地址,即栈中原调用返回地址处的栈指针位置pushl %edx # 并压入堆栈(即esp0 指针入栈)# 所有段寄存器都设置为内核数据段选择符,设置好数据寻址的基址movl $0x10,%edx # 初始化段寄存器ds、es和fs,加载内核数据段选择符mov %dx,%dsmov %dx,%esmov %dx,%fscall *%eax #* 号表示调用操作数指定地址处的函数,称为间接调用,这里就是执行C语言函数do_divide_error# 恢复被中断进程的上下文addl $8,%esppop %fspop %espop %dspopl %ebppopl %esipopl %edipopl %edxpopl %ecxpopl %ebxpopl %eax # 弹出原来eax中的内容iret # 返回中断处理之前的程序,继续执行后续指令
全文完,感谢您的阅读,如果我的文章对你有所帮助的话,还请点个免费的赞,你的支持会激励我输出更高质量的文章,感谢!
作者:小牛呼噜噜 ,首发于公众号 小牛呼噜噜,系列文章还有:
- 聊聊x86计算机启动发生的事?
- Linux0.12内核源码解读(2)-Bootsect.S
- Linux0.12内核源码解读(3)-Setup.S
- 图解CPU的实模式与保护模式
- Linux0.12内核源码解读(5)-head.s
- Linux0.12内核源码解读(6)-main.c
- Linux0.12内核源码解读(7)-陷阱门初始化
- 图解计算机中断
- Linux0.12内核源码解读(9)-blk_dev_init和chr_dev_init
- 什么是系统调用机制?结合Linux0.12源码图解