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树上,形成用户空间可见的层次结构。
二、匹配机制:驱动如何被“选中”
当一个新的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采集卡为例,上电后的典型流程如下:
- PCIe枚举器扫描配置空间,发现设备,创建
pci_dev并注册到pci_bus_type。 - 总线遍历驱动链表,调用
pci_match_device(),发现my_pcie驱动的id_table命中。 - 内核调用
my_pcie_probe(),驱动执行pci_enable_device()、pci_request_regions()、pci_iomap()、request_irq()。 - 驱动通过
device_create()或cdev_add()向用户空间暴露接口,并在sysfs下生成属性文件。 - 内核发出
KOBJ_ADDuevent,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验证 |
dev_err()输出带设备名的日志,便于通过dmesg定位匹配失败原因。掌握设备模型后,再看PCIe驱动代码,会发现probe/remove只是“被框架调用的回调”,真正决定调用时机的是总线匹配、sysfs暴露和uevent广播。把这三者串起来,才能写出既符合内核规范、又便于用户空间集成的PCIe驱动。