
浅谈中断
中断
整个计算机系统中,都是由中断驱动。通俗的讲就是CPU停下当前的工作任务,去处理其他事情,处理完后回来继续执行刚才的任务。中间涉及中断向量、中断描述符、中断控制器、上下文保存、中断返回等。
在数字逻辑方面,外部设备和处理器之间有一条专门的中断信号线,用于连接外设与CPU的中断引脚。当外部设备状态改变时,可以通过中断信号线向处理器发送中断请求。
处理器一般只有两根中断线,一根是 IRQ 线(中断请求线),一根是 FIQ 线(快速中断请求线)。而管理的外设却有很多,为了解决这个问题,外设的中断信号线并不与处理器直接相连,而是与中断控制器相连接,后者才跟处理器的中断信号线连接。中断控制器一般通过 CPU 进行配置。
中断分类
外部中断
不可屏蔽中断:通过NMI线向CPU请求的中断,如电源掉电、硬件故障等。因为硬件问题过于严重,所以不可屏蔽终端的意思简单的理解就是,屏蔽不了,不能屏蔽的意思。
可屏蔽中断:通过INTR线向CPU请求的中断,主要来自外部设备,如硬盘、打印机、网卡等。此类中断不会彻底影响整个操作系统的运行,因此可以随时处理甚至挂起不处理,所以为可屏蔽中断。
NMI、INTR均为CPU引脚
内部中断
- 陷阱:有意的、预先安排的异常事件,比如在程序运行时,开发人员在程序中设下陷阱指令,当CPU执行到相关语句时,CPU调用特定的程序进行处理,处理结束后返回到陷阱指令的下一条指令。系统调用、程序调试功能等。
因为高级语言对底层指令的多层抽象封装,已看不到中断的实现。例如在printf函数中,会有中断向量
int 0x80
指令,使用0x80号中断进行系统调用
- 故障:CPU在执行能引起故障的程序但还没有执行结束时,CPU检测到一类的意外事件。出错时交由故障处理程序处理,若能处理,就控制返回引起故障的指令CPU重新执行这条指令;若不能处理,便报错。
常见的故障如缺页。缺页异常是能够修正的,有着专门的缺页处理程序,它会将缺失的物理页从硬盘中调入内存,而后再执行引起故障的指令就能顺利执行。
- 终止:执行指令过程中发生了不可修复的错误,只能中止,通常还是硬件上的错误。终止处理程序不会像前面两种还会返回原程序,而是直接终止。
中断控制器(8259A)
上面讲到每一个独立的外部设备都需要一个中断源,能够向CPU发送中断请求,为了方便管理以及减少引脚数量,设定了中断控制器,所有的可屏蔽中断都通过INTR信号线和CPU进行交流。
以常见的Intel 8259A芯片为例,单个的8259A芯片只有8根中断请求信号(IRQ0,IRQ1, … IRQ7)因此通常采用串联即级联的方式,最多级联9个芯片,达到8 * 8 = 64个中断。

在级联过程中只有一个主片,其余全为从片。
8259A的一些功寄存器和功能部件
IMR:Interrupt Mask Register,中断屏蔽寄存器,其中的每个位标志着一个外设,1表示屏蔽该外设,0表示中断允许。
IRR:Interrrupt Request Register,中断请求寄存器,请求中断的外设在IRR对应的位 值为1。当有多个中断请求时,IRR寄存器中多位将会置1,相当于维持了一个请求中断的队列。
ISR:In_Service Register,中断服务寄存器,正在进行处理的中断在ISR对应的位值为1。
PR:Priority Resolver,优先级裁决器,用于从IRR中挑选一个优先级最大的中断。(IRQ接口号小的优先级大)。
中断请求
外设发出中断信号给8259A;
8259A检查IMR寄存器是否屏蔽来自IRQ的信号,当IMR为0时允许中断,并且将IRR寄存器中与IMQ中同时改为1,表示该设备已经请求中断;
PR优先级裁决器从IRR寄存器中挑选优先级最大的中断,8259A向CPU发送INTR信号;
中断响应
CPU收到INTR后得知有了新的中断,执行完当前命令后,向8259A发生一个中断回复新信号;
8259A收到回复信号后,将挑选出的优先级最大的中断在ISR寄存器中相应的位置1,表示正在处理该中断,同时将此中断在IRR寄存器中相应的位置0,表示将此中断从中断请求队列中移除。
CPU再次向8259A发送INTR信号,表示想要获取中断向量号。
8259A通过数据总线向CPU发送中断向量号,中断向量号 = 起始向量号 + IRQ接口号,一般其实向量号为32。
向量号范围 | 用途 |
---|---|
0—19 | 不可屏蔽中断和异常 |
20—31 | Intel保留 |
32—127 | 外部中断 |
128(0x80) | 系统调用 |
保护现场—压栈
CPU据中断向量号去IDT中获取中断描述符,取出选择子中的DPL与当前特权级CPL进行比较,若特权级发生变化,则需要切换栈。(不同特权级有着不同的栈,如Linux使用了0, 3特权级,则有两个栈,一个内核栈,一个用户栈)
于是处理器临时保存当前的旧栈SS和ESP的值,从TSS(每一个任务有一个TSS结构,其中保存着不同特权级栈的SS和ESP值)中获取与DPL特权级同的栈信息加载到SS和ESP寄存器。再将旧栈SS和ESP的值压入新栈中。若没有特权级变化,则跳过此步骤。
- SS(Stack Segment):栈段寄存器,存储当前堆栈所在的段选择子,即堆栈的段地址。
- ESP(Extended Stack Pointer):扩展堆栈指针,指向当前堆栈的栈顶位置。
- TSS(Task State Segment):任务状态段,存储了一个任务的状态信息,包括其正在执行的代码段和数据段等信息。
- 压入程序状态信息,即EFLAGS寄存器
EFLAGS寄存器是x86架构中的一个标志寄存器,用于存储指令执行的状态和控制CPU的操作。它的各个位表示不同的含义,例如:
- CF (Carry Flag):进位标志位,表示执行算术操作时是否发生进位。
- PF (Parity Flag):奇偶标志位,表示结果的低8位的奇偶性。
- AF (Auxiliary Carry Flag):辅助进位标志位,用于辅助实现16进制算术操作。
- ZF (Zero Flag):零标志位,表示结果是否为0。
- SF (Sign Flag):符号标志位,表示结果是否为负数。
- TF (Trap Flag):单步标志位,用于在调试时单步执行指令。
- IF (Interrupt Enable Flag):中断允许标志位,用于控制CPU是否响应中断。
- DF (Direction Flag):方向标志位,用于指定字符串操作的方向。
- OF (Overflow Flag):溢出标志位,表示执行算术操作时是否发生溢出。
EFLAGS寄存器的值可以通过pushf指令将其压入栈中,也可以通过popf指令将其弹出栈并恢复到寄存器中。在编写汇编程序时,可以通过修改EFLAGS寄存器来实现对CPU的控制。
- 压入断点,即返回地址,即当前任务的CS、EIP值
任务的CS是中断描述符中的段选择子,EIP是中断描述符中的偏移地址。
- 若该中断有错误码压入错误码
定位中断服务
根据中断向量号去IDT中索引中断描述符,具体操作:取出IDTR中的IDT地址,加上中断向量号 * 8,得到的地址指向所要的中断描述符。
据中断描述符中的段选择子去GDT中索引段描述符,具体操作:取出GDTR中的GDT地址。加上段选择子高13位 * 8, 得到的地址为中断处理程序所在段的段基址。
上一步得到的段基址加上段描述符中的段内偏移量得到的地址变为中断服务程序的地址。
中断处理过程
实际就是执行中断处理程序,Linux将中断处理程序分为上下两部分,需要紧急处理立即执行的归为上半部,不那么紧急的归为下半部。
中断返回—出栈
中断返回就是出栈的过程,将第三步保护现场压入栈中的信息弹出。
中断描述符表(IDT—x86)
中断描述符表(IDT)类似全局描述符表(GDT),表内存放的是描述符,与GDT不同的是IDT内可以存放4种描述符:任务门描述符,陷阱描述符,调用门描述符,中断门描述符。
任务门描述符:任务门描述符是为了实现任务切换而存在的,其中包含了任务切换所需要的所有信息。当任务发生切换时,CPU会根据任务门描述符中的信息来进行切换。
陷阱门描述符:陷阱门描述符是用于实现异常和陷阱处理的。当产生异常或者陷阱时,CPU会使用对应的陷阱门描述符来进行处理,并根据陷阱门描述符中的地址跳转到对应的处理程序。
调用门描述符:调用门描述符用于在不同的特权级之间进行函数调用,可以实现低特权级代码调用高特权级代码的功能。调用门描述符中包含了目标代码的选择子和偏移地址,以及参数的个数和类型等信息。
中断门描述符:中断门描述符和陷阱门描述符类似,用于实现中断处理。当产生中断时,CPU会使用对应的中断门描述符来进行处理,并根据中断门描述符中的地址跳转到对应的中断处理程序。和陷阱门描述符类似,中断门描述符也有32位和64位两种格式。
中断过程(SylixOS)
设备发生状态改变时,主动发送一个中断请求信号给中断控制器;
中断控制器接收到信号,并经过中断优先级控制器的处理,由中断控制器向处理器发送中断信号;
处理器接收到信号,并作出处理;
中断请求 —> 中断响应 —> 保护现场 —> 定位中断服务程序 —> 中断处理 —> 中断返回
中断响应前的准备
系统对中断信号进行编号,成为中断向量。在中断响应前,中断向量和中断信号的关系已经定义好。中断向量和中断服务的对应关系是有中断向量表描述,操作系统在中断向量表中设置好不同向量对应的终端服务函数,待CPU查询使用。
CPU在执行完每一条指令后,都会去确认在执行刚才指令过程中中断控制器是否发生中断请求。如果有,CPU就会在相应的时钟脉冲到来时从总线上读取该中断请求的中断向量。
之后的流程,同上
中断向量表
SylixOS中,系统默认存在一张大小为256(可手动更改)的中断向量表,管理SylixOS中的每一个中断向量。
1 | LW_CLASS_INTDESC _K_idescTable[LW_CFG_MAX_INTER_SRC]; |
_K_idescTable 是大小为 256 的数组,数组元素为 256 个中断向量;K_slVectorTable 是一个自旋锁,用于控制对中断向量表的互斥访问。
_K_idescTable 的类型为 LW_CLASS_INTDESC ,该类型是 SylixOS 的中断向量表结构,其详细描述如下:
1 |
|
中断描述符
一个中断服务函数对应一个中断描述符结构,SylixOS 将该中断描述符结构加入到中断向量表对应的表项中。如果一个中断向量对应多个中断服务函数,则这些中断服务函数对应的中断描述符就组成了一个链表,并由中断向量表对应的表项来进行管理
1 |
|
- IACT_plineManage:管理链表,用于将中断描述符加入到中断向量表表项。
- IACT_iIntCnt:中断计数器,每一次中断该数值加 1。
- IACT_pfuncIsr:中断服务函数。
- IACT_pfuncClear:中断清理函数。
- IACT_pvArg:中断服务函数参数。
- IACT_cInterName:中断服务函数名称。
中断服务函数流程
以ARM为例,CPU检测到中断时,自动将PC指针指向中断入口。定义为archIntEntry
archIntEntry –> bspIntHandle –> archIntHandle –> API_InterVectoriser –> 某个中断向量的中断服务函数
- archIntEntry 函数:在执行某一中断向量的中断服务函数之前,需要进行一些准备工作,例如上下文的保存、中断嵌套的判断等,执行完某一中断向量的中断服务函数之后,还需要进行上下文的恢复等操作。
- bspIntHandle 函数:该函数是底层中断入口函数,它通过读取硬件寄存器来获得中断向量号。
- archIntHandle 函数:对中断向量合法性进行判断,并判断是否需要开启中断抢占。
- API_InterVectorIsr 函数:向量中断总服务函数,根据中断号得到对应的中断服务函数链表,找到具体中断服务函数。
其中,API_InterVectorlsr函数的定义如下:
1 |
|
- 函数返回中断返回值。
- 参数 ulVector 是中断向量号。
返回值
宏名 | 含义 |
---|---|
LW_IRQ_NONE | 不是本中断服务函数产生的中断,继续遍历 |
LW_IRQ_HANDLED | 是本中断服务函数产生的中断,结束遍历 |
LW_IRQ_HANDLED_DISV | 中断处理结束,并屏蔽本次中断 |
中断的连接与释放
在SylixOS中,使用终端的设备需要申请和释放中断,申请和释放函数为 API_InterVectorConnect 函数和 API_InterVectorDisconnect 函数。
中断连接函数
API_InterVectorConnect 函数定义如下所示:
1 |
|
函数 API_InterVectorConnect 原型分析:
- 此函数成功返回 ERROR_NONE 失败设置错误号并返回。
- 参数 ulVector 是中断向量号。
- 参数 pfuncIsr 是中断服务函数。
- 参数 pvArg 是中断服务函数参数。
- 参数 pcName 是中断服务名称。
API_InterVectorConnect 函数的功能是将中断向量号与中断服务函数进行连接。
中断释放函数
函数 API_InterVectorDisconnect 的定义如下:
1 |
|
函数 API_InterVectorDisconnect 原型分析:
- 此函数成功返回 ERROR_NONE ,失败设置错误号并返回。
- 参数 ulVector 是中断向量号。
- 参数 pfuncIsr 是中断服务函数。
- 参数 pvArg 是中断服务函数的参数。
- 参数 ulOption 是删除选项。
API_InterVectorDisable 函数只是释放与参数 pfuncIsr 和参数 pvArg 对应的中断服务函数,当一个向量对应多个函数时,它并不会释放该向量的所有中断服务函数。
时钟机制(SylixOS)
在 SylixOS 中使用硬件定时器作为系统 tick 时钟,它是系统跳动的心脏。tick 时钟为系统的多任务调度提供依据,其驱动框架在内核中已经写好,驱动开发人员只需实现 tick 初始化、清除 tick 中断即可。
其中 LW_TICK_HZ 是系统的 tick 时钟频率,单位是 Hz,其数值需要根据具体的硬件性能来设置,频率越快系统的额外开销也就越大。通常情况下 tick 时钟频率设置为 100Hz 或者 1000Hz,对应的时间精度为 10ms 或者 1ms。
一般情况下,系统 tick 时钟的精度只有 10ms 或 1ms,不能满足一些对于时间精度要求较高的应用程序,因此,为了获取高精度时钟,系统内提供 bspTickHighResolution 函数,该函数通过读取硬件定时器的当前计数值来修正最近一次 tick 到当前的精确时间。