4. PCIe设备枚举与驱动绑定:pci_driver结构体、probe/remove回调、设备树与ACPI

PCIe设备从物理插入到被Linux内核驱动接管,经历了硬件枚举、资源分配、驱动匹配和绑定四个阶段。本章深入剖析pci_driver结构体的核心字段、probe/remove回调的生命周期管理,以及设备树(Device Tree)与ACPI在PCIe设备描述中的差异与协同。

核心要点:驱动绑定的本质是内核PCI核心层将枚举出的pci_dev设备对象,与驱动注册的pci_driver进行ID匹配,匹配成功后调用probe回调完成初始化。

4.1 pci_driver结构体深度解析

pci_driver是PCI设备驱动的核心数据结构,定义在include/linux/pci.h中。它封装了驱动与内核PCI子系统的所有交互接口。

struct pci_driver {
    const char *name;                       // 驱动名称,用于sysfs标识
    const struct pci_device_id *id_table;   // 支持的设备ID表(关键匹配依据)
    int  (*probe)(struct pci_dev *dev, const struct pci_device_id *id);
    void (*remove)(struct pci_dev *dev);
    int  (*suspend)(struct pci_dev *dev, pm_message_t state);
    int  (*resume)(struct pci_dev *dev);
    void (*shutdown)(struct pci_dev *dev);
    int  (*sriov_configure)(struct pci_dev *dev, int num_vfs);
    const struct pci_error_handlers *err_handler;
    struct device_driver driver;            // 内嵌通用驱动模型
    struct pci_dynids dynids;               // 动态ID支持
};
字段 作用 驱动开发者关注点
name 驱动名称,在/sys/bus/pci/drivers/下可见 保持唯一,与模块名一致
id_table 设备ID匹配表,决定驱动能绑定哪些设备 必须包含Vendor ID、Device ID、Subsystem ID等
probe 设备匹配成功后的初始化入口 分配资源、映射BAR、注册中断、创建设备文件
remove 设备移除或驱动卸载时的清理出口 释放所有probe中分配的资源,注销中断
suspend/resume 电源管理回调 保存/恢复设备上下文,处理D3hot/D0状态切换
sriov_configure SR-IOV虚拟功能配置 启用/禁用VF,分配VF资源
err_handler PCIe错误处理回调(AER) 处理可恢复/不可恢复错误
实战建议:在id_table中使用PCI_DEVICE(VENDOR_ID, DEVICE_ID)宏定义匹配条目。对于需要匹配多个子系统的设备,使用PCI_DEVICE_SUB宏精确匹配。

4.2 probe/remove回调的生命周期

probe和remove是驱动与设备绑定的两个关键时间点,其调用时序与内核PCI枚举流程紧密相关。

PCIe设备枚举与驱动绑定生命周期 1. 硬件枚举 2. 资源分配 3. 驱动匹配 4. probe() probe() 内部典型流程 pci_enable_device() pci_request_regions() pci_iomap() / ioremap() request_irq() / pci_alloc_irq_vectors() remove() 内部典型流程(与probe反向) free_irq() / pci_free_irq_vectors() pci_iounmap() / iounmap() pci_release_regions() pci_disable_device() ⚠ 关键规则:probe成功必须返回0;返回负值表示绑定失败,内核会尝试下一个匹配驱动 remove中必须释放所有probe分配的资源,否则导致内存泄漏或设备无法再次绑定

4.3 设备树(Device Tree)与PCIe

在ARM/ARM64等嵌入式平台中,PCIe控制器和设备的拓扑信息通过设备树描述。设备树节点提供PCIe总线地址映射、中断路由、复位GPIO等硬件配置信息。

/* 设备树中PCIe控制器节点示例 */
pcie0: pcie@f0000000 {
    compatible = "vendor,pcie-controller";
    reg = <0x0 0xf0000000 0x0 0x10000000>;  // 配置空间和寄存器基址
    bus-range = <0x0 0xff>;                  // 总线号范围
    device_type = "pci";
    #address-cells = <3>;
    #size-cells = <2>;
    ranges = <0x02000000 0x0 0x40000000 0x0 0x40000000 0x0 0x40000000>;
    // 中断映射
    interrupt-map-mask = <0xff00 0x0 0x0 0x7>;
    interrupt-map = <0x0000 0x0 0x0 0x1 &gic 0x0 0x0 0x0 0x7e 0x4>,
                    <0x0000 0x0 0x0 0x2 &gic 0x0 0x0 0x0 0x7f 0x4>;
    // MSI控制器
    msi-parent = <&its>;
    status = "okay";
};
设备树属性 含义 驱动获取方式
reg PCIe控制器寄存器基址和配置空间地址 platform_get_resource()
bus-range 此控制器管理的PCI总线号范围 内核PCI核心自动解析
ranges PCI地址空间到CPU地址空间的映射 内核PCI核心自动建立
interrupt-map PCI INTx中断到中断控制器的路由 of_irq_parse_pci()
msi-parent 指向MSI中断控制器(如GIC ITS) msi_domain_of_msi_parent()
常见陷阱:设备树中msi-parent属性缺失或指向错误,会导致MSI中断无法分配。在调试时检查/proc/interrupts确认中断类型是否为PCI-MSI。

4.4 ACPI与PCIe枚举

在x86和部分ARM64服务器平台,PCIe设备枚举依赖ACPI(高级配置与电源管理接口)。ACPI提供_SB_.PCI0命名空间下的设备描述,包括MMIO资源、中断路由表(PRT)和MSI控制器信息。

// ACPI命名空间中的PCIe设备描述(ASL语言片段)
Device(PCI0) {
    Name(_HID, EISAID("PNP0A08"))   // PCI Express Root Bridge
    Name(_CID, EISAID("PNP0A03"))   // PCI Root Bridge兼容
    Name(_ADR, 0x00000000)
    // 中断路由表
    Name(_PRT, Package() {
        Package() { 0x0000FFFF, 0x00, 0x00, 0x10 },  // INTA -> IRQ 16
        Package() { 0x0000FFFF, 0x01, 0x00, 0x11 },  // INTB -> IRQ 17
        Package() { 0x0000FFFF, 0x02, 0x00, 0x12 },  // INTC -> IRQ 18
        Package() { 0x0000FFFF, 0x03, 0x00, 0x13 }   // INTD -> IRQ 19
    })
    // MSI控制器信息
    Name(_DSD, Package() {
        ToUUID("daffd814-6eba-4d8c-8a91-bc9bbf4aa301"),
        Package() {
            Package() { "msi-parent", Package() { \_SB_.ITS0 } }
        }
    })
}
设备树 vs ACPI:PCIe枚举路径对比 设备树(Device Tree)路径 of_pci_scan_bus() 扫描设备树节点 pci_scan_bus() 枚举PCI总线 of_irq_parse_pci() 解析中断映射 msi_domain_of_msi_parent() 获取MSI域 ACPI路径 acpi_pci_root_add() 解析ACPI命名空间 pci_acpi_scan_root() 扫描PCI根桥 acpi_pci_irq_disable() / enable() 处理INTx acpi_msi_domain_alloc_irqs() 分配MSI 对比 对比 对比 对比

4.5 驱动绑定完整示例

以下是一个完整的PCIe MSI中断驱动绑定示例,展示pci_driver注册、probe中分配MSI中断、remove中清理的完整流程。

#include <linux/module.h>
#include <linux/pci.h>
#include <linux/interrupt.h>

#define DRV_NAME "pcie_msi_sample"
#define VENDOR_ID 0x10EE  // 示例Vendor ID
#define DEVICE_ID 0x7011  // 示例Device ID

static struct pci_device_id sample_ids[] = {
    { PCI_DEVICE(VENDOR_ID, DEVICE_ID) },
    { 0, }
};
MODULE_DEVICE_TABLE(pci, sample_ids);

static irqreturn_t sample_msi_handler(int irq, void *dev_id)
{
    struct pci_dev *pdev = (struct pci_dev *)dev_id;
    // 读取中断状态寄存器,处理中断事件
    u32 status = ioread32(pci_iomap(pdev, 0, 0) + 0x1000);
    // ... 中断处理逻辑 ...
    return IRQ_HANDLED;
}

static int sample_probe(struct pci_dev *pdev, const struct pci_device_id *id)
{
    int ret;
    int msirq;

    // 1. 使能PCI设备
    ret = pci_enable_device(pdev);
    if (ret)
        return ret;

    // 2. 请求BAR0内存区域
    ret = pci_request_region(pdev, 0, DRV_NAME);
    if (ret)
        goto err_disable;

    // 3. 设置DMA掩码(64位)
    ret = dma_set_mask_and_coherent(&pdev->dev, DMA_BIT_MASK(64));
    if (ret)
        goto err_release;

    // 4. 分配MSI中断向量(请求1个MSI)
    ret = pci_alloc_irq_vectors(pdev, 1, 1, PCI_IRQ_MSI);
    if (ret < 0)
        goto err_release;

    msirq = pci_irq_vector(pdev, 0);

    // 5. 注册中断处理函数
    ret = request_irq(msirq, sample_msi_handler, 0, DRV_NAME, pdev);
    if (ret)
        goto err_free_vectors;

    // 6. 保存设备私有数据
    pci_set_drvdata(pdev, pci_iomap(pdev, 0, 0));

    dev_info(&pdev->dev, "probe success, MSI IRQ=%d\n", msirq);
    return 0;

err_free_vectors:
    pci_free_irq_vectors(pdev);
err_release:
    pci_release_region(pdev, 0);
err_disable:
    pci_disable_device(pdev);
    return ret;
}

static void sample_remove(struct pci_dev *pdev)
{
    int msirq = pci_irq_vector(pdev, 0);

    // 1. 释放中断
    free_irq(msirq, pdev);

    // 2. 释放MSI向量
    pci_free_irq_vectors(pdev);

    // 3. 释放IO映射
    pci_iounmap(pdev, pci_get_drvdata(pdev));

    // 4. 释放PCI资源
    pci_release_region(pdev, 0);

    // 5. 禁用设备
    pci_disable_device(pdev);

    dev_info(&pdev->dev, "remove complete\n");
}

static struct pci_driver sample_driver = {
    .name     = DRV_NAME,
    .id_table = sample_ids,
    .probe    = sample_probe,
    .remove   = sample_remove,
};

module_pci_driver(sample_driver);

MODULE_LICENSE("GPL");
MODULE_AUTHOR("PCIe MSI Course");
MODULE_DESCRIPTION("PCIe MSI Interrupt Driver Sample");
调试技巧:使用modprobe pcie_msi_sample加载驱动后,检查/sys/bus/pci/drivers/pcie_msi_sample/目录下是否出现设备符号链接。使用lspci -vvv -s 01:00.0查看设备是否已绑定驱动,以及MSI中断是否启用。

4.6 设备树与ACPI的驱动适配策略

驱动开发者需要根据平台类型选择正确的资源获取方式。以下表格总结了两种平台下的关键差异:

操作 设备树平台 ACPI平台
获取设备资源 platform_get_resource() acpi_dev_resource()
中断解析 of_irq_parse_pci() acpi_pci_irq_lookup()
MSI域获取 msi_domain_of_msi_parent() acpi_msi_domain_get()
电源管理 pm_runtime_of() acpi_device_set_power()
设备匹配 of_match_table acpi_match_table
跨平台兼容方案:使用device_property_read_*()系列API(如device_property_read_u32())替代直接调用OF或ACPI专用函数。这些API在底层自动判断当前平台并调用对应的实现,实现驱动代码的跨平台统一。

PCIe设备枚举与驱动绑定是MSI中断驱动开发的基础。理解pci_driver结构体的每个字段、掌握probe/remove的正确资源管理顺序、熟悉设备树与ACPI的差异,是编写稳定、高效PCIe驱动的必备技能。下一阶段将深入MSI中断的配置与传输优化。