【操作系统】浅谈 Linux 中的中断机制
参考资料:
[2015 SP] 北京大学 Principles of Operating System 操作系统原理 by 陈向群(p7-p10)
认认真真的聊聊中断
什么是软中断?
认认真真的聊聊"软"中断
操作系统-x86中断机制
一、什么是中断机制?
1.1、中断的概念
在计算机中,中断是系统用来响应硬件设备请求的一种机制,操作系统收到硬性的中断请求,会打断正在执行的进程,然后调用内核中的中断处理程序来响应请求。
简单来说,中断会让 CPU 停止正在执行的程序,转而让 CPU 去执行中断处理函数程序,执行完再返回原程序。
另外,整个操作系统就是一个中断驱动的死循环,操作系统原理如果用一行代码解释,下面这样再合适不过了。
while(true) {
doNothing();
}
其他所有事情都是由操作系统提前注册的中断机制和其对应的中断处理函数完成,我们点击一下鼠标,敲击一下键盘,执行一个程序,都是用中断的方式来通知操作系统帮我们处理这些事件,当没有任何需要操作系统处理的事件时,它就乖乖停在死循环里不出来。
所以,中断,非常重要,它也是理解整个操作系统的根基,掌握它,不亏!
1.2、中断机制的分类
先来说说中断的分类,以Intel CPU 作为例子,Intel CPU 提供了三种[中断(动)] 程序执行的机制,分别是:
![image-20230606025011557](https://img-blog.csdnimg.cn/img_convert/28f420906392a7f9a21c886908ea03b3.png)
- 中断(interrupt): 由硬件设备触发,比如点击一下鼠标、敲一下键盘,这时候外部设备会给 CPU 发送一个中断号,属于异步事件;
- 异常(exception): CPU 在执行指令时检测到的反常条件,比如除法异常、错误指令异常,缺页异常等,然后CPU 自己给自己一个中断号,无需外界给,属于同步事件;
- INT 指令: INT 指令后面跟一个数字,就相当于直接用指令的形式,告诉 CPU 一个中断号。比如 INT Ox80就是告诉 CPU 中断号是 0x80。Linux 内核提供的系统用,就是用了 INT Ox80 这种指令。
根据触发的方式不同,将这三个机制分成两类
![image-20230606025152773](https://img-blog.csdnimg.cn/img_convert/7cd9052c314e50cddaf61c8d68f00b41.png)
- 中断和异常属于【硬件中断】,因为他们都是硬件自动触发的,中断是由外部设备触发的,异常是由 CPU 触发的。
- INT 指令属千【软件中断】,因为他是由软件程序主动触发的。
但是注意,中断、异常、INT指令都是属于【硬中断】,没有”件”字。为什么都属于硬中断呢?
因为这是 Intel CPU这个硬件实现的中断机制,注意这里是实现机制,并不是触发机制,因为触发可以通过外部硬件,也可以通过软件的 INT 指令。
二、中断机制实现的原理
2.1、中断如何给 CPU 传递中断信号
有一个设备叫做可编程中断控制器,它有很多的 IRQ 引脚线,接入了一堆能发出中断请求的硬件设备,当这些硬件设备给 IRQ 引脚线发一个信号时,由于可编程中断控制器提前被设置好了 IRQ 与中断号的对应关系,所以就转化成了对应的中断号,把这个中断号存储在自己的一个端口上,然后给 CPU 的 INTR 引脚发送一个信号,CPU 收到 INTR 引脚信号后去刚刚的那个端口读取到这个中断号的值。
![图片](https://img-blog.csdnimg.cn/img_convert/2aea4f89d675a22d4ec1266f97f42f70.gif)
你看,最终的目标,就是让 CPU 知道,有中断了,并且也知道中断号是多少。
比如上图中按下了键盘,最终到 CPU 那里的反应就是,得到了一个中断号 0x21。
那异常的机制就更简单了,是 CPU 自己执行指令时检测到的一些反常情况,然后自己给自己一个中断号即可,无需外界给。
比如 CPU 执行到了一个无效的指令,则自己给自己一个中断号 0x06,这个中断号是 Intel 的 CPU 提前就规定好写死了的硬布线逻辑。
好了,到目前为止,我们知道了无论是中断还是异常,最终都是通过各种方式,让 CPU 得到一个中断号。只不过中断是通过外部设备给 CPU 的 INTR 引脚发信号,异常是 CPU 自己执行指令的时候发现特殊情况触发的,自己给自己一个中断号。
还有一种方式可以给到 CPU 一个中断号,但 Intel 手册写在了后面,Chapter 6.4.4 INT n,就是大名鼎鼎的 INT 指令。
![图片](https://img-blog.csdnimg.cn/img_convert/bc3990ebd263eca292e840e5d46fe210.png)
INT 指令后面跟一个数字,就相当于直接用指令的形式,告诉 CPU 一个中断号。
比如 INT 0x80,就是告诉 CPU 中断号是 0x80。Linux 内核提供的系统调用,就是用了 INT 0x80 这种指令。
那我们上面的图又丰富了起来。
![图片](https://img-blog.csdnimg.cn/img_convert/9118d93e64c0df72538ec9ed8d8de615.png)
2.2、CPU 根据中断码查中断向量表
那 CPU 收到中断号后,如何处理呢?
先用一句不太准确的话总结,CPU 收到一个中断号 n 后,会去中断向量表中寻找第 n 个中断描述符,从中断描述符中获得与该中断相关的处理程序的入口地址,并将PC设置成该地址,新的指令周期开始时,CPU控制转移到中断处理程序,然后执行。
为什么说不准确呢?因为从中断描述符中找到的,并不直接是程序的地址,而是段选择子和段内偏移地址。然后段选择子又会去全局描述符表中寻找段描述符,从中取出段基址。之后段基址 + 段内偏移地址,才是最终处理程序的入口地址。
![img](https://img-blog.csdnimg.cn/img_convert/c8a21cea349383673b0ec466cb8b6943.png)
当然这个入口地址,还不是最终的物理地址,如果开启了分页,又要经历分页机制的转换,就像下面这样。
![图片](https://img-blog.csdnimg.cn/img_convert/db8684d08b67ddb465712632a4bd9dd9.gif)
不过不要担心,这不是中断的主流程,因为分段机制和分页机制是所有地址转换过程的必经之路,并不是中断这个流程所特有的。
所以我们简单的把中断描述符表中存储的地址,直接当做 CPU 可以跳过去执行的中断处理程序的入口地址,就好了,不影响理解他们。
![图片](https://img-blog.csdnimg.cn/img_convert/eecc495f671fe76836a9d013d6e6772d.jpeg)
2.3、CPU 执行中断处理程序地址
CPU 在收到一个中断号并且找到了中断描述符之后,究竟做了哪些事?
当然,最简单的办法就是,直接把中断描述符里的中断程序地址取出来,放在自己的 CS:IP 寄存器中,因为这里存的值就是下一跳指令的地址,只要放进去了,到下一个 CPU 指令周期时,就会去那里继续执行了。
但 CPU 并没有这样简单粗暴,而是帮助我们程序员做了好多额外的事情,这增加了我们的学习和理解成本,但方便了写操作系统的程序员,拿到一些中断的信息,以及中断程序结束后的返回工作。
但其实,就是做了一些压栈操作。
1. 如果发生了特权级转移,压入之前的堆栈段寄存器 SS 及栈顶指针 ESP 保存到栈中,并将堆栈切换为 TSS 中的堆栈。
2. 压入标志寄存器 EFLAGS。
3. 压入之前的代码段寄存器 CS 和指令寄存器 EIP,相当于压入返回地址。
4. 如果此中断有错误码的,压入错误码 ERROR_CODE
5. 结束(之后就跳转到中断程序了)
压栈操作结束后,栈就变成了这个样子。
![图片](https://img-blog.csdnimg.cn/img_convert/077e87349e6733b2399df4a61cb5913a.png)
特权级的转移需要切换栈,所以提前将之前的栈指针压入。错误码可以方便中断处理程序做一些工作,如果需要,从栈顶拿到就好了。
抛开这两者不说,剩下的就只有标志寄存器和中断发生前的代码地址,被压入了栈,这很好理解,就是方便中断程序结束后,返回原来的代码嘛~
2.4、中断处理程序结束,恢复上下文环境
你看,中断是如何切到中断处理程序的?就是靠中断描述符表中记录的地址。那中断又如何回到原来的代码继续执行呢?是通过 CPU 帮我们把中断发生前的地址压入了栈中,然后我们程序自己利用他们去返回,当然也可以不返回。
这就是 CPU 和操作系统配合的结果,把中断这个事给解决了。
2.5、流程总结
- CPU根据中断码查中断向量表,获得与该中断相关的处理程序的入口地址,并将PC设置成该地址,新的指令周期开始时,CPU控制转移到中断处理程序。
- 中断处理程序开始工作:
- 在系统栈中保存现场信息;
- 检查I/O设备的状态信息,操纵I/O设备或者在设备和内存之间传送数据等等;
- 中断处理结束时,CPU检测到中断返回指令,从系统栈中恢复被中断程序的上下文环境,CPU状态恢复成原来的状态,PSW和PC恢复成中断前的值,CPU开始个新的指令周期。
三、软中断机制
3.1、什么是软中断
前面我们也提到了,中断请求的处理程序应该要短且快,这样才能减少对正常进程运行调度地影响,而且中断处理程序可能会暂时关闭中断,这时如果中断处理程序执行时间过长,可能在还未执行完中断处理程序前,会丢失当前其他设备的中断请求。
那 Linux 系统为了解决中断处理程序执行过长和中断丢失的问题,将中断过程分成了两个阶段,分别是「上半部和下半部分」。
-
上半部用来快速处理中断,一般会暂时关闭中断请求,主要负责处理跟硬件紧密相关或者时间敏感的事情。
-
下半部用来延迟处理上半部未完成的工作,一般以「内核线程」的方式运行。
前面的外卖例子,由于第一个配送员长时间跟我通话,则导致第二位配送员无法拨通我的电话,其实当我接到第一位配送员的电话,可以告诉配送员说我现在下楼,剩下的事情,等我们见面再说(上半部),然后就可以挂断电话,到楼下后,在拿外卖,以及跟配送员说其他的事情(下半部)。
这样,第一位配送员就不会占用我手机太多时间,当第二位配送员正好过来时,会有很大几率拨通我的电话。
3.2、为什么要有软中断
Linux 某网卡接收了一个数据包,此时会触发一个硬中断,由于处理数据包的过程比较耗时,而硬中断资源又非常宝贵,如果占着硬中断函数不返回,会影响到其他硬中断的响应速度,比如点击鼠标、按下键盘等。
而且,中断处理程序在响应中断时,可能还会[临时关闭中断],这意味着,如果当前中断处理程序没有执行完之前,系统中其他的中断请求都无法被响应,也就说中断有可能会丢失,所以中断处理程序要短且快。
以常见的网卡接收网络包的例子:
网卡收到网络包后,通过 DMA 方式将接收到的数据写入内存,将网络包的数据写入到内存后,下一步就需要通知内核来处理,于是网卡会触发一个硬中断,内核就会调用网卡的中断处理程序来处理该事件,这个事件的处理会分成上半部和下半部:
- 上部分由硬中断处理程序处理,要做的事情很少,做完后就会发起了一次软中断,然后就结束了。这里发起的软中断,并不是向 CPU 发送中断信号,而是将软中断标记数组中的某一个位置标记一下。
- 下半部由软中断处理程序处理,内核守护进程会不断轮询软中断标记数组,看哪个位置被标记为 1了,接着就去软中断向量表里,寻找这个标志位对应的处理程序,然后执行软中断处理程序处理,其主要是需要从内存中找到网络数据,再按照网络协议栈,对网络数据进行逐层解析和处理,最后把数据送给应用程序。
所以,软中断的作用就是承接原本硬中断处理程序比较复杂且耗时的工作,让硬中断的中断处理函数的逻辑尽可能的简单,从而提高系统的中断响应速度。