4. 设备与驱动注册机制

Linux 设备模型的核心思想是“用统一的数据结构描述硬件与驱动,并让总线负责撮合”。在 2.6 到 6.x 的演进中,device、driver、bus、class、subsystem 这五个对象始终存在,但注册入口、匹配时机、生命周期管理和 sysfs 呈现方式发生了显著变化。理解它们之间的注册顺序与匹配规则,是跨版本适配的第一道门槛。

核心关系:bus 是匹配的“裁判”,device 与 driver 是参赛双方,class 负责向用户空间暴露统一接口,subsystem 则是更高层的组织容器。注册的本质是把对象挂到对应链表,并触发一次或多次匹配尝试。

一、五个核心对象的职责与注册入口

在旧版本(2.6 早期)中,驱动常直接调用 device_register()、driver_register(),并手动填充 bus_type 的 match() 回调。新版本则普遍使用 device_initialize() + device_add() 的两段式注册,以及 module_platform_driver()、module_i2c_driver() 等宏来简化驱动注册。下表对比了五个对象的关键注册接口与跨版本差异。

对象 核心结构 典型注册接口 跨版本差异要点
device struct device device_register() / device_add() 新版本推荐先 device_initialize() 再 device_add(),便于错误回滚
driver struct device_driver driver_register() 新版本大量使用 module_*_driver() 宏,自动处理 init/exit
bus struct bus_type bus_register() match() 从手写字符串比较演进为 of_match_table、acpi_match_table 多源匹配
class struct class class_register() / class_create() class_create() 参数从 (owner, name) 简化为 (name)
subsystem struct subsys_private 由 bus_register() 内部创建 sysfs 中 /sys/bus/、/sys/class/ 的符号链接布局基本稳定

二、注册流程与匹配规则

设备注册的典型路径是:分配 struct device → 设置 parent、bus、release 回调 → device_initialize() → device_add()。在 device_add() 内部,内核会把设备加入 bus 的设备链表,并调用 bus_probe_device(),进而遍历该总线上已注册的驱动,逐一调用 bus->match()。匹配成功后进入 really_probe(),依次执行 probe()、绑定 sysfs 链接、触发 uevent。

驱动注册则相反:driver_register() 先把驱动加入 bus 的驱动链表,再反向遍历该总线上尚未绑定的设备,尝试匹配。因此“先注册设备还是先注册驱动”并不影响最终结果,但会影响 probe 的先后顺序,这在依赖时序的系统中非常关键。

/* 新版本推荐的设备注册片段 */
struct device *dev = kzalloc(sizeof(*dev), GFP_KERNEL);
dev->bus = &my_bus_type;
dev->parent = &pdev->dev;
dev->release = my_dev_release;
device_initialize(dev);
ret = device_add(dev);
if (ret)
    put_device(dev);

/* 驱动侧使用宏简化注册 */
static struct platform_driver my_drv = {
    .probe = my_probe,
    .remove = my_remove,
    .driver = {
        .name = "my-device",
        .of_match_table = my_of_match,
    },
};
module_platform_driver(my_drv);

三、匹配规则的三条主线

  • 名称匹配:最传统的方式,比较 device->init_name 或 dev_name() 与 driver->name。platform、i2c、spi 等总线早期都依赖它。
  • 设备树匹配:通过 of_match_table 中的 compatible 字符串与 DTS 节点比对,是现代 ARM/ARM64 平台的主流方式。
  • ACPI 匹配:通过 acpi_match_table 中的 _HID/_CID 匹配,常见于 x86 服务器与部分嵌入式平台。

跨版本适配时最常见的坑是:旧驱动只实现了 .name 匹配,升级到新内核后 DTS 中只有 compatible,导致 probe 永远不触发。此时需要补上 of_match_table,并确保 compatible 字符串与驱动表完全一致。

device / driver 注册与 bus 匹配主线 device_initialize() 初始化 kobject / 引用计数 device_add() 加入 bus 设备链表 bus_probe_device() 遍历驱动链表尝试匹配 driver_register() 加入 bus 驱动链表 bus_for_each_dev() 反向遍历设备链表 driver_attach() 触发匹配与 probe bus->match() 1. name 比较 2. of_match_table compatible 3. acpi_match_table _HID really_probe() probe() → sysfs 链接 → uevent 匹配失败:保持未绑定状态 等待后续 device/driver 注册再次触发 class_create() /sys/class/xxx 用户接口 subsystem /sys/bus/xxx 组织容器

四、跨版本差异与适配案例

以 platform 驱动为例,2.6.32 时代常见写法是直接填充 platform_driver 并在模块 init 中调用 platform_driver_register();而 5.x 之后推荐使用 module_platform_driver() 宏,并配合 of_device_id 表。若把旧驱动直接移植到新内核,可能出现以下现象:

# 旧内核日志
platform my-device: registered
my_driver my-device: probed

# 新内核日志(缺少 of_match_table)
platform my-device: not probed
# 原因:DTS 只有 compatible,驱动只有 .name,bus->match() 返回 0
常见误区:认为“注册了 device 和 driver 就一定会 probe”。实际上 probe 依赖 bus->match() 的返回值,且受 driver_override、deferred_probe、依赖资源未就绪等因素影响。跨版本时优先检查匹配表,而不是怀疑 probe 函数本身。

五、复盘与适配检查清单

  1. 确认设备与驱动是否挂在同一个 bus_type 上,bus 指针不能为空。
  2. 检查 of_match_table、acpi_match_table、.name 三者是否至少有一条能命中。
  3. 确认 device_add() 与 driver_register() 的调用顺序不会导致依赖资源缺失。
  4. 查看 /sys/bus/<bus>/devices/ 与 /sys/bus/<bus>/drivers/ 下的符号链接,判断绑定是否成功。
  5. 使用 dmesg | grep -i "probe\|match\|deferred" 定位匹配失败或延迟探测原因。