本文发自 http://www.binss.me/blog/what-is-apic/,转载请注明出处。

由于课题需要,这几天又当了一回手册工程师。

本文主要是对 Intel SDM vol 3 Chapter 10 和 Intel 82093AA 的摘录和翻译。

2017.12.10 更新: 拜读了 @tcbbd 在 https://github.com/GiantVM/doc/blob/master/interrupt_and_io/ 的系列文章,受益匪浅,对本文进行修改

APIC

在 MP 架构下,为支持将中断传递给多个 CPU,引入了 APIC,其包含 LAPIC 和 IOAPIC 。一般来说,所有 LAPIC 都连接到一个 I/O APIC 上,形成一个一对多的结构(不排除有多 IOAPIC 的架构):

有两种工作模式:

  1. 8259A 模式: 禁用 LAPIC, APIC 直连 CPU
  2. 标准模式: 启用 LAPIC,所有的外部中断通过 IOAPIC 接收后转发给对应的 LAPIC

LAPIC

LAPIC (Local Advanced Programmable Interrupt Controller) 是一种负责接收 / 发送中断的芯片,集成在 CPU 内部,每个 CPU 有一个属于自己的 LAPIC。它们通过 APIC ID 进行区分。

每个 LAPIC 都有自己的一系列寄存器、一个内部时钟、一个本地定时设备 和 两条 IRQ 线 LINT0 和 LINT1。

可通过 cpuid (eax=1) 进行读取,如果存在 edx 的返回值中 bit 9 为 1,则表示 LAPIC 存在。 ebx 返回值的 bit 24-31 为 硬件分配的 APIC ID(Initial APIC ID)。

寄存器

APIC 寄存器是一段起始地址为 0xFEE00000 、长度为 4KB 的物理地址区域。在 MP 架构中,初始状态下所有 LAPIC 的寄存器都指向同一段内存空间,之后可以通过 relocate 来重定位到相应区域。

relocate 是P6时代遗留下来的上古特性,当时是为了防止与已经使用了这块地址的程序发生冲突,现在已无人使用。此外还需注意,该页在页表中必须设置为strong uncacheable (UC)

LAPIC 的基本信息会存在 IA32_APIC_BASE MSR 中,其中 bit 8 指示是否 BSP,bit 11 指示 LAPIC 是否启用(关闭再重新启用后 APIC 将回到断电重启时的状态),bit 12-35 为 LAPIC Base,左移 12 位可得到 LAPIC 寄存器起始地址。

LAPIC 寄存器的长度为 32/64/256 bit ,需要通过 32/128 bit 的 load 和 store 进行操作。注意这些寄存器都不属于 MSR 。

常用的寄存器包括:

  • ICR(Interrupt Command Register) 用于发送 IPI

  • IRR(Interrupt Request Register) 当前 LAPIC 接收到的中断请求

  • ISR(In-Service Register) 当前 LAPIC 送入 CPU 中 (CPU 正在处理) 的中断请求

  • TPR(Task Priority Register) 当前 CPU 处理中断所需的优先级

  • PPR(Processor Priority Register) 当前 CPU 处理中断所需的优先级,只读,由 TPR 决定

地址和寄存器的详细映射关系可参考 Table 10-1 Local APIC Register Address Map 。

优先级

中断向量的 bit4-7 为 Interrupt-Priority class ,1 为最低优先级,15 为最高优先级,每个 class 包含 16 个中断向量。0-15 号中断向量的 class 为 0,但其不合法,这些中断永远不会提交。在 Intel 64 和 IA-32 架构中,0-31 号中断向量被保留,因此 class 0-1 不可用。中断向量的 bit0-3 决定了同 class 下的优先级,越大在 class 内的优先级就越高。

软件还可以通过修改 TPR 中的 Task-Priority Class 和 Task-Priority Sub-Class ,配合 ISRV (优先级最高的中断向量号) 可以决定 PPR。

PPR 决定了 CPU 接受的中断。只有 Interrupt-Priority class 大于 Processor-Priority Class 的中断才会被送到 CPU 中(注意, NMI / SMI / INIT / ExtINT / SIPI 不受该限制)。Processor-Priority Sub-Class 不影响中断的送达,只是用来凑数而已。

如果在处理某个中断的时候 LAPIC 收到了一个优先级更高的中断,那么它会立刻发送给 CPU,于是当前 CPU 正在执行的中断处理例程会被打断,CPU 转而执行新中断的中断处理例程,直到该中断处理例程执行完毕后,原先的例程才会被恢复执行。

中断类型

LAPIC 主要处理以下中断:

  • APIC Timer 产生的中断(APIC timer generated interrupts)
  • Performance Monitoring Counter 在 overflow 时产生的中断(Performance monitoring counter interrupts)
  • 温度传感器产生的中断(Thermal Sensor interrupts)
  • LAPIC 内部错误时产生的中断(APIC internal error interrupts)
  • 本地直连 IO 设备 (Locally connected I/O devices) 通过 LINT0 和 LINT1 引脚发来的中断
  • 其他 CPU (甚至是自己,称为 self-interrupt)发来的 IPI(Inter-processor interrupts)
  • IOAPIC 发来的中断

其中前 5 种中断被称为本地中断,LAPIC 在收到后会设置好 LVT(Local Vector Table)的相关寄存器,通过 interrupt delivery protocol 送达 CPU。

LVT 实际上是一片连续的地址空间,每 32-bit 一项,作为各个本地中断源的 APIC register :

register 被划分成多个部分:

  • bit 0-7: Vector,即CPU收到的中断向量号,其中0-15号被视为非法,会产生一个Illegal Vector错误(即ESR的bit 6,详下)
  • bit 8-10: Delivery Mode,有以下几种取值:

    • 000 (Fixed):按Vector的值向CPU发送相应的中断向量号
    • 010 (SMI):向CPU发送一个SMI,此模式下Vector必须为0
    • 100 (NMI):向CPU发送一个NMI,此时Vector会被忽略
    • 101 (INIT):向CPU发送一个 INIT,此模式下Vector必须为0
    • 111 (ExtINT):令CPU按照响应外部8259A的方式响应中断,这将会引起一个INTA周期,CPU在该周期向外部控制器索取Vector。APIC只支持一个ExtINT中断源,整个系统中应当只有一个CPU的其中一个LVT表项配置为ExtINT模式
  • bit 12: Delivery Status(只读),取0表示空闲,取1表示CPU尚未接受该中断(尚未EOI)

  • bit 13: Interrupt Input Pin Polarity,取0表示active high,取1表示active low
  • bit 14: Remote IRR Flag(只读),若当前接受的中断为fixed mode且是level triggered的,则该位为1表示CPU已经接受中断(已将中断加入IRR),但尚未进行EOI。CPU执行EOI后,该位就恢复到0
  • bit 15: Trigger Mode,取0表示edge triggered,取1表示level triggered(具体使用时尚有许多注意点,详见手册10.5.1节)
  • bit 16: 为Mask,取0表示允许接受中断,取1表示禁止,reset后初始值为1
  • bit 17/17-18: Timer Mode,只有LVT Timer Register有,用于切换APIC Timer的三种模式

最后两种中断通过写 ICR 来发送。当对 ICR 进行写入时,将产生 interrupt message 并通过 system bus(Pentium 4 / Intel Xeon) 或 APIC bus(Pentium / P6 family) 送达目标 LAPIC 。

当有多个 APIC 向通过 system bus / APIC bus 发送 message 时,需要进行仲裁。每个 LAPIC 会被分配一个仲裁优先级(范围为 0-15),优先级最高的拿到 bus,从而能够发送消息。在消息发送完成后,刚刚发送消息的 LAPIC 的仲裁优先级会被设置为 0,其他的 LAPIC 会加 1。

中断发送流程

举个例子:当一个 CPU 想要向其他 CPU 发送中断时,就在自己的 ICR(interrupt command ragister) 中存放对应的中断向量和目标 LAPIC ID 标识。然后由 system bus(Pentium 4 / Intel Xeon) 或 APIC bus(Pentium / P6 family) 直接传递到目标 LAPIC。

中断接收流程

一个 LAPIC 在收到一个 interrupt message 后,执行以下流程:

  1. 判断自己是否属于消息指定的 destination ,如果不是,抛弃该消息
  2. 如果中断的 Delivery Mode 为 NMI / SMI / INIT / ExtINT / SIPI ,则直接将中断发送给 CPU
  3. 如果不是以上的 Mode ,则设置中断消息在 IRR 中对应的 bit。如果 IRR 中 bit 已被设置(没有 open slot),则拒绝该请求,然后给 sender 发送一个 retry 的消息
  4. 对于 IRR 中的中断,LAPIC 每次会根据中断的优先级和当前 CPU 的优先级 PPR 选出一个发送给 CPU,会清空该中断在 IRR 中对应的 bit,并设置该中断在 ISR 中对应的 bit
  5. CPU 在收到 LAPIC 发来的中断后,通过中断 / 异常处理机制进行处理。处理完毕后,向 LAPIC 的 EOI(end-of-interrupt)寄存器进行写入(NMI / SMI / INIT / ExtINT / SIPI 无需写入)
  6. LAPIC 清除 ISR 中该中断对应的 bit(只针对 level-triggered interrupts)
  7. 对于 level-triggered interrupt, EOI 会被发送给所有的 IOAPIC。可以通过设置 Spurious Interrupt Vector Register 的 bit12 来避免 EOI 广播

IRR + ISR 的机制决定了同一个中断最多可以 pending 两次,第一次已被送到 CPU 中进行处理,而第二次处于 IRR 中等待送到 CPU 中。

IOAPIC

IOAPIC (I/O Advanced Programmable Interrupt Controller) 属于 Intel 芯片组的一部分,也就是说通常位于南桥。

像 PIC 一样,连接各个设备,负责接收外部 IO 设备 (Externally connected I/O devices) 发来的中断,典型的 IOAPIC 有 24 个 input 管脚(INTIN0~INTIN23),没有优先级之分。

在某个管脚收到中断后,会进行查表,把中断转换为中断消息转发给对应的 LAPIC 。

寄存器

和 LAPIC 一样,IOAPIC 的寄存器同样是通过映射一片物理地址空间实现的:

  • IOREGSEL(I/O REGISTER SELECT REGISTER): 选择要读写的寄存器
  • IOWIN(I/O WINDOW REGISTER): 读写 IOREGSEL 选中的寄存器
  • IOAPICVER(IOAPIC VERSION REGISTER): IOAPIC 的硬件版本
  • IOAPICARB(IOAPIC ARBITRATION REGISTER): IOAPIC 在总线上的仲裁优先级
  • IOAPICID(IOAPIC IDENTIFICATION REGISTER): IOAPIC 的 ID,在仲裁时将作为 ID 加载到 IOAPICARB 中
  • IOREDTBL(I/O REDIRECTION TABLE REGISTERS): 有 0-23 共 24 个,对应 24 个引脚,每个长 64bit。当该引脚收到中断信号时,将根据该寄存器产生中断消息送给相应的 LAPIC

扩展

xAPIC(extended APIC)

取消了 APIC bus,LAPIC 与 IOAPIC 直接通过 system bus 通信。寄存器通过内存映射到物理地址来进行读写。

在 APIC 规范中 APIC ID 只有 4bit ,因此最多只能支持 15 个 CPU。 xAPIC 扩展到 8bit ,支持 255 个。

x2APIC

x2APIC 将 APIC ID 扩展到 32bit ,占 APIC ID Register 的32位,因此支持 2^32-1 个 CPU。

寄存器被改为只读,只会在开机时由硬件设置一次,其末8位被作为 xAPIC 模式下的 APIC ID 。

新增了 Self IPI Register ,向该寄存器写入 Interrupt Vector 可实现发送一个 Edge Triggered + Fixed Interrupt 的 Self IPI 。

MSI(Message Signaled Interrupt)

PCI Specification 2.2 引入,设备通过向某个 MMIO 地址写入 system-specified message 可实现向 CPU 发送中断的效果。

写入的数据仅能用来决定发送给哪个 CPU,而不能携带更多的信息。

具体的实现方式为设备通过 PCI write command 向 Message Address Register 指示的地址写入 Message Data Register 中内容来向 LAPIC 发送中断。

寄存器

Message Address Register

Message Address Register 的格式如下:

Destination ID 字段存放了中断要发往 LAPIC ID。该 ID 也会记录在 I/O APIC Redirection Table 中每个表项的 bit56-63 。Redirection hint indication 指定了 MSI 是否直接送达 CPU。 Destination mode 指定了 Destination ID 字段存放的是逻辑还是物理 APIC ID 。

Message Data Register

Message Data Register 的格式如下:

Vector 指定了中断向量号, Delivery Mode 定义同传统中断,表示中断类型。Trigger Mode 为触发模式,0 为边缘触发,1 为水平触发。 Level 指定了水平触发中断时处于的电位(边缘触发无须设置该字段)。

优点

允许设备分配 1/2/4/8/16/32 个中断。

传统中断基于的引脚 (pin) 往往被多个设备所共享。中断触发后,OS 需要调用对应的中断处理例程来确定产生中断的设备,耗时较长。而 MSI 中断只属于一个特定的设备,不存在该问题。

传统中断通常是设备写完数据 (DMA) 后,给 CPU 一个中断请求,通知 CPU 进行处理。但是可能由于某些原因(优化?),PCI bridge 或 Memory controller 可能会延迟写数据操作,导致 CPU 在收到中断时,数据还未到达内存。为了解决这个问题,interrupt handlers 必须从通过轮询来确保写操作已经完成,具体操作是访问一个寄存器,只有数据到达内存后,寄存器才会返回值(PCI 事务保证),这样导致性能不好。而 MSI 的中断本质上也是写内存,这样就保证了写内存后发中断这样的流程是串行的,因而避免了轮询的问题。

传统中断先发送到 IOAPIC 后再转发给对应的 LAPIC ,路径较长。MSI 能让设备直接将中断送达 LAPIC 。

缺点

无法保证 Interrupt Latency,MSG 可能会被 Host/Loading Cache 这样就可能会出现 Latency,另外当 Loading 重的时候也可能会出现比较大的 Latency。

MSI-X

PCI 3.0 引入。最多允许设备分配 2048 个中断,给每个中断都分配一个不同的目标地址和 data word,比 MSI 粒度更细(需要 LAPIC 的支持)。