2. PCI 子系统概述:总线拓扑、配置空间、BAR 空间与中断机制
PCI(Peripheral Component Interconnect)子系统是 Linux 内核中连接 CPU 与外围设备的核心总线架构。对于网络设备驱动开发者而言,理解 PCI 的拓扑结构、配置空间、BAR 空间以及中断机制,是编写稳定、高性能驱动的基础。本章将系统梳理这些关键概念,并辅以图表与代码示例,帮助你建立完整的 PCI 驱动知识框架。
2.1 PCI 总线拓扑结构
PCI 总线采用树形拓扑结构,由 根桥(Root Bridge)、PCI 桥(PCI Bridge) 和 PCI 设备(PCI Device) 组成。根桥连接 CPU 与主 PCI 总线,PCI 桥用于扩展总线层级,每个 PCI 设备挂载在一条逻辑总线上。
2.2 PCI 配置空间
每个 PCI 设备都拥有 256 字节 的配置空间(Configuration Space),其中前 64 字节为预定义头部(Header Type 0),包含设备识别、控制、状态和基地址寄存器等信息。Linux 内核通过 pci_read_config_* 和 pci_write_config_* 系列函数访问配置空间。
| 偏移(字节) | 字段名称 | 宽度 | 说明 |
|---|---|---|---|
| 0x00 | Vendor ID | 16 位 | 厂商识别号(如 Intel: 0x8086) |
| 0x02 | Device ID | 16 位 | 设备型号识别号 |
| 0x04 | Command | 16 位 | 控制命令(I/O 空间、内存空间、总线主控使能) |
| 0x06 | Status | 16 位 | 设备状态(中断状态、能力列表等) |
| 0x08 | Revision ID | 8 位 | 修订版本号 |
| 0x09 | Class Code | 24 位 | 设备分类(网络控制器: 0x020000) |
| 0x10 - 0x24 | Base Address Registers (BAR0 - BAR5) | 32 位 × 6 | 基地址寄存器,映射设备内存/I/O 空间 |
| 0x3C | Interrupt Line | 8 位 | 中断线号(由 BIOS/固件分配) |
| 0x3D | Interrupt Pin | 8 位 | 中断引脚(INTA#=1, INTB#=2, INTC#=3, INTD#=4) |
pci_get_device() 或 pci_register_driver() 匹配 Vendor ID 和 Device ID。配置空间的 Class Code 可用于识别设备类型,避免误匹配。
2.3 BAR 空间:内存映射与 I/O 映射
基地址寄存器(BAR)是 PCI 设备与 CPU 通信的桥梁。每个 BAR 描述一段设备地址空间,可以是 内存映射(Memory-Mapped) 或 I/O 映射(I/O-Mapped)。驱动通过读取 BAR 获取物理地址,再通过 ioremap() 将其映射到内核虚拟地址空间。
// 获取 BAR0 的物理地址和大小
resource_size_t bar_start = pci_resource_start(pdev, 0);
resource_size_t bar_len = pci_resource_len(pdev, 0);
// 将物理地址映射到内核虚拟地址
void __iomem *regs = ioremap(bar_start, bar_len);
if (!regs) {
dev_err(&pdev->dev, "ioremap failed\n");
return -ENOMEM;
}
// 通过映射后的地址读写寄存器
u32 val = readl(regs + REG_OFFSET);
writel(val | BIT(0), regs + REG_OFFSET);
2.4 中断机制:INTx、MSI 与 MSI-X
PCI 设备支持三种中断机制:传统的 INTx(中断引脚)、MSI(Message Signaled Interrupts)和 MSI-X(扩展 MSI)。现代高性能网络设备普遍采用 MSI-X,因为它支持每个队列独立中断,极大提升多核系统的负载均衡能力。
| 特性 | INTx | MSI | MSI-X |
|---|---|---|---|
| 中断触发方式 | 电平/边沿触发 | 内存写消息 | 内存写消息 |
| 最大中断向量数 | 4(INTA# ~ INTD#) | 1, 2, 4, 8, 16, 32 | 最多 2048 |
| 每个向量独立配置 | 否 | 否 | 是 |
| 支持中断亲和性 | 有限 | 是 | 是(每个向量可绑定不同 CPU) |
| 典型应用场景 | 旧设备、简单外设 | 中端设备 | 高性能网卡、NVMe SSD |
pci_alloc_irq_vectors() 的返回值。如果设备不支持 MSI-X,内核会自动回退到 MSI 或 INTx。驱动应始终处理回退情况,避免假设中断类型。
2.5 中断处理函数注册示例
以下代码展示了在 PCI 驱动探测函数中注册 MSI-X 中断的典型流程:
static int my_pci_probe(struct pci_dev *pdev, const struct pci_device_id *id)
{
struct my_adapter *adapter;
int ret, i, nvec;
// 1. 启用 PCI 设备
ret = pci_enable_device(pdev);
if (ret)
return ret;
// 2. 申请 MSI-X 中断向量(每个队列一个)
nvec = pci_alloc_irq_vectors(pdev, 1, num_queues, PCI_IRQ_MSIX);
if (nvec < 0) {
dev_err(&pdev->dev, "Failed to allocate MSI-X vectors\n");
pci_disable_device(pdev);
return nvec;
}
// 3. 为每个向量注册中断处理函数
for (i = 0; i < nvec; i++) {
int irq = pci_irq_vector(pdev, i);
ret = request_irq(irq, my_interrupt_handler, 0,
"my_driver", &adapter->queue[i]);
if (ret) {
dev_err(&pdev->dev, "Failed to request IRQ %d\n", irq);
goto err_free_irqs;
}
}
// ... 其他初始化 ...
return 0;
err_free_irqs:
while (--i >= 0)
free_irq(pci_irq_vector(pdev, i), &adapter->queue[i]);
pci_free_irq_vectors(pdev);
pci_disable_device(pdev);
return ret;
}
pci_irq_vector() 获取每个向量的 IRQ 号,而不是直接使用 pdev->irq。后者仅适用于 INTx 或单向量 MSI 场景。对于 MSI-X,每个队列的 IRQ 号可能不同,必须通过向量索引获取。
2.6 本章小结
- PCI 拓扑 采用树形结构,BDF 编号唯一标识设备。
- 配置空间 包含设备识别、BAR 地址和中断信息,是驱动探测的入口。
- BAR 空间 通过
ioremap()映射到内核虚拟地址,驱动通过readl/writel访问。 - 中断机制 优先使用 MSI-X,其次 MSI,最后 INTx。MSI-X 支持每个队列独立中断,是多队列网卡的最佳选择。