PCIe 驱动 MSI-X 中断注册 ------ 原理与实战详解
涵盖 MSI-X 协议原理、硬件 Capability 结构、Linux 内核 API、完整注册流程、中断处理与释放。
目录
- [MSI-X 概述与对比](#MSI-X 概述与对比)
- [硬件视角:MSI-X Capability 结构](#硬件视角:MSI-X Capability 结构)
- [MSI-X Table 与 PBA](#MSI-X Table 与 PBA)
- 内核数据结构
- 注册流程全景图
- [Phase 1:使能 MSI-X](#Phase 1:使能 MSI-X)
- [Phase 2:分配中断向量](#Phase 2:分配中断向量)
- [Phase 3:注册中断处理函数](#Phase 3:注册中断处理函数)
- [Phase 4:MSI-X 中断处理流程](#Phase 4:MSI-X 中断处理流程)
- [Phase 5:释放 MSI-X](#Phase 5:释放 MSI-X)
- 完整驱动示例代码
- [对比:MSI-X vs INTx vs MSI vs MSI-X per-CPU](#对比:MSI-X vs INTx vs MSI vs MSI-X per-CPU)
- 面试高频考点
- 调试与常见问题
1. MSI-X 概述与对比
1.1 什么是 MSI-X?
MSI-X (Message Signaled Interrupts eXtended) 是 PCIe 规范中定义的增强型消息信号中断 机制。与传统的 INTx 边带信号不同,MSI-X 通过 PCIe 带内 TLP (Transaction Layer Packet) 报文向 CPU 发送中断,无需额外物理引脚。
1.2 三种中断机制对比
┌────────────┬──────────────┬──────────────┬──────────────────────┐
│ 特性 │ INTx │ MSI │ MSI-X │
│ │ (Legacy) │ │ │
├────────────┼──────────────┼──────────────┼──────────────────────┤
│ 物理引脚 │ 需要 INTA-D │ 无 │ 无 │
│ 最大向量数 │ 4 (共享) │ 1~32 │ 1~2048 │
│ 中断地址 │ N/A │ 固定 │ 每个向量独立地址 │
│ 中断数据 │ N/A │ 固定/递增 │ 每个向量独立数据 │
│ Table位置 │ N/A │ Cap 内 │ BAR 空间 (MMIO) │
│ Per-CPU │ 不支持 │ 不支持 │ 支持 (4.3+内核) │
│ 实现复杂度 │ 低 │ 中 │ 较高 │
│ 典型用途 │ 老设备 │ 简单设备 │ 现代高吞吐设备 │
│ │ │ │ (NVMe/网卡/GPU) │
└────────────┴──────────────┴──────────────┴──────────────────────┘
1.3 MSI-X 的核心优势
- 独立地址/数据域:每个向量有自己独立的 Message Address 和 Message Data,中断路由更灵活
- 向量数量多:最多支持 2048 个中断向量,适合多队列设备 (NVMe 多队列、网卡多队列)
- Table 在 BAR 空间:MSI-X Table 位于设备 BAR 的 MMIO 空间,由设备直接管理
- 支持 Per-CPU 分发:Linux 4.3+ 支持 MSI-X per-CPU 向量,减少 IPI 开销
- 无需共享:每个向量独立,无需像 INTx 那样判断中断来源
2. 硬件视角:MSI-X Capability 结构
2.1 在 PCIe 配置空间中定位
MSI-X 属于 PCIe Capability 结构,通过链表的形式在配置空间串联:
配置空间偏移 0x34: Capability Pointer → 指向第一个 Capability
│
┌───────────────────────────────┘
▼
┌──────────────────────────┐
│ Capability ID (1 byte) │ ← 0x11 = MSI-X
│ Next Pointer (1 byte) │ ← 指向下一个 Cap
│ Capability-Specific │
└──────────────────────────┘
2.2 MSI-X Capability 结构 (8字节)
Offset Bits Name Description
───────────────────────────────────────────────────────────
0x00 [7:0] Capability ID = 0x11 (PCI_CAP_ID_MSIX)
0x01 [15:8] Next Pointer 指向下一个 Capability 的偏移
0x02 [31:16] Message Control (控制/状态)
[25:16] Table Size = (此字段值 + 1),即向量数量
[30] Enable = 1 表示 MSI-X 已使能
[31] Function Mask = 1 表示屏蔽所有 MSI-X
0x04 [63:32] Table Offset + Table BIR
[2:0] BIR (BAR Indicator Register) --- 表的 BAR 编号
[31:3] Offset --- 表在该 BAR 中的偏移
0x08 [95:64] PBA Offset + PBA BIR
[2:0] BIR --- Pending Bit Array 的 BAR 编号
[31:3] Offset --- PBA 在该 BAR 中的偏移
2.3 用 lspci 查看 MSI-X Capability
bash
# 查看设备的 MSI-X 信息
lspci -vvv -s 01:00.0 | grep -A 20 "MSI-X"
# 示例输出:
# Capabilities: [b0] MSI-X: Enable+ Count=32 Masked-
# Vector table: BAR=4 offset=00002000
# PBA: BAR=4 offset=00003000
Enable+:MSI-X 已使能Count=32:32 个中断向量Masked-:全局 Mask 未生效- Table 在 BAR4 偏移 0x2000 处
- PBA 在 BAR4 偏移 0x3000 处
3. MSI-X Table 与 PBA
3.1 MSI-X Table 结构
每个 Entry 占用 16 字节,位于设备 BAR 的 MMIO 空间:
Offset Size Field
───────────────────────────────────────────────────────
+0x00 4 bytes Message Address (低32位)
+0x04 4 bytes Message Address (高32位,通常为0)
+0x08 4 bytes Message Data
+0x0c 4 bytes Vector Control
[0] Mask Bit: 1=该向量被屏蔽
[31:1] Reserved
Message Address 的构成 (x86/ARM64):
Bits Description
─────────────────────────────────────────────────────
[31:20] Fixed: 0xFEE ← 这是 x86 APIC 的固定前缀
[19:12] Destination ID ← 目标 CPU 的 APIC ID
[11:4] Reserved (0)
[3] Redirection Hint
[2] Destination Mode (0=Physical, 1=Logical)
[1:0] Reserved (0)
Message Data 的构成:
Bits Description
─────────────────────────────────────────────────────
[7:0] Vector Number (中断向量号: 0~255)
[10:8] Delivery Mode (000=Fixed, 001=Lowest Priority, ...)
[14:11] Reserved
[15] Trigger Mode (0=Edge, 1=Level)
[31:16] Reserved
3.2 Pending Bit Array (PBA)
- PBA 每一 bit 对应一个 MSI-X Table Entry
- 当设备发送 MSI-X 但被屏蔽时,对应的 PBA bit 被硬件置 1
- 解除屏蔽后,设备自动重发被 Pending 的中断
- PBA 是只读的,由硬件维护
3.3 硬件发送 MSI-X 的流程
设备产生中断事件 (如: 网卡收包队列满)
│
├─ 1. 从 MSI-X Table 中读取对应 Entry N
│ ├─ Entry[N].Mask == 1 ?
│ │ YES → 将 PBA[N] 置 1,流程结束 (中断被屏蔽)
│ │ NO → 继续
│ ▼
├─ 2. 构造 Memory Write TLP
│ ├─ Address = Table[N].Message_Address
│ ├─ Data = Table[N].Message_Data
│ └─ Length = 1 DW (32-bit write)
│ ▼
├─ 3. 通过 PCIe Link 发送 TLP
│ ▼
└─ 4. Root Complex 接收 TLP
├─ 识别为 MSI-X (地址匹配)
├─ 将 Message Data 中的 Vector 送给 APIC/Local APIC
└─ CPU 收到中断 → 调用对应的 IRQ handler
4. 内核数据结构
4.1 struct msix_entry --- 用户请求/分配的向量
c
/* include/linux/pci.h */
struct msix_entry {
u32 entry; /* MSI-X Table 中的 Entry 索引 (0~Table_Size-1) */
u16 vector; /* 内核分配的 Linux IRQ 号 (由 pci_enable_msix_range 填充) */
};
使用方式:
c
struct msix_entry entries[16];
for (i = 0; i < 16; i++)
entries[i].entry = i; // 只填写 entry,vector 由内核回填
4.2 struct msi_desc --- 内核内部描述符
c
/* include/linux/msi.h - 关键字段 */
struct msi_desc {
struct list_head list; // 链表节点 (挂在 pci_dev->dev.msi_list)
unsigned int irq; // Linux IRQ 号
unsigned int nvec_used; // 已使用的向量数
struct device *dev; // 所属设备
union {
/* MSI 特有 */
struct { u32 address_hi, address_lo, data; } msi;
/* MSI-X 特有 */
struct {
u32 masked; // 当前 Mask 状态
u16 entry_nr; // MSI-X Table Entry 索引
} msix;
};
void __iomem *mask_base; // MSI-X Table 中 Mask 位的 MMIO 地址
};
4.3 msi_msg --- 写入 Table 的消息内容
c
struct msi_msg {
u32 address_lo; // 写入 Table Entry+0x00
u32 address_hi; // 写入 Table Entry+0x04
u32 data; // 写入 Table Entry+0x08
};
5. 注册流程全景图
设备 Probe
│
├─ Phase 1: 使能 MSI-X
│ ├─ pci_alloc_irq_vectors() ← 推荐(4.8+内核)
│ │ ├─ __pci_enable_msix_range() ← 底层核心函数
│ │ │ ├─ 1. 检查 Capability (cap_id == 0x11)
│ │ │ ├─ 2. 读取 Table Size
│ │ │ ├─ 3. 分配 msi_desc[] 数组
│ │ │ ├─ 4. 分配 Linux IRQ 向量
│ │ │ ├─ 5. 构造 msi_msg (address + data)
│ │ │ ├─ 6. 写 MSI-X Table (MMIO → BAR)
│ │ │ └─ 7. 清除 Function Mask,置 Enable 位
│ │ └─ 返回分配的向量数
│ │
│ └─ 或旧 API: pci_enable_msix_range()
│
├─ Phase 2: 分配的中断向量存于 entries[].vector
│
├─ Phase 3: 注册中断处理函数
│ ├─ 对每个向量:
│ │ request_irq(entries[i].vector, handler, ...)
│ │ 或 devm_request_irq()
│ └─ 可选: 设置 IRQ 亲和性 irq_set_affinity_hint()
│
├─ Phase 4: 运行期
│ ├─ 设备发中断 → MSI-X TLP → RC → APIC → CPU
│ └─ CPU 执行 request_irq 注册的 handler
│
└─ Phase 5: 释放
├─ 对每个向量: free_irq(entries[i].vector, ...)
└─ pci_free_irq_vectors() ← 或 pci_disable_msix()
6. Phase 1:使能 MSI-X
6.1 推荐 API:pci_alloc_irq_vectors()
这是 Linux 4.8 引入的统一接口,同时支持 INTx / MSI / MSI-X:
c
/**
* pci_alloc_irq_vectors - 分配 PCI 中断向量
* @dev: PCI 设备
* @min_vecs: 最少需要的向量数
* @max_vecs: 最多期望的向量数 (同时控制 MSI 还是 MSI-X)
* @flags: 中断类型标志
*
* Flags:
* PCI_IRQ_LEGACY - 允许 Legacy INTx
* PCI_IRQ_MSI - 允许 MSI
* PCI_IRQ_MSIX - 允许 MSI-X
* PCI_IRQ_ALL_TYPES - 允许所有类型 (驱动自动选择最佳)
* PCI_IRQ_AFFINITY - 使用自动 IRQ 亲和性管理
*
* 返回值: 成功分配的向量数 (>= min_vecs),失败返回负值
*/
int nvec = pci_alloc_irq_vectors(pdev, 1, 32,
PCI_IRQ_MSIX | PCI_IRQ_MSI);
if (nvec < 0)
return nvec; // 错误处理
6.2 旧 API:pci_enable_msix_range()
旧版内核 (4.8 以前) 使用此接口,需要手动管理 msix_entry 数组:
c
static int enable_msix(struct pci_dev *pdev, struct msix_entry *entries,
int min_vecs, int max_vecs)
{
int i, ret;
/* Step 1: 初始化 entry 编号 */
for (i = 0; i < max_vecs; i++)
entries[i].entry = i;
/* Step 2: 请求分配 MSI-X (尝试 max → min) */
ret = pci_enable_msix_range(pdev, entries, min_vecs, max_vecs);
if (ret < 0) {
/* Step 3: 失败则降级尝试 MSI */
dev_warn(&pdev->dev, "MSI-X failed, trying MSI (%d)\n", ret);
/* ... 降级逻辑 ... */
return ret;
}
/* ret 是实际分配的向量数 */
return ret;
}
6.3 内核底层:__pci_enable_msix_range() 的详细步骤
c
/* drivers/pci/msi/msi.c (简化逻辑) */
int __pci_enable_msix_range(struct pci_dev *dev,
struct msix_entry *entries,
int minvec, int maxvec, ...)
{
int rc, nvec;
// ─── Step 1: 查找 MSI-X Capability ───
int pos = pci_find_capability(dev, PCI_CAP_ID_MSIX);
if (!pos)
return -ENOTSUPP;
// ─── Step 2: 读取 Table Size ───
u16 control;
pci_read_config_word(dev, pos + PCI_MSIX_FLAGS, &control);
int table_size = (control & PCI_MSIX_FLAGS_QSIZE) + 1;
// 例如: NVMe SSD 通常 table_size = 32~64
// ─── Step 3: 限制 maxvec 不超过硬件能力 ───
maxvec = min(maxvec, table_size);
// ─── Step 4: 根据 maxvec 分配内核中断资源 ───
// - 分配 msi_desc 数组
// - 分配 Linux IRQ 号
// - 创建 sysfs 条目
for (;;) {
nvec = maxvec;
rc = msi_capability_init(dev, nvec, ...);
if (rc == 0)
break;
// 分配失败则减少向量数重试
if (nvec < minvec)
return -ENOSPC;
}
// ─── Step 5: 写 MSI-X Table ───
for (i = 0; i < nvec; i++) {
struct msi_desc *desc = ...; // 内核描述符
struct msi_msg msg;
// 5a: 构造 Message
__pci_write_msi_msg(desc, &msg);
// msg.address_lo = 中断目标地址 (APIC/ITS)
// msg.data = 中断向量号
// 5b: 填充 entries[] (回填给调用者)
entries[i].vector = desc->irq;
}
// ─── Step 6: 清除 Function Mask, 设置 Enable ───
pci_msix_clear_and_set_ctrl(dev,
PCI_MSIX_FLAGS_MASKALL, // 清除
PCI_MSIX_FLAGS_ENABLE); // 设置
// 等价于: Message Control 寄存器
// bit[30] Enable = 1
// bit[31] Function Mask = 0
return nvec;
}
6.4 写 MSI-X Table 的底层链
__pci_write_msi_msg(desc, &msg)
│
├─ [x86] __pci_write_msi_msg_x86()
│ → msg 中 address = APIC 寄存器地址, data = 向量号
│
├─ [ARM64 GICv3 ITS] __pci_write_msi_msg_its()
│ → msg 中 address = ITS 翻译地址, data = DeviceID+EventID
│
└─ 最终: __pci_msix_desc_mask_irq() / writel()
→ 将 msg 写入设备的 MSI-X Table (BAR MMIO 空间)
7. Phase 2:分配中断向量
pci_alloc_irq_vectors() 返回后,要获取每个向量的 Linux IRQ 号:
c
/* 方式 1: 使用 msix_entry 数组 (旧 API) */
struct msix_entry entries[32];
for (i = 0; i < 32; i++)
entries[i].entry = i;
int nvec = pci_enable_msix_range(pdev, entries, 1, 32);
for (i = 0; i < nvec; i++)
int irq = entries[i].vector; // Linux IRQ 号
/* 方式 2: 使用 pci_irq_vector() (新 API, 推荐) */
int nvec = pci_alloc_irq_vectors(pdev, 1, 32, PCI_IRQ_MSIX);
for (i = 0; i < nvec; i++)
int irq = pci_irq_vector(pdev, i); // 第 i 个向量的 IRQ 号
7.1 IRQ 亲和性配置
让特定中断向量绑定到特定 CPU 核心上,减少缓存失效和跨核开销:
c
/* 手动设置亲和性 */
int cpu = i % num_online_cpus(); // 轮询分配到各 CPU
cpumask_clear(&mask);
cpumask_set_cpu(cpu, &mask);
irq_set_affinity_hint(irq, &mask);
/* 自动亲和性 (PCI_IRQ_AFFINITY 标志) */
// 4.8+ 内核支持,自动均匀分布
int nvec = pci_alloc_irq_vectors(pdev, 1, 32,
PCI_IRQ_MSIX | PCI_IRQ_AFFINITY);
亲和性设置原则:
- NVMe:每个队列的完成中断绑定到提交对应 SQ 的 CPU
- 网卡 RX:各 RX 队列绑定到不同的 CPU,与 RSS/应用层对齐
- 建议使用内核提供的
irq_calc_affinity_vectors()等辅助函数
8. Phase 3:注册中断处理函数
8.1 为每个向量注册 handler
c
/* 推荐使用 IRQF_NO_THREAD 标志 (快速路径) */
for (i = 0; i < nvec; i++) {
int irq = pci_irq_vector(pdev, i);
snprintf(name, sizeof(name), "%s-msix%d", dev_name(&pdev->dev), i);
ret = devm_request_irq(&pdev->dev, irq,
my_irq_handler, // 中断处理函数
IRQF_SHARED, // 或其他 flags
name,
&my_dev->queues[i]); // 每个队列的私有数据
if (ret) {
dev_err(&pdev->dev, "request irq %d failed\n", irq);
goto free_irqs;
}
}
8.2 中断处理函数模板
c
static irqreturn_t my_msix_handler(int irq, void *data)
{
struct my_queue *q = data; // 获取私有数据结构
struct my_dev *dev = q->dev;
u32 status;
/* Step 1: 检查是否是本设备的中断 (中断共享场景) */
status = readl(q->regs + INTR_STATUS_REG);
if (!(status & INTR_PENDING))
return IRQ_NONE; // 不是我的中断 (共享场景)
/* Step 2: 清除硬件中断状态 */
writel(status, q->regs + INTR_STATUS_REG);
/* Step 3: 快速处理 (hardirq context)
* - 禁止睡眠
* - 尽量简短
* - 只做必要的数据搬运
*/
napi_schedule(&q->napi); // 网络: 调度 NAPI
/* 或者 */
tasklet_schedule(&q->tasklet); // NVMe: 调度 tasklet
/* 或者 */
return IRQ_WAKE_THREAD; // 唤醒 threaded handler
/* Step 4: 返回 */
return IRQ_HANDLED;
}
/* Threaded handler (当使用 request_threaded_irq 时) */
static irqreturn_t my_msix_thread(int irq, void *data)
{
struct my_queue *q = data;
/* 处理需要睡眠的操作: 内存分配、锁、I/O 等 */
process_completions(q);
return IRQ_HANDLED;
}
8.3 中断返回值的含义
| 返回值 | 含义 | 副作用 |
|---|---|---|
IRQ_HANDLED |
我处理了这个中断 | 内核标记该设备已处理 |
IRQ_NONE |
不是我的中断 | 内核可能继续尝试其他 handler |
IRQ_WAKE_THREAD |
唤醒 threaded 函数 | 调度 threaded handler 执行 |
9. Phase 4:MSI-X 中断处理流程
9.1 完整数据流向
硬件层 PCIe 链路层 CPU/中断控制器 驱动层
────── ────────── ────────────── ──────
[Phase 3]
reg irq handler
│
[设备] ┌──┘
│ │
├─ 事件发生 [APIC/ITS] │
│ (RX 包到达) ▲ │
│ │ │
├─ 查 Table[N] │ │
│ ├─ Mask? ─ No │ │
│ ├─ Addr ───┐ │ │
│ └─ Data ───┤ Memory Write TLP │ │
│ │ ┌──────────────┐ │ │
│ ├─►│ PCIe Switch ├───►│ 中断控制器 │
│ │ │ RC (Root Cpx) │ │ 识别 MSI-X │
│ │ └──────────────┘ │ 分配 CPU │
│ │ │ │
│ │ ▼ │
│ │ [CPU] │
│ │ 读取中断号 │
│ │ 跳转中断向量表 │
│ │ ┌────────────┐ │
│ │ │ do_IRQ() │───────►│
│ │ │ handle_irq │ 调用handler
│ │ │ _event() │ │
│ │ └────────────┘ │
│ │ ▼
│ │ my_msix_handler()
│ ◄─────────┘ ├─ 读硬件状态
│ [设备清除中断] ├─ 处理完成队列
│ └─ 返回 IRQ_HANDLED
│
▼
[等待下一中断]
9.2 MSI-X 中断与 Per-CPU 分发
c
/* 使用 PCI_IRQ_AFFINITY 时内核自动处理 */
// 设备发 MSI-X Entry 0 → APIC/ITS 路由到 CPU 0
// 设备发 MSI-X Entry 1 → APIC/ITS 路由到 CPU 1
// ...
/* NVMe 典型做法: 每核一个队列 */
int num_queues = min(num_online_cpus(), max_msix_vectors);
int nvec = pci_alloc_irq_vectors(pdev, num_queues, num_queues,
PCI_IRQ_MSIX | PCI_IRQ_AFFINITY);
for (i = 0; i < nvec; i++) {
int irq = pci_irq_vector(pdev, i);
// PCI_IRQ_AFFINITY 确保 irq 与 CPU i 绑定
request_irq(irq, nvme_queue_irq, 0, "nvme-q", &queues[i]);
}
10. Phase 5:释放 MSI-X
10.1 完整释放流程
c
static void my_dev_free_irqs(struct my_dev *dev)
{
int i, irq;
/* Step 1: 首先禁止设备产生新中断 */
writel(0, dev->regs + INTR_ENABLE_REG);
/* Step 2: 同步 --- 等待所有正在执行的中断完成 */
for (i = 0; i < dev->num_queues; i++) {
irq = pci_irq_vector(dev->pdev, i);
synchronize_irq(irq); // 等待此 IRQ 的所有 handler 返回
}
/* Step 3: 取消亲和性设置 */
for (i = 0; i < dev->num_queues; i++) {
irq = pci_irq_vector(dev->pdev, i);
irq_set_affinity_hint(irq, NULL);
}
/* Step 4: 释放每个 IRQ */
for (i = 0; i < dev->num_queues; i++) {
irq = pci_irq_vector(dev->pdev, i);
free_irq(irq, &dev->queues[i]);
}
// 使用 devm_ 的话: devm_free_irq()
/* Step 5: 禁用 MSI-X, 释放向量 */
pci_free_irq_vectors(dev->pdev);
// 或 pci_disable_msix(dev->pdev); (旧 API)
}
10.2 释放流程时序图
remove() / shutdown() 被调用
│
├─ my_dev_free_irqs(dev)
│ ├─ 写设备寄存器禁止中断
│ │ ↓ 确保没有新的 MSI-X TLP 发出
│ ├─ synchronize_irq() × N
│ │ ↓ 等待所有 processors 上正在运行的 handler 完成
│ ├─ irq_set_affinity_hint(NULL) × N
│ │ ↓ 清除 /proc/irq/*/affinity_hint
│ ├─ free_irq() × N
│ │ ↓ 从中断描述表中移除 handler
│ └─ pci_free_irq_vectors(pdev)
│ ↓ ┌────────────────────────────┐
│ │ 1. 设置 Function Mask 位 │ ← 屏蔽所有 MSI-X
│ │ 2. 清除 Enable 位 │ ← 禁用 MSI-X
│ │ 3. 清除 MSI-X Table 内容 │ ← 防止残留
│ │ 4. 释放 msi_desc[] 内存 │
│ └────────────────────────────┘
│
└─ 安全完成,可继续卸载驱动
11. 完整驱动示例代码
以下是一个 NVMe 风格的多队列设备 MSI-X 注册完整示例:
c
#include <linux/pci.h>
#include <linux/interrupt.h>
#define MY_DEV_MAX_QUEUES 64
struct my_queue {
struct my_dev *dev;
void __iomem *regs; // 队列相关的 MMIO 寄存器
int id; // 队列 ID
int irq; // Linux IRQ 号
char irq_name[32]; // /proc/interrupts 中显示的名字
};
struct my_dev {
struct pci_dev *pdev;
void __iomem *bar0; // BAR0 基址 (MSI-X Table 在 BAR 中)
struct my_queue queues[MY_DEV_MAX_QUEUES];
int num_queues;
int num_irqs;
};
/* ─── 中断处理函数 ─── */
static irqreturn_t my_queue_irq_handler(int irq, void *data)
{
struct my_queue *q = data;
struct my_dev *dev = q->dev;
u32 isr;
/* 1. 读取中断状态 */
isr = readl(q->regs + 0x00); /* INT_STATUS */
if (!(isr & BIT(0)))
return IRQ_NONE;
/* 2. 清中断 (写1清除) */
writel(isr, q->regs + 0x00); /* INT_STATUS */
/* 3. 快速处理 (不能睡眠!) */
/* ... 例如: 处理完成队列、调度 NAPI、唤醒 tasklet ... */
return IRQ_HANDLED;
}
/* ─── MSI-X 初始化和注册 ─── */
static int my_setup_msix(struct my_dev *dev)
{
struct pci_dev *pdev = dev->pdev;
int ret, i, irq;
int nvec;
/* ── Phase 1: 分配 MSI-X 向量 ── */
dev->num_queues = min_t(int, num_online_cpus(), MY_DEV_MAX_QUEUES);
nvec = pci_alloc_irq_vectors(pdev,
1, /* min_vecs */
dev->num_queues, /* max_vecs */
PCI_IRQ_MSIX | PCI_IRQ_AFFINITY);
if (nvec < 0) {
/* MSI-X 失败,尝试降级到 MSI */
dev_warn(&pdev->dev, "MSI-X failed (%d), trying single MSI\n", nvec);
nvec = pci_alloc_irq_vectors(pdev, 1, 1, PCI_IRQ_MSI);
if (nvec < 0) {
/* MSI 也失败,回退到 INTx */
nvec = pci_alloc_irq_vectors(pdev, 1, 1, PCI_IRQ_LEGACY);
if (nvec < 0)
return nvec;
}
}
dev->num_irqs = nvec;
dev_info(&pdev->dev, "enabled %d MSI-X/MSI vectors\n", nvec);
/* ── Phase 2 & 3: 为每个向量注册 handler ── */
for (i = 0; i < nvec; i++) {
irq = pci_irq_vector(pdev, i);
dev->queues[i].id = i;
dev->queues[i].dev = dev;
dev->queues[i].irq = irq;
snprintf(dev->queues[i].irq_name,
sizeof(dev->queues[i].irq_name),
"mydev-q%d", i);
ret = devm_request_irq(&pdev->dev, irq,
my_queue_irq_handler,
0, /* flags: 无 IRQF_SHARED (MSI-X 不共享) */
dev->queues[i].irq_name,
&dev->queues[i]);
if (ret) {
dev_err(&pdev->dev, "failed to request IRQ %d: %d\n", irq, ret);
goto free_irqs;
}
}
/* ── Phase 4 (可选): 在 /proc/irq/N/affinity_hint 显示亲和性 ── */
for (i = 0; i < nvec; i++) {
cpumask_t mask;
irq = pci_irq_vector(pdev, i);
cpumask_clear(&mask);
cpumask_set_cpu(i % num_online_cpus(), &mask);
irq_set_affinity_hint(irq, &mask);
}
return 0;
free_irqs:
/* devm_ 系列会自动释放,这里展示手动释放模式 */
while (--i >= 0) {
irq = pci_irq_vector(pdev, i);
irq_set_affinity_hint(irq, NULL);
devm_free_irq(&pdev->dev, irq, &dev->queues[i]);
}
pci_free_irq_vectors(pdev);
return ret;
}
/* ─── 释放 ─── */
static void my_teardown_msix(struct my_dev *dev)
{
struct pci_dev *pdev = dev->pdev;
int i, irq;
/* 1. 禁止设备产生中断 */
writel(0, dev->bar0 + 0x100); /* GLOBAL_INT_MASK */
/* 2. 同步现有中断 */
for (i = 0; i < dev->num_irqs; i++) {
irq = pci_irq_vector(pdev, i);
synchronize_irq(irq);
}
/* 3. 清亲和性 */
for (i = 0; i < dev->num_irqs; i++) {
irq = pci_irq_vector(pdev, i);
irq_set_affinity_hint(irq, NULL);
}
/* 4. 释放 IRQ (devm 管理则自动) */
for (i = 0; i < dev->num_irqs; i++) {
irq = pci_irq_vector(pdev, i);
devm_free_irq(&pdev->dev, irq, &dev->queues[i]);
}
/* 5. 释放 MSI-X 向量 */
pci_free_irq_vectors(pdev);
}
/* ─── Probe ─── */
static int my_pci_probe(struct pci_dev *pdev, const struct pci_device_id *id)
{
struct my_dev *dev;
int ret;
/* 1. 使能 PCI 设备 */
ret = pci_enable_device(pdev);
if (ret)
return ret;
/* 2. 设置 DMA 掩码 */
ret = dma_set_mask_and_coherent(&pdev->dev, DMA_BIT_MASK(64));
if (ret)
goto disable_device;
/* 3. 请求 BAR0 */
ret = pci_request_regions(pdev, "mydev");
if (ret)
goto disable_device;
/* 4. 分配私有结构 */
dev = devm_kzalloc(&pdev->dev, sizeof(*dev), GFP_KERNEL);
if (!dev) {
ret = -ENOMEM;
goto release_regions;
}
dev->pdev = pdev;
dev->bar0 = pci_ioremap_bar(pdev, 0);
pci_set_drvdata(pdev, dev);
/* 5. 总线主控使能 (DMA 需要) */
pci_set_master(pdev);
/* 6. ─── 核心:设置 MSI-X ─── */
ret = my_setup_msix(dev);
if (ret)
goto unmap_bar;
return 0;
unmap_bar:
pci_iounmap(pdev, dev->bar0);
release_regions:
pci_release_regions(pdev);
disable_device:
pci_disable_device(pdev);
return ret;
}
/* ─── Remove ─── */
static void my_pci_remove(struct pci_dev *pdev)
{
struct my_dev *dev = pci_get_drvdata(pdev);
my_teardown_msix(dev);
pci_iounmap(pdev, dev->bar0);
pci_release_regions(pdev);
pci_disable_device(pdev);
}
12. 对比:MSI-X vs INTx vs MSI vs MSI-X per-CPU
12.1 功能对比表
┌──────────────────┬──────────┬──────────────┬──────────────────────┐
│ │ INTx │ MSI │ MSI-X │
├──────────────────┼──────────┼──────────────┼──────────────────────┤
│ 最大向量数 │ 4 │ 32 │ 2048 │
│ 独立地址/数据 │ ✗ │ 地址固定 │ ✓ 每向量独立 │
│ Table 位置 │ N/A │ Cap 内 │ BAR MMIO │
│ 中断共享 │ 是 │ 可选 │ 不共享 (推荐) │
│ Per-CPU 分发 │ ✗ │ ✗ │ ✓ (内核 4.3+) │
│ 亲和性控制 │ 粗粒度 │ 粗粒度 │ 细粒度(per-vector) │
│ TLP 携带信息 │ N/A │ 有限 │ 丰富 │
│ 典型场景 │ 老设备 │ 简单设备 │ 多队列高性能设备 │
└──────────────────┴──────────┴──────────────┴──────────────────────┘
12.2 API 选择决策树
设备 Probe
│
├─ 需要 > 32 个中断向量?
│ YES → MSI-X
│ NO ↓
│
├─ 需要多个队列绑定不同 CPU?
│ YES → MSI-X (或 MSI-X per-CPU)
│ NO ↓
│
├─ 设备支持 MSI-X?
│ YES → MSI-X
│ NO ↓
│
├─ 设备支持 MSI?
│ YES → MSI
│ NO ↓
│
└─ 回退 → INTx
12.3 统一 API 的使用
c
/* 最佳实践: 使用统一 API + 自动降级 */
int nvec = pci_alloc_irq_vectors(pdev, 1, 32, PCI_IRQ_ALL_TYPES);
if (nvec < 0)
return nvec;
/* 检查分配到的类型 */
if (pdev->msix_enabled)
type = "MSI-X";
else if (pdev->msi_enabled)
type = "MSI";
else
type = "INTx";
13. 面试高频考点
Q1: MSI-X 和 MSI 的核心区别是什么?
| MSI | MSI-X | |
|---|---|---|
| Table 位置 | Capability 结构内 (6 bytes) | 设备 BAR MMIO 空间 (16 bytes/entry) |
| 向量数上限 | 32 | 2048 |
| 地址/数据 | 所有向量共享地址、数据相邻递增 | 每个向量独立的地址和数据 |
| Per-vector Mask | 不支持 | 支持(每个 Entry 有 Mask 位) |
| PBA | 不支持 | 支持(在独立的 BAR 区域) |
Q2: MSI-X Table 为什么放在 BAR 空间?
- 灵活性:Capability 空间只有 256 bytes,MSI-X Table 可达 (2048×16=32KB),放不下
- 设备直接写:设备硬件可以直接读取 BAR 中的 Table(通过内部总线),不需要每次都走 PCIe 配置空间
- Mask 支持:每个 Entry 有独立的 Mask 位(Bit 0 of Vector Control),设备硬件实时检查
Q3: pci_alloc_irq_vectors() vs pci_enable_msix_range() 的区别?
pci_alloc_irq_vectors()是 4.8+ 的统一 API,同时支持 INTx/MSI/MSI-Xpci_enable_msix_range()是老 API,只用于 MSI-X- 新代码推荐使用前者,简化代码并自动处理降级逻辑
Q4: MSI-X 中断的 Message Address 和 Message Data 是什么?
- Message Address:x86 上是 0xFEE00xxx 格式,指向 Local APIC 的 ICR (Interrupt Command Register);ARM64 GICv3 ITS 上是 ITS 翻译表对应的地址
- Message Data:x86 上低 8 位是中断向量号;ARM64 上是 LMSI 的 DeviceID + EventID 编码值
Q5: 为什么 MSI-X 不需要 IRQF_SHARED?
每个 MSI-X 向量对应唯一的硬件 Entry,设备保证不同事件使用不同的 Entry。因此不存在"两个设备共享一条中断线"的情况。使用 IRQF_SHARED 反而增加开销。
Q6: 如何保证 MSI-X 中断处理的正确性?
- synchronize_irq():释放前等待所有 handler 完成
- 中断禁止优先:先禁止设备中断,再释放 IRQ
- Mask 控制:MSI-X Table 中每个 Entry 的 Mask 位可单独控制
- Memory Barrier :写设备寄存器后需要
wmb()确保刷新到硬件
Q7: MSI-X 在 ARM64 和 x86 上的差异?
| x86 | ARM64 (GICv3 ITS) | |
|---|---|---|
| 中断控制器 | APIC / x2APIC | GICv3 ITS |
| 地址格式 | 0xFEE00xxx | ITS 翻译地址 |
| 数据格式 | Vector号 (8bit) | DevID + EventID |
| 映射管理 | IOMMU/APIC | ITS → LPI 翻译 |
| 亲和性 | IOAPIC/APIC ID | ITS redistributor |
Q8: 注册失败时的降级策略是什么?
1. 尝试 MSI-X (min=需要的队列数, max=硬件支持)
↓ 失败
2. 尝试单 MSI 向量
↓ 失败
3. 尝试 Legacy INTx
↓ 失败
4. 返回错误,放弃加载驱动
14. 调试与常见问题
14.1 查看 MSI-X 状态
bash
# 查看中断信息
cat /proc/interrupts | grep -i <devname>
# 示例输出:
# CPU0 CPU1 CPU2 CPU3
# 144: 0 0 15234 0 ITS-MSI 524288 Edge nvme0q0
# 145: 0 0 0 8921 ITS-MSI 524289 Edge nvme0q1
# 146: 4123 0 0 0 ITS-MSI 524290 Edge nvme0q2
# 查看亲和性
cat /proc/irq/144/smp_affinity # 位掩码
cat /proc/irq/144/affinity_hint # 驱动提示的亲和性
cat /proc/irq/144/effective_affinity # 实际生效的亲和性
# 查看 MSI-X Capability
lspci -vvv -s 01:00.0 | grep -A 20 MSI-X
14.2 常见问题排查
| 症状 | 可能原因 | 检查方法 |
|---|---|---|
| 驱动注册成功但无中断 | Function Mask 未清除 | lspci -vvv 查看 "Masked+" |
| Entry Mask 位未清除 | 读 BAR 中 Table Entry 的 Vector Control | |
| 设备未使能中断 | 检查设备特定的 INTR_ENABLE 寄存器 | |
| IRQ 亲和性未设置 | cat /proc/irq/N/effective_affinity |
|
| 所有中断都落到 CPU0 | 未配置亲和性或 PCI_IRQ_AFFINITY |
检查 irq affinity hint |
request_irq 返回 -EINVAL |
向量数超过硬件能力 | lspci -vvv 查看 Table size |
| 中断风暴(连续触发) | handler 未清硬件状态位 | 确保返回前写清中断标志 |
| remove 时 kernel panic | 释放顺序错误 | 先禁止设备 → synchronize → free |
14.3 核心检查清单
□ pci_alloc_irq_vectors 使用 PCI_IRQ_MSIX 标志
□ 检查返回值 >= min_vecs
□ 为每个向量检查 pci_irq_vector() 返回有效值
□ handler 中先读状态寄存器判断是否是自己的中断
□ handler 返回前清除硬件中断标志
□ 需要睡眠的处理用 request_threaded_irq 或 bottom-half 机制
□ 释放顺序: 禁止中断 → synchronize_irq → free_irq → pci_free_irq_vectors
□ 验证: cat /proc/interrupts 确认中断在递增
□ 验证: lspci -vvv 确认 Enable+ Masked-