xv6(4) 中断理论部分

简介: 中断理论部分

首发公众号:Rand_cs

中断理论部分

中断是硬件和软件交互的一种机制,可以说整个操作系统,整个架构都是由中断来驱动的。中断的机制分为两种,中断和异常,中断通常为 $IO$ 设备触发的异步事件,而异常是 $CPU$ 执行指令时发生的同步事件。本文主要来说明 $IO$ 外设触发的中断,总的来说一个中断的起末会经历设备,中断控制器,$CPU$&$OS$ 三个阶段:设备产生中断,中断控制器接收和发送中断,$CPU$&$OS$ 来实际处理中断

本文来捋一捋中断需要知道的一些理论知识,主要也是从这三个阶段来说,$emmm$实际两个阶段,其中第一个阶段设备如何产生信号不讲,超过了操作系统的范围,也超过了我的能力范围。不过在讲解各个硬件比如说磁盘,串口时会涉及到一些这些硬件何时会触发中断。各种硬件外设有着自己的执行逻辑,有各种形式的中断触发机制,比如边沿触发,电平触发等等。总的来说就是向中断控制器发送一个中断信号,中断控制器再作翻译发送给 $CPU$,$CPU$ 再执行中断服务程序对中断进行处理。

说到中断控制器,是个什么东西?中断控制器可以看作是中断的代理,外设是很多的,如果没有一个中断代理,外设想要给 $CPU$ 发送中断信号来处理中断,那只能是外设连接在 $CPU$ 的管脚上,$CPU$ 的管脚是很宝贵的,不可能拿出那么多管脚去连接外设。所以就有了中断控制器这个中断代言人,所有的 $IO$ 外设连接其上,发送中断请求时就向中断控制器发送信号,中断控制器再通知 $CPU$,如此便解决了上述问题。

中断控制器的发展可分为 $PIC$ 和 $APIC$ 两个阶段,前者适用于单处理器,在单处理器的时代叱咤风云,风靡全球,不过到了现代的多处理器时代不行了,渐渐地被更高级的 $APIC$ 所代替。但 $PIC $还是值得了解了解的,来看看

中断控制器 PIC

image.png

image.png

这就是中断控制器 $8259A$ 芯片,我们只需要了解:

  • $IRQ0-IRQ7$,8 个引脚,每个引脚可以连接一个外设,也就是说一个 $8259A$ 能支持 8 个中断。
  • $8259A$ 通过 $INT$,$INTA$($Interrupt$ $Acknowledge$) 与 $CPU$ $INTR$($Interrupt$ $Request$)通信

一个 8259A 芯片只能支持 8 个中断,的确有点少了,所以有了级联:

image.png

所谓级联,就是将多个 $8259A$ 给串起来,通常方式为将一个 $8259A$ 的引脚分出来连接另一个 $8259A$,就如上图所示。一般两个 $8259A$ 级联的时候将主 $8259A$ 的 $IRQ2$ 分配出去连接从 $8259A$。

$8259A$ 的引脚 $IRQ$ 是有优先级的,编号越低,优先级越高,$vector=IRQ编号+32$,前 32 个是留给异常使用和架构保留的。

$8259A$ 最重要的几个寄存器如下所示:

寄存器

  • $IRR$,$Interrup$ $Request$ $Register$,共 8 位,对应 $IRQ0-IRQ7$ 8 个中断引脚。当某个引脚接收到中断信号时,$IRR$ 中相应的位 置 1,表示 $PIC$ 已收到该设备的中断请求但还未交由 $CPU$ 处理
  • $ISR$,$In$ $Service$ $Register$,共 8 位,同样的每位表示对应引脚表示的中断,当 $IRR$ 中某个中断请求提交给 $CPU$ 时,IRR 相应的位被清 0,$ISR$ 相应的位就被置 1,表示 $CPU$ 正处理该中断。
  • $IMR$,$Interrupt$ $Mask$ $Register$,共 8 位,同样的每位表示对应引脚表示的中断,某位置 1 表示屏蔽相应的中断,清 0 表示允许相应的中断。
  • $PR$,$Priority$ $Register$,优先级仲裁寄存器,有多个中断同时发生时,它能找出哪个中断优先级最高
  • $EOI$,$End$ $of$ $Interrupt$,在 $PIC$ 里面 $EOI$ 不是一个寄存器,它是操作命令寄存器中的一个 $bit$,写 $EOI$ 表示中断结束。

$8259A$ 还有其他的寄存器比如初始化命令寄存器,操作命令寄存器,这里我们后面讲述 $xv6$ 并不会使用 $PIC$,所以这里也就不说明了,最后来看看通过 $PIC$ 的中断流程:

PIC 中断流程

  1. $IRQ$ 引脚接收到中断信号,若该中断没有被屏蔽,那么 $IRR$ 中相应的位置 1
  2. $PIC$ 通过 $INT$ 向 $CPU$ 发送 $INTR$ 信号
  3. $CPU$ 通过 $INTA$ 引脚发送应答信号给 $PIC$
  4. $PIC$ 收到应答信号后,将 $IRR$ 中最高优先级的中断相应的位清 0,将 $ISR$ 相应的位置 1
  5. $CPU$ 再次发送 $INTA$ 信号给 $PIC$,$PIC$ 收到后将 $vector$ 送到数据线
  6. $CPU$ 根据 $vector$ 索引 $IDT$ 中的门描述符,执行中断服务程序
  7. 中断处理完成之后写 $EOI$,将 $ISR$ 中相应的位清 0 表示中断完成

高级中断控制器 APIC

上述就是中断控制器 $PIC$ 的内容,PIC 只用于单处理器,对于如今的多核多处理器时代,PIC 无能为力,所以出现了更高级的中断控制器 APICAPIC($Advanced$ $Programmable$ $Interrupt$ $Controller$) 高级可编程中断控制器,APIC 分成两部分 LAPICIOAPIC,前者 LAPIC 位于 $CPU$ 内部,每个 $CPU$ 都有一个 LAPIC,后者 IOAPIC 与外设相连。外设发出的中断信号经过 IOAPIC 处理之后发送给 LAPIC,再由 LAPIC 决定是否交由 $CPU$ 进行实际的中断处理。

image.png

可以看出每个 $CPU$ 上有一个 LAPICIOAPIC 是系统芯片组一部分,各个中断消息通过总线发送接收。关于 APIC 的内容很多也很复杂,详细描述的可以参考 $intel$ 开发手册卷三,本文不探讨其中的细节,只在上层较为抽象的层面讲述,理清 APIC 模式下中断的过程。

下面就分别来看看 $IOAPIC$ 和 $LAPIC$:

IOAPIC

$IOAPIC$ 主要负责接收外部的硬件中断,将硬件产生的中断信号翻译成具有一定格式的消息,然后通过总线将消息发送给一个或者多个 LAPIC。$IOAPIC$ 主要组成如下:

  • 24 个中断管脚,一个 $IOAPIC$ 支持 24 个中断
  • 一张 24 项的中断重定向表($PRT$,$Programmable$ $Redirection$ $Table$),每个表项都是一个 64 位的寄存器
  • 一些可编程寄存器,例如窗口寄存器,版本寄存器等等
  • 通过 $APIC$ 总线发送和接收 $APIC$ 信息的一个信息单元

我们了解一个硬件主要就是了解它的寄存器,$IOAPIC$ 有两个内存映射的寄存器,$index$ 和 $data$ 寄存器,通过 $index/data$ 的方式访问 $IOAPIC$ 的其他寄存器。

$IOAPIC$ 其他寄存器一览:

image.png

内存映射的两个寄存器

image.png

这两个寄存器是内存映射的,IOREGSEL​,地址为 $0xFEC0\ 0000$;​IOWIN​,地址为 $0xFEC0\ 0010h$。IOREGSEL 用来指定要读写的寄存器,然后从 IOWIN 中读写。也就是常说的 index/data 访问方式,或者说 $adress/data$,用 index 端口指定寄存器,从 data 端口读写寄存器,data 端口就像是所有寄存器的窗口。

而所谓内存映射,就是把这些寄存器看作内存的一部分,读写内存,就是读写寄存器,可以用访问内存的指令比如 mov 来访问寄存器。还有一种是 IO端口映射,这种映射方式是将外设的 IO端口(外设的一些寄存器) 看成一个独立的地址空间,访问这片空间不能用访问内存的指令,而需要专门的 in/out 指令来访问

IOAPIC 寄存器

ID Register
  • 索引为 0

  • $bit24 - bit27$:ID

Version Register
  • 索引为 1
  • $bit0-bit7$ 表示版本,

  • $bit16-bit23$ 表示重定向表项最多有几个,这里就是 23(从 0 开始计数)

Redirection Table Entry

重定向表项,IOAPIC 有 24 个管脚,每个管脚都对应着一个 64 位的重定向表项(也相当于 64 位的寄存器),索引为 $0x10-0x3F$,重定向表项的格式如下所示:

image.png
image.png
image.png

这个表项/寄存器包含了该中断的所有属性信息,以什么方式触发中断,传送的方式状态,管脚极性等等,这是 $ZX_WING$ 大佬在他的 $Interrupt\ in\ Linux$ 中总结出来的,很全面也很复杂,只说几点:

  • $destination$ $field$ 和 $destination$ $mode$ 字段决定了该中断发送给哪个或哪些 $LAPIC$
  • $vector$,中断控制器很重要的一项工作就是将中断信号翻译成中断向量,这个中断向量就是 $IDT$ 的索引,$IDT$ 里面的中断描述符就存放着中断处理程序的地址在 $PIC$ 中,$vector = IRQ编号+32$,而在 $APIC$ 模式下,$IRQ$ 对应的 $vecotr$ 由操作系统对 $IOAPIC$ 初始化的时候设置分配
  • $IOAPIC$ 的管脚没有优先级之分,不像 $PIC$ 的 $IRQ0$ 的优先级比 $IRQ1$ 的优先级要高,而 $IOAPIC$ 对中断优先级的区分在于管脚对应的重定向表项的 $vector$ 字段。

IOAPIC 总结

由上,对 $IOAPIC$ 的工作总结:当 $IOAPIC$ 的管脚接收到外设发来的中断信号后,根据相应的重定向表项格式化出一条中断消息,然后发送给 $destination$ $field$ 字段列出的 $LAPIC$

LAPIC

LAPIC 要比 IOAPIC 复杂的多,先看张总图:

image.png

本文不会说这么多,也不可能说这么多,有的我也是不太明白,超出我的能力范围,这里只是来看看对其简单编程需要了解的部分:

$LAPIC$ 其主要功能是接收中断消息然后交由 $CPU$ 处理,再者就是自身也能作为中断源产生中断发送给自身或其他 $CPU$。所以其实 $LAPIC$ 能够收到三个来源的中断:

  • 本地中断:时钟,温度监测等
  • 外部中断:$IOAPIC$ 发来的
  • $IPI$:处理器间中断,其他 $LAPIC$ 发来的

$inel$ 手册里面做了更精细复杂的分类,私以为了解这三大类就行了。了解 $LAPIC $从它的一些重要寄存器入手,通过这些寄存器的作用来了解 LAPIC 如何工作的:

IRR(Interrupt Request Register)

中断请求寄存器,256 位,每位代表着一个中断。当某个中断消息发来时,如果该中断没有被屏蔽,则将 $IRR$ 对应的 bit 置 1,表示收到了该中断请求但 $CPU$ 还未处理

ISR(In Service Register)

服务中寄存器,256 位,每位代表着一个中断。当 $IRR$ 中某个中断请求发送给 $CPU$ 时,$ISR$ 对应的位上便置 1,相应的 $IRR$ 位清零,表示 CPU 正在处理该中断

TMR(Trigger Mode Register)

触发模式寄存器,256 位,每位代表一种中断的触发模式,若中断的触发模式为 $edge$(边沿)触发则相应的位清 0,若触发模式为电 $level$(电平) 触发则置 1。

上面三种寄存器如下图所示:

image.png

EOI(End of Interrupt)

中断结束寄存器,32 位,写 EOI 表示中断处理完成。写 $EOI$ 寄存器会导致 $LAPIC$ 清理 $ISR$ 的对应的位,对于 $level$ 触发的中断,还会向所有的 $IOAPIC$ 发送 $EOI$ 消息,通告中断处理已经完成,通常写 0 就行。

ID

image.png

用来唯一标识一个 $LAPIC$,$LAPIC$ 与 $CPU$ 一一对应,所以也用 $LAPIC$ $ID$ 来标识 $CPU$。$APIC$ 分为 $LAPIC$ 和 $IOAPIC$,但是如上图手册所示 $LAPIC$ $ID$ 一般叫做 $APIC$ $ID$,这里我为了将两者区分开就写得是 $LAPIC$ $ID$

TPR(Task Priority Register)

image.png

任务优先级寄存器,确定当前 $CPU$ 能够处理什么优先级别的中断,$CPU$ 只处理比 $TPR$ 中级别更高的中断,比它低的中断暂时屏蔽掉,也就是在 $IRR$ 中继续等到,直到 $TPR$ 的优先级下降到低优先级中断能够被处理。这个机制使得操作系统能够暂时屏蔽低优先级中断,防止打扰高优先级的处理。

一共有 256 种中断,所以 $TPR$ 只用到了 $bit0-bit7$,另外 $优先级别=vector/16$,$vector$ 为每个中断对应的中断向量号。所以 $Task$ $Priority$ 的高 4 位表示优先级别,有 $0-15$ 个取值。

如果优先级别设置为 15,则不会接受任何中断,如果优先级别设置为 0,表示接受所有中断,这也是 $Linux$ 设置的默认值。另外一些特殊中断如 $NMI$ 不可屏蔽的中断不受 $TPR$ 的规则限制。

PPR(Processor Priority Register)

处理器优先级寄存器,表示当前正处理的中断的优先级,$PPR$ 的值为 $ISR$ 中正服务的最高优先级中断和 $TPR$ 两者之间选取优先级较大的

IF TPR[7:4] ≥ ISRV[7:4]
  PPR[7:0] = TPR[7:0]
ELSE 
  PPR[7:4] = ISRV[7:4] AND PPR[3:0] = 0

在 $IRR$ 中等待的中断,只有优先级别高于 $PPR$ 的才会被送到 $CPU$ 处理,所以 $TPR$ 就是靠间接控制 $PPR$ 来实现暂时屏蔽比 $TPR$ 优先级小的中断的。

SVR(Spurious Interrupt Vector Register)

image.png

伪中断寄存器,$CPU$ 每响应一次 $INTR$(可屏蔽中断),就会连续执行两个 $INTA$ 周期。在 $MP\ Spec$ 中有描述,当一个中断在第一个 $INTA$ 周期后,第二个 $INTA$ 周期前变为无效,则为伪中断,也就是说伪中断就是中断引脚没有维持足够的有效电平而产生的

这里主要用到 $bit8$,可以通过将这位置 1 来使 $APIC$ 工作,原话 $To$ $enable$ $the$ $APIC$。

ICR(Interrupt Command Register)

image.png

中断指令寄存器,当一个 $CPU$ 想把中断发送给另一个 $CPU$ 时,就在 $ICR$ 中填写相应的中断向量和目标 $LAPIC$ 标识,然后通过总线向目标 $LAPIC$ 发送消息。$ICR$ 寄存器的字段和 $IOAPIC$ 重定向表项较为相似,都有 $destination$ $field$, $delivery$ $mode$, $destination$ $mode$ 等等。

这是处理器之间发送中断消息,所以又叫 $IPI$($InterProcessor$ $Interrupt$)消息。在启动的时候我们就用到了这个寄存器,$BSP$ 向 $APs$ 发送 $IPI$ 消息,当时 $BSP$ 发送了 $INIT-SIPI-SIPI$ 消息给 AP,这里我们就清楚了实际就是设置 $delivery$ $mode$ 为 $INIT$、$Start-up$。当然这期间还根据规范设置了其他字段。

本地中断

$LAPIC$ 本身还能作为中断源产生中断,$LVT$($Local$ $Vector$ $Table$) 就是自身作为中断源的一个配置表,总共 7 项,每项 32 位,同 $IOAPIC$,每一项也是一个寄存器,如下所示:

image.png

我们主要关注时钟中断,$APIC$ 自带一个时钟,我们可以在 $LVT$ 中配置 $Timer$ 一项来使用这个时钟。

$Timer$ $Mode$,设置时钟计数的模式,$one$-$shot$,一次性的倒数计时,$periodic$,周期性的倒数计时,$TSC$-$deadline$,使用 64 位的时间戳计数器。

一般使用 $periodic$ 来周期性的产生时钟中断,周期性的从某个数递减到 0,如此循环往复。这个数设置在 $TICR$ 寄存器

image.png

$TICR$ 寄存器里面存放 $Initial$ $Count$,也就是从哪个数开始倒数。而 $Current$ $Count$ 存放当前初始计数值,每当计时器 $count$ 到 0 ,产生时钟中断时,$Current$ $Count$ 就会自动地从 $Intial$ $Count$ 重新加载,接着新一次的倒数,所以其实 $Current$ $Count$ 似乎没什么用,xv6 里面也没用到这个寄存器。另外如果向 $TICR$ 寄存器里面写 0 的话会停止计时器。

递减得有个频率,这个频率是系统的总线频率再分频,分频系数设置在 $TDCR$ 寄存器

image.png

LAPIC 总结

对上面 $LAPIC$ 的寄存有一定了解后,对 $LAPIC$ 的工作应该也有一定了解了,对于从 $IOAPIC$ 发来的中断消息,首先判断自己是否接收这个消息,这要根据重定向表项中的 $destination$ $field$,$destination$ $mode$ 来判断:

$destination$ $mode$ 为 0 时表示物理模式,$destination$ $field$ 字段表示一个 $APIC$ $ID$,$LAPIC$ 根据 $ID$ 寄存器比对判断是否由自己来接收

$destination$ $mode$ 为 1 时表示逻辑模式,$LAPIC$ 需要另外两个寄存器 $LDR$ 和 $DFR$ 来辅助判断,具体判断方式很复杂,逻辑模式分为 $flat$ 和 $cluster$,$cluster$ 又分为 $flat$ $cluster$ 和 $hierachical$ $cluster$,了解就好,感兴趣的参考手册或者 $interrupt$ $in$ $linux$ ,有着很详细的讲解,这里不赘述。

判断不该自己接收就忽略,否则接收该中断, $IRR$,$ISR$,$TPR$,$PPR$,$EOI$ 等寄存器配合使用,来决定是否将该中断发送到 $CPU$ 进行处理,此外如果中断类型为 $NMI$ 等特殊中断是直接发送给 $CPU$ 进行处理,不需要上述步骤。

要实现处理器间中断,一个处理器想把中断发送给另一个处理器时,就在 $ICR$ 中填写相应的中断向量和目标 $Destination$ $Feild$,然后通过总线向目标 $LAPIC$ 发送消息。

$LAPIC$ 自己也可以作为中断源,可在 $LVT$ 中配置相关中断,主要留意时钟中断的设置,$xv6$ 就是使用 $LAPIC$ 自带的时钟来周期性产生时钟中断。

APIC 部分中断流程总结

  1. $IOAPIC$ 某个引脚收到了对应外设发出的中断信号

  2. $IOAPIC$ 根据引脚对应的重定向表项,将中断信号翻译成中断消息,然后发送给 $destination$ $field$ 字段列出的 $LAPIC$

  3. $LAPIC$ 根据消息中的 $destination$ $mode$,$destination$ $field$,自身的寄存器 $ID$,$LDR$,$DFR$ 来判断自己是否接收该中断消息,不是则忽略

  4. 如果该中断是 $SMI$/$NMI$/$INIT$/$ExtINT$/$SIPI$,直接送 $CPU$ 执行,因为这些中断都是负责特殊的系统管理任务。否则的话将 $IRR$ 相应的位置 1,等待 $CPU$ 来处理。

  5. 从 $IRR$ 中挑选优先级最大的中断,相应位置 0,$ISR$ 相应位置 1,然后送 $CPU$ 执行。

  6. $CPU$ 执行 $OS$ 中的中断服务程序来处理中断。

  7. 中断处理完成后写 $EOI$ 表示中断处理已经完成,写 $EOI$ 导致 $ISR$ 相应位置 0,对于 $level$ 触发的中断,还会向所有的 $IOAPIC$ 发送 $EOI$ 消息,通知中断处理已经完成。

CPU AND OS 处理中断

上述为中断控制器部分,主要功能就是接收外设的中断信号,然后交由 $CPU$ 来处理。最开始我把这部分只归结到了 $CPU$ 部分,后面想想处理中断是个软硬件协作的一个过程,有 $CPU$ 硬件部分,也有 $OS$ 软件部分,但总归是真正处理中断的过程,我就把它们归结到一起了。

在实际谈处理中断前先来了解与中断相关的数据结构

中断描述符表&门描述符&中断向量

中断描述符表 $IDT$ 里面存放的是门描述符,有三种门描述符,任务门,中断门,陷阱门:

image.png

任务门和任务状态段是 $intel$ 最开始提供的一种任务切换机制,可以使用任务门来切换任务,但因效率低下,现已经不使用,这部分在进程一节中还会提及。

中断门和陷阱门几乎一模一样,从描述符的结构来看就只有 $bit8-bit11$ 的 $TYPE$ 字段不一样,实际上从运行过程上说两者的唯一区别是中断门会影响 $EFLAGS$ 的 $IF$ 位,而陷阱门不会

通过中断门访问中断服务程序时,$CPU$ 会对 $EFLAGS$ 的 $IF$ 位清 0,即不允许其他中断打扰当前中断的执行,也就是中断的执行过程中关中断,在通过 $iret$ 指令从中断返回时恢复 $IF$ 位。而这里的恢复是指弹出栈中保存的 $eflags$ 值到 $EFLAGS$ 寄存器,这在后面中断流程再详述。

中断门和陷阱门的格式与 $GDT$ 中的段描述符很相像,段描述符描述符了一个段的位置和属性,同样的门描述符也描述了一个段的位置和属性。段的意思很灵活,就是指内存的一段数据信息,不是说只有代码段数据段才叫段,这里门描述符指向的段就是中断服务程序。

image.png

$intel$ 手册里面这个图画的不是很全,我重画了一张,跟段描述符差不多:

  • $DPL$,描述符特权级,与 $RPL$ $CPL$ 一起作特权级检查
  • $P$,该段在内存中是否存在,0 不存在,1 存在
  • $S$,0 表示系统段,1 表示非系统段,门结构都是系统段
  • $TYPE$,类型字段
  • 段选择子:段内偏移 指示中断服务程序的地址

定位中断服务程序

所以 $IDT$ 里面就存放着这些门描述符,门描述符又指向一个中断服务程序。在 $GDT$ 中有段选择子来充当索引指向一个段描述符,在 $IDT$ 中也有类似的结构,那就是中断向量 $vector$。它也是上述中断控制器 $APIC$ 中多次出现的那个东西。

所以这里我们对中断控制器应有一个更清晰的认识,中断控制器就是接收中断然后将该中断的 $vector$ 传给 $CPU$,$CPU$ 就会去 $IDT$ 中索引门描述符,根据其中记录的 段选择子:段内偏移 获取中断服务程序,然后执行处理中断

至于如果从逻辑地址 段选择子:段内偏移 经过段级转换到线性地址,线性地址又经过页级转换到物理地址,这个过程现在大家应该很熟悉了,就不再赘述,如果不是很清楚,可以看看前面启动部分的前导理论。

来看张定位中断服务程序的示意图:

image.png

IDT&IDTR

同 $GDT$ 有个 $GDTR$ 指示 $GDT$ 的位置,$IDT$ 也有个 $IDTR$ 指示 $IDT$ 的位置

image.png

$IDTR$ 里面存放着 $IDT$ 的地址和界限,很简单,一眼过去应该就能明白,不多说。同 $GDTR$ 有 lgdt 指令来加载 $GDT$ 的位置信息,IDTR 也有 $lidt$ 来加载 $IDT$ 的位置信息,指令格式为 LIDT m16&32

CPU AND OS部分的中断流程

$CPU$ 和 OS 部分处理中断从 $CPU$ 获取到中断控制器发来的 $vector$ 开始,这部分的中断流程可以分为三个步骤:

  • 保存上下文
  • 执行中断处理程序
  • 恢复上下文

保存上下文

保存上下文又分为两部分:

  • 硬件 $CPU$ 部分
  • 软件 $OS$ 部分
CPU 部分

$CPU$ 根据 $vector$ 索引门描述符,它会根据描述符的 $DPL$,描述符选择子中的 $RPL$,$CS$ 不可见部分的 $CPL$ 进行特权级检查,这里特权级检查很复杂,不是之前说的一般情况下只需要判断 $DPL \ge CPL\ \ \ \&\&\ \ \ DPL \ge RPL$ 即可,但主要一点就是判断是否有特权级转移,我们一般就是用两种特权级,内核态 0,用户态 3。所以这里就是判断发生中断的前一刻处于哪个特权级,如果处于用户态那么就要从用户态进入内核态,否则就不需要

保存上下文是保存在栈里面,如果没有特权级转移,发生中断前本身就在内核,那么就使用当前的内核栈保存上下文。如果有特权级转移,发生中断前处于用户态,那么就要进入内核态,进入用户态的一个重要标识就是换栈,换成内核栈

什么叫做换栈,换栈就是更改 $SS$ 和 $ESP$ 寄存器的值,换成内核栈就是将这两个寄存器换成内核栈的 $ss$ 和 $esp$,关键问题是:内核栈的 $ss$ 和 $esp$ 在哪?在 $TSS$ 里面,$TSS$,$Task$ $Segment$ $State$,任务状态段,这个结构我们后面进程会详细讲述,这里就只需要知道,$TSS$ 里面有用户态进入内核时需要的内核栈位置信息

好了现在栈已经换成中断要使用的栈了,$CPU$ 需要在里面保存上下文,有哪些呢?如下图所示:

image.png

上下文也分两种,一种是发生了特权级转移的情况,一种是未发生的情况,发生特权级转移的情况多了 $ss_old$ 和 $esp_old$,也很好理解,换了栈,当然要把旧栈信息保存到新栈对吧。

$eflags$ 表示中断前的一些标识信息,$cs_old$,$eip_old$ 表示中断点,中断退出后任务就会从这儿继续执行。

$error_code $表示错误码,有些中断会产生错误码

image.png

其主要部分就是选择子,所以它用来指明中断发生在哪个段上,其他字段 $IDT$ 表示是否指向 $IDT$,为 1 表示指向 $IDT$,0 表示指向 $GDT$/$LDT$,$TI$ 为 0 表示 $GDT$,$TI$ 为 1 表示 $LDT$,$EXT$ 表示中断源是否来自外部。

需要了解的是 Page Fault,缺页异常有错误码,它的错误码最后三位有不同的意思:

  1. bit0 U/S :
  2. bit1 W/R:
  3. bit2 P:

?????????????

我们关注错误码不是因为它有什么作用,当然有用也是有用的,只是这里主要关注它引起的格式问题,虽然有错误码的话 $CPU$ 会自动压入,但是 $iret$ 时 $CPU$ 不会自动弹出,$iret$ 时 $ESP$ 应指向 $eip_old$,所以错误码需要我们手动弹出。那没有错误码的怎么办呢?为此,我们手动地也压入一个值 0,那么栈里面的结构就统一了,如上图所示。栈中结构同意了,我们的操作也就统一了,$iret$ 前先把 $error_code/0$ 给弹出去,再执行 $iret$ 指令。

OS 部分

中断服务程序我也分为了三部分,中断入口程序,中断处理程序,中断退出程序。而中断入口程序主要就是用来保存上下文的,这里所谓的上下文就是各类寄存器的值,通常要高效的话,可以选择性的保存,但是省事简单的话直接一股脑儿地全保存了也没什么问题另外通常这部分也把向量号 $vector$ 也压进去,比如 $xv6$ 里就是保存完上下文之后就是这样:

image.png

执行中断处理程序

这部分其实没啥说的,就是 $CPU$ 来执行一个程序来处理中断,不同的中断处理程序肯定是不同的,实际的中断处理程序分布在各个章节详细讲述,这里就这样,了解流程就行。

恢复上下文

这部分其实也没啥说的,因为就是保存上下文的逆操作,我同样的分为两部分

先是 $OS$ 软件部分,执行中断退出程序,弹出通用寄存器,段寄存器等上下文

接着就是硬件 $CPU$ 部分,执行 $iret$ 弹出 $cs_old$, $eip_old$, eflags, ($ss_old$, $esp_old$,有特权级转移的话)。

开关中断

这里再说说开关中断的问题,$CPU$ 是能够屏蔽可屏蔽中断的,就是通过 $EFLAGS$ 的 $IF$ 位,$IF$ 位为 1 表示允许中断,$IF$ 为 0 表示屏蔽中断。

所以开关中断就是修改 $EFLAGS$ 的 $IF$ 位,有这么几种方式修改 $EFLAGS$ 寄存器值:

  • $sti$ 指令将 $EFLAGS$ $IF$ 位置 1,$cli$ 指令将 $EFLAGS$ $IF$ 位清 0,这两个指令有使用条件:$CPL \le IOPL$,IOPL 也是 ELFAGS 里面的位域,指的是 IO 特权级,这里不深入展开,到进程一块讲述 $TSS$ 的时候还会提及
  • $pushf$ 指令将 $EFLAGS$ 压栈,还有中断时 $EFLAGS$ 也会压栈,压入栈中后我们就可以修改栈里面的 $EFLAGS$ 的值,待到后续 $popf$ 或者 $iret$ 中断退出将修改后的 $EFLAGS$ 弹出后,就相当于修改了 $EFLAGS$ 的值。
  • 通过中断门进入中断时会将 $EFLAGS$ 的 $IF$ 位清 0.

通过更改 $EFLAGS$ 的 $IF$ 位来开关中断就只有这三种方法,所以通常我们处理中断时并不需要额外地做开关中断处理,为什么呢?因为硬件 $CPU$ 已经帮我们做了,通过中断门进入中断时自动地关闭中断,然后 $iret$ 后又恢复中断。

中断流程总结

私以为上述说的中断流程应是很清楚的,只不过像对什么中断处理的分类,中断服务程序分类是我自己杜撰的,可能与您平时看到的不甚一样,不过我认为这样来看是要清楚些,这里将上述说的总结一番:

  1. 根据 $vector$ 去 $IDT$ 中索引相应的门描述符
  2. 判断特权级是否发生变化,如果中断发生在用户态,则需要换成内核栈,切换到内核态
  3. 若发生特权级变化,需要保存用户态的 $ss_old$,$esp_old$ 到内核栈,否则不需要保存,然后再保存 $eflags$,$cs_old$,$ip_old$ 到内核栈中,如果有错误码,还要将错误码压进栈中
  4. 根据门描述符中的段选择子再去 $GDT$ 中索引相应的段描述符得到段基址,与中断描述符里的段偏移量结合起来找到中断服务程序
  5. 中断入口程序保存上下文,中断处理程序实际处理中断,中断退出程序恢复一部分上下文
  6. 最后执行 $iret$ 恢复 $CPU$ 保存的上下文,如果有特权级转移,则换到用户栈回到用户态。
  7. 中断完成,接着原任务执行

软件中断

最后这一部分简单说说软件中断($Software$-$Generated$ $Interrupts$),注意软件中断和 $Linux$ 里面上下半部分中的软中断机制是两个不同的概念。这里软件中断指的就是 $INT$ $n$ 指令来模拟一个 $n$ 号中断。以前 $Linux$ 中系统调用就是使用 $int$ 0x80 来实现的。

软件中断源来自内部,所以它的处理流程没有中断控制器这个部分,而剩下的 $CPU$&$OS$ 部分可以说是一模一样,$int$ $n$,这个 $n$ 就是 $vector$,后面的如何操作不赘述了,见前。只是说如果使用 $int$ $n$ 指令来实现系统调用的话,总归是有点特殊的,留待系统调用再详述。

好了本节就这样吧,有什么问题还请批评指正,也欢迎大家来同我讨论交流学习进步。
首发公众号:Rand_cs

目录
相关文章
|
6月前
|
存储 缓存 算法
xv6 启动理论部分
xv6 启动理论部分
145 2
|
6月前
LabVIEW编程NI 6602计数器DMA冲突例程与相关资料
LabVIEW编程NI 6602计数器DMA冲突例程与相关资料
54 7
|
6月前
|
存储
MCS-51单片机的中断源
MCS-51单片机的中断源
185 1
|
6月前
MCS接口技术----定时/计数,中断
MCS接口技术----定时/计数,中断
80 0
|
6月前
|
算法 Linux C++
【探索Linux】P.16(进程信号 —— 信号产生 | 信号发送 | 核心转储)
【探索Linux】P.16(进程信号 —— 信号产生 | 信号发送 | 核心转储)
53 0
|
6月前
|
存储 算法 Linux
【探索Linux】P.17(进程信号 —— 信号保存 | 阻塞信号 | sigprocmask() | sigpending() )
【探索Linux】P.17(进程信号 —— 信号保存 | 阻塞信号 | sigprocmask() | sigpending() )
93 0
|
6月前
中断编程实验
中断编程实验
57 0
|
6月前
|
存储 调度 芯片
xv6(5) 中断代码部分
中断代码部分
75 0
|
6月前
|
存储 算法 编译器
xv6(17) 进程三:代码部分
进程三:代码部分
141 0
|
6月前
|
存储 缓存 Linux
xv6(9) 文件系统理论部分
文件系统理论部分
134 0