说CPU中断前,先举一个生活中的例子,程序员小王正在聚精会神地写代码,项目经理急急忙忙地跑到小王面前,告诉小王,项目上有个很严重的Bug,需要小王赶紧解决掉,小王压了压火气,只能放下手头上的工作赶紧解决这个Bug,当小王正在热火朝天改这个Bug的时候,部门经理老杨找到了小王,说一会儿有个非常重要的会议要参加,小王跟部门经理说,有个严重的Bug要解决,部门经理和项目经理沟通后,项目经理同意这个Bug可以等会后再改,小王赶紧去参加会议了,会议结束后,小王又赶紧解决项目上的Bug,好不容易解决了项目上的Bug,下班时间到了,小王不禁埋怨,一天天的,正经事一点没干,只能明天了继续干了。
人们生活中时时都会出现中断,CPU也一样,CPU在执行一个程序时,往往因为计算机发生了一些更重要的事情,CPU需要把手头上执行的程序放下,转而去解决那件更重要的事情,等那件更重要的事情完成后,再回来执行刚才的程序,这个就叫做CPU中断。
下文关于中断的阐述分成两个部分:一是:中断的分类以及CPU是怎么接受到中断信号的,二是:CPU接受到中断信号后,处理过程是什么
中断的分类以及CPU是怎么接受到中断信号的
中断分为外部中断和内部中断两类,如下图所示
中断的分类
外部中断
外部中断就是来自于CPU外部的中断,外部的中断源来自于CPU外部的某个硬件,所以外部中断也可以成为硬中断,例如网卡从网络上接到一个网络包,会先把这个这个网络包放在网卡的缓冲区,然后就给CPU发出一个外部中断,告诉CPU火速来拿,CPU收到这个信号后,放下手头的工作,把网卡的缓冲数据拷贝到内核的缓冲中,然后CPU再继续干之前的事情。
外部设备通过两个引脚接入到CPU,通过这两个引脚发送外部中断信号给CPU,如下图所示
外部设备中断信号连接线
如上图所示,我们把来自INTR引脚的中断信号称为可屏蔽中断,来自NMI引脚的中断信号称为不可屏蔽中断信号。
不可屏蔽中断信号
在计算机运行过程中,有一些硬件会造成灾难性的错误,例如内存读写错误,电源掉电等,一旦出现这些错误,立马通过NMI中断信号线发送不可以屏蔽中断信号给CPU,无论多少个硬件同时发送不可屏蔽中断信号,CPU接受一个就够了,因为CPU处理完这一个信号后,CPU自己都歇了,其他的信号也就无所谓了。
正因为不可屏蔽中断信号的严重性,所以它的优先级最高,而且单独整一个引脚出来,CPU会第一时间接受NMI引脚的中断信号,这种硬伤不能被屏蔽,CPU会直接忽略寄存器eflags的IF(中断信号位,1表示屏蔽所有设备中断,0表示打开所有设备中断)。
所有不可以屏蔽中断信号的中断向量号为2,这个是CPU内置的。
可屏蔽中断信号
一些设备例如网卡,键盘,鼠标,打印机等,一旦接收到数据后,就会发送中断信号给CPU,中断信号最终通过INTR引脚传入CPU。
可屏蔽中断信号不会影响到系统的运行,甚至CPU拿到这个中断信号后都可以置之不理,假装没看见,CPU可以通过寄存器eflags的IF(中断信号位)来判断是否屏蔽这些外部设备发送过来的中断信号。
由上图所示,INTR线只有一根,一头连着CPU,那另一头连着哪里呢?有人说另一头直接连接到了设备,如果是多个设备呢?
理论上CPU可以直接连接到设备,如果每个设备一个引脚,那CPU得长多少条腿,它的每条腿都接收设备的中断信号,想想看,如果多个设备同时发送过来中断信号呢,CPU该处理哪一个呢,CPU太忙了,太重要了,哪有闲工夫管这些事情,于是CPU找了一个管家即中断代理设备(由8259A中断控制器芯片组合而成),这个中断代理设备装了N多个引脚,它的每个引脚连接到一个设备,每个设备发送中断信号给中断代理设备,由这个代理设备统筹安排哪个设备的中断信号优先发送给CPU。
所以INTR线的另一头连接到了中断代理设备,如下图所示
中断代理设备连接CPU
由上图得知,INTR连接到了一个叫做主8259A的中断代理设备,这个代理设备有八只脚IR0~IR7,理论上可以连接8台设备,那么如果多于8台设备怎么办?答案是将主8259A连接到其他8259A即从8259A上,可以串联多个,如果总共有n个8259A设备,那么可以连接的设备总数就是7n+1,举个例子,如下图为常见的个人计算机的8259A连接图
个人计算机8259A串联
由上图得知,个人计算机上中断代理设备由2个8259A串联组成,最终通过主8259A连接到CPU,所以个人计算机上可以支持的设备总数为7*2+1=15,为啥少了1个呢,因为主8259A的IR2口连接到了从8259A,因此少了一个。
好了,下面开始介绍下8259A的各个组件的作用,如下图
8259A中断代理设备
如上图所示:
INT:8259A选择出优先级最高的中断信号后,通过INT接口发送给CPU。
INTA:INT Acknowledge,中断响应信号。接受来自CPU INTA接口的中断响应信号。
IMR:Interrupt Mast Register,中断屏蔽寄存器,宽度为8位,每个位屏蔽一个设备的中断信号,1表示屏蔽设备中断信号,0表示打开设备中断信号。
IRR:Interrupt Request Register,中断请求寄存器,宽度为8位,每个位表示某个设备是否有中断信号,1表示有中断信号,0表示没有中断信号,设备发送中断后,经过IMR(中断屏蔽寄存器)后,如果该设备中断信号被屏蔽就直接被忽略,否则中断信号通过,8859A就会把IRR(中断请求寄存器)中设备相应位设置为1,所以说IRR就是个待发送的中断信号队列。
PR:Priority Resolver 优先级判别器,当有多个中断发生时,它会将IRR(中断请求寄存器)中的中断信号与当前正在处理的中断信号进行比较,选择出一个更高优先级的中断信号,选择的规则很简单:接口号越小优先级越大,例如IR0-IR7,优先级最大的是IR0,最小的是IR7。
ISR:In-Service-Register 中断服务寄存器,宽度为8位,每个位表示该位对应的设备中断信号是不是正在处理,1表示在处理,0表示还没处理。
好了,8259A讲解到此为止,下面来看看如果同时有3个设备发生中断了后,8259A的处理过程,如下图所示
3台设备发送中断信号
如上图红色接口部分,8259A接口接收到了IR1,IR3,IR4三个接口的中断信号,整个步骤处理过程如下:
1.中断信号经过IMR(中断屏蔽寄存器)后,发现IR3接口对应的位为1,表示该设备被屏蔽,因此只有IR1,IR4接口的中断信号能够通过,IR3接口的中断信号被抛弃。
2.IR1,IR4的中断信号通过后,IR1,IR4接口对应于IRR(中断请求寄存器)的相应位设置为1,如下图红色部分所示
IRR寄存器接受中断信号
3.PR(优先级判别器)在某个时机,从IRR(中断请求寄存器)中获取优先级最高的中断,发现IR1位优先级最高,因此8259A通过控制电路上INT接口发送中断信号给CPU,如下图
PR发送中断信号给CPU
4.CPU拿到中断请求信号后,把手头上的指令完成后,马上发送一个中断响应给8259A,告诉它我已经准备好了,8259A可以继续下面的工作了。
CPU通知8259A,我准备好了
5.8259A收到CPU的中断响应信号后,将ISR(中断服务寄存器)中IR1位设置为1,然后清除IRR(中断请求寄存器)IR1为,表示当前开始处理IR1中断信号了,如下图所示
设置ISR的IR1为1和清除IRR的IR1为0
6.CPU再次通过INTA发送信号给8259A,这次是告诉8259A,我要获取刚才优先级最高的中断信号的中断向量(一个整数)。
7.8259A收到了这个请求后,会将起始中断向量号+IR接口号,例如8259A的起始地址为10,IR1的接口号为1,那么中断向量号就是10+1=11,起始中断向量号是计算机安装的时候,每个8259A分配一个,不重复,岔开的,然后8259A通过数据总线发送中断向量号给CPU,如下图所示
8259A发送中断向量号给CPU
8.CPU拿到这个中断向量号后,开始根据这个中断向量号找到中断处理程序,执行中断处理程序,这个后面会阐述整个中断处理过程。
9.上述步骤完成后,那么什么时候,ISR(中断服务寄存器)的IR1清0呢即什么时候知道中断完成了,这里分两种情况,第一种情况是自动的,就是一旦第6个步骤完成后即CPU发送第二个INTA请求(索要中断信号)后,8259A就会将ISR(中断服务寄存器)的IR1清0,表示8259A认为中断已经处理完了,这有点类似异步操作,不等中断处理程序的结果,第二种情况是手动的,需要等待中断处理程序执行完成后,由中断处理程序手动发送一个EOI(End Of Interrupt)信号表示中断处理程序执行完了,8259A收到这个EOI信号后,就会将ISR(中断服务寄存器)的IR1清0,这有点类似同步操作,必须等中断处理程序的结果。
上述9个步骤就是外部设备通过8259A中断代理设备发送可屏蔽信号给CPU的过程,当然实际情况比这个复杂些,比如有更高优先级的中断来了,那个正在处理的的中断信号就会被中断,整个过程与1-9步骤的类似,可能就是从1-9步骤选取几个步骤执行一次就可以了,这里不再阐述。
好了,外部中断的两种中断信号,不可屏蔽中断信号和可屏蔽中断信号阐述完成,现在来看看内部中断。
内部中断
内部中断即CPU内部的中断,它包括软件中断和异常两类。
软件中断:
软件中断就是软件主动发起的中断,有一些常见的指令可以发起软中断
指令 | 说明 |
int 中断向量号 | 这种指令我们经常用,比如系统调用就是int 0x80,中断向量号范围为0~255 |
int3 | 这个是调试中断指令,我们平时写程序打断点时,通过该指令可以将程序停下来查看程序的变量和栈什么的。 |
into | 中断溢出指令,它的中断向量号为4,调用该指令后会检查eflags的OF位,如果为1则发送中断信号。 |
bound | 数组边界检查指令,一旦发现数组越界,就会发送中断向量号5给CPU |
异常
CPU内部执行过程中发生了错误,就会产生异常中断,其实上面的int3,into,bound虽然是软件主动发起的,属于软件中断,但其实这些中断都产生了错误,也可以属于异常中断。
异常中断不能被屏蔽,既然已经发生了错误,就要勇于承认错误,不能视而不见,异常中断也会忽略eflags寄存器的IF位。
举个例子,CPU执行除法指令时,发现分母为0,那就会触发异常中断,它的中断向量号为0,还有就是CPU执行指令时,发现指令编码不存在或者指令格式是错误的就会发送6号异常中断。
并不是所有的异常都是致命的,按照轻重缓急可以分为以下三种
Fault | 故障,这种异常可以被修复,经过中断处理程序的修复后,可以进行重试,回到异常发生的地方进行重试,例如缺页异常,发现页不在内存中,中断处理程序把页调入内存后,程序还可以继续运行。 |
Trap | 陷阱,软件运行过程中突然掉入了CPU挖的陷阱里,就会触发异常,停下来,例如int3 调试程序就是这样,此种异常中断处理程序执行后,往往会返回到导致陷阱异常的指令的下一个地址继续执行,比如我们在第10行打的断点,程序执行到第10行时,就会发生陷阱中断,然后进入到中断处理程序即我们开始调试程序,一旦调试程序执行完成后,就会从第11行开始继续执行。 |
Abort | 终止,这是最严重的异常类型,一旦出现,无法修复,操作系统将出现该异常的程序干掉 |
好了,中断的分类阐述完了,开启下一章,中断信号的处理过程。
CPU接受到中断信号怎么处理?
无论是外部中断(不可屏蔽中断信号+可屏蔽中断信号)还是内部中断(软件中断+异常)都会被分配一个中断向量号,中断向量号是一个整数,范围在0~255,如下表所示
向量号 | 中断原因 | 类型 |
0 | 除法运算,分母为0 | 异常(Fault) |
1 | 调试程序 | 异常(Fault/Trap) |
2 | 来自NMP信号线,不可以屏蔽中断 | 不可以屏蔽中断 |
3 | 断点调试,通过 INT3指令触发 | 异常(Trap) |
4 | 溢出,通过指令INTO触发 | 异常(Trap) |
5 | 数据越界,通过bound指令触发 | 异常(Fault) |
6 | 非法指令操作码 | 异常(Fault) |
...... | ........................................................... | ....... |
20-31 | 保留 | |
32-255 | 来自外部中断或者int 中断向量号指令 | 可屏蔽中断信号或者软件中断 |
上面的表中指明了中断向量号的范围为0~255,操作系统一般就支持这么多中断。
好了,步入正题,开始介绍当CPU接受到中断向量号后,怎么处理中断,主要分为两部分,第一部分为根据中断向量号定位中断处理程序,第二部分执行中断处理程序并返回。
中断向量号定位中断处理程序
在讲解中断向量号定位中断处理程序这个过程前,先了解下中断描述符表和全局描述符表,这两个了解清楚了,定位的过程就很简单了。
中断描述符表
中断描述表存储在内存中,它在内存的起始地址存储在IDTR即中断描述表寄存器,如下图
中断描述符表寄存器
如上图所示,0~15表示中断描述符表最多占用多少个字节,总共占用了16位就是65536字节,一个门描述符占用8个字节,那么就是65536/8=8192个,中断描述符表最多存储8192个门描述符,目前的操作系统只支持256项,15~47为中断描述符表在内存中的起始地址,总共占用32位。
中断描述表的每一项叫做门描述符,每个门描述符占用8个字节即64位,门描述符分为4类即任务门描述符,调用门描述符,中断门描述符,陷阱门描述符,任务门描述和调用门描述符,在中断描述表中用的非常少,这里就不做介绍,主要阐述中断门描述符合陷阱门描述。
门就是一个通往一段程序代码的大门,对于中断门和陷阱门来说,这段程序代码就是中断处理程序,之所以为门,就是要对进入门的的当前程序做检查,检查通过了才可以打开这扇门,就好像你要进入一个小区,先的经过业主和保安的同意才可进入小区,当前程序被中断后,应该打开中断处理程序这扇门,在打开之前,就要检查当前程序是否有权限打开,而门描述符主要描述了打开这扇门需要的权限以及这扇门通往的中断处理程序的地址。
下面来看看中断描述符和陷阱门描述的格式,如下图
中断门描述符
陷阱门描述符
如上图所示,中断门描述符和陷阱门描述符基本相同,不同的在于高32位中的8~10位,中断描述符的8~10位为110,陷阱门描述符的8~10位111,其他的字段都一样,8~10位主要是区分门描述符的类型。
其他位介绍如下:
P:表示目标段是否在内存中,一直为0
DPL:访问门的特权级,有0,1,2,3四个等级,0表示内核级,3表示用户级,数字越小权限越大,如果当前程序的特权级大于DPL,那么当前程序就没有权限进入门,CPU就会报异常。
D:D=1表示当前寄存器采用32位,0表示当前寄存器采用16位
高32位的16~31位和低32位的0~15:合并起来就是目标段的偏移量,总共32位。
低32位的16~31:目标段的选择子,什么是选择子呢?选择子就是16位的索引,如下图所示
选择子
由上图所示3~15位总共13位,它表示全局描述符表的索引,指向了全局描述表的某个段描述符,RPL位0~2表示请求特权级,同DPL一样,分为4个等级0,1,2,3,0表示内核级,3表示用户级,数字越小权限越大。
好了,中断描述符表阐述完成,来看看全局描述符表
全局描述符表
应用程序和操作系统加载到内存中后,会存储在一个段里,段分为代码段,数据段,系统段,系统段属于硬件范畴,这里不再阐述。
全局描述符表(GDT)由段描述符组成,每个段描述符占用8个字节即64位,段描述符主要用来描述内存中各类段的起始地址,权限,段的大小,特权级等。
全局描述表存储在内存中,全局描述表的起始地址存储在全局描述表寄存器即GDTR,如下图所示
全局描述表寄存器
由上图可以看出,0~15位共16位表示全局描述符表最多占用多少个字节,算下来是65536,每个段描述符是8个字节,那么全局描述表最多能有65536/8=8192个表项即最多能有8192个段,15~47共32位表示全局描述符表在内存中的起始地址。
全局描述符表怎么起作用的呢,举个例子:
在CPU保护模式下,CPU执行指令时,会从程序计数器(IP/EIP)拿出指令的地址即段内偏移地址,然后再从段寄存器(CS)拿出选择子(上文已经阐述),通过选择子的高13位即全局描述表索引查找全局描述符表,定位到某个段描述符,然后就可以从这个段描述符中找到段的起始地址,然后段的起始地址+段内偏移地址就定位到了内存的地址,接着就可以执行指令了。
上面的例子就是全局描述符表的一个应用场景。
下面来看看段描述符的结构,如下图:
由上图所示,阐述各个字段的含义
段基址:低32位的16~31,高32位的0~7,高32位的24~31,组成32位的段基址,真是不容易啊,这个段基址计算出来后,放入到了段寄存器(CS,DS,SS)。
段界限:低32位的15~0和高32位的19~16,组成20位的段界限,段界限就是段的大小,段界限有两种单位即4KB和1字节,用哪个单位由G位(高23位)决定,当程序访问的段内偏移量超过这个段界限后,CPU会报异常。
G:决定段界限的单位,如果G为0,段界限的单位为字节,如果G为1,段界限的单位为4KB。
TYPE:高32位的8~11位,表示段描述符的类型,由S位(高12位)决定它的实际含义,S位为0表示系统段,S位为1,表示代码段或者数据段,如果S位为1,TYPE表示通过4个位的不同组合,即可以表示段氏是代码段还是数据段,也可以表示段的各类权限,例如可读,可写,可执行等。
对于代码段,8~11位表示为XRCA,对于数据段表示为XWEA,其中X表示是否可执行(1表示可执行,表示该段位代码段),R表示是否可读(1表示可读),W表示是不是可写(1表示可写),C表示是否保持一致性(1表示一致性),A表示段是否被访问过(1表示已被CPU访问),E表示数据段是否向下扩展(1表示向下扩展即该数据段是栈)
DPL:高32位的14~13位表示段的特权级,分别为0,1,2,3,数字越小,权限越大,0表示内核级,3表示用户级。
AVL:高32位的20位,表示段是否可用,没啥用处。
L:高32位的21位,L为1表示64位的代码段,0表示为32位的代码段,目前默认都是0。
D/B:高32位的22位,
对于代码段:
如果为0,表示指令寄存器为16位即IP,如果为1,表示指令寄存器为32位即EIP。
对于栈段:
如果为0,表示栈指针寄存器为16位(SP),如果为1栈指针寄存器为32位(ESP)。
P:高32位的的第15位,表示段是否在内存中,默认为1,为0表示段不在内存中,则CPU抛出异常。
好了,全局描述表介绍到此,下面正式开始阐述根据中断号定位中断驱动程序,如下图所示:
中断信号定中断处理程序的过程
由上图得知根据中断信号定位中断处理程序经过了10个步骤,有了上文中断描述符表(IDT)和全局描述符表的介绍,这个过程就简单多了,步骤如下:
1.中断描述表的起始地址加上中断向量号乘以8(一个门描述符占用8个字节)即可定位到该中断向量号对应的门描述符。(1~3)
2.找到了门描述符后,这个时候就会根据中断向量号判断该中断是不是属于软中断,如果是软中断(即int 指令,int3,into等这些都是由当前用户程序主动发起的中断),则需要检查当前用户程序是否有权限打开这扇门,怎么比较呢?就是比较当前用户程序的特权级(CPL)和门描述符的特权级(DPL),如果CPL大于DPL,则表示当前程序没有打开门的权限,例如当前程序的特权级为3,门描述符的特权级为0,我们知道特权级越大,权限越小,因此3的权限反而小于0,所以CPU会报出异常,否则CPL<=DPL,那表示当前程序有打开门的权限,CPU就放行了,对于外部中断和异常类中断,这些是不用检查是否有打开门的权限,一律认为有权限。(4)
3.找到了门描述符后,门的权限也有了,下一步就根据门描述符找到中断处理程序目标代码段的选择子,通过这个选择子的高13位(索引值)乘以8(每个段描述符占用8个字节),然后在从全局描述符表寄存器取出全局描述符表的起始地址,两个相加就定位到了段描述符。(5~6)
4.现在门打开了,段描述符也找到了,下一步就马上要执行中断处理程序,不过在那之前,需要比较当前程序的特权级(CPL)和段描述符的特权级 (DPL),这里主要看看当前程序的特权级是不是比段描述符的特权级(DPL)大,如果CPL>DPL,这意味着当前程序的权限比段描述符的权限小,毕竟特权级越大,权限越小,这个时候CPU就会通过,否则CPU就会报异常,禁止执行中断处理程序,因为CPU要求,中断处理程序执行必须由是由更低权限的程序引起的,中断处理程序调用完成后返回时,则必须从中断处理程序(高权限 )返回到更低权限程序,举个例子就好像人爬坡和下坡,人都是从低到高爬上去,然后才能从高到低再爬下来,中断也类似爬坡和下坡,调用中断处理程序就是爬坡,中断处理程序调用完成后再返回就是下坡。
5.好了,段描述符找到了,段描述符的权限也通过,根据段描述符里可以找到段的起始地址,然后再从中断描述符中获取段内偏移地址,两者一相加即使中断处理程序的第一条指令的地址,下面就开始中断程序的执行过程了。
中断处理程序的执行过程
中断处理程序的执行就比较简单了,一般来说,中断处理程序分为两部分即上部分和下部分,上部分一般是不可以被中断的,下部分就随意了,随时可以被其他更高优先级的中断打断,举个例子网卡接受到网络流量包后,会放入到网卡自己的缓冲中,这个缓冲是很小的,所以网卡会发起中断,告诉CPU赶紧拿走,CPU收到中断信号后,开始执行中断处理程序,这个中断处理程序的上部分就是先屏蔽中断,将网卡缓冲区的数据拷贝到内核缓冲区,然后上半部分执行完了,下部分就无所谓了,把中断打开,其它更高优先级的中断就可以打断它了吧。
中断处理程序执行时,有以下步骤:
1.将被中断的程序的地址如cs(代码段寄存器),ip(程序寄存器)推入到中断处理程序的栈中
2.将flags(CPU标识寄存器)推入到中断处理程序的栈中
3.如果被中断的程序和中断处理程序在不同特权级,那么还得将被中断程序的ss(栈段寄存器)和栈指针寄存器sp推入到中断处理程序的栈中。
4.如果有上部分,开始执行中断处理程序的上部分
5.执行中断程序的下部分.
6.从中断处理程序的栈中推出和恢复cs,ip,flags,如果被中断的程序和中断处理程序在不同特权级,则推出和恢复ss和sp。
7.如果有必要,发送EOI通知,通知中断代理设备中断处理完成了,让中断代理设备继续下面的工作。
8.调用iret返回到被中断程序,被中断程序继续执行
好了,整个CPU中断就阐述完了。