3. PCIe设备枚举与发现:Linux PCI子系统初始化、pci_device_id匹配机制、probe()与remove()回调函数实现
PCIe设备的枚举与发现是驱动开发的基础环节。本章深入剖析Linux PCI子系统的初始化流程、设备ID匹配机制以及核心回调函数的实现模式,帮助开发者建立从硬件拓扑到驱动加载的完整认知链路。
3.1 Linux PCI子系统初始化流程
系统启动时,PCI子系统经历四个关键阶段完成初始化:
- ACPI/DT解析阶段:固件通过ACPI表或设备树描述PCIe主桥(Root Complex)的地址空间与中断映射。
- PCI总线扫描阶段:内核调用
pci_scan_bus()递归遍历总线拓扑,为每个设备分配BDF(Bus:Device.Function)编号。 - 资源分配阶段:
pci_assign_unassigned_resources()为设备分配MMIO、I/O端口及总线号。 - 设备注册阶段:每个PCI设备被封装为
struct pci_dev,挂入内核设备模型。
关键数据结构:
struct pci_bus 代表一条PCI总线,struct pci_dev 代表一个功能设备,两者通过bus_list和dev_list双向链表组织。
3.2 pci_device_id 匹配机制
当PCI设备注册到内核后,驱动核心根据pci_device_id表进行匹配。该表定义了驱动支持的设备集合,匹配字段包括:
| 字段 | 说明 | 匹配规则 |
|---|---|---|
vendor |
厂商ID(如0x8086为Intel) | 精确匹配 |
device |
设备ID(如0x10fb为82599ES) | 精确匹配 |
subvendor |
子系统厂商ID | 可设为PCI_ANY_ID通配 |
subdevice |
子系统设备ID | 可设为PCI_ANY_ID通配 |
class |
设备类别(如0x020000为以太网) | 支持掩码匹配 |
class_mask |
类别掩码 | 与class按位与后比较 |
driver_data |
驱动私有数据指针 | 匹配成功后传递给probe() |
最佳实践:优先使用vendor+device精确匹配,避免仅依赖class匹配导致驱动被错误加载到不兼容设备上。
匹配流程在内核函数pci_match_device()中实现,其核心逻辑如下:
static const struct pci_device_id *pci_match_device(
struct pci_driver *drv, struct pci_dev *dev)
{
const struct pci_device_id *id;
/* 遍历驱动支持的ID表 */
for (id = drv->id_table; id->vendor || id->subvendor ||
id->class_mask; id++) {
/* 检查vendor匹配 */
if ((id->vendor != PCI_ANY_ID) &&
(id->vendor != dev->vendor))
continue;
/* 检查device匹配 */
if ((id->device != PCI_ANY_ID) &&
(id->device != dev->device))
continue;
/* 检查subvendor/subdevice */
if ((id->subvendor != PCI_ANY_ID) &&
(id->subvendor != dev->subsystem_vendor))
continue;
if ((id->subdevice != PCI_ANY_ID) &&
(id->subdevice != dev->subsystem_device))
continue;
/* 检查class掩码匹配 */
if ((id->class_mask & id->class) !=
(id->class_mask & dev->class))
continue;
return id; /* 匹配成功 */
}
return NULL; /* 无匹配 */
}
3.3 probe() 回调函数实现
当pci_device_id匹配成功后,内核调用驱动的probe()函数。该函数是驱动初始化的核心入口,典型实现包含以下步骤:
- 启用设备:调用
pci_enable_device()使能PCI配置空间中的I/O和内存空间位。 - 请求资源:使用
pci_request_regions()声明设备的BAR空间所有权。 - 设置DMA掩码:通过
dma_set_mask()和dma_set_coherent_mask()配置设备DMA能力。 - 映射BAR空间:使用
pci_iomap()将BAR物理地址映射为内核虚拟地址。 - 初始化硬件:配置设备寄存器、中断、队列等硬件资源。
- 注册设备接口:如网络设备调用
register_netdev(),字符设备调用cdev_add()。
常见错误:probe()中未检查
pci_enable_device()返回值,导致设备未正确使能时继续执行后续操作,引发系统崩溃或内存访问异常。
以下是一个完整的probe()实现模板:
static int my_pci_probe(struct pci_dev *pdev,
const struct pci_device_id *id)
{
struct my_adapter *adapter;
int ret;
/* 1. 分配私有数据结构 */
adapter = kzalloc(sizeof(*adapter), GFP_KERNEL);
if (!adapter)
return -ENOMEM;
pci_set_drvdata(pdev, adapter);
/* 2. 启用PCI设备 */
ret = pci_enable_device(pdev);
if (ret) {
dev_err(&pdev->dev, "pci_enable_device failed: %d\n", ret);
goto err_free;
}
/* 3. 请求BAR资源 */
ret = pci_request_regions(pdev, DRV_NAME);
if (ret) {
dev_err(&pdev->dev, "pci_request_regions failed: %d\n", ret);
goto err_disable;
}
/* 4. 设置DMA掩码(64位优先,回退到32位) */
ret = dma_set_mask(&pdev->dev, DMA_BIT_MASK(64));
if (ret) {
ret = dma_set_mask(&pdev->dev, DMA_BIT_MASK(32));
if (ret) {
dev_err(&pdev->dev, "dma_set_mask failed: %d\n", ret);
goto err_regions;
}
}
/* 5. 映射BAR0 */
adapter->bar0 = pci_iomap(pdev, 0, 0);
if (!adapter->bar0) {
dev_err(&pdev->dev, "pci_iomap BAR0 failed\n");
ret = -ENOMEM;
goto err_regions;
}
/* 6. 读取设备配置信息 */
pci_read_config_word(pdev, PCI_STATUS, &adapter->status);
/* 7. 设置总线主控(DMA使能) */
pci_set_master(pdev);
/* 8. 注册网络设备(示例) */
adapter->netdev = alloc_etherdev(sizeof(struct my_priv));
if (!adapter->netdev) {
ret = -ENOMEM;
goto err_iounmap;
}
ret = register_netdev(adapter->netdev);
if (ret) {
dev_err(&pdev->dev, "register_netdev failed: %d\n", ret);
goto err_free_netdev;
}
dev_info(&pdev->dev, "device %04x:%04x probed successfully\n",
pdev->vendor, pdev->device);
return 0;
err_free_netdev:
free_netdev(adapter->netdev);
err_iounmap:
pci_iounmap(pdev, adapter->bar0);
err_regions:
pci_release_regions(pdev);
err_disable:
pci_disable_device(pdev);
err_free:
kfree(adapter);
return ret;
}
3.4 remove() 回调函数实现
remove()函数是probe()的逆操作,负责释放所有已分配的资源。其执行顺序必须与probe()严格相反:
| 步骤 | 操作 | 对应probe()步骤 |
|---|---|---|
| 1 | 注销设备接口(如unregister_netdev) | 8 |
| 2 | 释放网络设备(free_netdev) | 8 |
| 3 | 取消BAR映射(pci_iounmap) | 5 |
| 4 | 释放BAR资源(pci_release_regions) | 3 |
| 5 | 禁用PCI设备(pci_disable_device) | 2 |
| 6 | 释放私有数据(kfree) | 1 |
static void my_pci_remove(struct pci_dev *pdev)
{
struct my_adapter *adapter = pci_get_drvdata(pdev);
if (!adapter)
return;
/* 1. 注销网络设备 */
if (adapter->netdev) {
unregister_netdev(adapter->netdev);
free_netdev(adapter->netdev);
}
/* 2. 取消BAR映射 */
if (adapter->bar0)
pci_iounmap(pdev, adapter->bar0);
/* 3. 释放资源 */
pci_release_regions(pdev);
pci_disable_device(pdev);
/* 4. 释放私有数据 */
kfree(adapter);
pci_set_drvdata(pdev, NULL);
dev_info(&pdev->dev, "device removed\n");
}
关键原则:remove()必须能够处理probe()执行到一半失败的情况。例如,如果probe()在注册网络设备之前失败,remove()应检查
adapter->netdev是否为NULL再决定是否调用unregister_netdev()。
3.5 实践要点与常见陷阱
- ID表必须终止:
pci_device_id数组必须以全零条目结束,否则内核遍历时会越界。 - probe()失败回滚:使用goto标签实现分层错误处理,确保已分配的资源全部释放。
- 并发安全:probe()和remove()可能在不同CPU上并发执行,需使用锁保护共享数据结构。
- 热插拔支持:现代PCIe支持热插拔,remove()必须能够随时被调用,不能假设设备始终存在。
- 调试手段:在probe()入口和remove()出口添加
dev_dbg()日志,配合pci_info()输出设备BDF信息。
调试技巧:使用
echo 1 > /sys/bus/pci/devices/0000:XX:XX.X/remove手动触发remove(),验证资源释放是否完整。再通过echo 1 > /sys/bus/pci/rescan重新枚举设备,测试probe()的健壮性。