说明
本文讲的是Intel的x86架构下的中断。
参考的文档主要如下所示:
《64-ia-32-architectures-software-developer-manual.pdf》
《PCI Express体系结构导读》
《x86/x64体系探索及编程》
《82093AA I/O ADVANCED PROGRAMMABLE INTERRUPT CONTROLLER (IOAPIC).pdf》
《MultiProcess Specification.pdf》
《PCI.Local.Bus.Specification.Revision.3.0.pdf》
《PCI_Express_Base_Specification_Revision_3.0.pdf》
参考的代码主要来自EDKII源码(https://github.com/tianocore/edk2)。
因水平和篇幅有限,对中断的理解并非完全,如有错误,实属无奈......
分类
x86架构的中断类型和实现方式有很多种,这里做一个大致的分类。
从模块上来分,x86中有8259中断控制器、Local APIC和I/O APIC,另外在PCI /PCIE中还存在MSI中断。
对于这种分类,8259和APIC两者是旧和新的区分;Local APIC和I/O APIC是系统位置上的区分,前者属于CPU的一部分,后者属于Chipset的一部分(当然对于SoC来说区分就没有这么明显了);而MSI和前面这几个基本属于两个体系的东西,不过MSI的实现还是需要依赖于APIC,这个在后续会说明。
从类型上来分,有硬件中断和软件中断之分,有可屏蔽中断和不可屏蔽中断之分。
这部分的分类,前者是按照中断源来分的,可以是软件主动触发(通过INT等指令(存疑,这个不是很理解,比如int13h是用来表示读写硬盘的,但是参考文档中描述的是SIMD exception,根本是两回事儿,感觉两者并不是同一个东西...)),也可以是模块内部或者外部硬件触发的;
后者主要根据针对中断是否要被处理(大部分中断都可以通过设置来配置成可屏蔽或不可屏蔽)。
从实现上来分,有IDT(Interrupt Descriptor Table)和IVT(Interrupt Vector Table)之分。
这部分的分类主要是根据x86模式来分的,IVT主要用于实模式,而IDT主要用于保护模式及之后的模式。
IVT一般放在系统地址从0x0开始的4K空间,且一个Vector到实现代码之间的连接是比较直接的,Vector指针直接指向代码段。而IDT表中的项还有一些转换才指向到代码,这也跟保护模式相适应。
IVT或者IDT都可以通过IDTR寄存器来获取,而通过指令LIDT/SIDT可以读写IDTR寄存器,下面是IDTR到IDT的对应:
上图是在保护模式下,当在实模式下时对应的应该是IVT,IDTR中的Base Address在系统初始化的时候值为0,所以IVT才会放在0x0这个地址:
下面是IVT的基本配置:
上图也是在保护模式下的IDT表的说明,在保护模式下Intel将前面的32个中断作为自已保留使用的中断了。
因此从实模式转换到保护模式的时候,原本的中断也会向后移动(比如移动到0x20之后的位置)。
之后的介绍主要是按照模块来分。
8259中断控制器
8259控制器在系统中的布局大致如下:
实际的使用中,一般会使用两片8259芯片级联,一个主一个从。每一片有8个中断引脚,由于级联的关系,从片的INTR引脚会连到主片的一个中断引脚(IRQ2)中,所以实际的中断就是15个,分别是第一片上的IRQ0,IRQ1,IRQ3-IRQ7,和第二片的IRQ8-IRQ15。
这些中断引脚基本上的用法都固定,并且对应也都是一些比较老的设备。
8259控制器中对中断的响应优先级是有规定的,一般号越小优先级越高,不过IRQ8-IRQ15都是接在IRQ2上的,所以它们的优先级都当成2来看,所以优先级也比较高。
8259控制器(包括之后介绍的Local APIC)中,有几个比较重要的寄存器:
IRR:Interrupt Request Register,用来标志对应IRQ上发出了中断请求,它是一个8位的寄存器,刚好对应8个中断引脚,中断一次可以有多个,即可以有多个bit可以被置位;
ISR:Interrupt Service Register,用来记录IRQ的中断服务状态,它也是一个8位的寄存器,如果某个bit被置位,就表示中断请求正在被执行;
IMR:Interrupt Mask Register,用来屏蔽对应的中断请求,对应bit被置位表示屏蔽;
上述的寄存器并不能直接的读写。8259控制器在系统空间中使用I/O映射,对应的端口是20h,21h和A0h、A1h,分别对应主片和从片(这些值是硬件确定的软件配置的?)。
通过读写这些寄存器来访问和初始化控制器,写寄存器的值被称为ICW或者OCW,前者全称Initialization Command Word,用来初始化8259控制器,后者全称Operational Control Word,通过它就可以用来读写上述的寄存器以及其它的一些东西。
初始化
前面已经讲到了ICW,而8259控制器的初始化就是通过写ICW来实现的,大致的流程如下:
具体的初始化代码,在EDKII中可以参考8259.inf模块。
Interrupt8259SetVectorBase()函数就是一个初始化的过程。
这里需要注意的是寄存器的使用,写ICW1使用的是x0h这个端口,写ICW2-ICW4使用的是x1h这个端口。(x的值是2或者A)
此外,Interrupt8259WriteMask()函数设置了Mask和触发方式等。
对于触发方式还涉及到寄存器ELCR1和ELCR2,对应的I/O分别是4D0h和4D1h,两个寄存器都是8位的,共16位位对应到16个IRQ,0表示边沿触发1表示电平触发。
另外还安装了一个EFI_LEGACY_8259_PROTOCOL,通过它就可以获取对应的中断向量,并为此设置中断处理函数。下面是8254Timer.inf模块中的示例:
//
// Get the interrupt vector number corresponding to IRQ0 from the 8259 driver
//
TimerVector = 0;
Status = mLegacy8259->GetVector (mLegacy8259, Efi8259Irq0, (UINT8 *) &TimerVector);
ASSERT_EFI_ERROR (Status);
//
// Install interrupt handler for 8254 Timer #0 (ISA IRQ0)
//
Status = mCpu->RegisterInterruptHandler (mCpu, TimerVector, TimerInterruptHandler);
ASSERT_EFI_ERROR (Status);
上述代码为计时器提供了IRQ0处理函数,就是使IRQ0对应的中断向量指向了处理函数。
在中断发生后,经过一系列的处理,最终8259控制器向处理器发送一个中断向量,CPU在IDT表中找到这个向量对应的函数并执行。
需要说明的是,CPU检测中断,加载中断处理程序,从中断处理程序返回,这些动作都是硬件上的,软件并不参与,软件只需要设置中断向量,并配置中断处理函数,当然中断处理函数本身也是软件代码......
具体的CPU处理中断的过程可以参考http://www.cppblog.com/aaxron/archive/2011/11/16/160280.html。
以上是一个简介,8259芯片的具体介绍可以参考https://pdos.csail.mit.edu/6.828/2010/readings/hardware/8259A.pdf。
Local APIC
APIC的全称是Advanced Programmable Interrupt Controller,它算是8259控制器的升级版本。
引入它可以适应多处理器环境。
APIC包括了Local APIC和I/O APIC两部分内容,Local APIC是总的控制器,位于CPU内部;I/O APIC主要用于处理外部设备的中断。
下面是APIC在处理器中的逻辑框图:
上面的处理器单元在多线程处理器中指的是逻辑处理器,每个逻辑处理器都有自己的Local APIC,每个Local APIC都对应一组寄存器。这组寄存器可以是映射到系统地址(MMIO方式)中,也可以是在MSR寄存器中,这取决于Local APIC的模式,目前一般有xAPIC和x2APIC两种模式,后者使用MSR寄存器。
Local APIC寄存器介绍
下面首先了解这组寄存器:
上面的寄存器是以MMIO的形式展现。对于MSR形式的,是从0x802开始的一系列MSR,这里不再配图。
这些寄存器的位数存在32位、64位和256位几种情况。
其中256位的寄存器是以下的几个:
ISR:In-Service Register;
TMR:Trigger Mode Register;
IRR:Interrupt Request Register;
这些寄存器是跟中断个数对应的,系统中最多有256个中断,而上述寄存器中的每一个位都表示一个中断的状态。
当一个中断触发之后,对应的IRR位就被置位,不过此时中断并没有被CPU处理,只是在排队中,直到CPU要开始处理这个中断时,该位清零,而对应ISR位被设置,表示CPU开始处理这个中断了。当中断处理完成之后,会写EOI(End Of Interrupt)寄存器(也在上面的表中),这样Local APIC就会清零ISR对应的位。
TMR寄存器表示中断的触发方式。
另外还有几个需要详细介绍的寄存器:
Local APIC ID寄存器
Local APIC ID寄存器里面保存着APIC ID,它是逻辑处理器在系统中的唯一标识,这个APIC ID在多线程处理器下是很有用的。
Local APIC ID寄存器在xAPIC模式和x2APIC模式下略有不同:
xAPIC和x2APIC在表示Local APIC ID的位数上有差别。
另外,这些位还有一些细分,总共可以分为Cluster ID / Package ID / Core ID / SMT ID,这四层从高位到低位,而在系统中范围是从大到小的。Cluster是一组物理处理器,Package表示一个物理处理器,Core表示处理器中的一个核,SMT表示一个逻辑处理器。
这四层的架构其实包含了Intel多线程处理器中的两大个特性,即Hyper-Threading(又叫Simultaneous Multi-Threading,SMT)和Multi-Core。前者表示的是单个核里面里面有两个执行单元(线程),而后者表示一个处理器里面有多个核。所以提到Intel的处理器说4核8线程,就是应用了上述两者技术的结果,即一个处理器里面有4个核,每个核有两个线程。
参考《64-ia-32-architectures-software-developer-manual.pdf》中的说明会更清楚一些:
• Cluster — Some multi-threading environments consists of multiple clusters of multi-processor systems. The
CLUSTER_ID sub-field is usually supported by vendor firmware to distinguish different clusters. For non
clustered systems, CLUSTER_ID is usually 0 and system topology is reduced to three levels of hierarchy.
• Package — A multi-processor system consists of two or more sockets, each mates with a physical processor
package. The PACKAGE_ID sub-field distinguishes different physical packages within a cluster.
• Core — A physical processor package consists of one or more processor cores. The CORE_ID sub-field distin
guishes processor cores in a package. For a single-core processor, the width of this bit field is 0.
• SMT — A processor core provides one or more logical processors sharing execution resources. The SMT_ID
sub-field distinguishes logical processors in a core. The width of this bit field is non-zero if a processor core
provides more than one logical processors.
下面是上述概念的对应的MP架构拓扑图:
包含两个Package的可以看成是一个Cluster。
至于为什么要有这个Cluster的概念,并不是很清楚......
Local APIC版本寄存器
这是一个只读寄存器,它反映了Local APIC的特性,比如说是哪种模式的APIC(并不是xAPIC还是x2APIC,只有内部和外部APIC之分),比如支持对多多少个LVT(LVT后面会介绍)。
下面是它的具体信息:
LVT寄存器
Local APIC版本寄存器中确定了LVT的个数,目前的CPU一般支持7个LVT寄存器。LVT全称Local Vector Table,这些寄存器可以接收(主要是LINT0和LINT1)和产生本地中断源。
下面就是这些寄存器的图示:
需要说明的是LINT0和LINT1这两个寄存器,它们对应到Local APIC模块的INTR和NMI引脚,外部的中断会引发这两个寄存器的中断发起。
对于具体位的说明如下:
1. Vector(bit0-bit7):就是中断号,通过它在IDT中寻找对应的中断描述符,进而找到中断处理函数;
2. Delivery Mode(bit8-bit10):交付模式,有以下可取的值:
1)Fixed模式(000b),就是根据Vector找到中断处理函数并执行;
2)SMI模式(010b),触发的是SMI中断,这个时候Vector的值需要是00h;
3)NMI模式(100b),触发的是NMI中断,就不需要管Vector的值了;
4)INIT模式(101b),处理器执行INIT(不清楚处理器具体做了什么);
5)ExtINT模式(111b),表示中断源来自外部,比如8259控制器;
其它都是reserved。
典型的,可以将LINT0和LINT1配置成ExtINT和NMI交付模式。
3. Delivery Status(bit12),0表示当前没有中断交付或者中断已经交付给CPU处理,1表示中断已交付但是CPU还未处理;
4. Interrupt Input Pin Polarity(bit13),它只用于LINT0和LINT1,用于设置触发模式,0表示高电平触发1表示低电平触发;
5. Remote IRR Flag(bit14),它只用于LINT0和LINT1,使用在Fixed,电平触发模式中,1表示Local APIC接收并处理由INTR和NMI交付的中断,0表示接收到EOI命令;
6. Trigger Mode(bit15):它只用于LINT0和LINT1,0表示边沿触发,1表示电平触发;
7. Mask(bit16):1表示屏蔽中断响应;
8. Timer Mode(bit17-bit18):它只用于LVT Timer寄存器,用来设置Timer Count的计数模式。00b表示一次计数,01h表示循环计数,10表示指定TSC值计数;
LVT寄存器在上电和复位之后的值都是0x00010000。
CMCI寄存器、Thermal Monitor寄存器和Performance Monitor寄存器只能使用Fixed、SMI或NMI交付模式;
LINT0和LINT1寄存器只能使用Fixed、ExtINT或NMI交付模式;
ICR寄存器
ICR全称Interrupt Command Register,它用于逻辑处理器之间的通信,使用的中断称为IPI(Inter-Processor Interrupt)。
Local APIC初始化
Local APIC的初始化包括两个部分,一个上述寄存器的配置,一个是IDT表的配置。
在EDKII代码中,初始化在CpuDex.inf模块中:
//
// Setup IDT pointer, IDT and interrupt entry points
//
InitInterruptDescriptorTable ();
//
// Enable the local APIC for Virtual Wire Mode.
//
ProgramVirtualWireMode ();
ProgramVirtualWiredMode()函数主要配置了LINT0、LINT1和Spurious Interrupt Vector Register,最后一个寄存器是用来使能Local APIC的。
这里的Virtual Wire Mode是多处理器系统的一种中断形式,Local APIC下的Virtual Wire Mode结构如下:
关于多处理器系统的介绍,可以参考http://download.intel.com/design/archives/processors/pro/docs/24201606.pdf。
CpuDxe.inf并不会把所有的中断都配置好,这里更可以说是搭好了框架,后续不同模块根据其实现母的还会有中断配置和处理函数添加。
I/O APIC
前面已经讲过,APIC有两个部分,一个是Local APIC,一个是I/O APIC,前者位于CPU中,后者位于芯片组中,两者之间通过系统总线来通信。
相比8259控制器通过INTR引脚与处理器通信的方式,I/O APIC则直接通过它的MMIO来与CPU通信。
这通过“Local APIC初始化”那一章节中的Virtual Wire Mode配置图中就可看出来。
下面说明下通过MMIO访问的那些寄存器,它们分为两类,直接访问的寄存器和间接访问的寄存器。
直接访问的寄存器就是下面几个:
这里的xy的值需要另外确定,这个跟平台相关,一般就是00h。
间接方位寄存器有下面几个:
间接寄存器的访问需要通过直接寄存器来完成,可以看到直接寄存器两个分别是index和data,所以过程就是往index中写入需要读取的间接寄存器的偏移,然后通过data得到间接寄存器的值。下面是一个代码示例:
/**
Read a 32-bit I/O APIC register.
If Index is >= 0x100, then ASSERT().
@param Index Specifies the I/O APIC register to read.
@return The 32-bit value read from the I/O APIC register specified by Index.
**/
UINT32
EFIAPI
IoApicRead (
IN UINTN Index
)
{
ASSERT (Index < 0x100);
MmioWrite8 (PcdGet32 (PcdIoApicBaseAddress) + IOAPIC_INDEX_OFFSET, (UINT8)Index);
return MmioRead32 (PcdGet32 (PcdIoApicBaseAddress) + IOAPIC_DATA_OFFSET);
}
间接寄存器跟Local APIC中的寄存器组形式类似。10-3Fh称为Redirection Table寄存器,跟Local APIC中的LVT类似。
RT寄存器一般有24个,每个占64位。
关于这些寄存器的说明,可以参考http://www.intel.com/design/chipsets/datashts/29056601.pdf。
没有找到相关I/O APIC的初始化代码。不过有一个BaseIoApicLib.inf库有配置Redirection Table寄存器。
另外,对应有一个模块HpetTimerDxe.inf,里面有设置I/O APIC和添加中断处理函数的代码:
//
// Initialize I/O APIC entry for HPET Timer Interrupt
// Fixed Delivery Mode, Level Triggered, Asserted Low
//
IoApicConfigureInterrupt (mTimerIrq, PcdGet8 (PcdHpetLocalApicVector), IO_APIC_DELIVERY_MODE_LOWEST_PRIORITY, TRUE, FALSE);
// 中间代码省略
//
// Install interrupt handler for selected HPET Timer
//
Status = mCpu->RegisterInterruptHandler (mCpu, PcdGet8 (PcdHpetLocalApicVector), TimerInterruptHandler);
ASSERT_EFI_ERROR (Status);
MSI
MSI全称Message Signaled Interrupt,它是PCI/PCIE体系的一部分。
下面是引自《PCI Local Bus Specification》中的话,它也被《64-ia-32-architectures-software-developer-manual.pdf》所引用:
“Message signalled interrupts (MSI) is an optional feature that enables PCI devices to request
service by writing a system-specified message to a system-specified address (PCI DWORD memory
write transaction). The transaction address specifies the message destination while the transaction
data specifies the message. System software is expected to initialize the message destination and
message during device configuration, allocating one or more non-shared messages to each MSI
capable function.”
在PCI总线中,所有需要提交中断请求的设备,必须能够通过INTx引脚(这个引脚会连接到8259或者I/O APCI上)提交中断请求,而MSI机制是一种可选机制(说是可选,但是不确定在PCI上要怎么实现);
而在PCIE总线中,PCIE设备必须支持MSI或者MSI-X(MSI的升级版本)中断请求机制,而可以不支持INTx中断。目前一般的PCIE设备都使用MSI机制来提交中断。
MSI使用了MSI Capability结构来实现中断请求。PCIE设备在提交MSI请求时,总是想这种Capability结构中的Message Address的地址写Message Data,从而组成一个寄存器写TLP,向处理器提交中断请求。
MSI Capability的结构如下:
它有几种不同的类型,根据位数和是否带MASK来区分。
其中:
Capability ID:它的值是0x05,表示的是MSI的ID号;
Next Pointer:指向下一个Capability,这是PCIE配置结构的组成形式,这里可以不关注;
Message Control:状态和控制寄存器,具体位的意义如下:
Message Address/Message Upper Address:存放目的地址。
Message Data:用来存放MSI报文使用的数据。
关于Message Address/Data,在《64-ia-32-architectures-software-developer-manual.pdf》中有说明:
Mask Bits:一个PCIE设备使用MSI机制时,最多可以使用32个中断,对应到这里的32位,BIT置1时表示屏蔽中断。
Pending Bits:该部分只读,也是32位,对应到可用的32个中断。
Mask Bits和Pending Bits配合使用,当系统软件(中断为什么需要软件来触发,那硬件中断怎么办?)将Mask Bits的某一位从1改写为0,PCIE设备发送MSI报文想处理器提交中断请求,同事讲Pending Bits中对应的位清0(什么时候是1呢?)。
关于MSI的初始化,也可以参考HpetTimerDxe.inf这个模块,在这个模块中不仅可以使用I/O APIC,也可以使用MSI:
if (FeaturePcdGet (PcdHpetMsiEnable) && MsiTimerIndex != HPET_INVALID_TIMER_INDEX) {
//
// Use MSI interrupt if supported
//
mTimerIndex = MsiTimerIndex;
//
// Program MSI Address and MSI Data values in the selected HPET Timer
//
HpetTimerMsiRoute.Bits.Address = GetApicMsiAddress ();
HpetTimerMsiRoute.Bits.Value = (UINT32)GetApicMsiValue (PcdGet8 (PcdHpetLocalApicVector), LOCAL_APIC_DELIVERY_MODE_LOWEST_PRIORITY, FALSE, FALSE);
HpetWrite (HPET_TIMER_MSI_ROUTE_OFFSET + mTimerIndex * HPET_TIMER_STRIDE, HpetTimerMsiRoute.Uint64);
//
// Read the HPET Timer Capabilities and Configuration register and initialize for MSI mode
// Clear LevelTriggeredInterrupt to use edge triggered interrupts when in MSI mode
//
mTimerConfiguration.Uint64 = HpetRead (HPET_TIMER_CONFIGURATION_OFFSET + mTimerIndex * HPET_TIMER_STRIDE);
mTimerConfiguration.Bits.LevelTriggeredInterrupt = 0;
}
需要注意的是,HPET并不是传统的PCI或者PCIE设备(并没有BDF号),不过它所在的系统空间中,会给每一个Timer留有一个类似MSI Capability的结构体,通过它可以设置MSI中断需要的参数。
补充说明
本文主要介绍的是中断,但是对于异常(Exception),x86平台下的处理机制也是一样的。