4. Linux内核PCI子系统
PCI 子系统是 Linux 内核中连接物理 PCI/PCIe 设备与设备驱动程序的桥梁。理解 pci_dev 结构体、驱动注册流程以及 probe/remove 回调机制,是进行 PCIe 驱动移植与多队列配置的基础。本章将深入剖析这三个核心要素。
4.1 pci_dev 结构体:设备的软件抽象
在 Linux 内核中,每一个物理 PCI 或 PCIe 设备(或功能)都由一个 struct pci_dev 实例表示。该结构体包含了设备的所有硬件属性、资源映射以及状态信息,是驱动与硬件交互的入口。
核心字段解析: 驱动在
probe 回调中通过 pci_dev 指针获取设备资源,包括 BAR 地址、中断号、设备 ID 等。
| 字段 | 类型 | 说明 |
|---|---|---|
dev |
struct device |
通用设备模型基类,用于电源管理、sysfs 等。 |
vendor / device |
unsigned int |
厂商 ID 和设备 ID,用于驱动匹配。 |
subsystem_vendor / subsystem_device |
unsigned int |
子系统厂商和设备 ID,用于更精确的匹配。 |
class |
unsigned int |
设备类别代码(如网卡、存储控制器)。 |
irq |
unsigned int |
传统 INTx 中断号。对于 MSI/MSI-X,需通过 pci_irq_vector() 获取。 |
resource[] |
struct resource |
BAR 空间资源数组,最多 6 个(PCIe 最多 6 个 BAR)。 |
msi_enabled / msix_enabled |
int |
标志位,指示 MSI 或 MSI-X 是否已启用。 |
driver |
struct pci_driver * |
指向当前绑定到此设备的驱动实例。 |
移植要点: 在 MSI-X 多队列场景下,驱动应优先使用
pci_irq_vector() 而非 pci_dev->irq,因为后者仅代表传统中断线。
4.2 pci_driver 注册流程
每个 PCIe 设备驱动都需要定义一个 struct pci_driver 实例,并通过 pci_register_driver() 向内核注册。注册后,内核 PCI 核心层会遍历所有未绑定的设备,根据 ID 表进行匹配,匹配成功后调用驱动的 probe 回调。
4.2.1 pci_driver 结构体定义
static struct pci_driver my_pci_driver = {
.name = "my_pcie_device",
.id_table = my_pci_ids, // 支持的设备 ID 列表
.probe = my_pci_probe, // 设备发现回调
.remove = my_pci_remove, // 设备移除回调
.shutdown = my_pci_shutdown, // 系统关机回调(可选)
.driver = {
.pm = &my_pci_pm_ops, // 电源管理操作(可选)
},
};
4.2.2 注册与注销 API
| 函数 | 说明 | 调用时机 |
|---|---|---|
pci_register_driver() |
注册驱动到 PCI 子系统,触发设备匹配与 probe。 | 模块初始化入口(module_init) |
pci_unregister_driver() |
注销驱动,触发所有已绑定设备的 remove 回调。 | 模块卸载入口(module_exit) |
常见错误: 忘记在
id_table 中包含正确的 vendor 和 device ID,或者 ID 表以空条目结尾,会导致驱动永远无法匹配到设备。
4.3 probe 与 remove 回调
probe 和 remove 是驱动生命周期中最重要的两个回调函数,分别负责设备的初始化和清理工作。
4.3.1 probe 回调:设备初始化
当 PCI 核心层找到一个匹配的设备时,会调用驱动的 probe 函数。驱动在此函数中完成以下典型步骤:
- 启用设备: 调用
pci_enable_device()开启设备电源和 I/O 访问。 - 设置 DMA 掩码: 调用
dma_set_mask()和dma_set_coherent_mask()配置设备可访问的地址范围。 - 获取 BAR 资源: 使用
pci_resource_start()/pci_resource_len()获取 BAR 物理地址和长度,并通过ioremap()映射到内核虚拟地址空间。 - 分配中断: 调用
pci_alloc_irq_vectors()请求 MSI-X 或 MSI 中断向量(详见后续章节)。 - 初始化硬件: 通过映射后的寄存器地址配置设备内部状态。
- 注册设备: 将设备注册到相应的内核子系统(如网络、存储、字符设备)。
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 掩码(64 位寻址示例)
ret = dma_set_mask(&pdev->dev, DMA_BIT_MASK(64));
if (ret) {
ret = dma_set_mask(&pdev->dev, DMA_BIT_MASK(32));
if (ret)
goto err_disable;
}
// 3. 分配驱动私有数据结构
dev = kzalloc(sizeof(*dev), GFP_KERNEL);
if (!dev) {
ret = -ENOMEM;
goto err_disable;
}
pci_set_drvdata(pdev, dev);
// 4. 映射 BAR0
dev->bar0_phys = pci_resource_start(pdev, 0);
dev->bar0_len = pci_resource_len(pdev, 0);
dev->bar0_virt = ioremap(dev->bar0_phys, dev->bar0_len);
if (!dev->bar0_virt) {
ret = -ENOMEM;
goto err_free;
}
// 5. 分配 MSI-X 中断向量(后续章节详解)
// ...
// 6. 注册字符设备 / 网络设备等
// ...
return 0;
err_free:
kfree(dev);
err_disable:
pci_disable_device(pdev);
return ret;
}
4.3.2 remove 回调:设备清理
remove 回调在设备被拔出、驱动卸载或系统关机时调用。它必须逆序释放 probe 中分配的所有资源:
- 注销设备(字符设备、网络设备等)。
- 释放中断向量(
pci_free_irq_vectors())。 - 取消 I/O 映射(
iounmap())。 - 释放私有数据结构(
kfree())。 - 禁用 PCI 设备(
pci_disable_device())。
static void my_pci_remove(struct pci_dev *pdev)
{
struct my_dev *dev = pci_get_drvdata(pdev);
// 1. 注销设备
// device_destroy(...);
// class_destroy(...);
// 2. 释放中断
pci_free_irq_vectors(pdev);
// 3. 取消 I/O 映射
iounmap(dev->bar0_virt);
// 4. 释放私有数据
kfree(dev);
// 5. 禁用 PCI 设备
pci_disable_device(pdev);
}
最佳实践: 在
probe 中使用 goto 错误处理模式,确保任何一步失败时都能正确回滚已分配的资源。remove 中的清理顺序应与 probe 完全相反。
4.4 本章小结
- pci_dev 是内核中 PCI 设备的软件抽象,驱动通过它获取 BAR 资源、中断号和设备标识。
- pci_driver 通过
pci_register_driver()注册,内核根据id_table自动匹配设备并调用probe。 - probe 回调负责设备初始化,包括启用设备、设置 DMA、映射 BAR、分配中断和注册子系统。
- remove 回调负责逆序清理所有资源,确保设备拔出或驱动卸载时系统状态一致。