进入32位保护模式(四):中断和进程

中断

中断就是打断处理器当前的执行流程,去执行另外一些和当前工作不相干的指令,执行完之后,还可以返回到原来的程序流程继续执行。中断是处理器必备的机制,下面就来简单说一下中断的发生以及处理过程。

外部硬件中断

从处理器外部产生的中断叫外部中断。事有轻重缓急,中断也分为两种,一种是必须要马上处理的,叫做非屏蔽中断(Non Maskable Interrupt,NMI)。另外一种可以允许延迟处理,叫可屏蔽中断(Interrupt,INTER)。这两种中断是各通过一条信号线引入处理器内部,所以总共有两条中断信号线接入处理器。

当中断发生时,处理器通过中断引脚NMI和INTR得到通知。此外它还应当知道发生了什么事,以便采取适当的处理措施。每种类型的中断都被统一编号,这称为中断类型号、中断向量或者中断号。

32Model_4_1

非屏蔽中断

由于不可屏蔽中断的特殊性——几乎所有触发NMI的事件对处理器来说都是致命的,也不可恢复。所以这种情况下,就没有抢救的必要了。所以在实时模式下NMI中断的中断号统一都是2,不再区分更详细的情况。

可屏蔽中断

可屏蔽中断通过INTR引脚进入处理器,处理器每次只能处理一个中断。而且多个设备同时发出中断请求的几率也是很高的,所以需要一个中断代理来处理多设备以及并发仲裁的问题。在个人计算机中,用得最多的中断代理就是8259芯片,即中断控制器,从8086处理器开始,它就一直提供着这种服务。即使是现在,在绝大多数单处理器的计算机中,也依然有它的存在。

Intel处理器允许256个中断,中断号的范围是0~255,8259负责提供其中的15个,但中断号并不固定,允许软件根据自己的需要灵活设置中断号,以防止发生冲突。该中断控制器芯片有自己的端口号,可以像访问其他外部设备一样用in和out指令来改变它的状态,包括各引脚的中断号。所以它又叫可编程中断控制器。

每片8259只有8个中断输入引脚,而在个人计算机上使用它,需要两块。如图所示,第一块8259芯片的代理输出INT直接送到处理器的INTR引脚,这是主片(Master);第二块8259芯片的INT输出送到第一块的引脚2上,是从片(Slave),两块芯片之间形成级联(Cascade)关系。

32Model_4_2

如此一来,两块8259芯片可以向处理器提供15个中断信号。当时,接在8259上的15个设备都是相当重要的,如PS2键盘和鼠标、串行口、并行口、软磁盘驱动器、IDE硬盘等。现在,这些设备很多都已淘汰或者正在淘汰中,根据需要,这些中断引脚可以被其他设备使用。

如图所示,8259的主片引脚 0(IR0)接的是系统定时器/计数器芯片;从片的引脚 0(IR0)接的是实时时钟芯片RTC。在8259芯片内部,有中断屏蔽寄存器(Interrupt Mask Register,IMR),这是个8位寄存器,对应着该芯片的8个中断输入引脚,对应的位是0还是1,决定了从该引脚来的中断信号是否能够通过8259送往处理器(0表示允许,1表示阻断,和一般的逻辑是反过来的)。当外部设备通过某个引脚送来一个中断请求信号时,如果它没有被IMR阻断,那就可以被送往处理器。8259的主片的端口号是0x20和0x21,从片的端口号是0xa0和0xa1,可以通过这些端口访问8259芯片,设置它的工作方式,包括IMR的内容。

中断能否被处理,除了看8259芯片外,还取决于处理器。在处理器内部,标志寄存器有一个标志位IF,这就是中断标志(Interrupt Flag)。当为0时,所有从处理器INTR引脚来的中断信号都被忽略掉;当其为1时,处理器可以接受和响应中断。IF标志位可以通过两条指令 cli / sti 来关闭 / 开启中断。

中断向量表

中断处理程序本质就是处理器要执行一段与该中断有关的程序。处理器可以识别256个中断,那么理论上就需要256段程序。这些程序的入口点组成一个数组,放在一个指定的位置。这个由入口点组成的数组就叫中断向量表。在实模式下,中断向量表从物理地址0x00000开始,到0x003结束,共IKB的空间。在保护模式下,IDT寄存器保存了中断向量表的起始地址。

内部中断

内部中断发生在处理器内部,是由执行的指令引起的。比如,当处理器检测到div或者idiv指令的除数为零时,或者除法的结果溢出时,将产生中断 0(0号中断),这就是除法错中断。

再比如,当处理器遇到非法指令时,将产生中断6。非法指令是指指令的操作码没有定义,或者指令超过了规定的长度。操作码没有定义通常意味着那不是一条指令,而是普通的数。

内部中断不受标志寄存器IF位的影响,也不需要中断识别总线周期,它们的中断类型是固定的,可以立即转入相应的处理过程。

软中断

软中断是由int指令引起的中断处理。这类中断也不需要中断识别总线周期,中断号在指令中给出。

1
2
3
int3
int imm8
into

int3是断点中断指令,机器指令码为CC。这条指令在调试程序的时候很有用。指令都是连续存放的,所谓的断点,就是某条指令的起始地址。int3是单字节指令,这是有意设计的。当需要设置断点时,可以将断点处那条指令的第1字节改成0xc,原字节予以保存。当处理器执行到int3时,即发生3号中断,转去执行相应的中断处理程序。

注意,int3int 3不是一回事。前者的机器码为CC,后者则是CD03,这就是通常所说的int n,其操作码为0xCD,第2字节的操作数给出了中断号。

into是溢出中断指令,机器码为0xCE。处理器执行这条指令时,如果标志寄存器OF位是1,将会产生4号中断。反之这条指令什么也不做。

BIOS中断

调用中断处理程序比较方便,知道中断号以及参数传递规则就行了。事实上操作系统加载完自己之后,以中断处理程序的形式提供了很多基础功能,如硬盘读写功能,并把该例程的地址填写到中断向量表中。这样,无论在什么时候,用户程序需要该功能时,直接发出一个软中断即可。

在以软中断形式提供的功能中,最有名的是BOS中断,之所以称为BOS中断,是因为这些中断功能是在计算机加电之后,BIOS程序执行期间建立起来的。换句话说,这些中断功能在加载和执行主引导扇区之前,就已经可以使用了。

BIOS可能会为一些简单的外围设备提供初始化代码和功能调用代码,并填写中断向量表,但也有一些BIOS中断是由外部设备接口自己建立的。

首先,每个外部设备接口,包括各种板卡,如网卡、显卡、键盘接口电路、硬件控制器等,都有自己的只读存储器(Read Only Memory,ROM),类似于BIOS芯片,这些ROM中提供了它自己的功能调用例程,以及本设备的初始化代码。按照规范,前两个单元的内容是0x55和0xAA,第三个单元是本ROM中以512字节为单位的代码长度;从第四个单元开始,就是实际的ROM代码。

其次,我们知道,从内存物理地址A0000开始,到FFFF结束,有相当一部分空间是留给外围设备的。如果设备存在,那么,它自带的ROM会映射到分配给它的地址范围内。

在计算机启动期间,BIOS程序会以2KB为单位搜索内存地址C0000~E0000之间的区域。当它发现某个区域的头两个字节是0x55和0xAA时,那意味着该区域有ROM代码存在,是有效的。接着,它对该区域做累加和检査,看结果是否和第三个单元相符。如果相符,就从第四个单元进入。这时,处理器执行的是硬件自带的程序指令,这些指令初始化外部设备的相关寄存器和工作状态,最后,填写相关的中断向量表,使它们指向自带的中断处理过程。

保护模式下中断向量的处理

中断是随机产生的,不可预测。但是它发生时,当前处理器当前总会有个任务在执行。这个任务可能是在内核空间执行也可能在用户空间执行。如果中断处理代码的特权等级比当前任务的级别高,比如中断是ring0级别的,当前任务是ring3,则会切换栈。否则会直接使用当前任务的栈。

中断的处理过程如下:

  • 栈切换
    • 如果中断处理代码的特权等级比当前任务的级别高,则需要从TSS中选择合适的新栈,然后把SS和SP的值压入新栈。如图1.4(b)
    • 如果级别相同,就没有栈的切换,直接使用当前栈。如图1.4(a)所示
  • 处理器把EFLAGS、CS和EIP的当前状态压入新栈
  • 对于有错误代码的异常,处理器还要把错误代码压入新栈,紧挨着EIP之后,

1.4-a

1.4-b

从中断中返回的过程如下:

  • 在执行完中断处理程序以后,如果有错误代码,需要先把错误代码出栈
  • 从栈中依次弹出EIP和CS以及EFLAGS寄存器的值
  • 栈恢复
    • 判断当前的特权级别和弹出的CS选择子的特权级别,如果不同,说明之前有过栈切换。从栈中再弹出ESP和SS寄存器的值
    • 如果相同,则继续使用当前栈

进程

任务门

英特尔提供了保护模式下切换任务的方法,就是任务门。任务门类似于中断,需要先注册再使用。不过英特尔的设计不够灵活,工业界并没有按照英特尔的思路来使用任务门。所以在现代操作系统中已经很少使用任务门来进行进程切换了。

现代的系统普遍采用的是分时多任务处理,普遍都是用定时器中断来进行任务切换。

用中断实现任务切换

现代的操作系统绝大部分都是使用分时多任务,所以往往使用时钟中断来进行进程调度。时钟中断也是中断的一种,所以上面说的中断的一系列步骤在任务切换时都会发生。不过既然是任务切换,那肯定有现场保存以及恢复现场的逻辑。任务切换就是在中断的基础上增加了现场的保存和恢复。

sysenter和sysexit指令

使用中断进行任务切换时,会有一系列的特权级别校验、上下文寄存器压栈等操作。本来这个作为普通的中断来说是可以忍受的,因为系统中的外部中断没有那么频繁。但是当中断用来进行内核态和用户态切换,大量使用后,带来的性能问题逐渐明显。

所以英特尔专门设计量sysentersysexit 这两个指令来实现内核态和用户态的切换。这两个指令没有中断的校验和压栈动作,性能要好得多。(校验和压栈的操作已经用其他设计来替代了)。

参考