新闻详情

新闻详情

首页 / 资讯中心 / 详情

Linux内核bus_register源码解析:总线注册与设备驱动模型

发布时间:2026/10/1 23:11:06来源:尧图网络
Linux内核bus_register源码解析:总线注册与设备驱动模型
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_privateint 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 两个 ksetpriv-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;总线目录建好之后紧接着在它下面创建两个子 ksetdevices和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 注册属性文件与 notifierINIT_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_ksetbus_drivers_fail会注销drivers_ksetbus_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、namesubsys_private保存总线内部状态无直接呈现subsys、devices_kset、klist_devicesksetsubsys总线的目录节点/sys/bus/buskobj、listdevices_kset总线设备集合/sys/bus/bus/deviceskobj、listdrivers_kset总线驱动集合/sys/bus/bus/driverskobj、listklist_devices设备链表不可见k_list、krefklist_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 总线注册失败-ENOMEMbus_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_buskset_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下面找线索而不是瞎猜。
网站建设高端定制企业官网
RELATED

相关资讯

更多精彩内容,欢迎继续阅读

较早相关资讯

最新相关资讯

小样本学习数据集选型指南:27个真正可用的高质量数据集 2026/10/2 0:00:06

小样本学习数据集选型指南:27个真正可用的高质量数据集

1. 小样本学习的“弹药库”:为什么你总在找数据集,却总找不到真正能用的? 小样本、数据集——这两个词最近半年在我处理的200多个AI项目咨询里,出现频率排进前三。不是模型调不好,不是代码写不对,而是卡在…

阅读更多 →
LLM Agent记忆优化:hindsight回溯提炼与MCP集成实战 2026/10/1 23:59:52

LLM Agent记忆优化:hindsight回溯提炼与MCP集成实战

1. 从“hindsight”这个词说起:为什么它值得单独拿出来聊“hindsight”直译过来是“后见之明”,但在 LLM Agent 这个语境里,它指向的东西要具体得多——Agent 在任务执行完之后,对整段交互过程做一次回溯性提炼,把“当…

阅读更多 →
openrig 实战:用 YAML 与 npm 统一管理 Claude Code 和 Codex 配置 2026/10/1 23:59:52

openrig 实战:用 YAML 与 npm 统一管理 Claude Code 和 Codex 配置

1. 从"openrig"这个名字说起:它到底想解决什么问题第一次看到openrig这个词,我下意识把它拆成了两半:open和rig。rig在工程语境里通常指"装配好的成套设备"或者"工作台",比如矿机叫 mining rig&…

阅读更多 →
Univer 表格 SDK 实战:Canvas 渲染与权限控制填报表开发 2026/10/1 23:59:52

Univer 表格 SDK 实战:Canvas 渲染与权限控制填报表开发

1. 从一张“只能填几个格子”的表格说起第一次接触 Univer 是在一个内部数据填报系统里。业务方的需求听起来特别简单:给用户一张表格,只允许他们填写指定的几个单元格,其他区域全部锁死,不能改、不能删、不能新增行列。当时第一反…

阅读更多 →
Windows下安装CTeX环境全攻略:概念、步骤与报错排查 2026/10/1 23:59:52

Windows下安装CTeX环境全攻略:概念、步骤与报错排查

作为常年帮实验室新人配环境的“老工具人”,我几乎每年都要在 Windows 上装几遍 CTeX 相关的排版环境,也见过太多人卡在第一步:装完之后打开 WinEdt 写两行中文,编译报错、乱码、找不到 ctex.sty、或者 PDF 里全是黑方块。其实绝大…

阅读更多 →
科研AI平替:LabFlow零配置本地化AI协作方案 2026/10/1 23:59:52

科研AI平替:LabFlow零配置本地化AI协作方案

1. Codex不是“装了就能用”的工具,而是科研流程的智能协作者Codex这个词最近在学术圈里反复刷屏,但很多人点开官网、下载安装包、配置API密钥、折腾VS Code插件之后,发现它根本不像宣传里说的那样“写个注释就能生成论文级代码”——反而卡在…

阅读更多 →

今日资讯

本周资讯

本月资讯

看完文章仍有疑问?

联系尧图顾问,获取一对一建站咨询

立即免费咨询 📞 400-888-8888
📞 ✉