操作系统——中断(十四)

操作系统——中断(十四)

2020-09-27 10:29:23 hawk


概述

  这篇博客主要介绍一下中断相关的基础知识,如果有基础的话或者不敢兴趣的可以跳过,在实现的时候需要的时候再回过头查看。


中断知识

中断概念

  首先我们给出宏观的中断的概念——由于CPU获知了计算机中发生的某些事,CPU暂停正在执行的程序,转而去执行处理该事件的程序。而当这段程序执行完毕后,CPU继续执行刚才的程序,这整个过程称为中断处理,也成为中断。

  实际上,正是由于有了中断概念,才能显著提高并发(并发指单位时间内的积累工作量;并行指真正同时进行的工作量),大幅度提升计算机处理的效率。

操作系统与中断

  这里引用书中的描述,”操作系统是中断驱动的。没有中断,操作系统几乎什么都做不了“。

  由于操作系统需要处理各种事件,因此合理的操作系统就是完成初始化工作后,等待事件发生后,调用相关的处理程序即可。而这个事件大部分就是以中断形式通知给操作系统的。因此可以看出来,操作系统是由中断进行驱动的。

中断分类

  实际上把中断按照事件来源进行分类的话,可以简单的分为外部中断和内部终端。如对其进一步细分的话,则外部中断又可以分为可屏蔽中断和不可屏蔽中断;而内部中断可以分为软中断和异常。下面我们对其进行更为详细的分析

  1.  外部中断

  外部中断,是指来自CPU外部的中断,而外部的中断源又必须是某个硬件。因此,外部中断又可以被称为硬件中断。而为了让CPU接受每一个外部设备的中断信号,CPU提供了统一接口作为中断信号的公共线路,所有来自外设的中断信号都通过共享线路连接到CPU,如下所示

 

   

  可以看到,实际上提供了两条信号线,即INTR(INTeRupt)和NMI(Non Maskable Interupt),分别用来接收不同紧急情况的中断。对于单核CPU来说,如果接收到INTR中断信号,CPU可以处理,也可以推迟处理,甚至不处理,因为其并不影响CPU运行;而如果接收到NMI中断信号的话,CPU必须转去处理这些中断事件。

  对于可屏蔽中断来说,前面也已经分析过了,是可以屏蔽的,往往是通过设置eflags寄存器中的IF位,将所有外部设备的中断进行屏蔽;当然,也可以通过将这些设备接在某个中断代理设备上,从而单独屏蔽某个设备的中断。一般对于可屏蔽中断来说,其会把中断处理部分分为上部分和下部分,分开进行处理——将中断处理中需要立即执行的部分划分到上部分,而将中断处理中不紧急的部分则推迟到下部分完成。而不可屏蔽中断,无法通过设置eflags寄存器来进行屏蔽,只能直接进行处理。

  对于可屏蔽中断来说,其往往数量是有限的,因此每一种中断源往往都会获得一个中断向量号;而对于不可屏蔽中断来说,其往往都是无法通过软件进行解决的问题,因此没有必要细分原因,直接设置为不可屏蔽中断向量号为2即可。

  2.  内部中断

  前面已经简单介绍过了,内部中断大体上可以分为软中断和异常。对于软中断来说,其为由软件主动发起的中断,往往可以通过“int 8位立即数”、“int3”、“into”、”bound“以及”ud2“发起软中断。而对于异常来说,其是另一种内部中断,是指令执行期间,CPU内部产生的错误引起的。异常也大体可以分为三类——Fault、Trap以及Abort等。

  实际上,中断机制本质上是来了一个中断信号后,调用相应的中断处理程序。所以无论CPU有多少中类型的中断,为了统一管理,把外部中断和内部中断统统归结为一种管理方式——给每一个中断信号分配一个整数,用此整数作为中断的ID。这个整数就是所谓的中断向量,用此ID作为中断描述符表中的索引,从而可以找到对应的中断处理程序。

中断描述符表

  前面提到了中断描述符表,这里就分析一下。中断描述符表(Interrupt Descriptor Table,IDT),是保护模式下用于存储中断处理程序入口的表。当CPU接受一个中断时,需要用中断向量在此表中进行检索,然后根据描述符获取中断处理程序,然后执行中断处理程序即可。

  这里需要明确一点,中断描述符表中除了中断描述符,还有任务门描述符和陷阱门描述符等。所以,往往中断描述符表中的描述符又被称为门。对于中断描述符、任务门描述符和陷阱门描述符,其在前面的博客中也有提到,这里不再过多赘述,主要说一下中断门的使用——当通过中断门进入中断后,标志寄存器eflags中的IF位自动置0,也就是在进入中断后,自动把中断关闭,避免中断嵌套。

  而实际上根据前面讲过的GDT等的知识,我们自然很容易猜想到,CPU如何快速找到中断描述符表——CPU内部包含有中断描述符表寄存器(Interrupt Descriptor Table Register,IDRR)。该表类似于GDTR,格式如下所示

 

   实际上,IDT和GDT十分类似,这里只简单介绍一下不同之处——虽然IDT界限是16位,最多可容纳的描述符的个数是64KB/ 8 = 8K = 8192个,但是实际上CPU仅仅支持256个中断,即0-255,其余的中断描述符表中的描述符不可用;GDT中第0个描述符不可用,但是IDT中第0个描述符可以正常使用。

中断处理过程及保护

  实际上,一个完整的中断处理过程会分为CPU外和CPU内两部分。对于CPU外来说,外部设备的中断由中断代理芯片接受,处理后将该中断的中断向量号发送到CPU上;对于CPU内来说,CPU执行该中断向量号对应的中断处理程序即可。

  1.  处理器根据中断向量号定位中断描述符

   中断向量号是中断描述符的索引,当处理器收到一个外部中断向量后,它会使用此向量号,乘以8后,与IDTR中的中断描述符表地址相加,从而获取对应的中断描述符地址。

  2.  处理器进行特权级检查

   由于中断向量号仅仅是一个整数,并不可能包含RPL,所以特权级检查不涉及RPL。主要是对于软中断进行检查。如果是有”int n“, ”int3“和”into“产生的中断,则当前CPL需要大于等于门描述符的DPL,并且CPL还需要小于目标代码段DPL。

   如果是异常或外部中断引起的,只需要确定CPL小于目标代码段DPL即可。

  3.  执行中断程序

  如果前面的等级检查通过后,只需要将门描述符目标代码段选择子加载到代码段寄存器cs中,把对应的偏移加载到eip,即可开始执行中断处理程序。

 

   

  当然,需要额外说明的是,中断发生后,eflags中的NT位和TF位会被自动置为0;如果中断响应的门描述符是中断门,标志寄存器eflags中的IF位还会被自动置为0,避免中断嵌套。而从中断返回地指令则是iret,其会从栈中弹出数据到寄存器es、eip、eflags等,并且根据特权级是否改变,判断是否要恢复旧栈。

中断发生的压栈

  这里我们稍微详细分析一下中断发生时栈的变化。首先中断发生时,CPU会接收到一个中断向量号,在IDT中获取中断描述符,从而会将对应的数据加载到代码段寄存器cs和指令指针寄存器eip中。当然,为了可以正常返回,处理器会自动将当前的cs和eip的值保存到中断处理程序使用的栈中,并根据特权级的不同,还会保存标志寄存器eflags,如果还涉及到了特权级的变化,还需要压入ss和esp寄存器。

  下面在看一下具体的寄存器入栈情况以及顺序。我们首先分析一下需要特权转换的情况,即如果CPL比目标代码段的DPL小,则表示要向高特权级转移,自然需要切换到高特权级别的栈

  1.  首先保存当前旧栈SS和ESP的值,记为SS_old和ESP_old。然后在对应的TSS中找到同目标代码段DPL级别相同的栈加载到寄存器SS和ESP中,记为SS_new和ESP_new,同时需要将之前保存的SS_old和ESP_old压入新栈备份。如图所示

 

   2.  在新栈中压入eflags,如图所示

 

   3.  由于会切换代码段,则保存当前的CS段寄存器和EIP值,记作CS_old和EIP_old,如下所示

 

   4.  某些异常可能会产生错误码,这里也需要进行保存,如下所示

 

   而对于特权级没有变化的中断来说,其他步骤也都是基本一样的,除了没有第一步——因为其不需要更改栈,其栈情况如下所示

 

 

  最后,我们只需要通过iret指令进行恢复寄存器的值即可。其会一次弹出EIP、CS和eflags,并根据特权级是否发生变化弹出对应的SS和ESP。其规则是根据栈中CS_old指针和EIP_old指针中的DPL(也就是请求时候的RPL)和当前的CPL比较,从而判断是否发生特权级变化,并进行相应的操作。需要说明的是,如果特权级变化后,会检查数据段寄存器DS、ES、FS和GS的内容,将数据段描述符DPL高于当前CPL的,用0填充对应的段寄存器。

错误中断码

  这里主要简单介绍一下中断错误码的格式,如下所示

 

   

  这里,EXT表示EXTernel event,如果EXT位1,则表示不可屏蔽中断或外部设备,否则为0。IDT表示选择子是否指向中断描述符IDT,如果IDT位为1,表示选择子指向中断描述符表,否则指向GDT或LDT。TI表示GDT或者LDT中检索,只有当IDT为0时才有意义。

  通常能够压入错误码的中断属于中断向量号在0-32之内的异常,而外部中断和int软中断并不会产生错误码,因此我们通常也不太需要处理错误码。


可编程中断控制器8259A

  前面最开始提到了用来屏蔽中断的代理,即可编程中断控制器8259A。

  8259A主要用来对所有连接的中断进行仲裁,决定那个中断优先被CPU受理。其主要用于管理和控制可屏蔽中断。

  虽然Intel处理器共支持256个中断,但是8259A只可以管理8个中断,如图所示

 

   如果我们将其一个IRQ连接另一个8259A,从而可以控制更多中断设备。这被称为级联,目前最多支持9组级联,而我们很容易计算n片8259A级联支持的中断源,(7 * (n - 1) + 8) = 7n + 1。而一般的x86计算机(主要是我们的虚拟机),其为2个8259A级联,各个IRQ如下所示

 

 

  这里再简单介绍一下8259A芯片的内部结构,方便我们后面理解其工作机制,如下所示

 

 

  其中,INT为8259A选出优先级最高的中断请求后,发信号通知CPU;INTA,即INT Acknowledge,即中断响应信号,接受来自CPU的INTA接口的中断响应信号;IMR,即interrupt Mask Register,中断屏蔽寄存器,用来屏蔽某个外设中断;IRR,即Interrupt Request Register,中断请求寄存器,简单理解为未处理中断信号队列;PR,即Priority Resolver,优先仲裁器,与当前正在处理的中断进行比较,然后找出优先级更高的中断;ISR,In-Server Register,即中断服务寄存器,保存当前正在处理的中断。

  最后就是简单描述一下8259A的工作流程,为我们后面基于8259A实现中断做铺垫。

  当某个外设发出一个中断信号,则主板已经将该信号通路指向了对应的IRQ接口。首先判断IMR中对应的位,如果为1,则表示中断屏蔽;否则放行。然后IRR的对应的bit被置为1,并会在某个恰当的时机,优先冲裁器PR会从IRR中挑选一个优先级最大的中断,即IRQ接口号最小的,然后通过INT接口向CPU发送INTR信号。然后CPU通过INTA返回确认信息,收到该信号后,8259A将刚刚的中断在ISR寄存器中添加,置为1;同时从IRR寄存器中去掉,即置为0。此时,如果CPU再次发送INTA信号给8259A,即向CPU发送其实中断向量号 + IRQ接口号。然后CPU会根据中断向量号,执行相关的中断程序。

  当然,仅仅这些流程是不够的,我们并没有介绍ISR中的中断什么时候被去掉,这里和EOI模式相关。如果8259A的”EOI(End of Interrupt)“被设置为非自动模式,则中断处理程序结束处必须向8259A发送EOI代码,而当8259A在接受EOI后,会将当前正处理的ISR寄存器对应的bit置0,也就是去掉该中断。如果”EOI“被设置为自动模式,在8259A接收到第二次INTA信号,8259A会自动将当前中断从ISR中去除。

8259A的编程

  这里我们主要是对8259A进行各种初始化,设置主片与从片的级联方式,并指定起始中断向量号,还需要设置各种工作模式等任务,从而完成8259A的编程工作。

  实际上,在实模式下,BIOS给8259A的IRQ端口分配了0x8-0xf的中断向量号。而到保护模式下,CPU却重新将这些中断向量号分配给了各种异常,因此我们需要重新为8259A上的IRQ接口分配中断向量号。中断向量号是逻辑上的东西,他在物理上的表现是IRQ接口号——但IRQ的接口号是固定的,然而各个接口对应的中断向量号是不固定的,因此我们需要设置8259A,从而将IRQ接口映射到不同的中断向量号。

  而我们关于8259A的编称主要和两组寄存器有关,初始化命令寄存器组和操作命令寄存器组。其中初始化命令寄存器组用来保存初始化命令字(Initialization Command Words,ICW),ICW共4个,ICW1-ICW4。而操作命令寄存器组用来保存操作命令字(Operation Command Word,OCW),OCW共3个,OCW1-OCW3。因此我们对8259A的编称,也主要分为初始化和操作两部分——一部分使用ICW做初始化,用来确定是否需要级联,设置起始中断向量号,设置中断结束模式等,其编称是通过向8259A的端口发送一些列ICW实现,必须依次写入ICW1、ICW2、ICW3和ICW4;另一部分则是使用OCW来控制8259A,其通过往8259A端口发送OCW,从而实现中断屏蔽和中断结束。

  下面我们简单介绍一下ICW和OCW。首先是ICW1,启用来初始化8259A的连接方式和中断信号的触发方式。ICW1需要写入到主片的0x20端口和从片的0xA0端口,其中ICW1数据结构如下所示

 

 

   其中IC4表示是否需要写入ICW4,如果IC4为1,表示需要写入,否则不需要写入;SNGL,即single,若SNGL为1,表示单片,否则为级联模式;ADI,call address interval,用来设置8085调用间隔时间;LTIM,level/edge triggered mode,用来设置中断检测方式,若为0表示边沿触发,否则为电平触发。

  下面则是ICW2,其用来设置起始中断向量号,也就是会首先将IRQ0指定为该中断向量号,然后后面的IRQ接口对应的中断向量号会顺着自动排下去。这里需要说明到,ICW2需要写入到主片的0x21端口和从片的0xA1端口。由于IRQ接口的设置规则,实际上初始中断向量号始终是8的倍数,因此ICW2对应的格式如下所示

 

 

 

  这里ID0-ID2不需要我们进行设置,而T3-T7则是初始中断向量号的高5位。

  下面则是ICW3,其在启用级联的方式下设置主片和从片采用那个IRQ接口进行相连,其中ICW3需要写入主片的0x21端口和从片的0xA1端口。

 

   对于主片来说,其中ICW3中若为1,则表示对应的IRQ连接8259A,若为0则表示连接外部设备。其结构如下所示

 

 

  对于从片来说。实际上在中断响应时,主片会发送与从片做级联的IRQ接口号。因此我们只需要让从片的ICW3记录主片用于连接自己的那个IRQ接口即可。由于最多只有8个IRQ,所以只需要三位即可,其ICW如下所示

 

 

  最后则是ICW4,其用来进行设置工作模式,其中ICW3需要写入主片的0x21端口和从片的0xA1端口。格式如下所示

 

 

  其中SFNM,即Special Fully Nested Mode,特殊全嵌套模式,若SFNM为0,则表示全嵌套模式,若SFNM为1,则表示特殊全嵌套模式;BUF表示是否工作在缓冲模式,BUF为0,表示非缓冲模式,BUF为1,表示缓冲模式;M/S,Master/Slave,若工作在非缓冲模式下,该位无效,若工作在缓冲模式下,M/S位为1表示主片,否则是从片;AEOI,Auto End Of Interrupt,自动结束中断,若AEOI位为0,则非自动,手动结束中断,否则自动结束中断;最后则是uPM,microprocessor,若为0,表示8085处理器,否则为x86处理器。

 

  这里终于介绍完了所有的ICW,这里大家也不需要全部记住,只需要在需要的时候进行查看即可。

  下面还有OCW需要介绍。首先是OCW1,用来屏蔽连接在8259A上的外部设备的中断信号,实际上就是把OCW1写入IMR寄存器。同样需要说明的是,OCW1要写入主片的0x21或者从片的0xA1端口,其格式如下所示

 

   OCW1中某位为1表示屏蔽对应的IRQ中断信号。若为0的话表示放行。

  而OCW2用来设置中断结束方式和优先级模式,其写入到主片的0x20以及从片的0xA0端口。OCW2对应的数据结构如下所示

 

   R,Rotation,表示是否按照循环方式设置中断优先级;若R为1,表示优先级自动循环,否则采用固定优先级方式。SL,Specific Level,表示是否指定优先等级;若SL为1,会使用OCW2的低三位L0-L2进行指定,否则不指定优先等级。EOI,End of INterrupt,中断命令结束位,若EOI为1,则会将ISR寄存器中的相应中断位清0。L2-L0用来确认优先级的编码,当EOI为1时,表示被中断的优先级;否则指定循环优先级中的起始最低优先级。下面我们给出对应的组合及功能,如下表所示

R SL EOI 描述
0 0 1

普通EOI结束方式

当中断处理完成后,向8259A发送EOI命令,8259A会将ISR中当前级别最高的中断置0

0 1 1

特殊EOI结束方式

当中断处理完成后,向8259A发送EOI命令,8259A将ISR寄存器中由L2-L0指定的中断置0

1 0 1

普通EOI循环命令

当中断处理完成后,将ISR中当前优先级最高的位清0,并使此位的优先级变成最低,使原来第二高的优先级称为最高优先级,其余变化类似

1 1 1

特殊EOI循环指令

当中断处理完成后,8259A将ISR中由L2-L0指定的相应位清0,并使此位的优先级变成最低,使原来第二高的优先级成为最高优先级,其余变化类似

0 0 0 清除自动EOI循环命令
1 0 0

设置自动EOI循环命令

8259A自动将ISR寄存器中当前处理的中断位清0,并使此位的优先级变成最低,使原来第二高的优先级成为最高优先级,其他变化类似

1 1 0

设置优先级命令

将L2-L0指定的IRQ设置为最低优先级,然后将IRQ + 1设置为最高优先级,其他优先级类似。

0 1 0 无操作

 

  最后的COW3由于后面实际上没用上,这里就不再过多赘述了,已经分析的有点多了。

posted @ 2020-09-28 18:33  hawkJW  阅读(891)  评论(0编辑  收藏  举报