多彩编程 多彩编程MZPH · CODE BLOG
ARTICLE DETAIL

文章详情

深耕前端与后端开发技术的一线实战笔记与踩坑复盘。

深度解析bus_register:Linux设备模型总线上户口与sysfs目录构建

深度解析bus_register:Linux设备模型总线上户口与sysfs目录构建 1. bus_register是什么内核驱动模型的基石我得先说说为什么啃这块代码。Linux内核里的驱动模型Driver Model是整个设备管理的中枢它把总线bus、设备device、驱动driver这三者用一套统一的框架组织起来而bus_register就是给一套设备总线“上户口”的那个入口函数。无论是 PCI、USB、I2C、SPI还是你自己写的一个虚拟总线只要想在 Linux 内核里被设备模型管理起来都绕不开这个函数。打个比方如果把设备模型比作一个小区的物业管理系统那bus_register就是为新开辟的一条街道总线办理登记手续。登记之后这条街道上才能有门牌号设备、住户驱动、以及物业协调规则match 和 probe 机制。没有完成这个登记驱动和设备之间即便近在咫尺内核也不知道它们俩是“一家人”。研究bus_register的意义在于它是理解整个 Linux 设备模型穿透力最强的一个切入点。它本身不长但背后牵涉到 kobject、kset、sysfs、uevent、attribute、probe 时序等一系列核心机制。我当年是顺着bus_register - subsys_register - kset_register - kobject_add_internal这一条线把设备模型相关的代码通读了一遍读完之后再看driver_register和device_register基本就是降维打击。这篇文章适合有这么几类朋友正在看drivers/base/bus.c但被结构体指针绕晕想找一个能串起来的线索的同学。自己写了一个虚拟总线或者平台设备驱动想搞清楚总线和驱动、设备之间的绑定到底是怎么发生的。内核调试遇到“设备挂不上驱动”或者“sysfs 目录不生成”问题想从根上找原因的。我会带着你从函数入口一路走到 sysfs 目录和 uevent 事件把每一步背后的设计意图都拆开揉碎。代码以目前主流内核版本6.x为参考老版本4.x/5.x的差异我会在关键处单独标注。2. 从参数检查到 sysfs 目录完整流程拆解bus_register的函数原型长这样int bus_register(struct bus_type *bus)整个函数体其实不算特别长但逻辑密度很高。我们按照它执行的顺序一点一点过。2.1 入口处的合法性检查不要小看这几行函数开头非常朴素就是一个名字检查int ret; ret bus_type_find(bus); if (ret) return ret;我在阅读 6.x 内核源码时注意到新版本引入bus_type_find作为前置检查而在早期的 4.x、5.x 内核里这段代码是下面这样的retval kobject_set_name(priv-subsys.kobj, %s, bus-name); if (retval) goto out;看起来不起眼其实这里藏着第一个大坑总线的名字必须是唯一的。bus_type_find会在全局的总线列表中查找同名的总线如果已经存在函数直接返回-EEXIST。这是合理的因为总线名字会映射为 sysfs 下的/sys/bus/xxx目录名一个目录显然不可能对应两条总线。提示如果你在调试时发现bus_register返回了-EEXIST不用怀疑别的先检查是不是同一段驱动代码被加载了两次或者你的总线名跟内核现有总线重名了。像是platform、pci、usb这类名字是注册子系统时用掉的别去撞车。2.2 私有数据结构的分配一切基础设施的起点继续往下走有一个极其重要的操作——分配私有数据bus-p kzalloc(sizeof(struct subsys_private), GFP_KERNEL); if (!bus-p) return -ENOMEM;这个struct subsys_private是所有总线内部状态的聚合体。很多刚接触内核源码人最大的困惑在于为什么bus_type结构体本身看起来那么“干净”里面没有链表头、没有锁答案是这些脏活累活全部放在了bus-pprivate指向的subsys_private里了。从设计角度讲bus_type是面向驱动编写者的公共接口保持结构的稳定和精简而bus-p是设备模型内部用来管理状态的地方不需要对外暴露。这就像你去办业务只需要面对柜台而后台的审批流转、档案存储都在你看不到的办公室里完成。struct subsys_private的内容很关键我拣重点列一下struct subsys_private { struct kset subsys; // 总线对应的 kset struct kset *devices_kset; // 挂在这条总线上的所有设备集合 struct kset *drivers_kset; // 挂在这条总线上的所有驱动集合 struct klist klist_devices; // 设备链表 struct klist klist_drivers; // 驱动链表 struct blocking_notifier_head bus_notifier; // 总线事件通知链 struct bus_type *bus; // 回溯指针 ... };看到这里你应该明白了总线、设备、驱动之间就是通过kset和klist这两套机制建立联系的。kset负责组织 sysfs 目录和 kobject 的层次关系klist负责在内存中维护一个可以被遍历、增删的链接列表。2.3 初始化 kset 和 kobject内核对象模型登场私有数据分配完之后进入核心的初始化区域priv-devices_kset kset_create_and_add(%s, NULL, priv-subsys.kobj); if (!priv-devices_kset) goto err_devices_kset; priv-drivers_kset kset_create_and_add(drivers, NULL, priv-subsys.kobj); if (!priv-drivers_kset) goto err_drivers_kset;这里有一个我见过无数人栽跟头的细节devices目录的创建函数传的是%s格式也就是把总线名当作目录名而drivers目录直接写死了字符串drivers。这就解释了为什么你在/sys/bus/xxx下面看到的目录结构永远是/sys/bus/xxx/ ├── devices/ └── drivers/看清楚是devices复数和drivers复数这两个目录名是固定的。至于devices目录的%s展开之后其实就是/sys/bus/xxx/devices因为它的父 kobject 就是总线的 kobj。你仔细品一下这一层是总线名、下一层是固定的devices和drivers这就是 sysfs 对设备模型的直接呈现。实操心得很多人自己在写总线驱动时会纠结要不要在/sys/bus/xxx下面手动再创建一个目录来放自定义属性。其实完全没必要。kset_create_and_add建出来的devices_kset和drivers_kset会自动挂在总线的 kobj 下面你只需要在这两个 kset 上添加属性sysfs 的层级关系就自然成立了。2.4 总线属性与 uevent 处理函数总线本身的属性注册集中在几行代码里retval bus_create_file(bus, bus_attr_uevent); if (retval) goto err_uevent; retval bus_add_groups(bus, bus-bus_groups); if (retval) goto err_bus_groups;bus_create_file做的事情很直接——在/sys/bus/xxx目录下创建一个属性文件。其中bus_attr_uevent对应的就是在drivers/base/bus.c里定义的那个uevent_show回调。看到这里你不妨做一个实验在系统里随便找一个已经注册好的总线cat /sys/bus/pci/uevent你会看到输出列出了一些环境变量比如PCI_CLASS...、PCI_ID...、PCI_SLOT_NAME...。这个文件配合uevent内核事件构成了 udev/mdev 用户空间工具感知设备插拔的桥梁。当设备状态变化时内核通过kobject_uevent把环境变量组发送到用户空间用户空间的 udev 守护进程依据这些变量来创建设备节点、加载固件或者触发其他自定义规则。2.5 总线通知链的注册设备模型的事件广播机制接下来bus_register还有一个容易被忽略但非常重要的操作blocking_notifier_chain_register(bus-p-bus_notifier, bus-bus_notifier);这里注册了一个bus_notifier。它有什么用设备模型里的很多动作比如“设备即将添加”“设备已经添加”“驱动即将解绑”等等都会触发bus_notifier的调用。如果某个子系统需要感知总线发生的事件就可以在这里挂一个回调函数。举个例子你在研究 PCI 子系统时会发现PCI 核心利用这个机制来做一些热插拔相关的处理。而在你自己的总线上也可以利用这个 notifier 来感知设备或驱动的动态变化。注意如果你准备在总线注册之后通过bus_notifier接收所有设备或驱动的动态变化务必记得注销否则在总线模块卸载时会出现“notifier 链表已损坏”的警告严重时会导致内核 panic。我实际见过一个团队在模块卸载路径里漏了blocking_notifier_chain_unregister一卸载就崩溃排查了很久才发现是这个问题。到这里bus_register的主要执行内容就走完了。接下来我们看注册完成后sysfs 目录到底长什么样。3. sysfs 目录与属性组背后的内核对象模型从复杂程度来说bus_register的 sysfs 相关操作可能是整个函数最绕的部分。我们需要先补充一点背景知识kobject 是设备模型的最小单元kset 是 kobject 的集合。两者的配合决定了 sysfs 目录树的结构。3.1 kset_register 到 kobject_add_internal 的执行链路bus_register内部创建devices_kset和drivers_kset时会调用kset_create_and_add。这个函数内部会创建一个struct kset。初始化 kset 内部的 kobject。调用kobject_add把 kobject 挂到 parent kobject 下。kobject_add是理解 sysfs 的关键入口它的内部逻辑可以浓缩为kobject_add_varg(kobj, parent, fmt, vargs);这里parent参数非常关键它决定了新创建的目录挂在哪个目录下面。在bus_register里devices_kset的 parent 是priv-subsys.kobj而priv-subsys.kobj的 parent 又是总线 kset全局的bus_kset。于是目录层级就顺理成章地变成了/sys/bus/ └── xxx/ ├── devices/ └── drivers/这里的设计逻辑值得停下来想一想为什么devices_kset的创建要在总线的 kset 已经注册好之后答案很朴素——父目录不存在子目录无法创建。/sys/bus/xxx/devices的前提是/sys/bus/xxx已经存在。内核代码的执行顺序必须保证这一点。3.2 bus_groups、dev_groups、drv_groups 三者各自作用接着看bus_add_groups它在总线目录下批量添加属性文件。int bus_add_groups(struct bus_type *bus, const struct attribute_group **groups) { return sysfs_create_groups(bus-p-subsys.kobj, groups); }这里要澄清一个很常见的误区bus-bus_groups、bus-dev_groups、bus-drv_groups这三个属性组的作用范围完全不同。bus_groups创建在/sys/bus/xxx/下面属于总线自身的属性例如uevent。dev_groups创建在/sys/bus/xxx/devices/某个设备/下面属于每一个挂在这条总线上的设备。内核在device_add时遍历该总线的dev_groups将其附加到设备目录下。drv_groups创建在/sys/bus/xxx/drivers/某个驱动/下面属于每个驱动目录。我在不少实际项目里看到有人把dev_groups误写在bus_groups里结果属性出现在总线目录而不是设备目录找得一头雾水。搞清楚这三个组的生命周期和挂载点排查这类 sysfs 属性位置问题时就能少走很多弯路。3.3 从 sysfs 看总线生命周期如果你在开发一个自定义总线的驱动模块模块加载成功后你可以直接在终端里验证bus_register的成果# 假设你的总线名叫 mybus ls -l /sys/bus/mybus/ ls -l /sys/bus/mybus/devices/ ls -l /sys/bus/mybus/drivers/ cat /sys/bus/mybus/uevent正常输出中/sys/bus/mybus/devices和/sys/bus/mybus/drivers默认是空的因为还没有任何设备或驱动挂载上来。uevent文件默认为空也正常因为总线本身不需要像设备那样上报环境变量除非你在总线的uevent_show回调中实现了自定义逻辑。实操心得判断bus_register是否执行成功除了检查函数返回值最快的办法就是看/sys/bus/下面有没有你预期的目录。我在调试一个自定义总线时模块 load 后目录一直不出现最后加了几个printk才发现是bus_type_find返回了-EEXIST——原来同名总线早已存在只是我加打印之前没注意到返回值。4. 总线注册后与设备和驱动如何产生联动bus_register只是给总线“开了张”。真正让它运转起来的是后续device_register和driver_register与它产生的交互。这环节牵涉到match、probe等机制。4.1 device_register 时如何找上总线当一个设备要注册时内核会调用device_add。这个函数里有一个关键步骤bus device_get_bus(dev); if (bus) { ... error bus_add_device(dev); ... }device_get_bus的逻辑是如果设备的dev-bus指针已经指定则直接用它否则尝试通过platform等其他途径获取。拿到之后bus_add_device会将设备的 kobject 添加到总线的devices_kset中从而出现在/sys/bus/xxx/devices/下。这里还有一个细节设备属性目录dev_groups添加的时机也在device_add流程里而总线的dev_groups会被当成总线的“赠品”附加到每个设备目录中。内核是用下面的逻辑处理的if (dev-bus dev-bus-dev_groups) { error sysfs_create_groups(dev-kobj, dev-bus-dev_groups); ... }当你注册的设备目录在/sys/bus/xxx/devices/下出现同时还带了几个属性文件这些属性文件大多就是从dev_groups来的。4.2 driver_register 时如何完成匹配与绑定驱动注册的路径是driver_register - bus_add_driver - driver_attach。driver_attach会遍历总线上已有的设备链表对每个设备调用总线的match函数来判断是否匹配static int __driver_attach(struct device *dev, void *data) { ... if (!driver_match_device(drv, dev)) return 0; ... device_driver_attach(drv, dev); ... }driver_match_device会依次尝试调用总线的match回调如果存在。检查驱动与设备是否有相同的device ID 表如of_match_table、acpi_device_id。如果都没匹配上再看是否属于同一个driver_override。一旦匹配成功device_driver_attach会被调用最终触发really_probe执行驱动的probe函数。这个probe过程是设备驱动模型最核心的事件之一。从设计角度来说bus_type提供的match函数使得不同总线可以用完全不同的策略来决定设备与驱动是否对口。PCI 总线比较设备 IDUSB 总线比较 vendor/product IDI2C 总线比较名字或者 of 匹配表。这个灵活性得益于bus_register背后这套总线的统一抽象。4.3 bus-probe 与 driver-probe 的前后关系一个容易混淆的点在really_probe里有以下逻辑简化视图if (dev-bus-probe) ret dev-bus-probe(dev); else if (drv-probe) ret drv-probe(dev);注意这个顺序如果总线定义了probe会优先调用总线的 probe否则调用驱动自己的probe。很多初次接触设备模型的人会以为driver-probe总是第一个被执行其实不然。我见过有人在自定义总线上只实现了bus-probe而驱动里也写了probe结果驱动里那个probe一直没跑排查半天没找到原因最后才搞清楚是总线的probe先拦截了。所以你在写总线驱动时要想清楚一个问题你的probe是需要“分配资源、做通用初始化”还是要“留给具体驱动做个性化初始化”根据这个决定是放在bus-probe里还是留给drv-probe。4.4 总线注册与设备模型的关系总结到这里整个联动链路可以这么梳理bus_register ├─ 建立 /sys/bus/xxx/ ├─ 建立 /sys/bus/xxx/devices/ ├─ 建立 /sys/bus/xxx/drivers/ └─ 注册 bus_notifier device_register ├─ device_get_bus 找到总线 ├─ bus_add_device 加入总线的设备集合 └─ 创建设备属性含总线 dev_groups driver_register ├─ bus_add_driver 加入总线的驱动集合 ├─ driver_attach 遍历设备并调用 match └─ 匹配成功后执行 probe这个链路是研究设备模型的底图。我建议你自己在纸上把这四行画一遍再对照代码看一遍记忆会非常牢固。5. 深入 bus-p 内部kset、klist 与锁的正确理解很多人读bus.c时会被priv-subsys和bus-p-devices_kset之间绕来绕去的指针关系搞晕。这一节我专门把这块拆开。5.1 bus 的“身份证”与回溯指针subsys_private里有一个bus指针指向外部传入的struct bus_type。之所以保留这个回溯指针是因为在内核内部有很多地方通过bus-p拿到了subsys_private但要操作总线本身的属性或回调时还需要回到struct bus_type上来。你可以把bus_type想象成前台的业务规则手册把subsys_private当成后台的实际台账。当前台规则手册需要修改或查询时工作人员需要通过台账上登记的“手册位置”找到手册。这个“手册位置”就是subsys_private-bus指针。5.2 klist 的遍历与锁klist_drivers和klist_devices用于遍历时加锁。klist是一种特殊的链表实现为了支持并发条件下的安全遍历它内部有一个锁struct klist { spinlock_t k_lock; struct list_head k_list; ... };bus_register初始化它们时会调用klist_init。这里有个细节值得注意klist的遍历函数支持托管的引用计数即遍历过程中获得一个节点时会自动增加其引用计数遍历完毕再释放。这个设计避免了“遍历过程中节点被释放”导致的悬垂指针崩溃。在调试“设备或者驱动动态热插拔导致崩溃”时首先要怀疑的就是klist的并发访问。如果你的代码里手动在这两个链表上做了遍历而没有正确使用klist_iter_init_node这类接口那热插拔场景下十有八九会有问题。5.3 bus_type 与 subsys_private 在整个内核里的生命周期当一套总线注册成功后它的生命周期往往与模块一致。模块卸载时会执行bus_unregister这个函数会做几件事bus_remove_groups移除总线属性组。kset_unregister销毁设备的 kset 和驱动的 kset。清理bus-p中注册的 notifier。释放bus-p占用的内存。一个容易忽略的坑如果你驱动中还有设备或驱动没有卸载干净那bus_unregister时内核会通过klist检查到仍有设备或驱动挂载这时就会打出一条警告。常见的内核日志是unregister bus: mybus配合栈回溯你会看到设备模型对“总线卸载但设备仍在”这个违例的检查。实际项目里我遇到过模块卸载顺序不对导致bus_unregister之后还有设备引用释放不了的问题最终通过确保先卸载设备、再卸载驱动、最后卸载总线解决。6. 常见问题与排查技巧实录这一节我整理一下自己在调试总线注册相关问题时踩过的坑以及对应的排查思路。都是实际能落地的经验不是教科书式套话。6.1 bus_register 返回 -EEXIST但明明没有重复注册这种窗口期最容易被忽略bus_type_find是通过遍历总线链表来查重的。但如果你自己实现了一个“重新注册同名总线”的逻辑而且上一次注册失败或模块没有完全卸载这个总线的名字可能会残留在链表中。遇到这个问题先在/sys/bus/下用ls看一眼有没有同名目录再用grep在内核日志里搜索相关总线名字。最直接的办法是在模块卸载路径里把总线注册失败时的清理动作做完整。6.2 总线的 uevent 文件读出来是空如果你读了/sys/bus/mybus/uevent发现空白先看代码。总线的uevent_show回调如果不实现内核会返回一个默认值通常就是空串。对于总线来说这是正常的因为总线本身不像设备那样需要携带具体的环境变量。如果你期望总线级别的 uevent 有内容你需要自己实现总线的uevent_show。6.3 设备和驱动都在但 probe 就是不执行这个问题的排查路径我一般按顺序来确认device_register和driver_register返回 0。在/sys/bus/mybus/devices/和/sys/bus/mybus/drivers/下确认设备和驱动目录都存在。确认总线的match函数是否被调用。加个printk看匹配过程。如果match返回 1 但probe没执行检查device_driver_attach的调用路径上有没有device_lock锁冲突。这里我提供一个通用调试技巧在drivers/base/dd.c里的really_probe函数开头加一行printk打印dev_name和drv-name。这样每次系统尝试 probe 都能看到日志。如果 probe 函数被调用后又失败了这种日志能告诉你到底是没匹配上还是匹配后被拒绝。6.4 内核启动阶段注册总线 vs 模块加载阶段注册总线一个常被问到的点bus_register既可以在内核启动早期被直接调用也可以在模块加载时被调用。两者的主要区别在于依赖关系启动阶段调用的bus_register通常在driver_init之后、设备注册之前保证后续设备注册时已经有总线可用。模块加载阶段调用必须保证 module_init 的执行顺序和依赖关系。如果你的总线模块被其他模块依赖而加载顺序没控制好就会出现“设备先注册了但总线还没注册”的情况。在内核启动早期的场景里设备模型的初始化顺序是start_kernel - rest_init - kernel_init - do_basic_setup - driver_init - buses_init注册 system 总线和 platform 总线等这也是为什么自定义总线如果要在早期初始化需要确保它的初始化函数执行顺序排在依赖它的设备注册之前。6.5 使用 ftrace 追踪 bus_register 调用链如果你手头的内核开启了CONFIG_FUNCTION_TRACER可以用 ftrace 来追踪bus_register的调用链echo function /sys/kernel/tracing/current_tracer echo bus_register /sys/kernel/tracing/set_ftrace_filter echo 1 /sys/kernel/tracing/tracing_on # 执行要追踪的加载操作 cat /sys/kernel/tracing/trace这样能看到从模块加载到bus_register之间经过了哪些函数对于理解调用顺序和排查依赖问题很有帮助。7. 一个完整的自定义总线注册示例讲了这么多理论我们用一段最小示例收个尾。假设你要实现一个简单的虚拟总线mybus代码框架如下#include linux/device.h #include linux/module.h #include linux/kernel.h static int mybus_match(struct device *dev, struct device_driver *drv) { // 最简单的匹配规则设备名和驱动名一致 return strcmp(dev_name(dev), drv-name) 0; } static int mybus_probe(struct device *dev) { pr_info(mybus: probe device %s\n, dev_name(dev)); return 0; } static struct bus_type mybus_type { .name mybus, .match mybus_match, .probe mybus_probe, }; static int __init mybus_init(void) { int ret; ret bus_register(mybus_type); if (ret) pr_err(mybus: bus_register failed, ret%d\n, ret); return ret; } static void __exit mybus_exit(void) { bus_unregister(mybus_type); } module_init(mybus_init); module_exit(mybus_exit); MODULE_LICENSE(GPL);加载这个模块后/sys/bus/mybus/就会建立起来。如果这一步你能顺利完成再往里面添加设备和驱动整个设备模型的路子就算真正走通了。实操心得不要急着在总线的 probe 里写太多业务逻辑先把“总线注册 设备注册 驱动注册”这三个环节跑通让/sys/bus/mybus/devices/下的设备和/sys/bus/mybus/drivers/下的驱动能完成 match 和 probe再逐步填充业务代码。设备模型那一层如果有了 bug后续排查都会被干扰。8. 个人体会读 bus_register 的正确姿势最后分享一点个人经验。代码本身不算长但第一次读的时候很容易被 kset/kobject 之间的关系绕晕。我建议的读源码顺序是先看bus_register里每个函数调用的返回值处理理解它为什么要一步步建目录、建 kset再反过去读struct bus_type和struct subsys_private的注释最后再结合device_register和driver_register的源码倒推一遍。我自己试过最管用的一个方法是在bus_register的每个关键步骤后面加一行pr_info打印当前执行到哪里然后加载一个真实设备比如 I2C 设备看日志输出顺序。这样整个设备模型的时序就非常直观了。如果你手头有 QEMU 环境也可以用 kgdb 在bus_register处打断点单步执行看 kset 的创建过程效果更好。bus_register研究透了设备模型里剩下的函数大多是在它搭好的舞台上唱戏。以后再遇到“为什么设备没有 probe”“为什么 sysfs 目录不对”这类问题你会比大多数人都更快定位到根因。
返回列表