3. PCI设备驱动框架:pci_driver结构体、probe/remove回调、设备ID表、模块化注册
PCI(Peripheral Component Interconnect)设备驱动是Linux网络设备驱动中最典型的实现路径。无论是千兆以太网卡还是万兆光纤适配器,其驱动核心都围绕一个统一的框架展开:pci_driver。本章将拆解这个框架的四个关键要素:驱动结构体、探测/移除回调、设备ID匹配表以及模块化注册流程。
3.1 pci_driver结构体:驱动的身份证
在Linux内核中,每个PCI驱动都必须实例化一个struct pci_driver。这个结构体定义了驱动的名称、支持的设备列表、生命周期回调以及电源管理接口。它是内核与驱动之间的标准接口。
// 定义于 include/linux/pci.h
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);
// ... 其他高级回调(shutdown、sriov_configure等)
};
| 字段 | 类型 | 作用 |
|---|---|---|
| name | const char * | 驱动名称,在 /sys/bus/pci/drivers/ 下可见 |
| id_table | const struct pci_device_id * | 指向设备ID数组,内核据此判断驱动能否管理某设备 |
| probe | 回调函数指针 | 设备匹配成功后调用,完成硬件初始化、资源分配、netdev注册 |
| remove | 回调函数指针 | 设备移除或驱动卸载时调用,释放probe分配的所有资源 |
| suspend/resume | 回调函数指针 | 电源管理,支持系统休眠/唤醒(网络驱动通常必须实现) |
3.2 probe/remove回调:驱动的初始化和善后
probe 是PCI驱动中最重要的函数。当内核发现一个PCI设备的Vendor ID和Device ID与驱动的id_table匹配时,就会调用probe。probe的职责是“让设备准备好被使用”。
remove 是probe的逆操作。当设备被物理移除、驱动被卸载或系统进入低功耗状态时,内核调用remove。remove必须释放probe分配的所有资源,否则会导致内存泄漏或内核崩溃。
probe 典型执行步骤
- 启用PCI设备: 调用
pci_enable_device(),激活设备上的I/O和内存空间。 - 请求资源: 调用
pci_request_regions(),声明对BAR空间的独占访问。 - 设置DMA掩码: 调用
dma_set_mask()和dma_set_coherent_mask(),告知设备可用地址范围。 - 映射BAR空间: 使用
pci_iomap()将设备寄存器映射到内核虚拟地址空间。 - 初始化私有数据结构: 分配
net_device和私有数据块,设置硬件操作函数集netdev_ops。 - 注册网络设备: 调用
register_netdev(),使设备对网络层可见。
static int my_pci_probe(struct pci_dev *pdev, const struct pci_device_id *id)
{
struct net_device *netdev;
struct my_adapter *adapter;
int ret;
// 1. 启用PCI设备
ret = pci_enable_device(pdev);
if (ret)
return ret;
// 2. 请求I/O和内存区域
ret = pci_request_regions(pdev, DRV_NAME);
if (ret)
goto err_disable;
// 3. 设置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)
goto err_release;
}
// 4. 分配net_device + 私有数据
netdev = alloc_etherdev(sizeof(struct my_adapter));
if (!netdev) {
ret = -ENOMEM;
goto err_release;
}
adapter = netdev_priv(netdev);
adapter->pdev = pdev;
adapter->netdev = netdev;
// 5. 映射BAR0寄存器空间
adapter->hw_addr = pci_iomap(pdev, 0, 0);
if (!adapter->hw_addr) {
ret = -EIO;
goto err_free_netdev;
}
// 6. 设置netdev操作函数
netdev->netdev_ops = &my_netdev_ops;
netdev->ethtool_ops = &my_ethtool_ops;
// 7. 注册网络设备
ret = register_netdev(netdev);
if (ret)
goto err_iounmap;
// 保存pci_drvdata便于remove访问
pci_set_drvdata(pdev, netdev);
return 0;
err_iounmap:
pci_iounmap(pdev, adapter->hw_addr);
err_free_netdev:
free_netdev(netdev);
err_release:
pci_release_regions(pdev);
err_disable:
pci_disable_device(pdev);
return ret;
}
remove 回调实现
static void my_pci_remove(struct pci_dev *pdev)
{
struct net_device *netdev = pci_get_drvdata(pdev);
struct my_adapter *adapter = netdev_priv(netdev);
// 1. 注销网络设备(必须在释放资源之前)
unregister_netdev(netdev);
// 2. 取消映射寄存器
pci_iounmap(pdev, adapter->hw_addr);
// 3. 释放net_device
free_netdev(netdev);
// 4. 释放PCI资源
pci_release_regions(pdev);
// 5. 禁用PCI设备
pci_disable_device(pdev);
}
3.3 设备ID表:驱动与硬件的匹配规则
设备ID表是驱动声明“我能管理哪些硬件”的清单。内核PCI核心在枚举总线时,会读取每个PCI设备的Vendor ID、Device ID等信息,然后遍历所有已注册驱动的id_table,寻找匹配项。
// 定义于 include/linux/mod_devicetable.h
struct pci_device_id {
__u32 vendor, device; // 厂商ID和设备ID
__u32 subvendor, subdevice; // 子系统厂商ID和设备ID(可设为PCI_ANY_ID)
__u32 class, class_mask; // 设备类别和掩码
unsigned long driver_data; // 驱动私有数据,probe时通过id->driver_data获取
};
| 匹配模式 | vendor | device | subvendor | subdevice | 说明 |
|---|---|---|---|---|---|
| 精确匹配 | 0x8086 | 0x10D3 | 0x8086 | 0x0001 | 仅匹配特定厂商的特定子型号 |
| 通配子系统 | 0x8086 | 0x10D3 | PCI_ANY_ID | PCI_ANY_ID | 匹配所有使用该芯片的设备 |
| 类别匹配 | PCI_ANY_ID | PCI_ANY_ID | PCI_ANY_ID | PCI_ANY_ID | 配合class字段匹配网络控制器类别 |
// 实际驱动中的ID表定义示例
#define PCI_VENDOR_ID_MYVENDOR 0x1234
#define PCI_DEVICE_ID_MYCHIP_A 0x5678
#define PCI_DEVICE_ID_MYCHIP_B 0x5679
static const struct pci_device_id my_pci_tbl[] = {
{ PCI_VDEVICE(MYVENDOR, PCI_DEVICE_ID_MYCHIP_A), .driver_data = CHIP_A },
{ PCI_VDEVICE(MYVENDOR, PCI_DEVICE_ID_MYCHIP_B), .driver_data = CHIP_B },
{ 0, } // 必须以空条目结束
};
MODULE_DEVICE_TABLE(pci, my_pci_tbl); // 导出给用户空间热插拔工具
PCI_VDEVICE 宏可以自动填充vendor字段,减少重复代码。driver_data字段用于区分同一驱动支持的不同硬件版本,probe中通过 id->driver_data 获取。
3.4 模块化注册:pci_register_driver / pci_unregister_driver
驱动模块的入口和出口函数负责向内核注册和注销pci_driver。这是模块化编程的标准模式,通常放在模块的init和exit函数中。
static struct pci_driver my_pci_driver = {
.name = DRV_NAME,
.id_table = my_pci_tbl,
.probe = my_pci_probe,
.remove = my_pci_remove,
// 电源管理回调(可选但推荐)
.driver.pm = &my_pm_ops,
};
static int __init my_driver_init(void)
{
int ret;
pr_info("Loading %s PCI driver\n", DRV_NAME);
// 注册PCI驱动
ret = pci_register_driver(&my_pci_driver);
if (ret)
pr_err("Failed to register PCI driver: %d\n", ret);
return ret;
}
static void __exit my_driver_exit(void)
{
pr_info("Unloading %s PCI driver\n", DRV_NAME);
pci_unregister_driver(&my_pci_driver);
}
module_init(my_driver_init);
module_exit(my_driver_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("Your Name");
MODULE_DESCRIPTION("My PCI Ethernet Driver");
pci_register_driver() 是同步调用。在函数返回之前,内核会对所有已枚举的PCI设备进行ID匹配,并对匹配成功的设备立即调用probe。因此,probe回调中的所有依赖(如其他子系统、固件加载)必须在init函数中提前准备好。
3.5 本章小结
PCI设备驱动框架是网络驱动开发的基石。理解pci_driver结构体的每个字段、probe/remove的对称资源管理、设备ID表的匹配机制以及模块化注册的时序,是编写稳定、可移植网络驱动的第一步。在下一章中,我们将深入probe内部,详细讲解如何初始化PCI设备、分配DMA缓冲区以及注册net_device。
| 要素 | 核心API/宏 | 注意事项 |
|---|---|---|
| 驱动结构体 | struct pci_driver | name字段必须唯一,避免与现有驱动冲突 |
| 设备ID表 | struct pci_device_id + MODULE_DEVICE_TABLE | 必须以空条目 {0,} 结尾;使用PCI_ANY_ID通配 |
| probe回调 | pci_enable_device / pci_request_regions / register_netdev | 每一步失败都必须回滚;使用goto错误处理链 |
| remove回调 | unregister_netdev / pci_iounmap / pci_release_regions | 执行顺序与probe相反;确保所有DMA操作已停止 |
| 模块注册 | pci_register_driver / pci_unregister_driver | 注册是同步的;probe可能在注册函数内被调用 |