1. 先搞清楚总线在内核中的定位
1.1 总线不是物理概念,是软件抽象
很多刚开始读内核源码的兄弟,一看到bus_register就条件反射地往硬件上想:是不是要去操作某个控制器、读写某个寄存器?其实不是。Linux 驱动模型里的“总线”是一个纯粹的软件抽象,它做的事情用大白话说就是:把同一种类型的设备和驱动归拢到一起,然后负责牵线搭桥。
你手机里的 I2C 控制器、SPI 控制器、USB 控制器,它们本身是物理硬件,但内核里的i2c_bus_type、spi_bus_type、usb_bus_type这些总线对象,只是用来描述“哪类设备该找哪类驱动”的规则集合。platform_bus_type就更明显了,它背后根本没有一条叫“platform”的物理总线,它存在的意义就是把那些不挂在 I2C/SPI/USB 等标准总线上的设备(比如内存控制器、时钟控制器、很多 SoC 内部外设)统一管理起来。
bus_register就是把这些“规则集合”正式注册进内核驱动模型的那一步。它做的事情包括:在内核里创建一个总线对象、把这个对象挂到全局总线的kset目录下、在 sysfs 中生成对应的目录结构、初始化设备链表和驱动链表、注册 uevent 通知机制。这几件事做完,一条总线才算“活了”,后面device_register和driver_register才有地方可以挂靠。
1.2 bus_register 在整个驱动模型中的位置
整个 Linux 设备驱动模型的核心是四件套:device(设备)、driver(驱动)、bus(总线)、class(类)。它们之间的关系可以这样理解:bus是中间人,维护着两个链表,一个挂设备,一个挂驱动;每当有新设备或者新驱动注册进来,bus就会调用match方法去看看两边能不能配对;配对成功后再触发probe,让驱动去初始化设备。
bus_register位于驱动模型初始化的比较早的阶段。在内核启动时,drivers/base/init.c里的driver_init()会依次完成devtmpfs_init()、devices_init()、buses_init()、classes_init()、firmware_init()等操作,其中buses_init()做的就是创建全局的bus_kset,也就是/sys/bus这个目录。之后各个子系统(如platform、i2c、spi)在初始化时,就会调用各自的bus_register,把本类型的总线挂到/sys/bus下面。
这里有个容易混淆的点:platform_bus_register和bus_register不是一回事。platform_bus_register是平台子系统特有的入口,它内部会做更多平台相关的工作,但最终还是会调用到bus_register(&platform_bus_type)。所以不管哪种总线,只要是走标准驱动模型,绕不开bus_register这条主链路。
2. bus_register 源码逐行拆解
老规矩,先打开drivers/base/bus.c,找到bus_register函数。不同内核版本的实现细节略有差异,但主体逻辑变化不大。下面以 5.x 内核的常见实现为例拆解。
2.1 入口:分配并初始化 subsys_private
int bus_register(struct bus_type *bus) { int retval; struct subsys_private *priv; struct lock_class_key *key = &bus->lock_key; priv = kzalloc(sizeof(struct subsys_private), GFP_KERNEL); if (!priv) return -ENOMEM; priv->bus = bus; bus->p = priv; BLOCKING_INIT_NOTIFIER_HEAD(&priv->bus_notifier);函数一进来,不是急着去建目录,而是先分配一个subsys_private结构体。这个结构体是总线真正的“私有数据”,定义在drivers/base/base.h里,里面装着总线的各种内部管理对象:subsys(一个kset)、devices_kset、drivers_kset、klist_devices、klist_drivers、bus_notifier等等。
为什么要额外搞一个subsys_private,而不是把这些字段直接塞进bus_type?因为bus_type是面向驱动开发者的“公共接口”,里面很多字段需要由具体总线去实现和填充,而内部的管理数据结构属于驱动模型自己的“私事”。把它们隔离到一个私有结构体里,可以减少 ABI 接口的暴露,也方便内核在内部扩展功能时不破坏外部接口。
然后priv->bus = bus; bus->p = priv;这两行建立双向关联:私有结构体知道自己属于哪条总线,总线也知道自己的私有数据放在哪里。后面所有操作几乎都是通过bus->p来访问内部数据的。
紧接着初始化一个阻塞通知器链bus_notifier。这个是为了之后总线上的设备/驱动事件通知准备的,比如 BUS_NOTIFY_ADD_DEVICE、BUS_NOTIFY_DEL_DEVICE 这些事件都会经过这条链。
2.2 关键一步:把总线挂到 bus_kset 上
retval = kobject_set_name(&priv->subsys.kobj, "%s", bus->name); if (retval) goto out; priv->subsys.kobj.kset = bus_kset; priv->subsys.kobj.ktype = &bus_ktype; retval = kset_register(&priv->subsys); if (retval) goto out;这一段是bus_register的核心动作。priv->subsys是一个kset,你可以把它理解成“一组 kobject 的集合”。这里先把总线的名字bus->name设置到priv->subsys.kobj上,比如platform、i2c、spi。然后priv->subsys.kobj.kset = bus_kset,这一步是把自己挂到全局的bus_kset下面——bus_kset就是/sys/bus这个目录在内核里的对象。
priv->subsys.kobj.ktype = &bus_ktype设置 kobject 的类型,而bus_ktype里面定义了总线在 sysfs 中默认属性组和释放操作。所有kset_register注册的 kobject 最终都会在 sysfs 中生成对应目录。因此kset_register(&priv->subsys)执行完成后,/sys/bus/<bus->name>这个目录就出现了。
这里有一个细节值得琢磨:为什么用kset_register而不是kobject_add?因为kset本身是一个 kobject 的容器,它既需要在 sysfs 中呈现为一个目录,又要具备容纳子对象的能力。/sys/bus/i2c下面还有devices和drivers两个子目录,所以必须用kset来承载。如果你只是注册一个普通 kobject,那下面就没法再挂子目录了。
2.3 创建 devices 和 drivers 两个 kset
priv->devices_kset = kset_create_and_add("devices", NULL, &priv->subsys.kobj); if (!priv->devices_kset) goto bus_devices_fail; priv->drivers_kset = kset_create_and_add("drivers", NULL, &priv->subsys.kobj); if (!priv->drivers_kset) goto bus_drivers_fail;总线目录建好之后,紧接着在它下面创建两个子 kset:devices和drivers。这两个目录是总线“牵线搭桥”的物理体现。
devices目录下会动态出现挂在这条总线上的设备对象。比如/sys/bus/i2c/devices/0-0077就是挂在 I2C 总线地址0x0077上的设备。drivers目录下会动态出现注册在这条总线上的驱动对象。比如/sys/bus/i2c/drivers/at24对应的就是一个 I2C 驱动。
kset_create_and_add是一个便捷函数,它帮我们完成了kset的创建和注册两步操作。devices_kset和drivers_kset本身不干匹配的活,它们只是 sysfs 层面让用户空间能看到“总线上挂了哪些设备和驱动”,真正的设备/驱动链表管理靠的是后面初始化的klist。
2.4 注册属性文件与 notifier
INIT_LIST_HEAD(&priv->interfaces); klist_init(&priv->klist_devices, klist_devices_get, klist_devices_put); klist_init(&priv->klist_drivers, NULL, NULL); retval = add_probe_files(bus); if (retval) goto bus_probe_files_fail; retval = bus_add_attrs(bus); if (retval) goto bus_attrs_fail; retval = bus_register_notifier(bus, &bus->bus_notifier); if (retval) goto bus_notifier_fail; pr_debug("bus: '%s': registered\n", bus->name); return 0;这一段主要是把总线的“家务活”安排好。
klist_init初始化设备链表和驱动链表。klist是内核里一个带锁的链表封装,支持迭代时锁保护。klist_devices和klist_drivers才是后面匹配逻辑真正会用到的东西:注册一个设备时,设备会挂到总线的klist_devices上;注册一个驱动时,驱动会挂到klist_drivers上。而klist_devices_get和klist_devices_put是引用计数回调,避免设备在遍历过程中被释放。
add_probe_files创建drivers_autoprobe属性文件,这个文件在/sys/bus/<bus>/drivers_autoprobe下。它是一个控制开关,写 0 可以临时禁止该总线上的自动探测,调试的时候很实用。
bus_add_attrs把bus_type里定义的bus_groups属性组注册到 sysfs,让用户空间能读取或设置总线的一些自定义属性。
bus_register_notifier注册总线的通知器链。注意上面已经初始化了priv->bus_notifier,而这里的bus->bus_notifier是bus_type自带的struct blocking_notifier_head。在部分内核版本中,bus_notifier被移到了bus_type里面,由总线使用者自己初始化。不管怎样,这一步都是把注册的回调函数加入链表,以便后续发送总线事件通知。
2.5 错误处理路径
out: kfree(priv); bus->p = NULL; return retval; bus_devices_fail: kset_unregister(priv->devices_kset); bus_drivers_fail: kset_unregister(priv->drivers_kset); bus_uevent_fail: bus_remove_file(bus, &bus_attr_uevent); kset_unregister(&priv->subsys); goto out;源码里还有一堆错误跳转标签,我挑重点说。学过驱动开发的都知道,Linux 内核最讲究错误路径的对称释放。申请了就要释放,注册了就要注销。这里bus_devices_fail会注销devices_kset,bus_drivers_fail会注销drivers_kset,bus_uevent_fail会移除 uevent 属性文件并注销subsys,最后统一kfree(priv)并把bus->p置空。
这个错误处理看起来繁琐,但它保证了一个核心原则:任何时候失败,总线的 sysfs 目录和内部数据结构都能恢复到注册前的状态。如果你自己写内核模块,一定要照抄这种风格,别在错误路径上偷懒。否则一旦某个步骤失败,前面已经注册的东西就成了“孤儿”,sysfs 里残留半截目录,谁碰谁死机。
3. bus_register 背后的数据结构串联
只看代码可能还是有点懵,我把这里涉及的数据结构关系彻底捋一捋。
3.1 bus_type、subsys_private、kset、klist 的关系
bus_type是给具体总线实现者用的,里面定义了一系列回调:match、uevent、probe、remove、shutdown、suspend、resume等。
subsys_private是驱动模型内部管理总线状态用的,它包含了一个subsys这个kset,以及devices_kset、drivers_kset、两个klist、通知器链、属性链表等。
kset是 kobject 的集合,在 sysfs 中表现为一个目录,是“门面”;klist是带锁的设备/驱动链表,是“里子”。两者一个负责对用户空间可见,一个负责在内核内部高效遍历。
| 数据结构 | 作用 | sysfs 呈现 | 主要字段 |
|---|---|---|---|
bus_type | 定义总线行为 | 无直接呈现 | match、uevent、probe、name |
subsys_private | 保存总线内部状态 | 无直接呈现 | subsys、devices_kset、klist_devices |
kset(subsys) | 总线的目录节点 | /sys/bus/<bus> | kobj、list |
devices_kset | 总线设备集合 | /sys/bus/<bus>/devices | kobj、list |
drivers_kset | 总线驱动集合 | /sys/bus/<bus>/drivers | kobj、list |
klist_devices | 设备链表 | 不可见 | k_list、kref |
klist_drivers | 驱动链表 | 不可见 | k_list、kref |
简单说,bus_register就是给一条总线“办好了营业执照”:subsys是店铺招牌,devices_kset和drivers_kset是店里两个货架,klist_devices和klist_drivers是老板私下的账本。sysfs 里看到的目录是给外人看的,内核真正的匹配逻辑用的是私下账本。
3.2 uevent 与热插拔
bus_register在成功创建subsys后,会调用bus_create_file(bus, &bus_attr_uevent),这个细节容易被忽略,但它关系到一个重要机制:uevent。
/sys/bus/<bus>/uevent这个文件是总线的“事件出口”。用户空间往这个文件写内容,可以触发一次总线级 uevent 广播;内核在设备注册、驱动注册、总线事件发生时,也会通过kobject_uevent发送事件。bus_type里的uevent回调就是用来在事件发出前填充环境变量的,比如MODALIAS变量就是从这里产生的。bus_register阶段把这个属性文件建好,相当于为后续所有热插拔事件打通了通道。
关于 uevent 有几个实际经验:
- 很多时候你插入一个 USB 设备,
/dev下能自动生成节点,靠的就是设备侧的 uevent 和udev/devtmpfs配合,总线侧的 uevent 是其中一环。 - 如果你自己写总线驱动,
uevent回调里一定要记得设置MODALIAS,否则用户空间的自动加载驱动机制会因为不知道“该找哪个驱动”而失效。
3.3 match 方法如何被后续流程调用
bus_register本身不会调用match方法,它只是把基础设施搭好。后续当设备或驱动注册时,驱动模型的公共逻辑会通过bus->p->bus(也就是原来的bus_type)拿到match函数指针来调用。
整个流程大概是:
- 某设备调用
device_register->device_add->bus_probe_device->device_attach。这时会遍历总线驱动链表,调用bus->match判断驱动是否支持该设备。 - 某驱动调用
driver_register->driver_attach。这时会遍历总线设备链表,同样调用bus->match判断设备是否匹配该驱动。 - 如果
match返回真,就接着调用bus->probe或驱动自带的probe方法。
所以bus_register里初始化的两个klist并不是摆设,它们就是匹配操作的“数据集”。你常听说“Linux 设备驱动模型里设备和驱动通过总线匹配”,其实就是bus_register阶段建立的这两个链表在发挥作用。
4. 动手实践:自己注册一条总线
光看源码不实践,印象总是不够深。下面我带你把一条最简总线注册起来,跑一遍完整流程。这里以虚拟总线的形式实现,不涉及具体硬件,适合在 QEMU 或者开发板上验证。
4.1 最小实现示例
我们写一个内核模块my_bus.c,定义自己的总线类型并注册。
#include <linux/device.h> #include <linux/module.h> #include <linux/kernel.h> #include <linux/string.h> static int my_bus_match(struct device *dev, struct device_driver *drv) { return strcmp(dev_name(dev), drv->name) == 0; } static int my_bus_probe(struct device *dev) { dev_info(dev, "my_bus: probe device %s\n", dev_name(dev)); return 0; } struct bus_type my_bus_type = { .name = "my_bus", .match = my_bus_match, .probe = my_bus_probe, }; static int __init my_bus_init(void) { int ret; ret = bus_register(&my_bus_type); if (ret) { pr_err("my_bus: bus_register failed, ret=%d\n", ret); return ret; } pr_info("my_bus: bus registered\n"); return 0; } static void __exit my_bus_exit(void) { bus_unregister(&my_bus_type); pr_info("my_bus: bus unregistered\n"); } module_init(my_bus_init); module_exit(my_bus_exit); MODULE_LICENSE("GPL");这个例子里的my_bus_match实现很简单:设备名和驱动名完全相等就算匹配。实际项目中不会这么粗暴,比如 platform 总线用的是drv->name与dev->name精确匹配加上of匹配表;I2C 总线用的是设备地址和驱动id_table匹配。这里为了演示只保留最核心的逻辑。
my_bus_probe里面只是打印一条日志,实际项目中这里会做硬件初始化、申请中断、注册字符设备等动作。
4.2 编译与加载,观察 /sys/bus 变化
写一个 Makefile 把模块编出来:
obj-m := my_bus.o KDIR := /lib/modules/$(shell uname -r)/build PWD := $(shell pwd) all: $(MAKE) -C $(KDIR) M=$(PWD) modules clean: $(MAKE) -C $(KDIR) M=$(PWD) clean然后make,加载模块:
make sudo insmod my_bus.ko加载之后立即检查/sys/bus目录:
ls /sys/bus/你会看到新出现的my_bus目录。进入目录再看:
ls -l /sys/bus/my_bus/正常情况下能看到devices、drivers、uenvent、drivers_autoprobe这几个文件/目录。uevent就是前面说的属性文件,drivers_autoprobe是自动探测开关。
此时再观察内核日志:
dmesg | tail -n 10能看到my_bus: bus registered这行输出。这说明bus_register已经完整执行成功。
这个例子演示了一个容易被忽略的点:bus_register 之后,用户空间立刻就能在 sysfs 中看到这条总线的全部基础结构。这些目录不是某个设备或驱动注册时临时生成的,而是总线注册的时候就已经建好了。如果你写驱动时发现/sys/bus/xxx下没有devices或drivers目录,那大概率是bus_register中途失败,错误处理路径又把它清理掉了。
4.3 添加设备与驱动,触发匹配
光有总线还不行,我们再写两个小模块,一个注册设备,一个注册驱动,让它们在my_bus上配对。
设备模块:
#include <linux/device.h> #include <linux/module.h> #include <linux/kernel.h> extern struct bus_type my_bus_type; static struct device my_dev; static int __init my_dev_init(void) { device_initialize(&my_dev); my_dev.parent = NULL; my_dev.bus = &my_bus_type; dev_set_name(&my_dev, "test_device"); return device_add(&my_dev); } static void __exit my_dev_exit(void) { device_unregister(&my_dev); } module_init(my_dev_init); module_exit(my_dev_exit); MODULE_LICENSE("GPL");驱动模块:
#include <linux/device.h> #include <linux/module.h> #include <linux/kernel.h> extern struct bus_type my_bus_type; static int my_drv_probe(struct device *dev) { dev_info(dev, "my_drv: probe called\n"); return 0; } static struct device_driver my_drv = { .name = "test_device", .bus = &my_bus_type, .probe = my_drv_probe, }; static int __init my_drv_init(void) { return driver_register(&my_drv); } static void __exit my_drv_exit(void) { driver_unregister(&my_drv); } module_init(my_drv_init); module_exit(my_drv_exit); MODULE_LICENSE("GPL");这里设备名是test_device,驱动名也是test_device,按照my_bus_match的逻辑,两者必然匹配。驱动注册后,driver_attach会遍历my_bus的设备链表,发现test_device匹配,就会调用驱动里的probe函数。
在/sys/bus/my_bus/devices/下你能看到test_device这个符号链接,在/sys/bus/my_bus/drivers/下也能看到test_device这个目录。如果驱动挂在设备之后,你会看到dmesg里立刻出现my_drv: probe called;如果驱动先注册、设备后添加,设备device_add时也会触发bus_probe_device,同样会调用到probe。这就是“设备和驱动谁先注册都行”的关键原因——bus_register建好的两个链表保证了两边的主动扫描。
实际调试时建议把加载顺序换一换:先加载驱动再加载设备,然后先加载设备再加载驱动,你会发现两种情况下probe都能被正常调用。理解了这一点,你对 Linux 设备模型的认识就比只会写platform_driver的开发者深了一层。
5. 常见问题与调试心得
源码读完了,实践也跑通了,最后聊几个我实际踩过的坑。
5.1 总线注册失败:-ENOMEM
bus_register最常见的失败返回值是-ENOMEM。原因多半是kzalloc分配subsys_private失败,或者kset_create_and_add创建 kset 失败。如果是在模块初始化时调用,而且系统内存充足,那大概率不是真的没内存,而是/sys挂载有问题,或者 sysfs 内核对象已经达到了某些限制。排查技巧:
- 检查
/sys是否挂载:执行mount | grep sysfs。 - 检查
dmesg里有没有kobject_add failed之类的提示。 - 如果之前模块加载过但没卸载干净,重复注册相同名字的总线也会有问题,参见下一条。
5.2 总线名字冲突
/sys/bus下不允许出现同名目录,因为sysfs中的kobject名字在父目录下必须唯一。如果你insmod一个同名总线模块(比如再次注册my_bus),kset_register时会因为sysfs_create_dir_ns失败而返回-EEXIST。这不算 bug,而是设计如此。
实际开发中,如果你发现bus_register返回-EEXIST,优先检查是不是模块重复加载,或者内核里已经有一条同名的总线。可以通过ls /sys/bus对比查看,也可以grep bus_type /proc/slabinfo?这有点过度,直接看/sys更直观。
5.3 匹配不到的常见原因
总线上设备已经注册了,驱动也注册了,但probe就是不执行。我总结下来九成是这三种情况:
match方法写得不对,返回永远是 0。调试时可以在match里加printk,把dev_name(dev)和drv->name打印出来看看到底是什么。- 设备的
bus指针没设置对。比如你设备里my_dev.bus = &my_bus_type写成了别的总线,那它怎么都不会挂到my_bus的链上。检查/sys/bus/<bus>/devices里有没有这个设备,没有就说明设备没挂对总线。 - 驱动里忘了设置
.bus。driver_register的时候如果驱动对象里bus字段是NULL,驱动模型会尝试通过bus_find_device_by_name查找名字匹配的总线,找不到就会注册失败。最稳的做法是显式指定my_drv.bus = &my_bus_type。
5.4 调试技巧:使用动态打印
bus_register函数里有pr_debug("bus: '%s': registered\n", bus->name),平时不会打印。如果你想追踪总线的注册过程,可以打开动态打印:
echo 'file drivers/base/bus.c +p' > /sys/kernel/debug/dynamic_debug/control前提是内核开启了CONFIG_DYNAMIC_DEBUG。打开后dmesg里就能看到bus_register内部各个步骤的日志,对排查问题很有帮助。对比直接改源码加printk,动态调试不用重新编译内核,是比较现代的做法。
5.5 总线注销的坑
bus_unregister会做和bus_register相反的操作:注销 notifier、移除属性、注销devices_kset和drivers_kset、注销subsys、释放subsys_private。但有一个前提:卸载总线时,总线上不能有残留的设备或者驱动。如果某个设备还挂在总线上,注销时会触发警告。所以模块退出时务必先注销设备、驱动,再注销总线。
我自己调试时习惯在bus_unregister之前打印一下两个klist是否为空,但从外部不容易看。更简单的做法是卸载前手动rm掉对应 sysfs 下的符号链接,其实那也只是表象。真正严谨的做法是保证device_unregister和driver_unregister都执行完,再调用bus_unregister。
最后再说一点个人体会:很多人觉得bus_register只是一个基础设施函数,只要会用platform_driver_register就够了。可如果你去追过platform_driver_register的调用链,会发现它最终也走进bus_register建立的那套框架。真正把这块源码啃透之后,再看那些“总线匹配”“probe 时机”“驱动和设备谁先加载”的问题,心里会非常透亮。以后如果再遇到奇怪的驱动加载顺序问题,你至少知道该去/sys/bus/<bus>/devices和/sys/bus/<bus>/drivers下面找线索,而不是瞎猜。