中断向量和中断服务例程的对应关系主要是由 IDT(中断门描述符表)来描述。操作系统在IDT 中设置好各种中断向量对应的中断描述符,而中断描述符指出了中断服务例程的起始地址,留待 CPU 在产生中断后查询对应中断服务例程的起始地址。而 IDT 本身的起始地址保存在 IDTR 寄存器中。
80386 共支持 256 种中断,其中故障(Fault)和陷阱(Trap)由 CPU 自身产生,不使用中断控制器,也不能被屏蔽。外设中断又分为可屏蔽中断(INTR)和非屏蔽中断(NMI),I/O设备产生的中断请求(IRQ)引起可屏蔽中断,而紧急的外设事件(如掉电故障)引起的中断事件引起非屏蔽中断。
非屏蔽中断和异常的编号是固定的,而屏蔽中断的编号可以通过对中断控制器的编程来调整。256 个中断的分配如下:
0~31 号的中断对应于故障、陷阱和非屏蔽外设中断。
32~47 号的中断分配给可屏蔽外设中断。
48~255 号的中断可以用软件来设置。比如 ucore 可用其中的一个中断号来实现系统调用。
在中断产生过程中,中断控制器 8259A 监视外设产生的中断请求(IRQ)信号,如果外设产生了一个中断请求信号,则 8259A 执行如下操作:
a) 把接受到的 IRQ 信号转换成一个对应的中断编号;
b) 把这个中断编号值存放在中断控制器的一个 I/O 地址单元中,CPU 通过数据/地址总线可访问到此 I/O 地址单元;
c) 给 CPU 的 INTR 引脚触发信号,即发出一个中断;
d) 等待直到 CPU 通过 INTA 引脚确认这个中断信号,清除 INTR 引脚上的触发信号。
屏蔽外部 I/O 请求有两种方法。
一种是从 CPU 的角度清零 CPU 的 EFLAG 的中断标志位(IF);
另一种是从中断控制器的角度,即通过把中断控制器中的中断屏蔽寄存器(IMR)相应位置1,则表示禁用某条中断线。
中断服务例程包括具体负责处理中断(异常)的代码是操作系统的重要组成部分。需要注意区别的是,有两个过程由硬件完成:
硬件中断处理过程 1(起始):从 CPU 收到中断事件后,打断当前程序或任务的执行,根据某种机制跳转到中断服务例程去执行的过程。其具体流程如下:
- CPU 在执行完当前程序的每一条指令后,都会去确认在执行刚才的指令过程中中断控制器(如 8259A)是否发送中断请求过来,如果有那么 CPU 就会在相应的时钟脉冲到来时从总线上读取中断请求对应的中断向量;
- CPU 根据得到的中断向量(以此为索引)到 IDT 中找到该向量对应的中断描述符,中断描述符里保存着中断服务例程的段选择子;
- CPU 使用 IDT 查到的中断服务例程的段选择子从 GDT 中取得相应的段描述符,段描述符里保存了中断服务例程的段基址和属性信息,段描述符的基址+中断描述符中的偏移地址形成了中断服务例程的起始地址;
- CPU 会根据 CPL 和中断服务例程的段描述符的 DPL 信息确认是否发生了特权级的转换。比如当前应用程序正运行在用户态,而中断服务例程是运行在内核态的,则意味着发生了特权级的转换,这时 CPU 会从当前应用程序的 TSS 信息(该信息在内存中的起始地址存在 TR 寄存器中)里取得该程序的内核栈地址,即包括内核态的 ss 和 esp 的值,并立即将系统当前使用的栈切换成新的内核栈。这个栈就是即将运行的中断服务程序要使用的栈。紧接着就将当前程序使用的用户态的 ss 和 esp 压到新的内核栈中保存起来;如果当前程序运行在内核态,则不会发生特权转移,
5)CPU 需要开始保存当前被打断的用户态程序的现场(即一些寄存器的值),以便于将来恢复被打断的程序继续执行。这需要利用内核栈来保存相关现场信息,即依次压入当前被打断程序使用的 eflags,cs,eip,errorCode(如果是有错误码的异常)信息;
6)CPU 把中断服务例程的地址加载到 cs 和 eip 寄存器中,开始执行中断服务例程。这意味着先前的程序被暂停执行,中断服务程序正式开始工作。
硬件中断处理过程 2(结束):每个中断服务例程在有中断处理工作完成后需要通过 iret(或 iretd)指令恢复被打断的程序的执行。CPU 执行 IRET 指令的具体过程如下: - 程序执行这条 iret 指令时,首先会从内核栈里弹出先前保存的被打断的程序的现场信息,即 eflags,cs,eip 重新开始执行;
- 如果存在特权级转换(从内核态转换到用户态),则还需要从内核栈中弹出用户态栈的 ss 和 esp,这样也意味着栈也被切换回原先使用的用户态的栈了;
- 如果此次处理的是带有错误码(errorCode)的异常,CPU 在恢复先前程序的现场时,并不会弹出 errorCode。这一步需要通过软件完成,即要求相关的中断服务例程在调用 iret 返回之前添加出栈代码主动弹出 errorCode。