中断过程可以说几乎是每次面试都会被问到的问题,而且在平时工作中也是经常遇到的点。这里把相关的问题一起列出来,省得以后再找。
同步与异步
首先说为什么要用中断,中断是外部设备和CPU交互的方式,相比轮询来说CPU可以解放出来了。但因此带来的问题是,中断可能会在任何时候发生,所以说"中断是异步的"。与之相对应,异常就是同步的。怎么理解这个异步和同步呢?这是指的事件和引起这个事件的程序之间的关系。
对于异常来说,比如一条指令引起的除0异常,CPU检测到异常时可以肯定CS:EIP中的指令地址就是发生异常的这条指令,CPU的处理方式有跳过(执行下一条指令)、再执行一遍这条指令或者直接崩掉。但是对于中断来说就不敢确定了,比如程序写了一个数据到磁盘,可能先缓存在磁盘里,程序就继续向下执行了,等到pdflush刷到磁盘上可能都是猴年马月的事了,写完之后磁盘发出一个中断报告“我写完了”。这个时候发出写操作的程序早都不知道跑到哪了,也许被切换出去了也是有可能的——事实上pdflush能够执行说明原先的进程确实已经被从这个CPU上切出去了。所以这种情况叫“异步”的。这个事件只能由中断处理函数来处理。
中断过程
中断就是程序执行过程中发生的强制跳转,跳转到相应的处理程序,处理完成后再返回原来的程序继续执行.那这里就有两个问题,第一:跳转到哪去了?相应的处理程序是什么地方,CPU怎么知道的?第二:处理完成后还要回来,那就需要保存现场,记下来现在执行到哪了。那需要保存什么?
中断向量表
外部中断一般由8259中断控制器向CPU发出,8259是好早的东西了,现在已经是APIC了,不过原理是一样的。当一个中断到来时会将CPU的一个引脚的电位改变,当CPU执行完一条指令时,会检查是不是有中断到来,如果有的话,就进入中断处理流程,读8259传过来的中断向量号,然后根据向量号到IDT中去找到相应的中断处理程序。IDT的全称是中断描述符表(Interrupt Descriptor Table),顾名思议,里面放的全是中断描述符,描述的就是一个中断的处理函数的入口地址,当然还有一些权限检查的东西,这里不是我们关注的焦点。然后CPU跳过去执行就行了。好的,第一个问题搞掂。等等,这样跳过去就完了吗?那回来怎么办?下面是第二个问题,怎么回来。
中断栈
刚才说了,跳走之前要保存现场,以便将来回来。那么问题又来了,保存什么东西?保存在哪呢?
先回答第二个问题,很简单“目标栈”,这里说的目标栈就是中断处理程序用到的栈,为什么用这个栈呢,因为从设计上讲,中断处理程序不知道自己中断了谁。所以这里面有一个堆栈切换的问题,因为一般来说中断处理程序都是内核态的,即ring0上运行的。所以如果当被中断的程序也是运行在ring0上的,那好了,什么也不做,直接用被中断进程的栈就好了。但是如果被中断的程序运行在用户态(ring3),那就又涉及到一个堆栈切换的问题:从TSS(任务状态段)中得到ring0的堆栈地址,加载到ss:esp寄存器中。但加载之前需要先把原来的ss:esp寄存器保存起来,等加载新的堆栈后,先把保存起来的原来的堆栈地址压进去,为回来做好准备。
知道东西保存到哪个栈了之后,回到第一个问题:保存什么?首先保存eflags, cs, eip, errorCode(如果有的话),注意这时这几个寄存器中存放的都是被中断进程的。从这以后,CPU才load cs, eip 进入中断处理程序,所以保存其他寄存器的操作要中断处理程序完成,这也是为什么在中断处理程序的开始部分,先是一个SAVE_ALL宏,这个宏展开之后就是一系列的push操作了。至此,一切良好,中断处理程序开始执行了。
中断返回
最后中断处理程序执行完成,要返回了,一条iret指令,先要从栈中恢复已经保存好的eflags, cs, eip, 然后再切换回原来的堆栈,这个过程中同样可能有ring0到ring3的转变,不过反正原来的堆栈已经保存了,加载就是了,当然出于安全考虑,还有对ds, es, gs, fs等寄存器的权限检查,因为有些恶意程序可能用这种方式来访问内核空间。
最后总结一下:
中断是异步的,中断发生时CPU从IDT中找到要跳转的中断处理函数的入口地址,准备好栈(如果切换优先级的话要保存原先的栈并在加载目标栈后压入),压入eflags, cs, eip,如果有errorCode的话也要压入,然后加载中断处理函数。中断处理函数保存其他的寄存器。中断返回是iret指令,恢复保存的寄存器。
补充一句,中断返回前如果是进入用户态的话,可能会做一次schedule,以免刚切回去的进程时间片到了再被切出来,还得再保存一次上下文。
再补充一句,从上面的描述可以看出,中断没有task_struct结构,所以不能被调度出去,这也就是为什么说中断只是一个执行流,在中断上下文不能休眠,或者其他任何可能引起休眠的操作,比如发个IO下去。