设备已经出现在 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
设备先注册或驱动先注册,都可以触发匹配。总线负责匹配规则: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 指针,随后继续解引用,就可能访问已经释放的驱动数据。
下面只计注册者与一个外部持有者的引用,不复刻内核内部所有临时引用。状态模型把注册、绑定、私有资源和对象释放分开,便于观察哪个保证随哪一步结束。
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 将资源挂到设备上,探测失败或解绑时通常按逆序清理。它能减少错误分支,却不会自动解决所有异步活动:
- 阻止新请求进入。
- 按硬件协议停止 DMA、关闭中断源。
- 等待或取消正在运行的工作、回调及请求。
- 才能释放这些路径仍会访问的资源。
等待操作不能持有完成回调需要的锁。managed 与 unmanaged API 可以配合,但必须清楚每个资源由谁释放,避免手工释放后再次自动释放。可参阅设备生命周期与 devres 文档。
先取得调用者引用,再解除驱动绑定或注销设备。观察 device 对象还活着时,哪些驱动资源已经失效;引用计数不等于设备可访问性的开关。
正在呈现知识画面
设备对象仍存活,managed 资源也还在吗?device 的引用寿命长于驱动绑定寿命是合法状态。get_device 保住对象内存,不保住已解绑的 devres 或已经离线的硬件。
{"id":"device-binding-lifetimes","title":"设备对象仍存活,managed 资源也还在吗?","summary":"device 的引用寿命长于驱动绑定寿命是合法状态。get_device 保住对象内存,不保住已解绑的 devres 或已经离线的硬件。","height":1000,"html":"<h2 data-i18n=\"heading\"></h2><p class=\"intro\" data-i18n=\"intro\"></p><div class=\"actions\"><button id=\"get\" data-i18n=\"get\"></button><button id=\"put\" data-i18n=\"put\"></button></div><div class=\"actions\"><button id=\"unbind\" data-i18n=\"unbind\"></button><button id=\"bind\" data-i18n=\"bind\"></button></div><div class=\"actions\"><button id=\"unregister\" data-i18n=\"unregister\"></button></div><div class=\"track-row\"><span data-i18n=\"refs\"></span><div id=\"refs\" class=\"track\"></div></div><div class=\"track-row\"><span data-i18n=\"binding\"></span><div id=\"binding\" class=\"track\"></div></div><div class=\"metrics\"><div><span data-i18n=\"object\"></span><strong id=\"object\"></strong></div><div><span data-i18n=\"count\"></span><strong id=\"count\"></strong></div></div><div class=\"actions\"><button id=\"access\" data-i18n=\"access\"></button></div><div class=\"card\"><header data-i18n=\"result\"></header><div id=\"result\" class=\"readout\"></div></div><div class=\"copy\"><p id=\"explanation\" role=\"status\"></p><p class=\"reserve\" aria-hidden=\"true\" data-i18n=\"note\"></p></div><div class=\"actions\"><button id=\"reset\" data-i18n=\"reset\"></button></div><details class=\"scope\"><summary data-i18n=\"scopeLabel\"></summary><p data-i18n=\"scope\"></p></details>","css":".intro{margin:8px 0 18px;color:var(--muted);font-size:14px}.caption,.hint{font-size:12px;color:var(--muted);margin-top:12px}.control{display:grid;grid-template-columns:minmax(0,1fr) auto;gap:6px 12px;align-items:center;margin:14px 0;font-size:13px}.control label{font-size:13px}.control output{color:var(--accent);min-width:5em;text-align:right;font-variant-numeric:tabular-nums}.control input{grid-column:1/-1;width:100%;margin:0}.presets{display:flex;gap:4px;align-items:stretch}.presets button{flex:1 1 0;min-width:0;overflow-wrap:anywhere;font-size:13px}.actions{display:flex;gap:8px;flex-wrap:wrap;margin:14px 0}.actions button{flex:1;min-width:0;font-size:13px;min-height:42px}.copy{display:grid;font-size:14px;margin:16px 0}.copy>*{grid-area:1/1;margin:0}.reserve{visibility:hidden}.scope{margin-top:14px;color:var(--muted);font-size:12px}.scope summary{padding:8px 0;cursor:pointer}.scope p{margin-top:10px}.two{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:12px;margin:16px 0}.card{border:1px solid var(--rule);border-radius:9px;padding:14px;min-width:0}.card header{font-size:14px;font-weight:600;margin-bottom:10px}.card button{font-size:12px;width:100%;min-width:0}.card small{display:block;font-size:12px;color:var(--muted)}.card code{font-size:12px;overflow-wrap:anywhere}.metrics{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:12px;margin:16px 0}.metrics>div{border-left:2px solid var(--accent);padding-left:10px;min-width:0}.metrics span{display:block;min-height:3.2em;color:var(--muted);font-size:12px}.metrics strong{font-size:23px;display:block;font-weight:500;overflow-wrap:anywhere;font-variant-numeric:tabular-nums}.formula{border-top:1px solid var(--rule);padding-top:12px;margin-top:14px;font-size:13px;font-variant-numeric:tabular-nums;overflow-wrap:anywhere}.value{font:500 20px var(--viz-font-ui)}.legend{display:flex;flex-wrap:wrap;gap:6px 14px;font-size:12px;color:var(--muted);margin:12px 0}.legend span:before{content:'';display:inline-block;width:8px;height:8px;border-radius:2px;background:var(--accent);margin-right:6px}.legend .second:before{background:var(--second)}.check{display:flex;align-items:center;gap:8px;font-size:13px;margin:12px 0}.check input{width:16px;height:16px;flex-shrink:0;accent-color:var(--accent)}select,input[type=text],input[type=number]{background:var(--paper);color:var(--ink);font:inherit;font-size:13px;padding:10px;border:1px solid var(--rule);border-radius:7px;max-width:100%;min-width:0}select{width:100%;min-height:42px}.band{height:20px;display:flex;background:var(--surface);border-radius:4px;overflow:hidden}.band i{height:100%;display:block;background:var(--accent);transition:width .25s}.band i.second{background:var(--second)}button:disabled{cursor:default}.status{font-size:13px;min-height:4.8em}.mono{font-family:var(--viz-font-mono);font-variant-numeric:tabular-nums}.muted{color:var(--muted)}\n@media(max-width:480px){.two{gap:8px}.card{padding:12px 9px}.actions button,.presets button{font-size:12px}.metrics strong{font-size:20px}.metrics{gap:8px}}\n@media(prefers-reduced-motion:reduce){*,*::before,*::after{transition:none!important;animation:none!important}}\n.track-row{margin:16px 0}.track-row>span{display:block;font-size:12px;color:var(--muted);margin-bottom:7px}.track{display:flex;gap:5px;min-width:0}.cell{flex:1;min-width:0;min-height:48px;border:1px solid var(--rule);border-radius:5px;background:var(--surface);display:flex;align-items:center;justify-content:center;text-align:center;padding:5px 3px;font-size:12px;overflow-wrap:anywhere;font-variant-numeric:tabular-nums}.cell.on{background:var(--accent-soft);border-color:var(--accent)}.cell.gold{background:var(--second-soft);border-color:var(--second)}.cell.empty{border-style:dashed;color:var(--muted)}.cell.error{border-color:var(--second);text-decoration:line-through}.choices{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:6px;margin:14px 0}.choices button{min-width:0;padding:10px 4px;font-size:12px;overflow-wrap:anywhere}.choices button[aria-current=true]{border-color:var(--accent);background:var(--accent-soft)}.relation{display:grid;grid-template-columns:minmax(0,1fr) 28px minmax(0,1fr);align-items:center;gap:6px;margin:12px 0}.relation .cell{min-height:72px}.relation>i{text-align:center;font-style:normal;color:var(--accent)}.readout{font-size:13px;white-space:pre-line;min-height:6.4em;overflow-wrap:anywhere}.plot{position:relative;border-left:1px solid var(--rule);border-bottom:1px solid var(--rule);padding:8px 0;margin:18px 0}.plot-row{height:28px;position:relative;margin:4px 0;background:var(--surface)}.plot-row i{position:absolute;inset-block:5px;background:var(--accent);min-width:2px;transition:width .2s,left .2s}.plot-row b{position:relative;padding:4px 6px;font:11px/28px var(--viz-font-ui);background:var(--paper);border-radius:3px}.axis{display:flex;justify-content:space-between;font-size:11px;color:var(--muted)}\n\n.readout{min-height:5em}","js":"const $=s=>document.querySelector(s);const set=(id,v)=>$('#'+id).textContent=v;const t=k=>viz.t(k);function cells(id,values){$('#'+id).replaceChildren(...values.map(v=>{const e=document.createElement('div');e.className='cell '+(v.cls||'');e.textContent=v.text;e.title=v.title||v.text;return e;}));}function pressed(mode){document.querySelectorAll('[data-mode]').forEach(e=>e.setAttribute('aria-pressed',String(e.dataset.mode===mode)));}let registered=true,bound=true,extra=0,result='pending';function draw(){const count=Number(registered)+extra,alive=count>0;cells('refs',[{text:'registry '+Number(registered)+' + caller '+extra+' → device',cls:alive?'on':'empty'}]);cells('binding',[{text:bound?'driver → MMIO / buffer':'driver × MMIO / buffer',cls:bound?'gold':'empty'}]);set('object',t(alive?'yes':'no'));set('count',count);$('#get').disabled=!alive||extra===4;$('#put').disabled=extra===0;$('#unbind').disabled=!bound;$('#bind').disabled=!registered||bound;$('#unregister').disabled=!registered;set('result',t(result));set('explanation',t('note'));}$('#get').onclick=()=>{extra++;draw();};$('#put').onclick=()=>{extra--;draw();};$('#unbind').onclick=()=>{bound=false;draw();};$('#bind').onclick=()=>{bound=true;draw();};$('#unregister').onclick=()=>{registered=bound=false;draw();};$('#access').onclick=()=>{result=bound?'allowed':'denied';draw();};$('#reset').onclick=()=>{registered=bound=true;extra=0;result='pending';draw();};draw();","audio":false,"strings":{"access":"检查寄存器是否可访问","allowed":"绑定仍有效,可按设备协议访问","bind":"重新绑定","binding":"驱动绑定 → managed 资源","count":"模型引用总数","denied":"不能访问:对象引用不保住绑定资源","get":"取得额外引用","heading":"设备对象仍存活,managed 资源也还在吗?","intro":"取得额外 device 引用,然后解绑驱动或撤销注册。比较注册引用、调用者引用、绑定资源和最后一次访问检查。","no":"已释放","note":"先 get,再 unbind:对象仍存活,但资源已释放。接着 unregister,最后 put:这一次才消失的是设备对象本身。","object":"对象是否仍存活","pending":"尚未检查","put":"交还一份额外引用","refs":"引用 → device 对象","reset":"重置","result":"最近访问检查","scope":"一个已注册且绑定的设备,注册者持一份引用,额外引用最多四份。unbind 已正确停止硬件/异步工作并释放 managed 资源;unregister 包含解绑及交还注册引用。省略核心内部临时引用和硬件热插拔,不执行真实设备操作。","scopeLabel":"模型范围与约定","unbind":"解绑驱动","unregister":"撤销设备注册","yes":"存活"}}
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 链接缺失只说明当前没有绑定,原因还需结合匹配表、模块、探测日志与设备状态分析。
继续阅读