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->p(private)指向的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 总线比较设备 ID,USB 总线比较 vendor/product ID,I2C 总线比较名字或者 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 目录不对”这类问题,你会比大多数人都更快定位到根因。