侧边栏壁纸
  • 累计撰写 53 篇文章
  • 累计收到 3 条评论

P4-02 · MSI-X 中断机制:Table 在哪、怎么触发,一个内存写就够了

PacketForge
2026-08-18 / 0 评论 / 0 阅读 /
对应路线图:Phase 4 (Day 120-140) | 内核 v6.6 | 代码出处 source/linux-stable/drivers/pci/msi/ | 上一篇:P4-01 Config Space 与 BAR | 下一篇:P4-03 最小 PCIe 驱动

一句话定位

MSI-X 把"中断线"变成了"内存写事务":设备想打断 CPU 时,往一个特殊地址写一个 16 位数据,根复合体(RC)把它翻译成中断投递。每个向量在 MSI-X Table 里有一张 16 字节的表项,存着自己的"地址+数据"。所以问题拆成两个:Table 在哪(Capability 链 → BAR 内偏移),谁写表项(内核用 ioremap 后的 MMIO 直接写)。

真实源码精读

① 表在哪:从 Capability 链到 BAR 内偏移(msi.c:561-580)

static void __iomem *msix_map_region(struct pci_dev *dev,
                     unsigned int nr_entries)
{
    resource_size_t phys_addr;
    u32 table_offset;
    u8 bir;

    pci_read_config_dword(dev, dev->msix_cap + PCI_MSIX_TABLE,
                  &table_offset);
    bir = (u8)(table_offset & PCI_MSIX_TABLE_BIR);
    // ...省略(pci_resource_flags 检查,:572-574)

    table_offset &= PCI_MSIX_TABLE_OFFSET;
    phys_addr = pci_resource_start(dev, bir) + table_offset;

    return ioremap(phys_addr, nr_entries * PCI_MSIX_ENTRY_SIZE);
}
  1. 找 Capability:内核沿 Capability 链表(起点 PCI_CAPABILITY_LIST,pci_regs.h:121)找到 ID 为 PCI_CAP_ID_MSIX = 0x11(pci_regs.h:226)的能力结构,缓存偏移到 dev->msix_cap(pci.h:359);
  2. 读 Table dword:PCI_MSIX_TABLE(能力内偏移 4,pci_regs.h:327)低 3 位是 BIR(PCI_MSIX_TABLE_BIR,pci_regs.h:328),高 29 位是 BAR 内偏移(PCI_MSIX_TABLE_OFFSET,pci_regs.h:329);
  3. BAR 基址 + 偏移 → ioremap:pci_resource_start(dev, bir) 拿到 P4-01 里解析好的 BAR 物理地址,加偏移直接映射。

MSI-X 表项数与 PCI_MSIX_FLAGS(pci_regs.h:323)里的 QSIZE 相关:msi.h:6 定义 msix_table_size(flags) = (flags & PCI_MSIX_FLAGS_QSIZE) + 1——QSIZE 存的是"个数减 1"。

② 表项布局与触发:16 字节一张"名片"(pci_regs.h:336-342)

/* MSI-X Table entry format (in memory mapped by a BAR) */
#define PCI_MSIX_ENTRY_SIZE        16
#define PCI_MSIX_ENTRY_LOWER_ADDR    0x0  /* Message Address */
#define PCI_MSIX_ENTRY_UPPER_ADDR    0x4  /* Message Upper Address */
#define PCI_MSIX_ENTRY_DATA        0x8  /* Message Data */
#define PCI_MSIX_ENTRY_VECTOR_CTRL    0xc  /* Vector Control */
#define  PCI_MSIX_ENTRY_CTRL_MASKBIT    0x00000001

中断触发不需要任何"电平拉低":设备做一次 Memory Write TLP,写地址 = 表项里的 address、数据 = data,RC/IOMMU 拦截后路由到本地 APIC/ITS,最终给 CPU 一个向量号。INTx 是物理线共享,MSI-X 是每个向量独享一张名片——这就是多队列网卡每队列一个中断还互不打扰的原因。

③ 内核怎么写表项:先 mask 再改(msi.c:205-227,pci_write_msg_msix 节选)

    /*
     * The specification mandates that the entry is masked
     * when the message is modified:
     *
     * "If software changes the Address or Data value of an
     * entry while the entry is unmasked, the result is
     * undefined."
     */
    if (unmasked)
        pci_msix_write_vector_ctrl(desc, ctrl | PCI_MSIX_ENTRY_CTRL_MASKBIT);

    writel(msg->address_lo, base + PCI_MSIX_ENTRY_LOWER_ADDR);
    writel(msg->address_hi, base + PCI_MSIX_ENTRY_UPPER_ADDR);
    writel(msg->data, base + PCI_MSIX_ENTRY_DATA);

写完 address/data 再解除 mask,并用一次读强制写可见(:231-232)。消息本身来自 msi_desc.msg(msi.h:193),由中断子系统(x86 上多为 irq_matrix / MSI domain)分配。

④ 使能全流程:mask 一切,再逐步放开(msi.c:712-766,msix_capability_init 节选)

    /*
     * Some devices require MSI-X to be enabled before the MSI-X
     * registers can be accessed.  Mask all the vectors to prevent
     * interrupts coming in before they're fully set up.
     */
    pci_msix_clear_and_set_ctrl(dev, 0, PCI_MSIX_FLAGS_MASKALL |
                    PCI_MSIX_FLAGS_ENABLE);
    // ...省略(msix_enabled=1、读 control、msix_table_size)
    dev->msix_base = msix_map_region(dev, tsize);
    // ...
    /* Disable INTX */
    pci_intx_for_msi(dev, 0);
    // ...省略(Marvell 设备 quirk 注释)
    msix_mask_all(dev->msix_base, tsize);
    pci_msix_clear_and_set_ctrl(dev, PCI_MSIX_FLAGS_MASKALL, 0);

顺序是防御性的:先置 MASKALL+ENABLE(防止设置期间漏进中断)→ map Table → 分配描述符 → 关 INTx(避免与 MSI-X 并存导致双份中断)→ mask 全部表项 → 解除 MASKALL。全部搞定后驱动才逐个 unmask 自己的向量。

⑤ 驱动侧入口:MSI-X 优先的 fallback 链(api.c:254-298)

    if (flags & PCI_IRQ_MSIX) {
        nvecs = __pci_enable_msix_range(dev, NULL, min_vecs, max_vecs,
                        affd, flags);
        if (nvecs > 0)
            return nvecs;
    }

    if (flags & PCI_IRQ_MSI) {
        nvecs = __pci_enable_msi_range(dev, min_vecs, max_vecs, affd);
        // ...
    }

    /* use legacy IRQ if allowed */
    if (flags & PCI_IRQ_LEGACY) {
        if (min_vecs == 1 && dev->irq) {
            // ...
            pci_intx(dev, 1);
            return 1;
        }
    }

驱动传 PCI_IRQ_MSIX | PCI_IRQ_MSI | PCI_IRQ_LEGACY(pci.h:1076-1078)时,有多少本事用多少:MSI-X 不够换 MSI,再不行退回共享的 INTx。成功之后用 pci_irq_vector(dev, nr)(api.c:313)把"设备侧向量号"翻译成 Linux IRQ 号传给 request_irq。

🐧 内核机制对照:INTx vs MSI vs MSI-X

维度INTx(PCI_INTERRUPT_PIN)MSIMSI-X
载体物理线 INTA#-INTD#,多设备共享内存写 TLP内存写 TLP
向量数1(共享)1/2/4/8/16/32最多 2048(QSIZE+1)
每向量独立地址/数据无共享一份每表项一份
内核路径传统 irq domainMSI domain(msi.c:270-279)Table ioremap(msi.c:579)
亲和性由 APIC 路由每向量可绑核每向量可绑核

对 DPDK 的意义:多队列网卡每队列一个 MSI-X 向量,是"每队列独立中断源"的硬件基础;PMD 不想要中断时把 mask 位一置即可。

面试 3 问

  1. MSI-X Table 在物理内存的哪个位置? → Capability 链找到 PCI_CAP_ID_MSIX(pci_regs.h:226),读 PCI_MSIX_TABLE dword(pci_regs.h:327)拆出 BIR(pci_regs.h:328)与偏移(pci_regs.h:329),BAR 基址加偏移 ioremap(msi.c:569-579)。
  2. 为什么改表项前必须 mask? → PCI 规范原文:unmasked 状态下改 Address/Data 结果 undefined,内核在 msi.c:213-222 引用并遵守。
  3. QSIZE 为什么加 1? → QSIZE 编码的是"表项数 - 1",msix_table_size 统一加 1(msi/msi.h:6),读表项数走 pci_msix_vec_count(api.c:71-80)。

🔧 动手实验

lspci -vvv -s 01:00.0 | grep -A5 MSI-X   # 记下 "Capabilities: [70] MSI-X" 的能力偏移
sudo lspci -xxxx -s 01:00.0 | grep -A4 '^70:'  # 手工解析 Table/BIR dword
sudo setpci -s 01:00.0 0x72.L           # Message Control: QSIZE / Enable / MaskAll 位

对照任务:把 0x72.L 读出的 QSIZE 与 lspci -v 的 "Count=xx" 互验;流量打满时观察 /proc/interrupts 各队列分布是否均匀(RSS 生效则各队列计数接近)。数据落点 perf-data/P4/02-msix.txt。

下一篇:P4-03 · 从 0 写一个最小 PCIe 内核驱动

0

评论 (0)

取消