设备模型

本页目录

设备已经出现在 sysfs,驱动却未绑定;驱动解绑后,设备对象还可能被引用。阅读设备模型,需要沿发现、匹配、功能注册和最终释放分别定位。

查阅目标阅读位置
对象与 probe第 1~2 节
引用与 managed 资源第 3 节
用户态视图与只读实验第 4~5 节

1. 四个对象分别解决什么问题

对象关心的问题例子
device这是哪个设备,父设备是谁,有哪些资源?一项 PCI function、一个 USB interface
device_driver哪个实现能够操作它?PCI 网卡驱动、USB 串口驱动
bus_type如何发现、匹配并组织设备与驱动?PCI、USB、platform
class用户从什么功能角度查找设备?net、block、tty

总线与 class 是不同视角。PCI 网卡首先是 PCI 总线上的设备,其网络接口又通过网络子系统提供功能对象;不能把 /sys/class/net/eth0 当成 PCI 设备对象的简单改名。一个物理设备也可能暴露多个功能对象。

kobject 是更底层的命名、层级和引用计数基础,device 内嵌它。kset 组织相关 kobject,并参与事件管理。它们不是一套替代总线模型的驱动接口。通常应使用已有子系统提供的 device API,而不是为驱动自行搭建 kobject 树。

内核结构体会随版本变化;阅读 include/linux/device.h、include/linux/kobject.h 时关注成员职责即可。kobject.sd 指向 kernfs 节点,并不是 VFS 的 dentry;对象最终释放依赖 release 回调及其容器管理,引用计数归零不会自动猜出该如何释放外层结构。

2. 从硬件信息到 probe

发现、绑定与释放是三个不同阶段 设备注册 枚举或固件描述 总线匹配 ID / compatible / HID 驱动注册 内建或模块加载 probe:取得资源并初始化 依赖未就绪可延迟探测 设备可供子系统使用 remove:停止并解绑 最后引用释放对象 解绑不等于立即释放;sysfs 可见也不等于设备已经可用。

设备先注册或驱动先注册,都可以触发匹配。总线负责匹配规则:PCI 常用 vendor/device/class,USB 可以按设备或接口描述符匹配,platform 驱动可通过固件匹配表等途径匹配。

match 只表示候选驱动可能支持设备;probe 还要取得时钟、供电、寄存器、中断等资源,初始化硬件,并向功能子系统注册。任一步失败,都不能留下仍在运行的半初始化设备。

如果依赖的 regulator、clock 或其他 supplier 尚未就绪,可以返回 -EPROBE_DEFER,由核心在后续条件满足时重试。它表示依赖暂缺,不能用来掩盖永久不支持的硬件或错误参数。设备链接还可表达 supplier/consumer 关系,影响探测与电源管理顺序。

延迟探测是在重试初始化,不是在延续半次初始化

设传感器 consumer 已枚举,但它引用的供电 regulator 驱动尚未完成注册。第一次 probe 可能已经建立私有上下文,再在获取 regulator 时得到 -EPROBE_DEFER。这次 probe 必须退出并清理当前尝试的资源;后续依赖条件变化而再次探测时,应从一致的未绑定状态重新开始。

时刻consumer 状态本次尝试的资源
已枚举,尚未探测device 已存在,未绑定没有成功驱动实例
第一次 probe 获取 supplier 失败返回 EPROBE_DEFER,仍未完成绑定回收本次 managed 资源,手工资源也要处理
supplier 后来就绪可触发后续探测路径不能假设之前的私有缓冲仍有效
后续 probe 成功完成驱动绑定及功能注册新一次尝试的资源保持到解绑

因此重复出现 probe 日志可能是在重试依赖,不必然是设备被枚举了多次。反过来,把错误的固件参数或不支持的设备统一改写成 EPROBE_DEFER,会使真实错误看起来像永远缺少依赖。驱动应保留资源 API 的错误含义,并结合设备链接、延迟原因与原始返回值定位;不能靠增加固定 sleep 时间让初始化顺序偶然成立。

固件描述与总线枚举如何配合

  • Device Tree:描述地址、中断、时钟和设备连接等信息,compatible 表示兼容接口。常见于 ARM、RISC-V,但不是这些架构的专属格式。
  • ACPI:提供设备身份、资源及电源管理方法等;x86 常见,arm64 服务器也使用 ACPI。
  • PCI/USB 枚举⁠:通过总线协议发现设备和读取标识。PCI 根桥、配置空间访问方式、中断路由等仍可能依赖 ACPI 或 DT,不能概括为“自枚举就完全不需要固件描述”。

platform bus 常用于不能通过这类总线协议自行枚举的片上设备。platform device 是设备模型中的真实对象,不意味着硬件是模拟的;DT 节点也不一定都变成 platform device,节点的所在总线决定实例化方式。

3. 对象寿命与驱动绑定寿命

get_device() / put_device() 管理设备引用。device_unregister() 撤销注册并释放注册者的引用;若还有引用,对象可继续存在,到最后一次 put 才进入最终释放。

边界结束的是什么
remove驱动服务与绑定关系
release设备对象自身的内存寿命
仍有引用仅说明对象内存可继续存在

引用不会让已经拔掉的硬件重新在线,也不保证寄存器还可以访问。

device_register() 包含初始化和添加步骤。即使注册失败,调用方通常也需要 put_device() 交还初始化得到的引用,不能随手 kfree() 一个已经交给设备模型管理的对象。具体容器的分配和 release 规则应遵循所属子系统。

保住 struct device,不等于保住 driver 私有数据

设注册者持有一个设备引用,异步使用者再获得一个引用。驱动解绑时,devm 私有内存可以被释放,但异步使用者持有的 device 引用仍让设备对象本身存活。若异步使用者保存了旧的 drvdata 指针,随后继续解引用,就可能访问已经释放的驱动数据。

引用保护设备对象,不能延长已解绑驱动资源的寿命 绑定中 解绑完成 撤销注册 最后一次 put 引用 2 私有数据有效 引用 2 私有数据已释放 引用 1 对象仍存在 引用 0 执行 release 旧 drvdata 指针不会因为仍持有 device 引用而继续有效 示意只计注册者与外部持有者;真实内核可能还有其他临时引用。

下面只计注册者与一个外部持有者的引用,不复刻内核内部所有临时引用。状态模型把注册、绑定、私有资源和对象释放分开,便于观察哪个保证随哪一步结束。

class Device:
    def __init__(self):
        self.refs = 1  # 初始化所得引用,由注册者负责交还
        self.registered = False
        self.bound = False
        self.private_live = False
        self.releases = 0

    def get_from_live_reference(self):
        if self.refs == 0:
            raise RuntimeError("不能复活已经释放的对象")
        self.refs += 1

    def put(self):
        assert self.refs > 0
        self.refs -= 1
        if self.refs == 0:
            assert not self.registered and not self.bound
            self.releases += 1

    def add_and_bind(self):
        assert self.refs > 0 and not self.registered
        self.registered = self.bound = self.private_live = True

    def unbind(self):
        assert self.bound
        self.bound = False
        self.private_live = False

    def unregister(self):
        assert self.registered
        if self.bound:
            self.unbind()
        self.registered = False
        self.put()

    def io(self):
        if not self.bound or not self.private_live:
            raise RuntimeError("对象引用不代表驱动仍可提供服务")
        return "ok"

dev = Device()
dev.add_and_bind()
dev.get_from_live_reference()
assert dev.refs == 2 and dev.io() == "ok"
dev.unbind()
assert dev.registered and dev.refs == 2 and not dev.private_live
try:
    dev.io()
except RuntimeError:
    pass
else:
    raise AssertionError("解绑后不能依靠引用继续访问驱动")
dev.unregister()
assert dev.refs == 1 and dev.releases == 0
dev.put()  # 最后一个外部引用归还
assert dev.refs == 0 and dev.releases == 1
try:
    dev.get_from_live_reference()
except RuntimeError:
    pass
else:
    raise AssertionError("已经结束的生命周期不能被 get 重新开启")

failed_add = Device()  # 已初始化,但添加步骤失败
failed_add.put()
assert failed_add.releases == 1

真实 get_device() 只是引用操作,不会替调用者从一个可能已失效的裸指针安全找回对象。首次取得引用需要依赖受锁保护的查找、已有引用或所属子系统规定的获取协议。已经获得的引用保护对象内存寿命,也不替代解绑同步或私有状态自己的引用规则。

本地内核实现可沿 drivers/base/core.c 的 device_unregister 看到先 device_del、后 put_device;解绑清理则在 drivers/base/dd.c 中包含 devres_release_all 和清除 drvdata。两条路径的责任不同,驱动设计应明确异步使用者到底持有哪一层对象。设备对象生命周期、devres 释放语义

devres 管理的是资源释放顺序

devm_kzalloc() 等 managed API 将资源挂到设备上,探测失败或解绑时通常按逆序清理。它能减少错误分支,却不会自动解决所有异步活动:

  1. 阻止新请求进入。
  2. 按硬件协议停止 DMA、关闭中断源。
  3. 等待或取消正在运行的工作、回调及请求。
  4. 才能释放这些路径仍会访问的资源。

等待操作不能持有完成回调需要的锁。managed 与 unmanaged API 可以配合,但必须清楚每个资源由谁释放,避免手工释放后再次自动释放。可参阅设备生命周期与 devres 文档。

先取得调用者引用,再解除驱动绑定或注销设备。观察 device 对象还活着时,哪些驱动资源已经失效;引用计数不等于设备可访问性的开关。

正在呈现知识画面
设备对象仍存活,managed 资源也还在吗?

device 的引用寿命长于驱动绑定寿命是合法状态。get_device 保住对象内存,不保住已解绑的 devres 或已经离线的硬件。

4. sysfs、devtmpfs、udev 的分工

/sys/devices 展示设备层级;/sys/bus/.../devices 和 /sys/class/... 提供其他查找视角,经常包含符号链接。sysfs 文件是内核导出的属性接口,不是普通磁盘文件,也不是每个文件都对应 device_attribute。

设备属性的 show 常用 sysfs_emit() 输出,store 可用 kstrto*() 解析输入。属性仍需正确的锁、权限和设备状态检查;稳定导出后还要考虑用户态 ABI,不能随意改变单位和含义。

对具有设备号且使用相应机制注册的设备,devtmpfs 可以由内核创建基本设备节点⁠。udev 接收 uevent、读取属性,再依据规则设置权限、创建稳定符号链接,并可根据 modalias 触发模块加载。内建驱动不需要加载模块,而且很多 device 根本没有 /dev 节点,例如网络接口通常通过 socket 和 netlink 使用。

看不到预期设备时下一层证据
是否发现总线枚举与 device 对象
是否绑定driver 关联、匹配与 probe 结果
是否可提供功能子系统功能对象的注册
名称/访问是否正确用户态命名、权限与规则

仅缺少某个 /dev 名字,不能直接证明 probe 未执行。

5. 只读查看一项 PCI 设备

下面仅读取 sysfs,不解绑设备、不修改资源。热拔插可能使文件在遍历期间消失,因此处理了读取异常。

from pathlib import Path

for dev in sorted(Path('/sys/bus/pci/devices').glob('*'))[:8]:
    try:
        vendor = (dev / 'vendor').read_text().strip()
        product = (dev / 'device').read_text().strip()
        link = dev / 'driver'
        driver = link.resolve(strict=True).name if link.is_symlink() else '(unbound)'
        print(dev.name, vendor, product, driver)
    except OSError as exc:
        print(dev.name, type(exc).__name__)

例如名称 0000:03:00.0 包含 PCI domain、bus、device、function。driver 链接缺失只说明当前没有绑定,原因还需结合匹配表、模块、探测日志与设备状态分析。

继续阅读