继续“那些事儿”系列,这次的主题是Intel的中断处理。参考的资料主要来自Intel文档第三册的第六、第十和第二十九章节,以及这篇文章。其中,有一部分的内容来自于上面提到的那篇文章。
以下内容主要围绕下面五个问题来展开:
接下来会对它们一一进行解答。
我们经常会将中断(Interrupt)和异常(Exception)混在一起进行说明,可以说,它们有相似的地方,也有不同的地方。相似的地方在于,它们都是因为系统发生了某些事件,使得处理器需要暂停当前的执行流,从而抽出精力(进到某个预先设定好的路径中)来处理这些事件。不同的地方在于它们的来源,以及从事件发生到最终被处理的整条路径都是不一样的。这篇博文主要关注中断,所以对异常就不去详谈了。
一般来说,中断主要是由一些硬件设备产生的,表示这些硬件有一些重要的事件需要通知处理器,比如某些从外部设备请求的数据准备好了,需要通知处理器对其进行读取等。当然这里所谓的“一般来说”是指也可以通过软件的方式来触发中断,比如调用INT n
指令,当然这种方式产生的中断和通过意见产生的中断最终的处理方式会有很大的不同。
因此从种类来分,可以将中断分为通过硬件产生的外部中断(External interrupt)和通过软件产生的软件中断(Software interrupt)。不管是外部中断还是软件中断,每个中断都有一个中断号与之对应,对于外部中断来说,可使用的中断号范围从16到255(0到15)为系统预留的中断号,而对于软件中断来说,可使用的中断号为0到255。除此之外,16到255范围内的中断是可以通过EFLAGS中的IF flag
进行disable的,如果EFLAGS中的IF flag
被清零,则表示当前CPU不接受这个范围内的中断,如果其被置为1,则表示当前CPU可以正常处理这个范围内的中断。
中断在进入CPU之前,首先会进入一个被称为Advanced Programmable Interrupt Controller(APIC)的控制器中,可以说,每个CPU都有一个APIC,被称为该CPU的Local APIC(LAPIC)。每个LAPIC由一系列的寄存器组成,这些寄存器控制了LAPIC如何将中断送到处理器中。而根据实现的不同,对这些寄存器的访问方式也不一样,比如,对于传统的APIC和xAPIC来说,这些寄存器都是被映射在内存中的,可以直接通过内存读写的方式对其进行访问,而对于x2APIC来说,需要通过访问MSR的方式来访问这些寄存器,具体的地址和访问方法可以查看Intel文档。
下图展示了一个LAPIC的架构,里面包含了各种各样的寄存器,大部分的功能都可以通过查询Intel文档获得,其中有几个特别重要的寄存器:In-Service Register (ISR),Interrupt Request Register (IRR),EOI Register,Task Priority Register (TPR),Processor Priority Register (PPR),Interrupt Command Register (ICR),Local Vector Table (LVT)。这些会在接下来的篇幅中一一进行介绍。
对于目前的LAPIC来说,它可能从以下几个来源接收到中断:
其中,前面五种中断来源被称为本地中断源(local interrupt sources),LAPIC会预先在Local Vector Table (LVT)表中设置好相应的中断递送(delivery)方案,在接收到这些本地中断源的时候根据LVT中的方案对相关中断进行递送。
除此之外,对于从IOAPIC中发送过来的外部中断,以及从其它处理器中发过来的IPI中断,LAPIC会直接将该中断交给本地的处理器进行处理。而如果需要向其它处理器发送IPI,则可以通过写LAPIC中的ICR寄存器完成。这部分这里就不详述,直接看文档就可以了。
那么我们现在就来看看当一个外部设备产生中断,到这个中断被发送给相应的CPU,这中间都会经历些什么过程。
在IOAPIC内部,有一个非常重要的数据结构,叫做可编程重定向表(Programmable Redirection Table,PRT),在PRT表中,包含了若干个重定向表项(Redirection Table Entry,RTE),每个RTE对应一个中断管脚,比如,典型的IOAPIC可能包含24个中断管脚,相应的PTR表中就有24个与之相对应的RTE。 通常情况下,每个外部设备都会通过特定的管脚和IOAPIC相连,中断产生之后,会通过该管脚进入IOAPIC,而当IOAPIC的某个管脚接收到中断信号后,会根据该管脚对应的一个RTE,格式化出一条中断消息,发送给某个(或多个)处理器的LAPIC。下表列出了RTE的格式:
Bits | 描述 |
---|---|
63:56 | Destination Field,目的字段,R/W(可读写)。根据 Destination Filed(见下)值的不同,该字段值的意义不同,它有两个意义:Physical Mode(Destination Mode 为 0 时 ): 其值为 APIC ID,用于标识一个唯一的 APIC。Logical Mode(Destination Mode 为 1 时):其值根据 LAPIC 的不同配置,代表一组CPU。 |
55:17 | Reserved,预留未用。 |
16 | Interrupt Mask,中断屏蔽位,R/W。置一时,对应的中断管脚被屏蔽,这时产生的中断将被忽略。清零时,对应管脚产生的中断被发送至LAPIC。 |
15 | Trigger Mode,触发模式,R/W。指明该管脚的的中断由什么方式触发。1:Level,电平触发;2:Edge,边沿触发。 |
14 | Remote IRR,远程 IRR,RO(只读)。只对level触发的中断有效,当该中断是edge触发时,该值代表的意义未定义。当中断是level触发时,LAPIC接收了该中断,该位置一,LAPIC写EOI 时,该位清零。 |
13 | Interrupt Input Pin Polarity(INTPOL),中断管脚的极性,R/W。指定该管脚的有效电平是高电平还是低电平。0:高电平;1:低电平。 |
12 | Delivery Status,传送状态,RO。0:IDEL,当前没有中断;1:Send Pending,IOAPIC 已经收到该中断,但由于某种原因该中断还未发送给LAPIC |
11 | Destination Mode,目的地模式,R/W。0:Physical Mode,解释见 Destination Field;1:Logical Mode,同上。 |
10:8 | Delivery Mode,传送模式,R/W。用于指定该中断以何种方式发送给目的 APIC,各种模式需要和相应的触发方式配合。详见Intel文档。 |
7:0 | Interrupt Vector,中断向量,R/W。指定该中断对应的vector,范围从10h到FEh。 |
从上表我们可以看出,该消息包含了一个中断的所有信息。其中Destination field和Destination mode定义了该中断将被递送的目标处理器。 从IOAPIC到LAPIC有两种可能的路径,如下图所示:第一种是通过系统总线(System bus),该种路径实现在Pentium 4和Intel Xeon系列的处理器上;第二种是通过APIC bus,这种路径实现在Pentium and P6家族的处理器上。至于它们有什么区别,还是去看文档的解释吧。
总之,外部设备产生的中断最终通过IOAPIC被递送到了某个(或者多个)处理器中的LAPIC中。接下来,就要看LAPIC是如何将这些中断递送给处理器进行处理了。
LAPIC无论是接收到来自IOAPIC的中断,来自本地中断源的中断,还是来自其他处理器发送的IPI中断,都会将其交由CPU进行处理,但是由于CPU这个时候可能正在处理其它中断,所以需要一套机制来保证中断处理的安全性。
首先需要注意的是,在RTE格式那张表中,中断的delivery mode可能有好几种,其中NMI、SMI、INIT、ExtINT和SIPI这几种delivery mode的中断将会直接交由CPU进行处理,如果当前CPU正在处理这些delivery mode的中断,则会禁止相同的中断被递送进来。除此之外,还有一种被称为fixed的delivery mode,也就是普通的中断,它们的递送机制是通过IRR和ISR寄存器完成的。在X86平台上,这两个都是256bits的寄存器(其实是由8个64bits的寄存器组成的),每个bit代表一个中断的vector,其中第0到第16个bit是reserve的。IRR和ISR每个bit代表的意思分别如下:
n
位的bit被置上,则代表LAPIC已接收vector为n
的中断,但还未交CPU处理。n
位的bit被置上,则代表CPU已开始处理vector为n
的中断,但还未完成。需要注意的是,当CPU正在处理某中断时,如果又被递送过来一个相同vector的中断,则相应的IRR bit会再次置一; 如果某中断被pending在IRR中,同类型的被再次递送过来,则ISR中相应的bit被置一。 这说明在APIC系统中,同一类型中断最多可以被计数两次。
另外,当某个中断被处理完之后,LAPIC需要通过软件写EOI寄存器来告知。
因此,根据处理器的不同,一个典型的LAPIC中断处理流程是这样的:
在上面的这两套流程中,涉及到几个关键的寄存器(TPR,PRR)和delivery mode(lowest priority),这就涉及到中断的优先级问题了,会在“中断的优先级问题”中进行解释。
当CPU开始处理中断的时候,会查询一个被称为中断描述符表(Interrupt Descriptor Table,IDT)的数据结构,该数据结构的每一项都被预先填上了一个门描述符(gate descriptor),其中有三种门描述符:task, interrupt和trap,这里我们主要关注的是interrupt-gate descriptor。下图显示了interrupt gate的相关信息:
通过它,就可以找到相应vector的中断的处理函数了。在进入处理函数之前,一般会对栈进行一个切换,并且将相应的寄存器信息(包括RFLAGS, CS, RIP等)压入栈中,从而保证在中断处理结束之后可以恢复相关信息。切换栈和保存相关信息的过程如下图所示:
主要包括两种情况,第一种情况是被中断的进程不是内核进程,则需要有一个权限级别的切换,因此需要换一个栈;第二种情况是被中断的进程是一个内核进程,因此不需要切换栈,只需要在原来的栈中保存信息就可以了。整个流程还是比较清楚的,因此这里也不详述了。
就像之前提到的,中断是有优先级概念的,具体体现在优先级高的中断会被先递送给CPU进行处理,而优先级低的中断往往需要在优先级高的中断被处理完之后才会被处理。为了简单起见,中断的优先级是由中断本身的vector信息来得到的。
我们知道每个中断都有一个vector与之对应,而中断的优先级别由下列公式得到:
优先级别 = vector / 16
因此,16~255号vector的中断构成了1~15共15个优先级别。而对于同一个级别的中断,vector号越大的优先级越高。例如vector33、34都属于级别2,34的优先级就比33 高。所以,对于8bit的vector,又可以划分成两部分,高4bit表示中断优先级别,低4bit表示该中断在这一级别中的位置。
除此之外,LAPIC中还有两个寄存器是和优先级相关的,它们分别是任务优先级寄存器(task priority register, TPR)和处理器优先级寄存器(processor priority register, PPR)。
其中,TPR确定当前CPU可处理什么优先级别范围内的中断。具有如下的格式:
TPR寄存器接收0~15共16个值,对应16个CPU规定的中断优先级级别,值越大优先级越高。CPU只处理比TPR中值优先级别更高的中断。例如TPR中值为8,则级别小于等于8的中断被屏蔽(注意,屏蔽不代表拒绝,LAPIC 接收它们,把它们pending到IRR中,但不交CPU处理)。值15表示屏蔽所有中断;值0表示接收所有中断,这也是Linux为TPR设置的默认值。注意,TPR是由软件读/写的,硬 件不更改它。因此,TPR的值增加 1,将会屏蔽16个vector对应的中断。当然,NMI、SMI、ExtINT、INIT、start-up delivery的中断不受TPR约束。
而PPR决定当前CPU正在处理的中断的优先级级别,以确定一个pending在IRR上的中断是否发送给CPU。具体格式如下图所示:
与TPR不同,它的值由CPU写而不是软件写。PPR取值范围为[0,15],计算方式由下列伪代码描述:
1 2 3 4 5 |
|
这里,ISRV[7:4]标识当前ISR中,最高优先级中断对应vector的高4bit,如前面所说,这代表了该中断的优先级级别。简而言之,取TPR和正在服务的最高优先级中断中,优先级级别高的。所以说,IRR中pending的中断,优先级级别必须高于PPR中值才会被发送给CPU处理,否则,继续等…
最后一个概念是lowest priority。RTE的delivery mode有一中模式为lowest priority,即最低优先级。需要注意的是,这里的最低优先级不是指中断的优先级,而是指将中断发送给destination field列出的CPU中,优先级最低的一个。而如何选择所有CPU中优先级最低的一个呢,答案应该是通过每个CPU所对应的TPR来决定的。
这里举一个例子:假设有CPU1、CPU2、CPU3三个CPU,相应的TPR值为:TPR1=5、TPR2=6、TPR3=10,IOAPIC以lowest priority模式发送一条中断消息,该中断对应的优先级级别为3。则CPU1具有最低优先级,接收该中断。此时,该中断被pending到CPU1的IRR中,但不会交给CPU1处理,因为其优先级级别低于TPR值。
最后我们来谈谈硬件虚拟化对中断提供了哪些支持。该部分主要参考Intel文档第三册的第二十九章节。
中断的虚拟化主要分为两个部分:第一,需要模拟虚拟机对APIC控制寄存器的读写操作;第二,需要虚拟化中断的delivery步骤,换句话说,当虚拟机正在运行的时候来了一个中断,虚拟化层需要判断该中断是否应该递送给虚拟机,以及如何递送。
在虚拟机中,不可避免地会对APIC中的寄存器进行访问,而虚拟化层有两种方式可以对其进行模拟:
EPT violation
的下陷,从而在虚拟化层对其进行模拟。Secondary Processor-Based VM-Execution Controls
域中的virtualize APIC accesses
bit。在这种情况下,通过设置特定的VM-execution controls
的位,使得虚拟机在访问APIC对应的页的时候可能产生APIC-access VM exit
的下陷,或者不产生下陷。我们主要考虑第二种方式。
第二种方式的前提是virtualize APIC accesses bit
被置一。在这个前提下,如果non-root中的虚拟机通过linear address对APIC page进行访问,则需要对相关操作进行虚拟化。这里有两个比较重要的VMCS域:APIC-access address
和Virtual-APIC address
。其中,APIC-access address
表示当虚拟机访问该地址,将会触发之后APIC的虚拟化步骤,也就是说,它是真实的APIC在内存中映射的地址;而virtual-APIC address
表示一个virtual-APIC page
的物理地址,而这个virtual-APIC page
是在APIC虚拟化过程中,实际被访问的页,所以它是一个被虚拟化的APIC页,但是是被实际访问的,之后会进行详细描述。这里需要注意的是,这两个address存放的都是真实主机的物理地址。
接下来,我们通过对APIC的读和写操作分别进行APIC虚拟化步骤的阐述。在介绍之前,需要先解释一下,以下对memory mapped的内存页的读写是基于xAPIC环境下的,而在x2APIC环境下,都是通过RDMSR和WRMSR来对相应APIC的寄存器进行读写的,这里就略过了。
当non-root环境下虚拟机对APIC-access address
进行了一个读操作,当满足下列任何一个条件时,会发生VMExit:
Primary Processor-Based VM-execution control
的use TPR shadow bit
为0;否则,这个对APIC-access address
的读操作会触发以下虚拟化过程:
Secondary Processor-Based VM-Execution Controls
中的APIC-register virtualization bit
为0,则只虚拟化page offset为080H(task priority)
的读操作,否则,触发APIC-access的VMExit;Secondary Processor-Based VM-Execution Controls
中的APIC-register virtualization bit
为1,则对以下page offset的读操作会进行虚拟化过程:描述 | Page offset |
---|---|
local APIC ID | 020H–023H |
local APIC version | 030H–033H |
task priority | 080H–083H |
end of interrupt | 0B0H–0B3H |
logical destination | 0D0H–0D3H |
destination format | 0E0H–0E3H |
spurious-interrupt vector | 0F0H–0F3H |
in-service | 100H–103H, 110H–113H, 120H–123H, 130H–133H, 140H–143H, 150H–153H, 160H–163H, 170H–173H |
trigger mode | 180H–183H, 190H–193H, 1A0H–1A3H, 1B0H–1B3H, 1C0H–1C3H, 1D0H–1D3H, 1E0H–1E3H, 1F0H–1F3H |
interrupt request | 200H–203H, 210H–213H, 220H–223H, 230H–233H, 240H–243H, 250H–253H, 260H–263H, 270H–273H |
error status | 280H–283H |
interrupt command | 300H–303H, 310H–313H |
LVT entries | 320H–323H, 330H–333H, 340H–343H, 350H–353H, 360H–363H, 370H–373H |
initial count | 380H–383H |
divide configuration | 3E0H–3E3H |
除此之外,其它offset的读访问都会造成VMExit。而对于这些offset的读访问,数据可以直接从virtual-APIC page
中相应的offset中获得。
对APIC的写比读操作复杂一些。首先,和读操作类似,我们得先确定什么时候会触发APIC写操作的虚拟化过程。
当non-root环境下虚拟机对APIC-access address
进行了一个写操作,当满足下列任何一个条件时,会发生VMExit:
Primary Processor-Based VM-execution control
的use TPR shadow bit
为0;否则,是否对APIC-access address
的写操作进行虚拟化由以下条件决定:
Secondary Processor-Based VM-Execution Controls
中的APIC-register virtualization bit
和virtual-interrupt delivery bit
同时为0,则只虚拟化page offset为080H(task priority)
的写操作,否则,触发APIC-access的VMExit;Secondary Processor-Based VM-Execution Controls
中的APIC-register virtualization bit
为0,而virtual-interrupt delivery bit
为1,则只虚拟化page offset为080H(task priority)
,0B0H(end of interrupt)
和300H(interrupt command — low)
的写操作,否则,触发APIC-access的VMExit;Secondary Processor-Based VM-Execution Controls
中的APIC-register virtualization bit
为1,则对以下page offset的写操作会进行虚拟化过程:描述 | Page offset |
---|---|
local APIC ID | 020H–023H |
task priority | 080H–083H |
end of interrupt | 0B0H–0B3H |
logical destination | 0D0H–0D3H |
destination format | 0E0H–0E3H |
spurious-interrupt vector | 0F0H–0F3H |
error status | 280H–283H |
interrupt command | 300H–303H, 310H–313H |
LVT entries | 320H–323H, 330H–333H, 340H–343H, 350H–353H, 360H–363H, 370H–373H |
initial count | 380H–383H |
divide configuration | 3E0H–3E3H |
除此之外,其它offset的写操作都会造成VMExit。而对于这些offset的写操作,数据直接被写到virtual-APIC page
相应的offset中。但是,由于对APIC某些寄存器的写会产生一些side-effect,因此需要进行一些所谓的APIC-write emulation
,具体的emulation操作由APIC page offset来决定(参考Intel手册第三册的29.4.3.2),这里就不详述了。
接下来列举几个在virtual-APIC page
中比较重要的寄存器:
Virtual task-priority register (VTPR), Virtual processor-priority register (VPPR), Virtual end-of-interrupt register (VEOI), Virtual interrupt-service register (VISR), Virtual interrupt-request register (VIRR), Virtual interrupt-command register (VICR_LO), Virtual interrupt-command register (VICR_HI)。
以及它们的虚拟化过程:
TPR Virtualization:
发生在以下三个场景中:(1)对MOV to CR8
指令的虚拟化;(2)对APIC-access page
的offset为080H
进行访问的虚拟化(xAPIC);(3)对WRMSR指令中ECX = 808H
的虚拟化(x2APIC)。虚拟化的过程伪代码如下:
1 2 3 4 5 6 7 8 9 |
|
PPR Virtualization: 发生在以下三个场景中:(1)VM entry;(2)TPR virtualization;(3)EOI virtualization。虚拟化的过程伪代码如下:
1 2 3 4 5 6 |
|
EOI Virtualization:
发生在以下两个场景中:(1)对APIC-access page
的offset为0B0H
进行访问的虚拟化(xAPIC);(2)对WRMSR指令中ECX = 80BH
的虚拟化(x2APIC)。EOI的虚拟化会使用和更新VMCS中的guest interrupt status
域中的SVI。虚拟化的过程伪代码如下:
1 2 3 4 5 6 7 8 9 10 11 |
|
Self-IPI Virtualization:
发生在以下两个场景中:(1)对APIC-access page
的offset为300H
进行访问的虚拟化(xAPIC);(2)对WRMSR指令中ECX = 83FH
的虚拟化(x2APIC)。self-IPI的虚拟化会更新VMCS中的guest interrupt status
域中的RVI,相应的伪代码如下:
1 2 3 |
|
首先,在VMCS中的Pin-Based VM-Execution Controls
域中,有一个bit用于控制External-interrupt exiting
,如果该bit置一,则表示所有的外部中断都会产生VMExit,否则,所有的外部中断不会产生VMExit,这就意味着,如果当前CPU处于non-root模式,那么中断就直接由虚拟机进行处理了。
当然这种将所有中断都直接让虚拟机自身来处理的做法很不安全,所以,一般情况下发生中断还是会引起下陷的,而在虚拟化层处理完返回虚拟机(VMEntry)时,就需要做中断的evaluation和delivery了。
所谓的evaluation,其实就是判断当前是否有中断需要交给虚拟机进行处理,而delivery就是将evaluation好的中断交由虚拟机内核中的相应的IDT进行处理。
当Secondary Processor-Based VM-Execution Controls
中的virtual-interrupt delivery bit
为1时,以下场景会触发处理器evaluate pending的中断:(1)VM entry;(2)TPR virtualization;(3)EOI virtualization;(4)self-IPI virtualization;(5)posted-interrupt processing。对pending virtual interrupts的evaluation会使用guest interrupt status
中的RVI,相应的伪代码如下:
1 2 3 4 |
|
当该中断被recognized了,并且满足以下四个条件,就会触发该虚拟中断的delivery:(1)RFLAGS.IF = 1;(2)没有因为STI产生的blocking;(3)没有因为MOV SS
或者POP SS
产生的blocking;(4)Primary Processor-Based VM-Execution Controls
中的interrupt-window exiting bit
为0。
虚拟中断的delivery会更新guest interrupt status
中的RVI和SVI,并且在non-root环境下产生一个中断事件,相应的伪代码如下:
1 2 3 4 5 6 7 8 9 10 11 |
|
至此,对“中断处理的那些事儿”的介绍就结束了,对于这一块的内容,我也还在学习中,很多细节上的东西之后也会慢慢再补充进去吧。