在软件开发中,中断是一个绕不开的重要话题。不过,不知道大家有没有遇到过这样的困惑:很多书籍和文章在介绍中断相关知识点的时候,说的都是很有道理的。这篇文章对中断的解释是正确的,那篇文章对中断的描述也很正确。但是,为什么这两篇文章有些内容是矛盾的?!任何文章单独看都有道理,越看越糊涂?这就像迷失在森林里。如果你只有一个指南针,你绝对可以出去。但是,如果你有两个圆规,却指向相反的方向,这时候你该相信谁呢?!我们仔细梳理了一下,发现每篇文章都是在一定语境和一定语境下写的,不同文章的矛盾是在环境中解释的,正是因为它们描述的语境和环境不同。上下文环境是描述与当前正在执行的程序相关的静态信息,比如:有哪些代码段,堆栈空间在哪里,进程描述信息在哪里,当前正在执行哪条指令等等。如果我们没有全球视野,在同一语境下比较不同的文章,我们的理解和认识就会越来越混乱。所以,对于这类比较复杂,无法按一定逻辑深入的知识点,脑子里一定要有一张全局图。只有掌握了这张全球地图,详细学习每个地方的知识点,才能知己知彼,才不会误入歧途。本篇我们继续化繁为简,从最简单的处理器8086开始,讲讲中断的一些知识。有了这个储备,在理清了基本脉络之后,以后在学习linux系统中的中断时,也会有同样的感受!中断向量和中断描述符中断向量这个词很时髦也很神秘!按照逻辑,中断向量这个硬菜第一部分应该不上,应该从中断源说起。不过,中断我们毕竟学了那么多,脑子里肯定对中断有一些基本的了解。因此,这里先明确一下中断向量和中断描述符的问题。在上一篇文章中,我们已经谈到了实模式和保护模式的问题。在[Linuxfromscratch]系列中,我们一直在描述实模式的东西。本篇是实模式的最后一篇,下一篇会进入保护模式。然后,中断向量工作在实模式,处理器通过中断号和中断向量定位到相应的中断处理程序。至于中断描述符,它工作在保护模式下,处理器通过中断号和中断描述符定位到相应的中断处理程序。也就是说:中断向量和中断描述符具有相同的基本功能。只是它们存在的环境不同,从描述中可以感受到保护模式下的中断描述符会更复杂更强大。他们就像一对兄弟,外形相似,功能相似。但是如果你往里看,你会发现有很多不同。因此,本文所讲解的是实模式下的中断。请先了解这一点。中断的分类在x86系统中,中断的分类如下:内部中断所谓内部中断是在CPU内部产生和处理的。例如:当CPU遇到除0的指令时,会产生0号中断,并调用相应的中断处理程序;当CPU遇到不存在的非法指令时,会产生6号中断,并调用相应的中断处理程序;对于内部中断,有时称为异常。软中断也是内部中断,用处很大,由int指令触发。例如,gdb使用指令int3来调试应用程序。很久以前写过这样一篇文章。原来gdb的底层调试原理就是这么简单。它描述了gdb如何通过插入一条int指令来替换被调试程序的指令代码,从而实现断点调试功能。外部中断x86CPU有2个中断引脚:INT和INTR,分别对应:不可屏蔽中断和可屏蔽中断。所谓不可屏蔽的意思是:中断不能被忽略,CPU必须处理这个中断。如果不处理,程序就不能继续执行。至于可屏蔽中断,CPU可以忽略不执行,因为这种中断不会对系统的执行造成致命的影响。对于外部可屏蔽中断,CPU上只有一个INTR引脚,但是需要产生中断信号的设备那么多,中断信号众多,如何区分呢?一般是通过可编程中断控制器(ProgrammableInterruptController,PIC),计算机中使用最多的是8259a芯片。虽然现代计算机已经是APIC(AdvancedProgrammableInterruptController),但是因为8259a芯片太经典了,所以大部分描述外部中断的文章都会以它为例。每个8259a可提供8个中断输入引脚,两片芯片级联在一起可提供15个中断信号:主控芯片的输出引脚INT与CPU的INTR引脚相连;从芯片INT的输出引脚连接到主芯片的2脚;这样两片8259a芯片就可以向CPU提供15个中断信号,如:鼠标、键盘、串口、硬盘等外设。1、8259a之所以称为可编程是因为它内部有相关的寄存器。2、可以通过指定的端口号来设置这些寄存器,使8条IRQ中断线上的信号发送到CPU时对应不同的中断号。另外,对于外部可屏蔽中断,有两层屏蔽机制:在8259芯片中,有中断屏蔽寄存器,可以屏蔽IRQ0~IRQ7输入引脚;CPU内部,还有一个标志寄存器,可以屏蔽中断信号;x86处理器中的中断号一共支持256个中断,每个中断分配一个中断号,从0到255。其中,中断向量0到31保留用来处理异常和非屏蔽中断(只有向量2用于非屏蔽中断,其余均为异常)。BIOS或操作系统提供异常处理程序后,当发生异常时,会通过中断向量表查找对应的异常处理程序,查找过程稍后介绍。从中断号32开始,全部分配给外部中断。例如:1、系统定时器中断IRQ0被分配中断号为32;2、linux系统调用分配128号中断;我们来看一下内部中断和外部中断相关的中断号:编程中断控制器访问的中断信号分布如下图所示:前面说了8259a是可编程的,如果我们通过设置IRQ0的中断号为32配置寄存器,那么主芯片上的IRQ1~IRQ7对应的中断号依次递增1,芯片上IRQ8~IRQ15对应的中断号也依次递增。因此,有时我们可以在中断代码中看到如下宏定义:中断向量和中断处理程序当一个中断发生时,CPU获取到该中断对应的中断号,接下来就是判断调用哪个函数来处理这个interrupt,这个函数叫做InterruptServiceRoutine(ISR),有时也叫interrupthandler,中断处理函数,本质是一样的。中断方向是通过中断号找到处理程序的重要桥梁!中断向量的本质在8086中。一个中断向量是一对数据,如段地址:中断处理程序偏移量。通过这个数据,可以定位到内存中指定位置的中断处理函数。它很像高级编程语言中的函数指针,用来指向函数的起始地址。8086规定:必须从内存地址0开始存放256个中断向量。每个中断向量占用4字节(2字节段地址,2字节偏移地址),256个中断共占用1024字节空间。在上一篇文章中已经介绍了相关的内存模型,如下图所示:如果把一个中断向量看成一个函数指针,那么中断向量表就相当于一个函数指针数组。例:假设触发了2号中断,CPU会在中断向量表中查找2号中断的中断向量。因为每个中断向量占用4个字节,所以中断向量2的起始地址为2*4=8,即第8个字节。然后从第8个字节开始,取4个字节的内容:0x1000:0x2000。意思是:2号中断的处理函数位于段地址0x1000,偏移量0x2000。然后CPU计算8086的物理地址,得到中断处理函数的物理地址为0x12000(段地址左移4位+偏移地址),然后跳转到函数地址执行。1、由于Linux系统运行在保护模式下,在这种模式下,当有中断发生时,通过中断描述符查找中断处理程序。2、每个中断描述符都描述了中断处理函数所在段的选择符和偏移量,本质上是用来寻找一个中断处理函数的。InterruptHandler的安装既然通过中断向量找到了中断处理程序,那么谁把这些中断处理程序放在内存中呢?如果看过一些计算机低级书籍,可以看到一般都有例子:Howtomanuallysetanordinaryfunctionasaninterrupthandler。操作步骤为:在代码中,编写一个普通函数;将该函数的指令代码移动到内存中的某个位置;将该位置(段地址:offset)作为中断向量,并设置到中断向量表中;这时,如果中断发生,你提供的函数就会作为中断处理程序执行。当然,在计算机系统中,BIOS、操作系统以及各种外设都会自动为我们提供很多基本的中断处理功能。例如:BIOS提供了软中断、内部中断、硬件中断等处理功能。这些功能都固化在BIOS代码中(映射到BIOS所在的ROM芯片),BIOS只需要把这些处理功能的地址放上去。写入中断向量表的相应位置即可。上一篇文章提到,内存中的一些位置映射到外设的ROM中,这些外设的ROM中也有一些外设自带的程序。BIOS启动时,会扫描映射到外设的内存空间。通过一定的关键字信息,如果发现外设有自己的程序,就会执行。这些外设程序一般会进行一些自初始化,并填写相关的中断向量表,使其指向外设的中断处理程序。对于操作系统就更不用说了,它会重新安排它所需要的中断处理函数。这部分我们稍后再研究讨论!中断现场保护与恢复当一个中断发生时,必然有一个正在执行的程序被中断。中断处理函数执行完成后,被中断的程序需要从刚才被中断的地方继续执行(暂时不考虑从中断点返回进行多任务切换)。程序执行的上下文是处理器中各种寄存器的内容:代码段寄存器cs、指令指针寄存器sp、标志寄存器FLAGS。然而,在中断处理程序中,这些寄存器也是必需的。处理器中的这些寄存器是每个程序执行的上下文信息的存储容器,当然也包括终端处理程序!因此,在进入中断处理程序之前,CPU会自动将这些寄存器压入栈中进行保存,然后再跳转到中断处理程序去执行。当中断处理程序执行结束时,CPU会将这些内容从栈中弹出,并恢复到相应的寄存器中,这样被中断的程序才能继续执行。总结:从功能的角度来看,中断的本质有两个功能:提供一种执行异步序列的机制;很明确了,就是通过中断,让应用层的程序有机会进入系统代码执行。因为应用层和操作系统层的代码工作在不同的安全级别。为了系统的安全,Linux操作系统提供了这样一种机制,允许低安全级别的应用程序进入高安全级别的操作系统代码执行。毕竟,所有的硬件和其他系统资源都是由操作系统统一的。管理。从中断处理程序的安装来看,中断本质上是增加了一层间接:通过固定位置的中断向量表,可以动态地将中断处理程序的实际地址放在任意位置。你为什么这么做?如果操作系统要为某个中断提供处理函数,那么这个处理函数在内存中的地址在哪里呢?需要考虑CPU、内存大小和布局等因素,很复杂!并且通过使用中断向量表,在一个固定的位置存储了很多“指针”。当中断处理函数放在内存中的任意位置时,只要让“指针”指向这个函数的地址就可以了,从而达到解耦的目的。这样无论是发生硬件中断还是应用层代码通过中断门调用操作系统提供的函数,只需要触发相应的中断即可,简化了CPU的设计。End中断要学的东西还很多,任重而道远!特别是在Linux 系统中,中断处理分为上半部分和下半部分,下半部分可以根据不同的系统功能需求,采用不同的机制来处理。我还是坚持以前的观点:磨刀不误不是樵夫。延长学习周期,一点一点积累,欲速则不达!本文转载自微信公众号「IOT物联网小镇」
