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 设备挂载在一条逻辑总线上。

关键概念: 每个 PCI 设备由 总线号(Bus Number)、设备号(Device Number) 和 功能号(Function Number) 唯一标识,合称为 BDF(Bus:Device.Function)。
PCI 总线拓扑结构 CPU / 根桥 (Root Bridge) PCI 总线 0 设备 A (00:00.0) 设备 B (00:01.0) PCI 桥 (00:02.0) PCI 总线 1 设备 C (01:00.0) 设备 D (01:01.0) 设备 E (01:02.0) 说明:PCI 桥将总线 0 扩展出总线 1,设备 C/D/E 挂载在次级总线上,BDF 编号随之变化。

2.2 PCI 配置空间

每个 PCI 设备都拥有 256 字节 的配置空间(Configuration Space),其中前 64 字节为预定义头部(Header Type 0),包含设备识别、控制、状态和基地址寄存器等信息。Linux 内核通过 pci_read_config_* 和 pci_write_config_* 系列函数访问配置空间。

PCI 配置空间头部关键字段(前 64 字节)
偏移(字节) 字段名称 宽度 说明
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() 将其映射到内核虚拟地址空间。

BAR 空间映射流程 PCI 设备 BAR0: 0xF0000000 BAR1: 0xF0010000 pci_resource_start() 内核虚拟地址 ioremap(0xF0000000, size) → 0xC1000000 readl() / writel() CPU / 驱动 读写寄存器 说明:驱动通过 pci_resource_start() 获取 BAR 物理地址,ioremap() 建立映射,之后通过 readl/writel 访问设备寄存器。
BAR 空间操作示例代码:
// 获取 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
INTx / MSI / MSI-X 中断机制 INTx(传统引脚中断) 设备通过 INTA# 引脚 向中断控制器发送信号 所有中断共享一个处理函数 最多 4 个引脚 MSI(消息信号中断) 设备向特定地址写入数据 触发中断,无需物理引脚 支持 1/2/4/8/16/32 向量 所有向量共享配置 MSI-X(扩展 MSI) 每个向量独立配置地址/数据 最多 2048 个向量 每个队列可绑定独立 CPU 网络设备首选方案 驱动开发中的中断选择策略 1. 优先尝试 MSI-X:pci_alloc_irq_vectors() 指定 PCI_IRQ_MSIX 2. 回退到 MSI:pci_alloc_irq_vectors() 指定 PCI_IRQ_MSI 3. 最后使用 INTx:pci_alloc_irq_vectors() 指定 PCI_IRQ_INTX 4. 释放中断向量:pci_free_irq_vectors() 在移除设备时调用
注意事项: 在申请 MSI/MSI-X 中断时,务必检查 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 支持每个队列独立中断,是多队列网卡的最佳选择。