中断的基本概念
- 程序中断是指在计算机执行现行程序的过程中,出现某些急需处理的异常情况或特殊请求,CPU 暂时中止现行程序,而转去对这些异常情况或特殊请求进行处理,在处理完毕后 CPU 又自动返回到现行程序的断点处,继续执行原程序
- 用生活中的例子来理解,比如你正在用手机玩游戏,这时候突然来电话了,玩游戏就被电话给中断了,于是,你需要先接听电话,挂了电话后又继续玩游戏
- 我们接下来讲的中断都是针对 x86 处理器(对于其它处理器也是大同小异)
中断的分类
- 在 x86 系列处理器中,中断的分类如下:
内部中断
- 所谓的内部中断,是在 CPU 内部产生并进行处理的。比如:
- CPU 遇到一条除以 0 的指令时,将产生 0 号中断,并调用相应的中断处理程序
- CPU 遇到一条不存在的非法指令时,将产生 6 号中断,并调用相应的中断处理程序
- 对于内部中断,有时候也称之为异常
- 软中断也属于内部中断,是非常有用的,它是由 int 指令触发的。比如 int3 这条指令,gdb 就是利用它来实现对应用程序的调试(这里就不深入研究了)
外部中断
- 所谓的外部中断,是在 CPU 外部产生
- x86 CPU 上有 2 个中断引脚:INT 和 INTR,分别对应:不可屏蔽中断和可屏蔽中断
- 所谓不可屏蔽,就是说:中断不可以被忽视,CPU 必须处理这个中断,如果不处理,程序就没法继续执行
- 而对于可屏蔽中断,CPU 可以忽略它不执行,因为这类中断不会对系统的执行造成致命的影响
中断号
- 中断号在有的也叫中断向量号,在 x86 处理器中,一共支持 256 个中断,每一个中断都分配了一个中断号,从 0 到 255
- 其中,0 ~ 31 号中断向量被保留,用来处理异常和非屏蔽中断(其中只有 2 号向量用于非屏蔽中断,其余全部是异常)
- 当 BIOS 或者操作系统提供了异常处理程序之后,当一个异常产生时,就会通过中断向量表找到响应的异常处理程序
- 从中断号 32 开始,全部分配给外部中断,比如:系统定时器中断 IRQ0,分配的就是 32 号中断;Linux 的系统调用,分配的就是 128 号中断
中断向量表和中断服务程序
- 当一个中断发生的时候,CPU 获取到该中断对应的中断号,下一步就是要确定调用哪一个函数来处理这个中断,这个函数就称作中断服务程序(Interrupt Service Routine,ISR),有时候也称作中断处理程序、中断处理函数,本质都一样
- 中断向量表(IVT,Interrupt VectorTable)中断号和中断处理函数之间的重要桥梁
- 从图中可以看出,每一个中断向量都指向对应的中断处理函数,我们把中断向量当做函数指针(中断服务程序入口地址)
- 中断向量表英文名:Interrupt VectorTable (缩写:IVT)
- 中断向量表的本质:就是一段从实际物理地址 0x0 开始,连续 1024 个字节的内存空间(每个中断向量占 4 个字节(2 个字节的段地址,2 个字节的偏移地址),共 256 个中断向量),我们可以把它当成一个 C 语言数组:unsigned int VectorTable[256],数组的每个单元中存放一个中断服务程序的入口地址
- 当中断发生时,CPU 是怎么找到对应得中断服务程序的呢?
- 当中断发生后,CPU 会根据自己产生的中断号自动从中断向量表中找到对应的中断向量(类似于数组访问 VectorTable[中断号]),然后跳转到这个中断向量指向的地址处执行(函数指针)
- 中断号的产生和跳转都是 CPU 实现,软件只负责中断向量表的构建和中断服务程序具体实现即可
中断现场的保护和恢复
- 当一个中断发生的时候,肯定有一个正在执行的程序被打断,当中断处理函数执行结束之后,这个被打断的程序需要从刚才被打断的地方继续执行(暂时先不要考虑从中断返回点,进行多任务切换的事情)
- 而一个程序执行的上下文环境,就是处理器中的各种寄存器内容:代码段寄存器 cs,指令指针寄存器 sp,标志寄存器 EFLAGS
- 但是,在中断处理程序中,也需要使用这些寄存器
- 处理器中的这些寄存器,就是每一个程序执行时上下文信息的存储容器,当然也包括中断处理程序!
- 因此,在进入中断处理程序之前,CPU 会自动的把这些寄存器 push 到栈中保存起来,然后再跳转到中断处理程序中去执行
- 当中断处理程序执行结束后,CPU 会从栈中弹出这些内容,恢复到相应的寄存器中,于是被打断的程序就可以继续执行了
中断处理程序的安装
- 既然通过中断向量,找到了中断处理程序,那么这些中断处理程序都是谁放在内存中的呢?
- 如果您看过一些比较底层的计算机书籍,就能看到一般都会举例:如何手动的把一个普通函数设置为一个中断处理函数
- 操作步骤是:
- 在代码中,写一个普通函数
- 把这个函数的指令码,搬运到内存中的某一个位置
- 把这个位置(段地址:偏移量),作为一个中断向量,设置到中断向量表中
- 此时,如果发生了该中断,你所提供的函数就作为中断处理函数被执行了
- 当然了,在一个计算机系统中,BIOS、操作系统和各种外设,会自动为我们提供很多基本的中断处理函数的
- 比如:BIOS 中就提供了软中断、内部中断、硬件中断等处理函数,这些函数是固化在 BIOS 的代码中的(映射到 BIOS 所在的 ROM 芯片上),BIOS 只需要把这些处理函数的地址,写入到中断向量表中的相应位置即可
- 内存中的某些位置是映射到外设的 ROM,在这些外设的 ROM 中也存在一些外设自带的程序。BIOS 在启动时,会扫描这些映射到外设的内存空间,通过某些关键字信息,如果发现外设有自带的程序,就会去执行。这些外设程序一般是进行一些自身的初始化,并填写相关的中断向量表,使它们指向外设自带的中断处理程序
- 对于操作系统来说就更不用说了,它会重新安排自己需要的中断处理函数,这部分内容接下来我们就会介绍到
保护模式下的中断
- 注意了,前面所说的中断都是实模式下的中断处理方式,中断向量表的建立以及中断服务程序的实现全部是由 BIOS 完成。因此我们在实模式下才能直接使用 int 中断
- 疑问:既然中断是由 BIOS 实现的,那么为啥还要费那么多力气讲解呢?
- 中断不仅实模式下有,保护模式下也有,操作系统自古以来就是中断驱动的,有了实模式下的中断讲解,我们才能更好的理解保护模式下的中断实现
- 有一个疑问:既然实模式下已经有中断处理了,为啥保护模式下又要实现一遍?
- 原因是保护模式下有了保护机制,我们不能够直接访问内存了,中断处理也必须融入保护的思想,而这种思想的体现就是中断描述符表
中断描述符表
- 在保护模式下,中断描述符表取代了实模式下的中断向量表
- 中断描述符表(Interrupt Descriptor Table,IDT)是保护模式下用于存储中断处理程序入口的表,当 CPU 接收一个中断时,需要用中断向量在此表中检索对应的描述符,在该描述符中找到中断处理程序的起始地址,然后执行中断处理程序
- 中断描述符表中有什么?
- 哈哈,你肯定会说,中断描述符表中当然是中断描述符啦。其实中断描述符表可以包含中断门,陷阱门和任务门三种描述符
- 门,顾名思义,是通往某处的入口,在计算机中,用门来表示一段程序的入口。拿它和段描述符对比一下就容易理解了,段描述符中描述的是一片内存区域,而门描述符中描述的是一段代码
- 中断描述符表(IDT)中存放的三种类型的门描述符格式
- 下面简要说下这几种门描述符
- 任务门:任务门和任务状态段(Task Status Segment,TSS)是 Intel 处理器在硬件一级提供的任务切换机制,所以任务门需要和 TSS 配合在一起使用,在任务门中记录的是 TSS 选择子,偏移量未使用。任务门可以存在于全局描述符表 GDT、局部描述符表 LDT、中断描述符表 IDT 中。描述符中任务门的 type 值为二进制 0101。顺便说一句大多数操作系统(包括 Linux)都未用 TSS 实现任务切换,咱们这里也不讨论啦
- 中断门:中断门包含了中断处理程序所在段的段选择子和段内偏移地址。当通过此方式进入中断后,标志寄存器 eflags 中的 IF 位自动置 0,也就是在进入中断后,自动把中断关闭,避免中断嵌套。Linux 就是利用中断门实现的系统调用,就是那个著名的 int 0x80。中断门只允许存在于 IDT 中。描述符中中断门的 type 值为二进制 1110
- 陷阱门:陷阱门和中断门非常相似,区别是由陷阱门进入中断后,标志寄存器 eflags 中的 IF 位不会自动置 0。陷阱门只允许存在于 IDT 中。描述符中陷阱门的 type 值为二进制 1111
- 回顾一下,我们前面是不是学过调用门,跟上面的三种门有啥关系呢?其实没啥关系,仅仅只是描述符的数据格式相近罢了。现在我们再来回顾一下调用门
- 调用门是提供给用户进程进入特权 0 级的方式,其 DPL 为 3。它不能用 int 指令调用,只能用 call 和 jmp 指令。调用门可以安装在 GDT 和 LDT 中。描述符中调用门的 type 值为二进制 1100
保护模式下的中断处理
- 处理器对异常和中断处理过程的调用操作方法与使用 CALL 指令调用程序过程和任务的方法类似。当响应一个异常或中断时,处理器使用异常或中断的向量作为 IDT 表中的索引,在 IDT 表中找到对应的中断描述符后,该描述符中仅有偏移值,想要找到一个程序的确定地址,肯定还需要段基址啊,那么段基址从哪来呢?我们又看到,该描述符中包含了选择符,这个选择符就是中断描述符表在描述符表 GDT 或 LDT 中对应的选择符,于是,由这个选择符就找到了 GDT 或 LDT 中对应的段描述符,从段描述符中获得段基址。此时段基址,偏移值是不是都有了,于是 CPU 不就能找到 [段基址:偏移值] 所指向的中断服务程序入口了嘛。当然了,在跳转之前肯定还有属性,特权级检查等保护机制啦
- 对比中断向量表,中断描述符表有两个区别
- 中断描述符表地址不限制,在哪里都可以
- 中断描述符表中的每个描述符用 8 字节描述
- 不管是实模式还是保护模式,当中断发生后,中断源所对应的中断号是固定的,所以中断向量表和中断描述符表中顺序是一致的,这是由 CPU 决定的,软件必须根据硬件规定构建中断向量表或中断描述符表
- IDT 中必须提供每种中断所对应的中断服务程序(ISR)入口地址
总结
- 从功能的角度看,中断有 2 个作用:
- 提供执行异步序列的机制
- 给应用程序提供进入系统层的入口
- 关于第 2 点,Linux 中也是通过 int 0x80 中断,让应用层的程序有机会进入到系统代码中去执行
- 因为应用层与操作系统层的代码,是工作在不同的安全级别。为了系统的安全,Linux 操作系统提供了这样的一个机制,让低安全级别的应用程序,进入到高安全级别的操作系统代码中去执行,毕竟所有的硬件等系统资源都是由操作系统来统一管理的
- 我们再从中断处理程序的安装角度来看,中断本质上就是增加了一层间接性:通过固定位置的中断向量表,让中断处理函数的实际地址可以被动态的放在任意位置