嗨,您好 我是 vnjohn,在互联网企业担任 Java 开发,CSDN 优质创作者
推荐专栏:Spring、MySQL、Nacos、Java,后续其他专栏会持续优化更新迭代
文章所在专栏:网络 I/O
我当前正在学习微服务领域、云原生领域、消息中间件等架构、原理知识
向我询问任何您想要的东西,ID:vnjohn
觉得博主文章写的还 OK,能够帮助到您的,感谢三连支持博客
代词: vnjohn
⚡ 有趣的事实:音乐、跑步、电影、游戏
目录
在了解 Epoll 多路复用模型时,操作系统底层的中断处理机制是要有一定认识的
任何操作系统内核的核心任务,都包含有对连接到计算机上的硬件设备进行有效管理,如:硬盘、鼠标、键盘以及无线电等,而当要管理这些设备,首先要和它们互相通信才行。
众所周知,处理器的速度跟硬件设备处理的速度不在一个数量级别上,因此,若内核采取让处理器向硬件发出一个请求,然后有一个专门等待回应的方法,显然查强人意,既然硬件的响应这么慢,那么内核就应该在此期间可以去处理其他的事务,等到硬件真正完成了请求的操作以后,再回过头来对它进行处理.
如何让处理器和硬件设备能够协同工作,且不会降低机器的整体性能?
轮询(polling) 可能是一种解决办法,它可以让内核定期对设备的状态进行查询,然后再作出相应的处理,不过这种方法很可能会让内核做很多无用的功,因为无论硬件设备是正在忙碌完成任务还是已经完成,轮询总会周期性的重复执行,更好的办法是提供一种机制,让硬件在需要的时候再向内核发出信号(由内核主动变为硬件主动)
由内核主动变为硬件主动:这就是中断机制
中断可分为中断和异常,异常又可以分为故障、陷阱和中止
CPU 提供了两种中断程序执行的机制:中断和异常
An interrupt is an asynchronous event that is typically by an I/O device
中断:中断就是一个异步事件,通常由 I/O 设备触发
An exception is a synchronous event that is generated when the processor detects one or more predefined while executing an instruction.
异常:异常是一个同步事件,是 CPU 在执行指令时检测到的反常条件,比如 > 除法异常、错误指令异常、缺页异常等
两者的执行机制,都是让 CPU 收到一个中断号,再到中断描述符表找出具体的执行处理的程序入口即可响应该中断事件要处理的事情.
中断使得硬件得以发出通知给到处理器,例如:当你敲击键盘时,键盘控制器(控制键盘的硬件设备)会发送一个中断,通知操作系统有键按下,中断本质上是一种特殊的电信号,由硬件设备发向处理器,处理器接收到中断以后,会马上向操作系统反映此信号的由来,然后就交由给操作系统负责处理这些新到来的数据
硬件设备生成中断时并不考虑与处理器的时钟同步,换句话就是说中断可以随时产生,因此,内核随时可能因为新到来的中断而被打断
整个操作系统就是一个由中断驱动的死循环,用最简单的伪代码解释再合适不过了,如下:
while(true) {
doNothing();
}
所有的事情都是由操作系统提前注册的中断机制和其对应的中断处理函数来完成的,当有信号到来时,会通知操作系统来帮我们处理这些信号对应的事件,当没有任何操作系统处理的事件时,就停留在死循环里不做任何事情。
不同的硬件设备对应的中断不同,而每个中断都通过一个唯一的数字标志,因此,来自键盘的中断就有别于来自硬盘的中断,从而使得操作系统能够对中断进行区分,并知道是由那个硬件设备产生的哪个中断,这样,操作系统才能给不同的中断提供对应的中断处理程序。
在操作系统中,异常与中断不同,它在产生时必须考虑与处理器时钟同步;实际上,异常也常常称为同步中断,在处理器执行到由于编程失误而导致的错误指令(如被 0 除)时,或者是在执行期间出现特殊情况(如缺页)必须靠内核来处理时,处理器就会产生一个异常
在提及中断时,说到了中断号,在发生中断或异常以后,都会给 CPU 发送一个中断号过去,那么 CPU 是如何接收到这个中断号的呢?
CPU 内部内置 APIC 单元,Intel多处理规范的核心就是高级可编程中断控制器(Advanced Programmable Interrupt Controllers–APICs)的使用。 CPU 通过彼此发送中断来完成它们之间的通信。
APIC 可编程中断控制器,它有很多的 IRQ 引脚线,接入了一堆能发出中断请求的硬件设备。当这些硬件设备给 IRQ 引脚线发出一个信号时,由于可编程中断控制器提前被设置好了 IRQ 与中断号之间的对应关系,所以就转化成了对应的中断号,把这个中断号存储在自己的一个端口上,然后给 CPU 的 INTR 引脚发送一个信号,CPU 收到 INTR 引脚信号以后,去刚刚的那个端口上可读取到这个中断号的值.
IRQ 是中断请求(Interrupt Request)的缩写,它在计算机中用来发送一个由其他硬件中断 CPU 的请求。
当一个设备请求中断时,INTR 值是来自各个设备的请求的逻辑或。处理IRQ 所涉及的事件序列:设备引发 IRQ,处理器中断当前正在执行的程序
最终的目标就是让 CPU 知道,有中断需要处理,并且也知道这个中断号是多少
如上图,这些中断值通常被称为中断请求(IRQ)线,每个 IRQ 线都会被关联一个数值,例如:在 PC 机上, IRQ 0 是时钟中断,IRQ 1 是键盘中断
比如:按下了键盘,最终到 CPU 那一边的反应就是得到了一个中断号 0x21
异常机制就非常简单,当 CPU 自身执行指令时检测到的一些反常情况,然后自己给自己一个中断号,无须外界提供
无论是中断还是异常,最终都是通过这种方式得到一个中断号,只不过中断是通过外部设备给 CPU 的 INTR 发送信号,异常是自身执行指令时发现特殊情况触发的,自己给自己一个中断号
另外一种方式可以给 CPU 一个中断号,这个就是大名鼎鼎的 INT 指令
INT 指令后面跟一个数字,相当于直接用指令的形式,告诉 CPU 一个中断号
比如:INT 0x80,就是告知 CPU 中断号是 0x80,它是由 Linux 内核提供的系统调用,就是用了 INT 0x80 这个指令
给 CPU 一个中断号有三种方式
CPU 收到一个中断号 n 以后,会去中断描述符表中寻找第 n 个中断描述符,从中断描述符中找到中断处理程序的地址,然后跳过去执行
更准确的来说,从中断描述符中找到的,并不直接是程序的地址,而是段选择子和段内偏移地址,然后,段选择子又会去全局描述符表(GDT Global Descriptor Table) 中寻找段描述符,从中取出段基地址,再由段基址+段内偏移地址,才能得到最终处理程序的入口地址
若在内存中开启了分页 > Page Cache,还要经历分页机制的转换
在操作系统中所有的地址转换,都需要经过分段机制和分页机制,这不是中断流程所持有的,可以直接理解为中断描述符表中存储的地址,直接当作 CPU 可以跳过去执行的程序入口地址即可。
中断描述符表是什么?
中断描述符表(IDT)本质上就是一个存放在内存中的数组,全称为 Interrupt Descriptor Table
中断描述符是什么?
中断描述符是中断描述符表这个数组里存储的数据结构,它内部存放了段选择子和段内偏移地址
CPU 如何找到中断描述符表?
CPU 提前预留了一个寄存器,叫做 IDTR 寄存器,这里面存放的就是中断描述符表的起始地址以及中断描述符表的大小
如上所说,若忽略分段和分页的处理过程,直接拿到的就是程序入口地址,CPU 在找到了程序的入口地址以后,它如何做的?
CPU 实际上做的事情就是压栈,并跳转到入口地址处执行代码,而压栈的目的就是为了保护现场(原来的程序地址、原来的程序堆栈、原来的标志位)和传递信息(错误码)
以上所说的中断内容都是硬中断,它是由 Intel CPU 硬件实现的中断机制,触发可以通过外部硬件,也可以通过软件的 INT 指令进行触发
与硬中断对应的还有软中断,软中断是纯粹由软件实现的一种类似中断的机制,实际上它就是模仿硬件,在内存的一个地方存储软中断的标志位,然后由内核中的一个线程不断轮询这些标志位,若哪个标志位是有效的,就寻找这个软中断对应的中断处理程序
前面所描述的硬中断并不是硬件中断,硬中断的概念范围更大.
硬中断包括中断、异常及 INT 指令的这种软件中断,整个中断机制是纯硬件实现的逻辑,不管是由谁触发的,都叫硬中断
这里也需要有软件的配合,软件需要提前把中断向量表提前写在内存里,并通过 IDTR 寄存器告诉 CPU 它的起始位置在哪
软中断是纯软件实现的,从宏观和微观层面来说明它们之间的相同点和不同点
中断从宏观层面来看,就是打断当前正在运行的程序,转而去执行中断处理程序,执行完以后再返回到原始程序
从这个层面来看,硬中断可以达到这个效果,软中断也可以达到这个效果,两者效果是一样的
微观层面,两者的处理方式却大有不同
硬中断的微观层面,就是 CPU 会在每一个指令周期的最后,都会留一个 CPU 周期去查看是否有中断,若有中断,就把中断号取出来,去中断向量表中寻找中断处理程序,然后跳过去执行
软中断的微观层面,简单来说会有一个单独的守护进程,不断轮询一组标志位,若哪个标志位有值了,就去这个标志位对应的软中断向量表数组中的相应位置,找到软中断的处理函数(回调函数)然后跳过去执行
软中断就是一组一位一位 bit 的软中断标志位,对应着软中断向量表中一个一个的中断处理函数,然后有一个内核守护进程不断去循环判断调用
软中断是 Linux 中处理一个中断的下半部分的主要方式,比如:Linux 某网卡接收到了一个数据包,此时会触发一个硬中断,由于处理数据包的过程比较耗时,而硬中断资源又非常的宝贵,若占着硬中断函数不返回,会影响其他硬中断的响应速度,假如说同时点击鼠标、按下键盘,所以一般 Linux 会把中断拆分为上下两部分执行,上半部分处理简单的逻辑,将下半部分直接丢给一个软中断去异步处理
为了方便下篇 Epoll 多路复用模型的理解,中断这一块的工作机制需要有一定的理解
当客户端数据到达网卡以后,每个客户端 socketfd 都有各自的 buffer 缓冲区,网卡数据到达以后会有不同的中断标识会交由 DMA 去协调,DMA 此时会经过 kernel,再到 CPU 处基于系统调用所需要处理的函数,进行内核内的处理过程
DMA(Direct Memory Access) 协处理器,它是由内存里开辟的一块区域,可以加速应用程序与内核之间的访问,用来专门处理中断时,不阻塞现有硬件资源的使用,基于信号/异步的方式来告知硬件需要做事情了,你有新的工作需要处理
网卡到来的数据有不同的组装方式交由给 DMA 去协调处理,如下:
应用程序与内核之间的中断,通过 INT 0x80 Hex 等十六进制标志位,将中断标志位给到 CPU 以后,它会根据中断向量表所匹配到的条目,基于中断处理函数去进行函数的处理工作.
中断最终会基于回调的方式,在处理 I/O 时,触发 event 回调事件
在 select/poll 模型之前的回调处理中,只是将网卡发来的数据,走内核网络层协议栈,经过传输控制层、网络层、链路层,最终关联到客户端 socketfd 的 buffer 缓冲区内,所以说,当你某个时间内从应用程序询问 kernel 哪些 fd 可读或可写,会有状态进行返回
若在内核中 event 回调函数处理的时候,再加入一些新的处理逻辑,所能实现的功能就大有不同了,关于下篇的 Epoll 多路复用也就是基于这些新的处理逻辑,来区分它与 select/poll 之间的优势的!!!
该篇博文作为引入 Epoll 多路复用函数前的钩子,先简略得了解中断机制是怎样的「内核主动变为硬件主动」在其中说到了中断的种类:中断、异常,两者的本质都是让 CPU 收到一个中断号,再到中断描述符表找出具体的执行处理的程序入口即可响应事件要处理的事情,再者就是,给 CPU 一个中断号有三种方式:1、通过中断控制器给 CPU INTR 引脚发送信号,也就是硬件中断的方式,2、CPU 执行某条指令时发生了异常,自身会触发一个中断号,3、执行 INT n 指令,会直接给 CPU 一个中断号 n,这就是在软件中断场景下才会发生的,例如某些特定的系统函数调用;注意的是:整个中断机制都是由硬件实现的逻辑,不管是由谁触发的,都叫做硬中断;硬中断会留有一个 CPU 去查看是否有中断,有则处理;软中断会有一个单独的守护进程,不断去轮询标志位组中是否有标志位有值了,有值的情况下就会去中断向量表中找出具体的位置,然后找到软中断对应的回调函数进行执行;关于本篇讲解中断的细枝末节希望您能够喜欢,感谢三连支持!
参考文献:
学习帮助文档:
愿你我都能够在寒冬中相互取暖,互相成长,只有不断积累、沉淀自己,后面有机会自然能破冰而行!
博文放在 网络 I/O 专栏里,欢迎订阅,会持续更新!
如果觉得博文不错,关注我 vnjohn,后续会有更多实战、源码、架构干货分享!
推荐专栏:Spring、MySQL,订阅一波不再迷路
大家的「关注❤️ + 点赞 + 收藏⭐」就是我创作的最大动力!谢谢大家的支持,我们下文见!