对应路线图: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);
}- 找 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); - 读 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); - 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) | MSI | MSI-X |
|---|---|---|---|
| 载体 | 物理线 INTA#-INTD#,多设备共享 | 内存写 TLP | 内存写 TLP |
| 向量数 | 1(共享) | 1/2/4/8/16/32 | 最多 2048(QSIZE+1) |
| 每向量独立地址/数据 | 无 | 共享一份 | 每表项一份 |
| 内核路径 | 传统 irq domain | MSI domain(msi.c:270-279) | Table ioremap(msi.c:579) |
| 亲和性 | 由 APIC 路由 | 每向量可绑核 | 每向量可绑核 |
对 DPDK 的意义:多队列网卡每队列一个 MSI-X 向量,是"每队列独立中断源"的硬件基础;PMD 不想要中断时把 mask 位一置即可。
面试 3 问
- 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)。
- 为什么改表项前必须 mask? → PCI 规范原文:unmasked 状态下改 Address/Data 结果 undefined,内核在 msi.c:213-222 引用并遵守。
- 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。
评论 (0)