4. Linux设备模型与驱动框架

在PCIe驱动开发中,真正决定“驱动能不能被调用、什么时候被调用、以什么顺序被调用”的,并不是驱动代码本身,而是Linux设备模型。设备模型把硬件拓扑抽象成总线(bus)、设备(device)、驱动(driver)、类(class)四类核心对象,并通过sysfs、uevent和匹配机制把它们连接成一个可枚举、可热插拔、可自动加载的统一框架。理解这套框架,是写出可维护PCIe驱动的前提。

一、核心对象与它们的关系

Linux设备模型本质上是一组用kobject/kobject_type构建的对象树。对PCIe而言,最常打交道的对象如下:

  • 总线(bus_type):代表一类连接方式,如pci_bus_type。它维护两条链表:设备链表和驱动链表,并负责在设备或驱动加入时触发匹配。
  • 设备(device):描述一个具体硬件实例,如struct pci_dev。它带有vendor/device ID、BAR、中断号等资源信息。
  • 驱动(device_driver):描述“我能驱动哪些设备”,如struct pci_driver。它提供id_table和probe/remove回调。
  • 类(class):从功能视角对设备分组,如pci_bus、net、block。类决定设备在/dev下如何呈现,与总线视角互补。

总线是“连接关系”,设备是“被连接者”,驱动是“操作者”,类是“功能归类”。四者通过kobject挂到sysfs树上,形成用户空间可见的层次结构。

Linux设备模型核心对象与匹配关系 总线 bus_type pci_bus_type 设备 device struct pci_dev vendor / device / BAR 驱动 device_driver struct pci_driver id_table / probe / remove 匹配 match() id_table 比对 → probe() 类 class net / block / pci_bus sysfs /sys/bus/pci/... uevent KOBJ_ADD / KOBJ_REMOVE

二、匹配机制:驱动如何被“选中”

当一个新的PCIe设备被枚举出来,或者一个新的pci_driver被注册时,总线都会调用bus_match()。对PCI总线而言,匹配的核心是pci_match_device(),它把设备的vendor/device ID与驱动的id_table逐项比对。匹配成功后,内核调用驱动的probe(),此时驱动才真正“接管”设备。

static const struct pci_device_id my_pcie_ids[] = {
    { PCI_DEVICE(0x1234, 0x5678) },
    { PCI_DEVICE_CLASS(0x020000, ~0) },  /* 以太网类 */
    { 0, }
};
MODULE_DEVICE_TABLE(pci, my_pcie_ids);

static struct pci_driver my_pcie_driver = {
    .name     = "my_pcie",
    .id_table = my_pcie_ids,
    .probe    = my_pcie_probe,
    .remove   = my_pcie_remove,
};
module_pci_driver(my_pcie_driver);

匹配顺序上,内核优先使用id_table精确匹配;若驱动未提供id_table,则可能退化为名称匹配。对PCIe热插拔设备,匹配发生在设备加入之后,因此probe必须能处理“设备已存在但资源尚未映射”的状态。

三、sysfs与uevent:用户空间的入口

sysfs是设备模型在用户空间的投影。每个设备、驱动、总线都会在/sys下生成目录和属性文件。例如一块PCIe网卡会出现在:

/sys/bus/pci/devices/0000:03:00.0/
├── vendor
├── device
├── class
├── resource0
├── driver -> ../../../../bus/pci/drivers/my_pcie
└── uevent

其中driver符号链接指向当前绑定的驱动,resource0暴露BAR空间,uevent文件允许用户空间主动触发事件。uevent是内核向用户空间广播“设备状态变化”的机制,常见动作包括add、remove、change。udev/mdev监听这些事件,完成设备节点创建、权限设置和模块加载。

调试时可用udevadm monitor --kernel --property实时观察uevent,用udevadm info -a -p /sys/bus/pci/devices/0000:03:00.0查看设备属性链。

四、案例:从设备枚举到probe的完整链路

以一块PCIe采集卡为例,上电后的典型流程如下:

  1. PCIe枚举器扫描配置空间,发现设备,创建pci_dev并注册到pci_bus_type。
  2. 总线遍历驱动链表,调用pci_match_device(),发现my_pcie驱动的id_table命中。
  3. 内核调用my_pcie_probe(),驱动执行pci_enable_device()、pci_request_regions()、pci_iomap()、request_irq()。
  4. 驱动通过device_create()或cdev_add()向用户空间暴露接口,并在sysfs下生成属性文件。
  5. 内核发出KOBJ_ADD uevent,udev创建/dev/my_pcie0节点。

移除设备时,顺序相反:先发出KOBJ_REMOVE,再调用remove()释放资源,最后销毁设备对象。任何一步顺序错误,都可能导致资源泄漏或悬空指针。

五、常见误区与复盘

误区现象正确做法
在probe中直接访问BAR而不先enable 读取返回0xFFFFFFFF,驱动异常 先pci_enable_device(),再pci_request_regions()
忽略remove路径的资源释放 热插拔后内存/中断泄漏 remove中严格逆序释放iomap、irq、regions
把class和bus混为一谈 设备节点创建位置错误 bus管匹配,class管功能呈现,二者独立
uevent未处理导致节点缺失 /dev下无设备节点 确认udev规则或手动mknod验证
PCIe驱动中,probe可能被延迟或重试。不要在probe中假设设备一定可用;应使用dev_err()输出带设备名的日志,便于通过dmesg定位匹配失败原因。

掌握设备模型后,再看PCIe驱动代码,会发现probe/remove只是“被框架调用的回调”,真正决定调用时机的是总线匹配、sysfs暴露和uevent广播。把这三者串起来,才能写出既符合内核规范、又便于用户空间集成的PCIe驱动。