Loading... 中断这个东西说起来不是很难,实现起来比较麻烦,主要是和硬件有一定联系,会略显复杂。 ### 宏观视角 宏观地来看,一个中断的过程就是 CPU 接受中断信号,然后执行对应的处理函数。这里的中断分外部中断和内部中断两种。 #### 外部中断 所谓外部中断,顾名思义就是外部设备产生的中断。CPU 中有两条型号线 **INTR**(**INTeRrupt**) 和 **NMI**(**Non Maskable Interrupt**)来接受外部中断信号。前者接受的是**可屏蔽中断**,后者则是**不可屏蔽中断**。 ##### 可屏蔽中断 这种中断是各类外设向 CPU 发出的 ,由于不甚危急,CPU 可以选择不予理会。对于许多中断,也可以将对中断的响应和处理分开(Linux 就是这样做的)以加快对中断的响应速度。 对中断的屏蔽是通过 eflags 寄存器的 IF 位来实现的。此位为 1 的时候,CPU 不会再响应中断。 ##### 不可屏蔽中断 不可屏蔽中断往往代表非常严重的错误,往往会造成宕机,这种型号不能被屏蔽,任何时候 CPU 收到此型号都需要立即进行处理。 #### 内部中断 内部中断往往是由软件产生的,分为软中断和异常。 软中断大多由软件自己调用,一般的语法为 `int 8 位立即数`,此指令较常用,Linux 中就是通过 `int 0x80` 来进行系统调用的。类似的指令还有 `int3`(实现下断点)等。 异常则是运行时的错误,比如缺页异常、除数为零等。 内部中断**往往不可屏蔽**,因为如果屏蔽了这些中断往往会影响正常运行。比如如果屏蔽用户发出的软中断,就可能造成用户希望进行的系统调用无法执行,又比如屏蔽了缺页异常,就可能造成无法正确执行代码,所以这些中断不受 IF 控制。 ### 底层视角 CPU 对中断的响应必然需要通过特定的函数来响应,由于中断数量非常多(处理器支持 256 个中断,编号 0 ~ 255),自然需要一个数据结构来维护响应的函数并在中断到来时进行响应,这个结构就是中断描述符表。 #### 中断描述符表 和 GDT 类似,**中断描述符表**(**IDT**,**Interrupt Descriptor Table**)也是一个类似于数组的数据结构,通过中断号就可以寻址到每个特定的描述符项。特别的,一般情况下中断都会伴随着特权级的切换(比如系统调用进入内核态),为了实现这个切换,需要用到门,四个门中我们选用中断门(事实上,中断门只允许放在 IDT 中,可见两者有多么搭配),其结构为 高 32 位: | 31~ 16 | 15 | 14 ~ 13 | 12 | 11 ~ 8 | 7 | 6 | 5 | 4 ~ 0 | | - | - | - | - | - | - | - | - | - | | 中断处理程序在段中的偏移的<br /> 31 ~ 16 位 | P | DPL | S = 0 | TYPE | 0 | 0 | 0 | 未使用 | 低 32 位: | 31 ~ 16 | 15 ~ 0 | | - | - | | 中断处理程序的段选择子值 | 中断处理程序在段中的偏移的 15 ~ 0 | 我们就在中断描述符表的每一项里放入中断门(事实上 IDT 中只存放门),这样就可以在中断到来时通过中断门来进行相应的处理。 每个中断门的结构可以用这样一个结构体来表示 ```cpp /* interrupt gate descriptor */ struct INT_gate_desc { uint16_t function_offset_low_word; uint16_t selector; uint8_t dword_count; /* fixed value */ uint8_t attribute; uint16_t function_offset_high_word; }; ``` 通过建立一个结构体数组就可以建立起 IDT ```cpp static struct INT_gate_desc IDT[IDT_DESC_SUM]; /* Interrupt Descriptor Table */ ``` 通过 `lidt 48 位数` 可以设置 LDTR 寄存器,类似于 GDTR,存储了 LDT 表的基址。 ```cpp /* load IDT */ uint64_t idt_operand = ((sizeof(IDT) - 1) | ((uint64_t)((uint32_t) IDT << 16))); __asm__ volatile ("lidt %0": : "m" (idt_operand)); ``` 这样就设置好了 LDT 表。 #### 中断处理过程 光有 IDT 肯定不够,还需要一个动态的过程来处理整个中断。下面先总结一下中断处理的过程 * 外中断:外设向 CPU 发送中断信号,通过中断代理芯片的转换调度后使 CPU 收到中断,CPU 对 IDT 查表后执行对应的中断处理程序 * 内中断:CPU 对 IDT 查表后执行对应的中断处理程序 若暂且不考虑硬件层面的处理,可知中断的处理过程主要就是查表执行,这个过程具体如下 1. 处理器根据中断号查表得到中断门。这个过程比较容易,只要将中断号 * 8 加上 IDTR 就可以了。 2. 特权级检查:对于软中断,保证数值上 CPL <= 中断门的 DPL,数值上 CPL >= 目标代码的 DPL;对于外中断,只要满足数值上 CPL >= 目标代码的 DPL 就可以了。 3. 执行中断处理程序 执行中断处理程序时,首先通过 IDT 和 GDT 协同的方式计算出目标代码的位置,也就是获得 IDT 中的段选择子后通过 GDT 计算出基地址再加上 IDT 中的偏移来计算。 跳转至目标函数的时候会进行多次压栈操作,首先处理特权级转移时栈寄存器的保存问题,即如果存在特权级转移,就将旧的 ss 和 esp 寄存器压入栈中。然后压入 eflags 进行备份,再压入 cs 和 eip 备份原来的代码地址,最后根据是否有异常错误码压入错误码,最后跳至异常处理函数处执行。注意在压入 eflags 备份之后、跳至异常处理函数执行之前 eflags 的 IF 位会被置 0,此时处理器不再接受可屏蔽中断,以避免相同中断嵌套造成 GP 中断。 对应于压栈过程,有 iret 语句来处理返回的情况,然而,iret 默认没有错误码的,也就是说,编写异常处理函数的人必须手动地清理掉错误码。一种解决方案是在函数开头时根据有无错误码进行一次压栈操作(异常有无错误码压栈是事先已知的,可以直接在编译期完成判断),这样在返回前就可以一并清空栈空间,避免代码冗余。 *关于错误码:一般只有外部中断才会压入中断码。* #### 关于特权级检查 对于软中断,经过了两步特权级检查: * 第一步是数值上 CPL <= 中断门的 DPL。CPL 大于 DPL 代表调用者有调用该门的权限,这里检查的是权限级别。 * 第二步是数值上 CPL >= 目标代码的 DPL。CPL 大于目标代码的 DPL,代表特权级向高处转移,这里检查的是操作的合法性,是保证特权级只向高处转移(仅在返回时向低处转移)。 外部中断比较简单,可以和软中断的第二步类比。 #### 总结 从内核外面向里面来看,一个中断的处理过程就是硬件或进程向 CPU 发送中断信号,CPU 通过 IDT 取得中断有关的信息(处理程序的段选择子和偏移地址以及权限信息),通过特权级检查后调用中断处理程序。而对应的中断处理程序需要处理好中断,并在返回前清理栈中的错误码。 ### 硬件视角 这个主要是针对外部中断的,比较复杂。 CPU 自己无法接受所有的中断,因为每个中断都需要一条数据线接到 CPU 上的话,会导致 CPU 体积的大幅膨胀,为了解决这个问题,Intel 推出了一系列的中断代理芯片来进行对中断的缓冲,该芯片可以暂存并挑选中断后,将选中的中断发送给 CPU。这个系列就是 8259A 可编程中断控制器。书上花了大量篇幅介绍该芯片,我觉得不是很有必要,毕竟我们是在学习操作系统而不是硬件,而且花了很久看完我也基本上全忘了。我这里只随便说一下我记得的东西,具体操作方法不再赘述。 该芯片每一张都有 8 个接口来接入外设,一个接口来输出中断信号,通过级联(类似于电路中的串联,把一个芯片(从片)的输出口接入另一个芯片(主片)的输入口)的方法可以实现接口的扩展。每个外设都将其信号线接到芯片组的一个接口上,当有中断信号发出时,芯片组会接受信号并暂存(每个接口都对应了一个寄存器对中断信号进行处理),起到缓存的作用,并在合适的时机将信号发送给 CPU。 别的不记得了。需要用的时候再查吧。 内部中断在硬件层面上的工作方式我们不关心,因为其实这个不需要我们进行硬件层面的控制。 ### 时钟 时钟分内部时钟和外部时钟,内部时钟在主板上,我们不可控,外部时钟往往由可编程芯片控制,我们通过与对应芯片 IO 可以设置时钟频率。 以 8253 芯片为例,其振晶发出的脉冲信号频率为 1.19318 MHZ,在芯片内部有一个计数器,计数器的结构这里不说了,每次芯片接受到脉冲信号时,计数器会减一,减到 0 时,会通过 OUT 端向 CPU 发出中断信号(实际上是由中断代理芯片代理接受的)。 此芯片有 6 中工作模式,我们使用其比率发生模式,设置其计数器起始值,然后芯片就会从起始值开始自减,减到 0 后会自动复位到我们设置的起始值进行下一轮自减。通过设置计数器起始值,我们可以控制中断的频率,公式为 起始值 = 1.19318MHZ / 中断频率。 ### 实现 代码比较多,这里也就不放了,可以看笔者的 [commit](https://github.com/chujDK/chuj-elephant-os/commit/31e2055bc3e257875d372ca95c3b673ec90b182d)。 最后修改:2021 年 05 月 23 日 © 允许规范转载 打赏 赞赏作者 支付宝微信 赞 4 如果觉得我的文章对你有用,那听听上面我喜欢的歌吧