3. MSI 中断原理:消息、格式与寄存器

MSI(Message Signaled Interrupt)是 PCI/PCIe 设备向 CPU 发送中断请求的一种机制。与传统的 INTx 引脚边带信号不同,MSI 将中断请求编码为一个内存写事务(Memory Write Transaction),直接写入系统指定的地址。这个写事务本身就是一个“中断消息”,无需专用中断线。

核心思想:MSI 将中断信号转化为一次特定的内存写操作,写入目标地址的数据即代表中断向量号。这使得中断传递路径从物理引脚变为数据包,极大提升了可扩展性和灵活性。

3.1 MSI 工作机制

MSI 中断的完整生命周期包含三个阶段:设备触发、消息路由、CPU 接收。

  1. 设备触发:当 PCIe 设备内部产生中断事件(如 DMA 完成、错误发生),设备内部逻辑将预先配置好的 Message Address 和 Message Data 组合成一个内存写 TLP(Transaction Layer Packet)。
  2. 消息路由:该 TLP 通过 PCIe 交换结构(Switch/Root Complex)逐级转发,最终到达 Root Complex。Root Complex 根据地址域判断这是一个 MSI 消息,而非普通内存写。
  3. CPU 接收:Root Complex 将消息中的 Data 字段解析为中断向量号,并注入本地 APIC(Local Advanced Programmable Interrupt Controller)。APIC 根据向量号查找中断描述符,最终触发 CPU 的 INTR 引脚或直接通过 x2APIC 消息中断。
关键区别:INTx 中断是电平/边沿触发的物理信号,需要中断控制器轮询或采样;MSI 是边带数据包,中断向量号直接包含在消息中,无需中断控制器查询设备状态。
MSI 中断消息传递流程 PCIe 设备 触发中断事件 消息生成 Addr + Data → TLP Root Complex 解析地址 → 提取向量 Local APIC 向量 → 中断描述符 CPU Core 中断事件 内存写 TLP 向量注入 INTR 说明 • 消息地址由系统软件配置 • 消息数据包含向量号 • 无需中断线,可扩展性好

3.2 MSI 消息格式

MSI 消息本质上是一个 PCIe 内存写 TLP,其关键字段如下:

字段 宽度 描述
Fmt/Type 8 bit 标识为内存写 TLP(MemWr 或 MemWr64)
Requester ID 16 bit 发起设备的 Bus:Device:Function 编号
Tag 8 bit 事务标签,用于完成排序(MSI 通常为 0)
Address 32/64 bit 目标地址,指向系统指定的 MSI 内存区域
Data 16/32 bit 中断向量号(低 8 位有效)
Length 10 bit 数据负载长度,MSI 固定为 1 DW(4 字节)
注意:MSI 消息的 Data 字段宽度取决于设备支持的 MSI 向量数量。对于支持 32 个向量的设备,Data 字段低 5 位用于编码向量号;对于支持 2 个向量的设备,仅低 1 位有效。驱动在配置时必须根据 Multiple Message Capable 字段正确设置。

3.3 地址寄存器(Message Address Register)

每个 MSI 功能设备包含一个 Message Address Register(偏移 04h),用于存储中断消息的目标地址。该寄存器由系统软件(BIOS 或 OS)在枚举阶段写入。

  • 地址格式:对于 32 位 MSI,地址为 32 位;对于 64 位 MSI,地址为 64 位(占用两个连续的 32 位寄存器)。
  • 地址含义:该地址通常指向 Root Complex 内部的一个特定内存区域,该区域被映射到中断控制器(如 I/O APIC 或 xAPIC/x2APIC)的配置空间。
  • 对齐要求:地址必须按 DW(4 字节)对齐,低 2 位固定为 0。
典型地址值:在 x86 系统中,MSI 消息地址通常为 0xFEE00000(32 位)或 0xFEE00000_00000000(64 位),对应 Local APIC 的 ICR(Interrupt Command Register)地址空间。

3.4 数据寄存器(Message Data Register)

Message Data Register(偏移 08h)存储中断消息的数据负载,即中断向量号。其格式如下:

位域 宽度 描述
Vector 8 bit 中断向量号(0-255),对应 IDT 表项
Delivery Mode 3 bit 中断投递模式:000=Fixed, 001=Lowest Priority, 010=SMI, 100=NMI, 101=INIT, 111=ExtINT
Destination Mode 1 bit 0=物理模式(Physical), 1=逻辑模式(Logical)
Trigger Mode 1 bit 0=边沿触发(Edge), 1=电平触发(Level)
Level 1 bit 电平触发时有效:0=低电平, 1=高电平
Destination ID 8 bit 目标 CPU 的 APIC ID(物理模式)或逻辑组 ID(逻辑模式)
驱动视角:驱动通常不需要手动构造 Data 寄存器的所有位域。Linux 内核的 pci_write_msi_msg() 函数会根据 irq_chip 和 irq_data 自动填充正确的向量号和投递模式。驱动只需调用 pci_alloc_irq_vectors() 分配向量即可。
MSI 地址/数据寄存器结构 Message Address Register (32-bit) 0xFEE00000 (典型值) → 指向 Local APIC ICR Message Data Register (32-bit) Vector[7:0] DM[2:0] DSTM TM LVL Reserved[15:8] Dest ID[23:16] Reserved[31:24] 说明:DM=Delivery Mode, DSTM=Destination Mode, TM=Trigger Mode, LVL=Level, Dest ID=目标 APIC ID

3.5 MSI 与 MSI-X 的寄存器差异

MSI-X 是 MSI 的增强版本,允许每个中断向量拥有独立的地址和数据寄存器。下表对比了两者的寄存器结构:

特性 MSI MSI-X
向量数量 1, 2, 4, 8, 16, 32(2 的幂次) 最多 2048(任意数量)
地址/数据寄存器 共享一对寄存器(所有向量共用) 每个向量独立一对寄存器
寄存器位置 PCI 配置空间(Capability 结构内) BAR 空间(通过 Table Offset 和 PBA Offset 指向)
64 位地址支持 可选(通过 Capability 中的 64-bit 标志位) 强制支持 64 位地址
向量掩码 全局掩码(Mask Register) 每个向量独立掩码(Vector Control 寄存器)
配置灵活性 低(所有向量必须使用相同的投递模式) 高(每个向量可独立配置目标 CPU 和模式)
驱动适配要点:当设备同时支持 MSI 和 MSI-X 时,优先使用 MSI-X,因为它提供更好的灵活性和可扩展性。Linux 内核的 pci_alloc_irq_vectors() 会自动优先尝试 MSI-X,失败后回退到 MSI。

3.6 内核中的 MSI 消息配置示例

以下代码展示了 Linux 内核如何配置 MSI 消息的地址和数据寄存器:

/* drivers/pci/msi.c - 简化示例 */
void __pci_write_msi_msg(struct pci_dev *dev, struct msi_desc *desc)
{
    struct msi_msg msg;
    struct irq_data *irqd = irq_get_irq_data(desc->irq);

    /* 由 irq_chip 填充 msg 结构 */
    irqd->chip->irq_compose_msi_msg(irqd, &msg);

    /* 写入 Message Address 寄存器 */
    pci_write_config_dword(dev, desc->msi_attrib.pos + PCI_MSI_ADDRESS_LO,
                           msg.address_lo);
    if (desc->msi_attrib.is_64) {
        pci_write_config_dword(dev, desc->msi_attrib.pos + PCI_MSI_ADDRESS_HI,
                               msg.address_hi);
    }

    /* 写入 Message Data 寄存器 */
    pci_write_config_dword(dev, desc->msi_attrib.pos + PCI_MSI_DATA_64,
                           msg.data);
}
关键点:驱动开发者通常不需要直接调用 __pci_write_msi_msg()。内核的 MSI 框架会在 pci_alloc_irq_vectors() 和 request_irq() 过程中自动完成配置。驱动只需关注中断处理函数的正确注册即可。