《操作系统真象还原》中断
中断
中断处理,就是处理器接受到中断信号后,暂停当前执行的任务,转而去查找中断向量表,去执行中断服务程序,执行完后,恢复到中断前的状态,继续执行刚才的程序。
形象一点就是有个人或有个急事打断你现在做的事情,让你不得不处理这件紧急的事情,当你处理完这件事情后(当然也有可能继续被另一件事情打断),再做回你刚才没做完的事情。
为什么要有中断
那为什么要有中断呢?拿工作做例子,突然有件事情打断你去做别的事情,一般人都会觉得很烦躁,毕竟重新集中精力做回原来的事情,是一件很耗精力的事情;但大多情况下“别的事情”都是你的本职工作,而且很有可能别人的工作也需要你的协助才能进行下去,如果没有你,可能别人的工作根本进行不下去,导致项目进展停滞了。尽管“中断”了你现在的工作,但完成另一个工作很可能会促进整个项目的进展,让更多的任务能够得到完成。
需要中断的原因和上面的例子类似。有了中断,我们可以让多个任务看起来能够同时运行(在单CPU中实际上不是同时运行,而是串行、交替,一个时刻只有一个任务运行,同时运行只不过是假象,这就是并发),比如任务A和任务B,我不用等到任务A完成才去完成任务B,我可以一会儿去做任务A,一会儿去做任务B,这样子的话任务B不需要等待任务A了。如果我们把任务A看作一个进程,把任务B看作外部设备需要处理的工作(比如键盘输入),这样子CPU既可以执行内存里的程序,也可以抽时间处理外部设备的事情。总之,中断,能够让我们享受计算机的多个“服务”。
中断类型
中断有两类,一类是内部中断,就是发生在CPU内部的中断,另一类是外部中断,即发生在CPU外部中断,也可以说是外设发出的中断。
外部中断
外部中断又可以分为可屏蔽中断和不可屏蔽中断。
可屏蔽中断,顾名思义,可以屏蔽的中断,像时钟、键盘、硬盘、网卡等发出的中断都是可以屏蔽的。
不可屏蔽中断,它既有不能屏蔽的意思,更准确地来说,一旦出现这种中断,操作系统将面临奔溃,无论你是否屏蔽似乎都没什么意义了,出现这种中断大多数是硬件问题,需要专业的硬件工程师做修理,像电源掉电、内存读写错误、奇偶校验错误都是不可屏蔽中断。
可屏蔽中断处理分为两部分,上半部和下半部。上半部是需要处理器紧急处理的,下半部是可以推迟一点处理的。例如,网卡接受到的网络数据将要装满缓冲区,它就会向处理器发出可屏蔽中断,通知处理器做上半部的处理,因为再不搬,数据就要丢失了,之后处理器就会将网卡里的网络数据搬到内存里,完成中断的上半部。下半部的话,就是对网络数据做一定的处理,这部分倒不是很紧急,放在下半部挺合适,处理器找个时间处理一下就好。
外部中断是通过两根信号线通知处理器的,一条是INTR(INTeRrupt),另一条是NMI(Non Maskable,Interrupt),分别用于传输可屏蔽中断信号和不可屏蔽中断信号。
内部中断
内部中断分为软中断和异常。软中断是用户主动发起的中断。异常是执行指令时CPU内部遇到错误。
先说软中断。以下时发起中断的指令,把书上的介绍复制到下面:
• “ int 8位立即数”。这是我们以后常用的指令,我们要通过它进行系统调用,8位立即数可表示256种中断,这与处理器所支持的中断数是相吻合的。
• “ int 3”。这可不是 int 空格3,它们之间无间隙。 int 3是调试断点指令,其所触发的中断向量号是3,以后在中断和异常表中大家会看到。我们用 gdb 或 bochs 调试程序时,实际上就是调试器 fork 了一个子进程,子进程用于运行被调试的程序。调试器中经常要设置断点,其原理就是父进程修改了子进程的指令,将其用 int 3指令替换,从而子进程调用了 int 3指令触发中断。用此指令实现调试的原理是 int 3指令的机器码是 Oxcc ,断点本质上是指令的地址,调试器(父进程)将被调试进程(子进程)断点起始地址的第1个字节备份好之后在原地将该指令的第1字节修改为 Oxcc 。这样指令执行到断点处时,会去执行机器码为 Oxcc 的 int 3指令,该指令会触发3号中断,从而会去执行3号中断对应的中断处理程序,由于中断处理程序在运行时也要用到寄存器,为了保存所调试进程的现场,该中断处理程序必须先将当前的寄存器和相关内存单元压栈保存(提醒,当前寄存器和相关内存都属于那个被调试的进程),用户在査看寄存器和变量时就是从栈中获取的。当恢复执行所调试的进程时,中断处理程序需要将之前备份的1字节还原至断点处,然后恢复各寄存器和内存单元的值,修改返回地址为断点地址,用 iret 指令退出中断,返回到用户进程继续执行。
• into 。这是中断溢出指令,它所触发的中断向量号是4。不过,能否引发4号中断是要看 eflags 标志寄存器中的 OF 位是否为1,如果是1才会引发中断,否则该指令悄悄地什么都不做,低调得很。
• bound 。这是检査数组索引越界指令,它可以触发5号中断,用于检査数组的索引下标是否在上下边界之内。该指令格式是 “bound 16/32位寄存器,16/32位内存”。目的操作数是用寄存器来存储的,其内容是待检测的数组下标值。源操作数是内存,其内容是数组下标的下边界和上边界。当执行 bound 指令时,若下标处于数组索引的范围之外,则会触发5号中断。
• ud 2。未定义指令,这会触发第6号中断。该指令表示指令无效, CPU 无法识别。主动使用它发起中断,常用于软件测试中,无实际用途。
int指令是比较常用的软中断,像Linux系统int 80就是用来实现系统调用的,所以调用的很频繁。
再说说异常。异常根据严重分为三种:
最轻的异常称为故障(Fault),这种异常是可以修复的,像缺页异常(Page fault)就是这种,处理器给软件以此改过自新的机会。LINUX虚拟内存就是基于page fault。
第二种是陷阱(Trap),这种异常一般是用户用于调试设下的。
第三种是最严重的,终止(Abort)。一旦无法修复,程序就没办法再运行下去,操作系统为了自保,只能将这个进程杀死。
中断处理流程
①首先,中断要触发,正如上面说的,内部中断由CPU内部触发,外部中断由外设触发然后通过两根信号线通知CPU。
②CPU要知道这个中断是什么,这个是通过中断向量号判断的,中断向量号又是从哪里拿呢,软中断由软件提供,不可屏蔽中断和异常都是由CPU提供,可屏蔽中断时一种叫中断代理提供(我们这里是8259A中断代理);外设发送可屏蔽中断时首先会经过中断代理,然后再由代理发送给CPU,所以上面说到的连接到CPU的INTR线,并不是直接连到外设的,而是直连到代理,中断设备也连到代理,这样代理就相当于一个中转站,后面会有所介绍。
③CPU得到中断向量号后,会到内存的中断向量表或中断描述符表里面找到对应的中断服务程序。
④处理完服务程序之后,恢复到中断前的状态,继续执行刚才执行的任务。
中断向量表和中断描述符表
在实模式下,BIOS会初始化中断向量表,可以使用int指令触发中断,中断向量表的位置是固定再0~0x3ff,每个中断向量用4个字节描述。
而再保护模式下,我们不再需要中断向量表,而采用另外一种结构“中断描述符表”,与中断向量表有两个区别:
①中断描述符表地址不限制
②每个中断描述符由8个字节描述。
结构如下图:
触发中断时栈的变化
和之前调用调用门时栈会对栈操作,触发中断时栈也会有变化,过程和触发调用门的类似。
假设这里触发中断涉及特权级别转移,如执行用户程序时由用户态陷入到内核态,即特权级别3转移到特权级别0。
①处理器接到中断后,到中断描述符表里根据中断向量号索引对应的中断描述符,得到目标代码段的选择子和偏移位置。
②根据选择子继续再全局描述符或局部描述符索引段描述符,根据DPL判断是否需要转移特权级别,如果涉及特权级别转移,首先将当前特权级别的ss和esp临时保存下来,然后查看TSS中高特权级别的ss和esp,转移过去高特权级别的栈里,再将之前临时保存的ss和esp压进当前栈里,再压入eflags、cs、eip和错误码,然后跳到目标代码段执行代码。
③执行完中断处理程序之后,栈跳过错误码,因为错误码对后面恢复现场没有意义,再通过iret指令将eip、cs、elfags、esp、ss弹回到寄存器,恢复到中断前的状态,继续执行中断前的代码。
如果没有涉及特权级别转移,那么不需要查找TSS,直接用原来的旧栈,也不用压进ss、esp,其他的寄存器和错误码还是要压进,恢复中断前的状态的操作也一样,少了特权级别的转移。
触发中断时特权检查
特权级别检查也和调用门的类似。
由于中断触发时,不用指定选择子,只有中断向量号,因此也不需要检查RPL。所以只需要用CPL与中断描述符的DPL和目标代码段的DPL相比较。
如果中断是软中断,即用户主动触发的中断,需要检查CPL、中断描述符的DPL和目标代码段的DPL。要求如下:
①和调用门类似,CPL数值上要小于等于中断描述符的DPL。
②CPL数值上要大于等于目标代码段的DPL。(这里书上写的是大于,感觉是写错了)
如果中断时外部中断和异常,则只需要检查CPL和目标代码段的DPL,即CPL数值上要大于等于目标代码段描述符的DPL。
中断时特权检查这部分,只在官方文档找到一段话描述,放在下面,各位可以判断一下:
6.12.1.2 Protection of Exception- and Interrupt-Handler Procedures The privilege-level protection for exception- and interrupt-handler procedures is similar to that used for ordinary procedure calls when called through a call gate (see Section 5.8.4, “Accessing a Code Segment Through a Call Gate”). The processor does not permit transfer of execution to an exception- or interrupt-handler procedure in a less privileged code segment (numerically greater privilege level) than the CPL. An attempt to violate this rule results in a general-protection exception (#GP). The protection mechanism for exception- and interrupt-handler procedures is different in the following ways:
• Because interrupt and exception vectors have no RPL, the RPL is not checked on implicit calls to exception and interrupt handlers.
• The processor checks the DPL of the interrupt or trap gate only if an exception or interrupt is generated with an INT n, INT3, or INTO instruction.4 Here, the CPL must be less than or equal to the DPL of the gate. This restriction prevents application programs or procedures running at privilege level 3 from using a software interrupt to access critical exception handlers, such as the page-fault handler, providing that those handlers are placed in more privileged code segments (numerically lower privilege level). For hardware-generated interrupts and processor-detected exceptions, the processor ignores the DPL of interrupt and trap gates.
Because exceptions and interrupts generally do not occur at predictable times, these privilege rules effectively impose restrictions on the privilege levels at which exception and interrupt- handling procedures can run. Either of the following techniques can be used to avoid privilege-level violations.
• The exception or interrupt handler can be placed in a conforming code segment. This technique can be used for handlers that only need to access data available on the stack (for example, divide error exceptions). If the handler needs data from a data segment, the data segment needs to be accessible from privilege level 3, which would make it unprotected.
• The handler can be placed in a nonconforming code segment with privilege level 0. This handler would always run, regardless of the CPL that the interrupted program or task is running at.
可屏蔽中断代理
最后讲一下可屏蔽中断的代理8259A芯片,当多个中断到来时,代理可以用队列(IRR)存储中断,判别哪个中断会被处理器处理,并与处理器通信。
中断设备会连接到代理相应的IRQ接口,多个8259A芯片级联在一起可以支持更多的中断信号,一个8259芯片支持8种中断,两个芯片支持15种中断(其中1个IRQ用来连接从片)。
芯片内部结构如下图:
先把各个模块的作用先说一下:
• INT : 8259 A 选出优先级最髙的中断请求后,发信号通知 CPU 。
• INTA : INT Acknowledge ,中断响应信号。位于8259 A 中的 INTA 接收来自 CPU 的 INTA 接口的中断响应信号。
• IMR: InterruptMaskRegister, 中断屏蔽寄存器,宽度是 8 位,用来屏蔽某个外设的中断。
• IRR : Interrupt Request Register ,中断请求寄存器,宽度是8位。它的作用是接受经过 IMR 寄存器过滤后的中断信号并锁存,此寄存器中全是等待处理的中断,“相当于“5259 A 维护的未处理中断信号队列。
• PR : Priority Resolver ,优先级仲裁器。当有多个中断同时发生,或当有新的中断请求进来时,将它与当前正在处理的中断进行比较,找出优先级更高的中断。
• ISR: In-Service Register, 中断服务寄存器,宽度是 8 位。当某个中断正在被处理时,保存在此寄存器中。
工作原理
当中断到来时,首先会经过中断屏蔽寄存器(IMR),查看该中断是否被屏蔽了,如果屏蔽的话IMR对应的位是为1的(比如主片IMR最低位为1的话,时钟中断就被屏蔽了,看上面那个级联的图)。如果没有被屏蔽,进入到IRR中断请求寄存器里,将该中断对应的位置为1,这个操作相当于将中断放进了队列里,因为有可能会有多个中断等待处理,所以需要一个队列来做缓存。
再讲讲处理中断的流程:
①PR优先权判别器会从IRR里挑选出优先级最高的中断(IRQx,x越低优先级最高,所以时钟优先级最高),准备让处理器处理,代理通过INTR向处理器发送INT信号,告诉处理器有中断要处理。
②处理器处理好现在执行的指令后,通过INTA发送信号告诉代理,我已经准备好处理中断信号了。
③代理接受该信号后,将刚才选出来最高优先级的中断在ISR对应位置为1,IRR对应位置为0。
④处理器再次发送INTR信号给代理,告诉代理我需要这个中断的中断向量号。
⑤代理通过数据总线向处理器发送PR判别的中断向量号。
⑥如果8259A芯片的EOI(End Of Interrupt信号)被置为非自动模式(手工模式), 处理器处理中断后,向代理发送一个EOI(End Of Interrupt信号),代理接受后知道中断已经处理完了,将ISR对应位置为0;
⑦如果8259A芯片的EOI(End Of Interrupt信号)被置为自动模式,在接受到第二个INTR信号就会将ISR对应位置为0。
以上就是接收中断和处理中断的流程。
还有一种情况是,如果在代理发送中断向量号之前,来了一个优先级别更高的中断,就会将ISR之前准备处理中断对应位置为0,IRR对应位置为1,将新来的中断的向量号发给处理器。