[TOC]

全面解析Linux设备模型的核心枢纽:drivers/base/bus.c

drivers/base/bus.c 是Linux内核设备模型中负责实现**总线(Bus)**这一核心概念的代码文件。它提供了一套通用的API和数据结构,用于管理系统中的各种总线类型,其最核心的职责是充当设备(Device)和驱动(Driver)之间的“撮合者”(Matchmaker),建立它们之间的连接。

历史与背景

这项技术是为了解决什么特定问题而诞生的?

在统一设备模型(详见drivers/base的解析)被引入之前,不同类型的总线(如PCI、USB)各自为政,都有一套自己独立的逻辑来注册设备、注册驱动,并进行匹配。这导致了以下问题:

  1. 代码冗余:每种总线都需要重复实现相似的设备列表管理、驱动列表管理以及遍历匹配的逻辑。
  2. 缺乏统一性:没有一个统一的接口来查询系统中所有的总线类型,或者在不同总线之间建立关联。
  3. 管理复杂:向系统中添加一种全新的总线类型,需要从头编写大量的管理和匹配代码,而非复用现有框架。

drivers/base/bus.c 的诞生,正是为了将这种通用的“匹配”和“管理”逻辑抽象出来,形成一个所有总线实现都可以复用的基础框架。

它的发展经历了哪些重要的里程碑或版本迭代?

  • 内核 2.5 系列:作为统一设备模型的一部分,struct bus_type 和相关的核心功能(如 bus_register)被引入,奠定了总线抽象的基础。
  • 内核 2.6 系列:PCI、USB、I2C、SPI等主要的总线驱动都迁移到了这个新的模型之上,使其成为内核的标准实践。
  • 后续演进:该文件的核心逻辑非常稳定。后续的改进主要集中在功能增强上,例如:
    • sysfs 接口的完善:提供了更丰富的总线、设备和驱动属性,以便用户空间工具进行查询和控制。
    • 驱动自动加载:与 kobject_uevent 机制深度集成,使得当一个新设备连接时,总线可以生成一个事件,通知用户空间的 udev 去加载匹配的驱动模块。
    • 异步探测支持:为了优化启动速度,增加了对驱动异步探测的支持,允许不相互依赖的设备并行初始化。

目前该技术的社区活跃度和主流应用情况如何?

drivers/base/bus.c 中的代码是Linux内核中最稳定、最基础的部分之一。它的活跃度不体现在频繁的代码变更,而是体现在内核中几乎所有的设备驱动都构建于其上。无论是PCI、USB这样的物理总线,还是Platform Bus、AMBA这样的片上系统(SoC)总线,都依赖这个文件提供的框架。它是编写任何现代Linux驱动程序都必须使用的基础组件。

核心原理与设计

它的核心工作原理是什么?

bus.c 的核心是 struct bus_type 结构体。任何一种具体的总线(如PCI总线)都需要定义一个该类型的全局变量,并填充其成员,其中最重要的是 match 函数指针。

其核心工作流程如下:

  1. 总线注册:内核模块(如PCI驱动)调用 bus_register(&pci_bus_type) 来向内核注册一个新的总线类型。此操作会在 /sys/bus/ 目录下创建一个以总线名字(如 pci)命名的目录。
  2. 设备/驱动的归属:当一个设备(struct device)被注册时,它的 bus 指针会指向它所属的 bus_type。同样,一个驱动(struct device_driver)注册时,其内部的 bus 指针也会指向它能驱动的总线类型。
  3. 匹配过程
    • 当一个新设备被注册时(调用device_add()):系统会遍历该设备所属总线上的所有驱动,并对每一个驱动调用总线定义的 match 函数,即 bus->match(device, driver)
    • 当一个新驱动被注册时(调用driver_register()):系统会遍历该驱动所属总线上的所有未绑定驱动的设备,并对每一个设备调用 match 函数。
  4. 绑定(Binding):如果 match 函数返回 true,表示设备和驱动匹配成功。此时,设备模型核心会进行“绑定”操作:将设备的 driver 指针指向该驱动,并调用驱动的 probe 函数。probe 函数是驱动的入口点,它负责初始化硬件、申请资源等。

它的主要优势体现在哪些方面?

  • 逻辑分离:清晰地将“匹配逻辑”(由总线定义)与“驱动逻辑”(在驱动的probe/remove中)和“设备表示”(在struct device中)分离开。
  • 代码高度复用:所有总线共用一套设备/驱动列表管理、遍历和绑定流程的代码,极大地减少了内核代码量。
  • 统一的sysfs视图:自动在 /sys/bus/<bus_name>/ 下创建 devicesdrivers 目录,并用符号链接清晰地展示设备和驱动的绑定关系,极大地增强了系统的可观察性。
  • 强大的可扩展性:定义一种新的总线类型变得非常简单,只需要实现一个 struct bus_type,特别是其 match 方法即可。

它存在哪些已知的劣势、局限性或在特定场景下的不适用性?

  • 抽象开销:对于一些极度简单的场景,这套框架可能显得有些“重”。但考虑到它带来的统一性和可维护性,这点开销通常是值得的。
  • 匹配机制的单一性:核心框架只负责调用 match 函数,具体的匹配逻辑完全由总线自己实现。如果一个设备可能被多种不同类型的驱动支持,就需要更复杂的逻辑,这通常在驱动层面而非总线层面解决。
  • 并非技术本身,而是适用范围:它是一个基础框架,本身没有不适用的场景。但在实际开发中,开发者需要选择合适的总线类型。例如,对于SoC内部不可枚举的设备,就不应该试图为其发明一种新的物理总线,而应该使用平台总线(Platform Bus)。

使用场景

在哪些具体的业务或技术场景下,它是首选解决方案?请举例说明。

在内核驱动开发中,只要涉及到设备和驱动的匹配,它就是唯一的标准方案。区别在于使用哪种具体的总线实现:

  • 物理可枚举总线
    • 场景:PCI/PCIe、USB总线。
    • 说明:这些总线上的设备可以通过电信号被主机控制器发现和枚举。它们的 bus_typematch 函数会比较硬件ID(如PCI的Vendor ID和Device ID,USB的idVendor和idProduct)来精确匹配设备和驱动。
  • 片上系统(SoC)总线
    • 场景:嵌入式系统中的平台设备(Platform Device)。
    • 说明:SoC内部的很多IP核(如定时器、串口)是无法被CPU自动发现的。开发者通过设备树(Device Tree)或板级文件来“描述”这些设备的存在。平台总线(platform_bus_type)的 match 函数通常只是简单地比较设备名和驱动名字符串是否一致,或者检查设备树的 compatible 属性。
  • 虚拟总线
    • 场景:用于管理一类特定的虚拟设备,例如VirtIO。
    • 说明:VirtIO是一种用于虚拟机的半虚拟化设备接口。它有自己的 virtio_bus,其 match 函数根据VirtIO设备ID进行匹配,尽管这些设备并非物理存在。

是否有不推荐使用该技术的场景?为什么?

没有。在现代Linux内核中,任何脱离设备模型和bus.c框架来编写的驱动都会被认为是过时和不规范的,因为它无法利用内核提供的电源管理、热插拔、sysfs等一系列高级功能。

对比分析

请将其 与 其他相似技术 进行详细对比。

由于bus.c是内核内部的基础设施,我们无法将其与其他“技术”横向对比。更有意义的是对比基于bus.c框架的不同总线实现

特性 PCI 总线 (pci_bus_type) USB 总线 (usb_bus_type) 平台总线 (platform_bus_type)
实现方式 硬件枚举,由PCI控制器扫描总线,发现设备。 硬件枚举,由USB主控制器与设备通信,获取描述符。 软件描述,通常通过设备树(Device Tree)或C代码来定义设备。
匹配逻辑 (match函数) 复杂。比较Vendor ID, Device ID, Subsystem ID, Class Code等。支持通配符。 复杂。比较idVendor, idProduct, bDeviceClass等。支持多种匹配标志。 简单。优先匹配设备树compatible属性,其次匹配设备和驱动的name字符串。
资源占用 设备自带配置空间,资源(IRQ, MMIO)可由硬件动态分配。 资源由USB协议栈管理。 资源(内存地址、中断号)通常是固定的,在设备树中静态定义。
隔离级别 硬件级别隔离,设备功能独立。 设备功能独立,共享USB带宽。 逻辑隔离,物理上都在同一SoC上,可能共享时钟、电源等。
典型用途 高性能外设:显卡、网卡、NVMe SSD。 通用外设:键盘、鼠标、U盘、摄像头。 SoC内部集成的控制器:串口、I2C、SPI、DMA、时钟控制器。

入门实践 (Hands-on Practice)

“可以提供一个简单的入门教程或关键命令列表吗?”

要使用bus.c,你通常不是直接调用它的函数,而是定义一个struct bus_type并注册它。这是一个非常高级的操作(定义全新的总线类型)。大多数开发者是使用已有的总线,如平台总线。

以下是一个定义自定义总线的极简概念示例:

  1. 定义 match 函数:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    #include <linux/device.h>
    #include <linux/module.h>

    // 简单的匹配逻辑:比较设备和驱动的名称
    static int my_bus_match(struct device *dev, struct device_driver *drv)
    {
    // dev_name(dev) 是一个安全的宏,用于获取设备名称
    return sysfs_streq(dev_name(dev), drv->name);
    }
  2. 定义 bus_type 结构:

    1
    2
    3
    4
    5
    struct bus_type my_bus_type = {
    .name = "mybus", // 这将是 /sys/bus/mybus
    .match = my_bus_match,
    };
    EXPORT_SYMBOL(my_bus_type); // 如果要在其他模块中使用,则导出
  3. 注册和注销总线:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    static int __init my_bus_init(void)
    {
    return bus_register(&my_bus_type);
    }

    static void __exit my_bus_exit(void)
    {
    bus_unregister(&my_bus_type);
    }

    module_init(my_bus_init);
    module_exit(my_bus_exit);
    MODULE_LICENSE("GPL");

    加载这个模块后,你会在 /sys/bus/ 下看到一个新的 mybus 目录。之后你就可以注册挂载在这条总线上的设备和驱动了。

“在初次使用时,有哪些常见的‘坑’或需要注意的配置细节?”

  • match 函数必须快速且无副作用match 函数可能在持有锁的上下文中被频繁调用。它绝不能睡眠(如申请内存),也不能执行耗时操作或改变设备状态。它唯一的职责就是判断“是”或“否”。
  • 命名空间冲突:你定义的总线名称(.name 成员)在内核中必须是唯一的。
  • 复杂的匹配逻辑:如果你的匹配逻辑很复杂(例如需要访问设备特定的配置空间),需要确保在 match 函数中访问这些数据是安全的。通常,match 只依赖于 struct devicestruct device_driver 中已有的、稳定的信息。
  • 忘记导出符号:如果你将总线、设备、驱动的定义分在不同的模块,别忘了使用 EXPORT_SYMBOL 导出你的 bus_type 变量,否则其他模块将无法链接。

安全考量 (Security Aspects)

“使用这项技术时,需要注意哪些主要的安全风险?”

drivers/base/bus.c 本身是内核核心代码,经过了严格审查,其本身的安全风险极低。风险主要来自于基于它构建的总线实现或驱动

  • 暴露不安全的sysfs属性:如果一个总线通过 bus_attrs/sys/bus/<bus_name>/ 目录下添加了可写的sysfs文件(如一个触发总线重新扫描的开关),那么必须对来自用户空间的输入进行严格验证,否则可能导致DoS攻击或内核内存破坏。
  • 有漏洞的match函数:虽然罕见,但如果 match 函数在处理畸形的设备或驱动名称时存在漏洞(如越界读取),可能会导致信息泄露或系统不稳定。
  • 驱动绑定攻击:在某些场景下,如果一个物理设备可以被一个恶意的驱动程序绑定,该驱动就可以获得对硬件的完全控制权。内核的模块签名(CONFIG_MODULE_SIG)等机制可以缓解这类风险。

“业界有哪些增强其安全性的最佳实践或辅助工具?”

  • 遵循最小权限原则:总线暴露的sysfs属性应尽可能为只读。
  • 代码审查:对 match 函数和所有添加到总线、驱动或设备上的 sysfs 属性处理函数进行仔细的代码审查。
  • 使用现有总线:除非有极强的理由,否则应优先使用内核中已经存在且经过充分测试的总线(如 platform_bus),而不是创建新的总线类型。
  • 内核安全特性:启用内核的栈保护、KASLR、模块签名等标准安全特性。

生态系统与社区 (Ecosystem & Community)

“围绕这项技术,有哪些流行的关联工具或项目?”

  • sysfs 文件系统/sys/bus/ 目录是 bus.c 框架在用户空间最直接的体现。系统管理员和开发者可以通过它来查看所有总线、以及每条总线上的设备和驱动。
  • udev/systemd-udevd:它依赖总线发出的 uevent 来自动加载驱动模块。例如,当一个USB设备插入,USB核心驱动会注册设备,USB总线会发出uevent,udev收到后会根据设备的ID信息,使用 modprobe 加载对应的驱动 .ko 文件。
  • lspci, lsusb:这些用户空间工具通过读取 /sys/bus/pci/sys/bus/usb 下的信息来展示设备列表和详细信息。

“如果遇到问题,有哪些推荐的官方文档、活跃社区或学习资源?”

  • 官方文档:内核源码树中的 Documentation/driver-api/driver-model/bus.rst 是最权威的文档。
  • 源码本身drivers/base/bus.c 的代码注释非常清晰。同时,研究 drivers/pci/pci-driver.c (PCI总线实现) 和 drivers/base/platform.c (平台总线实现) 是理解其用法的最佳途径。
  • 书籍:《Linux Device Drivers, 3rd Edition》 (LDD3) 详细阐述了设备模型,包括总线的概念。

性能与监控 (Performance & Monitoring)

“在生产环境中使用时,如何监控其性能指标?”

bus.c 本身的性能监控通常没有必要,它的开销非常小。监控的焦点应放在总线上的设备探测(probe)上。

  • 启动时间分析:使用 systemd-analyze plot > boot.svg 可以生成启动过程的SVG图,从中可以看到哪些设备的 probe 函数耗时最长,拖慢了启动速度。
  • ftrace:可以使用 ftrace 工具来精确测量某个驱动 probe 函数的执行时间,或者跟踪 bus_match 函数的调用情况。

“有哪些常见的性能调优技巧?”

  • 优化 match 函数:确保 match 函数尽可能快。字符串比较通常比解析复杂的ID要快。在嵌入式系统中,设备树的 compatible 属性匹配是经过高度优化的。
  • 使用异步探测:对于不影响系统启动关键路径的设备,可以在其驱动中设置 probe_typePROBE_PREFER_ASYNCHRONOUS,允许内核并行地探测它,从而缩短总的启动时间。
  • 减少 probe 中的耗时操作:将固件加载等可能耗时的操作推迟到设备被实际打开时再执行,而不是在 probe 中一次性完成。

“这项技术未来的发展方向是什么?”

bus.c 的核心框架已经极为稳定。未来的发展主要是在其上构建更高级的功能:

  • 设备链接(Device Links):为了更精确地处理设备之间的依赖关系(例如一个设备必须在另一个设备probe成功后才能被probe),内核引入了设备链接,这使得跨总线的设备依赖管理成为可能。
  • 软件节点(Software Nodes):为了更好地描述那些不由固件(如ACPI/设备树)描述的、纯软件定义的复杂设备拓扑,引入了软件节点框架,它与设备模型紧密集成。
  • 对新总线的支持:随着 CXL、UCIe 等新一代互联总线的出现,bus.c 框架将被用来支持对这些新总线的抽象和管理。

“社区中是否有正在讨论的替代技术或下一代方案?”

没有。bus.c 所代表的总线抽象是设备模型不可或缺的一部分,在Linux内核的宏内核架构下,没有替代方案的讨论。它的设计被证明是非常成功和有远见的。

总结

drivers/base/bus.c 是Linux设备和驱动世界的“婚姻介绍所”。它提供了一个通用的、可扩展的框架,其核心职责就是通过一个可定制的 match 函数,为海量的设备找到它们各自正确的驱动程序,并触发它们的结合(probe)。

关键特性总结

  • 核心抽象:定义了 struct bus_type,将总线的通用行为标准化。
  • 匹配核心:提供了设备与驱动的自动匹配和绑定机制。
  • Sysfs集成:自动为每种总线在 /sys/bus/ 下创建标准化的目录结构。
  • 基础服务:为电源管理、驱动自动加载等高级功能提供基础支持。

学习该技术的要点建议

  1. 不要一开始就尝试创建新总线:首先要彻底理解如何在一个已有的总线上编写驱动,平台总线(Platform Bus)是最好的起点,因为它最简单。
  2. 理解匹配过程:弄清楚当设备或驱动被添加时,内核是如何遍历、调用 match 函数,以及成功后如何调用 probe 的,这是核心。
  3. 多看现有总线实现:通过阅读 platform.c, pci-driver.c, usb/core/driver.c 等代码,来理解不同总线是如何利用 bus.c 提供的框架实现各自独特的匹配逻辑的。

buses_init 创建总线与系统设备的sysfs根目录

  • 它的核心任务是在sysfs中创建用于按“总线类型”组织设备和驱动的目录结构,并为不属于任何物理总线的“系统设备”创建容器
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
static int bus_uevent_filter(const struct kobject *kobj)
{
const struct kobj_type *ktype = get_ktype(kobj);

if (ktype == &bus_ktype)
return 1;
return 0;
}

static const struct kset_uevent_ops bus_uevent_ops = {
.filter = bus_uevent_filter,
};

// `__init`宏指示编译器将此函数放入特殊的".init.text"段。
// 内核启动后会释放此段内存,这对内存有限的STM32系统是关键优化。
int __init buses_init(void)
{
// 创建一个名为"bus"的kset,并将其添加到sysfs的根目录。
// 这将在sysfs中创建 /sys/bus/ 目录。
// 这个目录是所有不同类型总线(如 "spi", "i2c", "platform")的根容器。
// `&bus_uevent_ops` 为这个kset关联了处理总线相关内核事件的回调函数。
bus_kset = kset_create_and_add("bus", &bus_uevent_ops, NULL);
// 如果kset创建失败(通常因为内存不足),则返回错误。
if (!bus_kset)
return -ENOMEM;

// 创建一个名为"system"的kset。
// 关键在于第三个参数 `&devices_kset->kobj`,它指定了此kset的父对象。
// `devices_kset` 是在 `devices_init` 中创建的,对应 /sys/devices/。
// 因此,此行代码的效果是在 /sys/devices/ 目录下创建名为 "system" 的子目录,
// 即 /sys/devices/system/。
system_kset = kset_create_and_add("system", NULL, &devices_kset->kobj);
if (!system_kset) {
// 如果创建 "system" kset 失败,则执行错误处理。
// 首先,撤销本函数中已成功的操作,即注销 "bus" kset。
kset_unregister(bus_kset);
// 将全局指针设为NULL,防止后续误用。
bus_kset = NULL;
// 打印一条内核错误日志,这在STM32上通常会输出到串口控制台。
pr_err("%s: failed to create and add kset 'bus'\n", __func__);
// 返回内存不足错误。
return -ENOMEM;
}

// 所有初始化步骤成功完成。
return 0;
}

bus_probe_device: 为新设备探测驱动程序

此函数的核心作用是为一个新注册的设备 (dev) 在其所属的总线 (bus) 上启动驱动程序探测过程。当一个新设备被添加到系统中时, 内核会调用此函数, 试图为该设备找到并绑定一个合适的驱动程序。

该函数的原理分为两个主要部分:

  1. 自动探测 (Autoprobe): 它首先检查总线是否设置了 drivers_autoprobe 标志。如果设置了 (这是绝大多数总线的默认行为), 它会调用 device_initial_probe 函数。device_initial_probe 会将该设备与挂载在该总线上的每一个驱动程序进行匹配测试 (通常通过调用驱动的 probe 函数)。如果找到匹配的驱动, 设备和驱动就会被绑定。
  2. 子系统接口通知: 在自动探测之后, 它会遍历注册在该总线上的所有 “子系统接口” (subsys_interface)。这是一种回调机制, 允许其他内核子系统(例如一个跨总线的驱动程序或者一个特殊的设备管理器)对新设备的添加做出响应。它会调用每个接口提供的 add_dev 函数, 将新设备通知给它们。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
/**
* bus_probe_device - 为一个新设备探测驱动
* @dev: 需要被探测的设备
*
* - 如果总线允许, 自动为设备探测驱动.
*/
void bus_probe_device(struct device *dev)
{
/*
* 定义一个指向 subsys_private 结构体的指针 sp.
* subsys_private 结构体包含了与总线相关的私有数据, 例如驱动列表、设备列表和锁.
* bus_to_subsys 是一个宏, 用于从总线(dev->bus)的 kobject 中获取其内嵌的 subsys_private 成员.
* 这一行的作用是获取 dev 设备所属总线的私有数据结构, 并增加其引用计数.
*/
struct subsys_private *sp = bus_to_subsys(dev->bus);
/*
* 定义一个指向 subsys_interface 结构体的指针 sif.
* subsys_interface 提供了一种机制, 让其他代码可以挂钩(hook)到总线的事件, 如设备的添加/移除.
*/
struct subsys_interface *sif;

/*
* 检查 sp 是否为 NULL. 如果 dev->bus 为 NULL, 或者转换失败, sp 就会是 NULL.
* 这是一个保护性检查, 确保设备确实属于一个有效的总线.
*/
if (!sp)
/*
* 如果没有总线私有数据, 就无法进行探测, 直接返回.
*/
return;

/*
* 检查总线的 drivers_autoprobe 标志位.
* 这个标志位 (默认为1) 控制着总线是否应该自动为新加入的设备寻找驱动.
*/
if (sp->drivers_autoprobe)
/*
* 如果允许自动探测, 则调用 device_initial_probe 函数.
* 这个函数会启动实际的设备-驱动匹配过程. 它会遍历总线上的所有驱动,
* 并调用它们的 probe 函数来检查是否支持此设备.
*/
device_initial_probe(dev);

/*
* 对总线的互斥锁 sp->mutex 加锁.
* 在单核抢占式内核中, 这可以防止在遍历 interfaces 链表时,
* 被另一个正在修改此链表 (例如, 注册或注销一个 subsys_interface) 的任务抢占,
* 从而保证了链表访问的安全性.
*/
mutex_lock(&sp->mutex);
/*
* 遍历注册到该总线上的所有子系统接口.
* sp->interfaces 是一个链表头, 包含了所有通过 bus_register_notifier 注册的接口.
*/
list_for_each_entry(sif, &sp->interfaces, node)
/*
* 检查当前接口 sif 是否定义了 add_dev 回调函数.
*/
if (sif->add_dev)
/*
* 如果定义了, 就调用它, 并将新设备 dev 和接口自身 sif 作为参数传递过去.
* 这允许了其他模块对新设备的加入做出响应.
*/
sif->add_dev(dev, sif);
/*
* 解锁互斥锁, 允许其他任务访问该总线的数据.
*/
mutex_unlock(&sp->mutex);
/*
* 调用 subsys_put(sp) 减少对总线私有数据的引用计数.
* 这与函数开头的 bus_to_subsys(dev->bus) 调用相对应, 遵循引用计数的管理规则.
*/
subsys_put(sp);
}

bus_to_subsys: 从公共 bus_type 查找内部 subsys_private

此函数是一个内部转换和查找函数。它的作用是根据一个公开的、只读的 struct bus_type 指针, 在内核中找到并返回与之对应的、可写的、包含所有内部状态的 struct subsys_private 结构体指针。

工作原理:

  1. 锁定全局列表: 它知道所有已注册的总线都被链接在一个全局的 bus_kset 链表中。为了安全地遍历这个可能被并发访问的链表, 它首先获取 bus_kset->list_lock 自旋锁。在STM32H750这样的单核抢占式系统上, 这个操作会禁用内核抢占, 防止在遍历过程中链表被其他任务修改。
  2. 遍历查找: 它通过 list_for_each_entry 遍历 bus_kset 链表中的每一个 kobject
  3. 反向定位容器: 对于每一个 kobject, 它使用 container_of 宏技巧, 先找到包含该 kobjectkset (即 subsys), 再找到包含该 ksetsubsys_private 结构体。
  4. 匹配指针: 它比较找到的 subsys_private 结构体中的 bus 成员, 看其地址是否与传入的 bus 指针完全相同。
  5. 增加引用并返回: 如果找到匹配项, 它会调用 subsys_get() (这通常是 kobject_get 的一个封装) 来增加该 subsys_private 对象的引用计数, 然后释放自旋锁并返回指针。增加引用计数是一个契约: 调用者在使用完返回的指针后, 必须调用 subsys_put() 来减少引用计数。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
/**
* bus_to_subsys - 将一个 struct bus_type 指针转换为一个 struct subsys_private 指针
*
* @bus: 指向要查找的 struct bus_type 的指针
*
* 驱动核心的内部机制需要处理 subsys_private 结构体, 而不是外部的 struct bus_type 指针.
* 此函数会遍历系统中已注册的总线列表, 找到匹配的那一个, 并返回与之相关的内部 struct subsys_private.
*
* 注意, 如果返回值不为 NULL, 它的引用计数会被增加.
* 当使用完这个指针后, 必须调用一次 subsys_put(), 以便它能被正确释放.
*/
struct subsys_private *bus_to_subsys(const struct bus_type *bus)
{
struct subsys_private *sp = NULL;
struct kobject *kobj;

/*
* 检查 bus 指针和全局的 bus_kset (代表 /sys/bus) 是否有效.
*/
if (!bus || !bus_kset)
return NULL;

/*
* 获取全局总线列表的自旋锁.
* 在单核抢占式系统上, 这将禁用内核抢占, 确保在遍历列表时不会被中断.
*/
spin_lock(&bus_kset->list_lock);

/*
* 检查列表是否为空, 如果为空则直接跳转到 done.
*/
if (list_empty(&bus_kset->list))
goto done;

/*
* 遍历 bus_kset->list 链表中的每一个 kobject.
*/
list_for_each_entry(kobj, &bus_kset->list, entry) {
/*
* 使用 container_of 宏, 从 kobject 成员找到其容器 kset.
* 这里的 kset 实际上就是 subsys_private 结构体中的 subsys 成员.
*/
struct kset *kset = container_of(kobj, struct kset, kobj);

/*
* 再次使用 container_of 宏, 从 subsys (kset) 成员找到其最终的容器 subsys_private.
*/
sp = container_of_const(kset, struct subsys_private, subsys);
/*
* 比较在 subsys_private 中存储的 bus 指针是否与我们要查找的 bus 指针相同.
*/
if (sp->bus == bus)
/*
* 如果找到了, 跳转到 done 标签, 此时 sp 指向目标结构体.
*/
goto done;
}
/*
* 如果遍历完整个列表都没有找到, 将 sp 显式设置为 NULL.
*/
sp = NULL;
done:
/*
* 调用 subsys_get(sp). 如果 sp 不为 NULL, 此函数会增加其引用计数.
* 如果 sp 为 NULL, 此函数什么也不做, 直接返回 NULL.
*/
sp = subsys_get(sp);
/*
* 释放自旋锁, 如果之前禁用了抢占, 则重新启用.
*/
spin_unlock(&bus_kset->list_lock);
/*
* 返回找到的 subsys_private 指针 (如果找到) 或 NULL (如果没找到).
*/
return sp;
}

bus_create_file: 为总线创建属性文件

此函数的作用是在指定总线的 sysfs 目录下创建一个属性文件。例如, bus_register 函数就会调用它来创建 /sys/bus/<bus_name>/uevent 这个标准文件。

工作原理:

  1. 它首先通过 bus_to_subsys() 函数将外部可见的 struct bus_type 指针转换为内核内部使用的 struct subsys_private 指针。这是必需的, 因为真正的 kobject (代表 sysfs 目录) 存放在 subsys_private 结构中。
  2. 获取到 subsys_private 结构后, 它就得到了代表总线目录的 kobject (&sp->subsys.kobj)。
  3. 然后, 它调用通用的 sysfs_create_file() 函数, 将具体的属性 (attr) 和目标目录 (kobject) 传递给 sysfs 核心层, 由 sysfs 核心层完成在虚拟文件系统中的文件创建工作。
  4. 最后, 它调用 subsys_put() 来释放之前通过 bus_to_subsys() 获得的引用, 保持引用计数的平衡。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
/*
* bus_create_file: 在一个总线的 sysfs 目录下创建一个属性文件.
* @bus: 指向目标总线类型的 const struct bus_type 指针.
* @attr: 指向要创建的总线属性结构体的指针. 该结构体定义了文件名和读写操作.
* @return: 成功时返回 0, 失败时返回负的错误码.
*/
int bus_create_file(const struct bus_type *bus, struct bus_attribute *attr)
{
/*
* 调用 bus_to_subsys() 函数, 将公开的 bus 类型指针转换为内核内部使用的 subsys_private 指针.
* 这个查找操作会增加 subsys_private 对象的引用计数.
*/
struct subsys_private *sp = bus_to_subsys(bus);
int error;

/*
* 检查 bus_to_subsys() 是否查找失败. 如果 sp 为 NULL, 说明总线无效或未注册.
*/
if (!sp)
/*
* 返回 -EINVAL (无效参数) 错误.
*/
return -EINVAL;

/*
* 调用 sysfs_create_file() 来执行实际的文件创建操作.
* &sp->subsys.kobj: 这是总线目录对应的 kobject, 告诉 sysfs 在哪里创建文件.
* &attr->attr: bus_attribute 结构体中内嵌了一个标准的 attribute 结构体, 它包含了文件名和操作方法.
*/
error = sysfs_create_file(&sp->subsys.kobj, &attr->attr);

/*
* 调用 subsys_put() 减少 subsys_private 对象的引用计数, 与前面的 bus_to_subsys() 调用配对.
* 这是必需的, 以确保在不再需要时可以正确释放相关内存.
*/
subsys_put(sp);
/*
* 返回 sysfs_create_file() 的执行结果.
*/
return error;
}
/*
* 将 bus_create_file 函数导出, 使其对其他内核模块可用 (遵循GPL许可证).
*/
EXPORT_SYMBOL_GPL(bus_create_file);

bus_register: 注册一个总线类型

此函数是Linux设备模型的核心功能之一。它的作用是接收一个 struct bus_type 的定义, 并在内核中注册一个新的总线。这个注册过程不仅仅是记录一个名字, 它会创建一整套的数据结构和 sysfs 目录层次, 为之后在该总线上注册设备和驱动程序奠定基础。

其核心原理是:

  1. 分配私有数据结构: 函数首先分配一个 subsys_private 结构体, 这个结构体将作为该总线在内核中的核心管理单元, 存放着总线的所有设备列表、驱动列表、锁以及 sysfs 相关的 kobjectkset
  2. 创建sysfs目录: 它会在 sysfs 文件系统中创建一系列目录。首先是总线的主目录, 即 /sys/bus/<总线名>。然后, 在这个主目录下, 创建两个至关重要的子目录:
    • devices: 用于存放链接到此总线上的所有设备的符号链接。
    • drivers: 用于存放注册到此总线上的所有驱动程序的目录。
  3. 初始化内部结构: 函数会初始化管理设备和驱动所需的链表 (klist) 和用于保护这些共享数据在并发访问时不出错的互斥锁 (mutex)。
  4. 创建属性文件: 它还会在总线目录下创建一些标准的属性文件, 例如 uevent (用于触发用户空间事件), drivers_probe (手动触发探测), 和 drivers_autoprobe (控制自动探测的开关)。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
/**
* bus_register - 注册一个驱动核心的子系统(即总线)
* @bus: 需要被注册的总线
*
* 一旦我们有了它, 我们就用kobject基础设施来注册这个总线,
* 然后注册它所拥有的子系统: 属于这个总线的设备和驱动.
*/
int bus_register(const struct bus_type *bus)
{
int retval;
struct subsys_private *priv;
struct kobject *bus_kobj;
struct lock_class_key *key;

/*
* 使用 kzalloc 分配一个 subsys_private 结构体的内存. 这个结构体将包含此总线的所有内部管理数据.
* GFP_KERNEL 标志表示这是一个常规的内核内存分配, 在单核系统上, 如果内存不足, 进程可以休眠等待.
*/
priv = kzalloc(sizeof(struct subsys_private), GFP_KERNEL);
/*
* 如果内存分配失败, 返回 "内存不足" 错误码.
*/
if (!priv)
return -ENOMEM;

/*
* 将传入的 bus_type 结构体指针保存到私有数据中.
*/
priv->bus = bus;

/*
* 初始化一个阻塞通知链的头部. 其他内核模块可以通过这个通知链来监听此总线上发生的事件(如设备增删).
*/
BLOCKING_INIT_NOTIFIER_HEAD(&priv->bus_notifier);

/*
* 获取私有数据中内嵌的 kset (priv->subsys) 里的 kobject 指针.
* 这个 kobject 将代表总线自身, 即 /sys/bus/<bus->name>.
*/
bus_kobj = &priv->subsys.kobj;
/*
* 使用总线的名字来设置 kobject 的名字. 如果失败 (如名字无效), 则跳转到 out 标签进行清理.
*/
retval = kobject_set_name(bus_kobj, "%s", bus->name);
if (retval)
goto out;

/*
* 将此总线的 kobject 的 kset 设置为 bus_kset.
* bus_kset 是所有总线的父 kset, 代表 /sys/bus 目录.
*/
bus_kobj->kset = bus_kset;
/*
* 将 kobject 的类型设置为 bus_ktype.
* bus_ktype 定义了总线 kobject 的通用行为, 如 sysfs 文件的 release 方法.
*/
bus_kobj->ktype = &bus_ktype;
/*
* 默认启用此总线上驱动程序的自动探测功能.
*/
priv->drivers_autoprobe = 1;

/*
* 注册总线的 kset (它本身也是一个 kset, 因为它将包含 drivers 和 devices 两个 kset).
* 这步操作会在 /sys/bus/ 下创建名为 <bus->name> 的目录.
*/
retval = kset_register(&priv->subsys);
if (retval)
goto out;

/*
* 为总线创建一个标准的 "uevent" 属性文件.
* 向这个文件写入内容可以触发 uevent 事件.
*/
retval = bus_create_file(bus, &bus_attr_uevent);
if (retval)
goto bus_uevent_fail;

/*
* 在总线目录下, 创建并添加一个名为 "devices" 的 kset (目录).
* 这个目录将用于容纳属于此总线的所有设备的符号链接.
*/
priv->devices_kset = kset_create_and_add("devices", NULL, bus_kobj);
if (!priv->devices_kset) {
retval = -ENOMEM;
goto bus_devices_fail;
}

/*
* 在总线目录下, 创建并添加一个名为 "drivers" 的 kset (目录).
* 这个目录将用于容纳属于此总线的所有驱动程序的目录.
*/
priv->drivers_kset = kset_create_and_add("drivers", NULL, bus_kobj);
if (!priv->drivers_kset) {
retval = -ENOMEM;
goto bus_drivers_fail;
}

/*
* 初始化一个链表头, 用于管理注册到此总线上的所有 subsys_interface.
*/
INIT_LIST_HEAD(&priv->interfaces);
/*
* 为锁依赖检查器 (lockdep) 注册一个新的锁类别. 这是内核的调试功能, 用于发现潜在的死锁.
*/
key = &priv->lock_key;
lockdep_register_key(key);
/*
* 初始化互斥锁. 在单核抢占式系统上, 这个锁保护 priv 结构中的共享数据(如interfaces, klist_devices等)
* 不被并发访问而破坏.
*/
__mutex_init(&priv->mutex, "subsys mutex", key);
/*
* 初始化用于管理设备列表的 klist. klist 是一种带引用计数的链表, 允许安全的遍历和移除操作.
*/
klist_init(&priv->klist_devices, klist_devices_get, klist_devices_put);
/*
* 初始化用于管理驱动列表的 klist.
*/
klist_init(&priv->klist_drivers, NULL, NULL);

/*
* 为总线添加探测相关的 sysfs 文件 (如 drivers_probe 和 drivers_autoprobe).
*/
retval = add_probe_files(bus);
if (retval)
goto bus_probe_files_fail;

/*
* 如果 bus->bus_groups 定义了额外的属性文件组, 在此创建它们.
*/
retval = sysfs_create_groups(bus_kobj, bus->bus_groups);
if (retval)
goto bus_groups_fail;

/*
* 打印调试信息, 表示总线注册成功.
*/
pr_debug("bus: '%s': registered\n", bus->name);
return 0;

/* --- 错误处理回滚路径 --- */
/* 如果创建 bus_groups 失败 */
bus_groups_fail:
remove_probe_files(bus);
/* 如果创建 probe 文件失败 */
bus_probe_files_fail:
kset_unregister(priv->drivers_kset);
/* 如果创建 drivers kset 失败 */
bus_drivers_fail:
kset_unregister(priv->devices_kset);
/* 如果创建 devices kset 失败 */
bus_devices_fail:
bus_remove_file(bus, &bus_attr_uevent);
/* 如果创建 uevent 文件失败 */
bus_uevent_fail:
kset_unregister(&priv->subsys);
/*
* 注意: 上面的 kset_unregister() 会在其内部调用 kobject_put(), 最终释放 priv 指向的内存.
* 因此, 将 priv 设置为 NULL, 避免在 out 标签中被重复释放.
*/
priv = NULL;
/* 如果 kobject_set_name 或 kset_register 失败 */
out:
/*
* 释放 priv 的内存 (除非它已经在上面的 kset_unregister 中被释放了).
*/
kfree(priv);
return retval;
}
/*
* 将 bus_register 函数导出, 使其对其他内核模块可用 (遵循GPL许可证).
*/
EXPORT_SYMBOL_GPL(bus_register);

bus_add_driver: 将驱动程序添加到总线

此函数是 driver_register 的核心工作者。它的作用是执行将一个驱动程序添加到指定总线所需的所有底层操作。这包括为驱动程序分配内部管理结构, 在sysfs中创建其目录, 将其添加到总线的驱动列表中, 并立即触发一次对总线上现有设备的探测。

工作原理:

  1. 获取总线并分配私有结构: 它首先通过 bus_to_subsys 找到总线对应的内部 subsys_private 结构。然后, 它为驱动程序分配一个 driver_private 结构体, 这个私有结构将用于存放驱动的内部状态, 例如其 kobject 和它所绑定的设备列表。
  2. 创建Sysfs目录: 调用 kobject_init_and_add, 在总线的 drivers 目录下 (即 /sys/bus/<bus_name>/drivers/) 创建一个以驱动程序名字命名的目录。这是通过初始化 driver_private 中的 kobject 并将其添加到 sp->drivers_kset 来实现的。
  3. 加入总线驱动列表: 它将驱动的私有结构中的 knode_bus 节点添加到总线的 klist_drivers 链表中。从此刻起, 该驱动就正式成为总线的一部分, 可以被 driver_find 等函数找到。
  4. 触发探测: 如果总线开启了自动探测功能 (drivers_autoprobe), 它会立即调用 driver_attachdriver_attach 会遍历该总线上所有尚未绑定驱动的设备, 并尝试将这个新注册的驱动与它们进行匹配和 probe
  5. 创建属性文件和链接: 它会为驱动程序创建一系列标准的 sysfs 文件, 如 uevent, bind, unbind, 并建立到其所属内核模块的链接。
  6. 严格的错误回滚: 该函数使用了典型的内核 goto 链来进行错误处理。如果在任何一步失败, 它会按照与添加时相反的顺序, 精确地撤销所有已经成功的操作(例如, 从链表中删除、注销kobject), 从而保证即使注册失败, 系统也能回到一个干净一致的状态。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
/**
* bus_add_driver - 将一个驱动程序添加到总线.
* @drv: 要添加的驱动程序.
*/
int bus_add_driver(struct device_driver *drv)
{
/*
* 将公开的 bus 类型指针转换为内部的 subsys_private 指针.
*/
struct subsys_private *sp = bus_to_subsys(drv->bus);
/*
* 定义一个指向 driver_private 的指针. 这个结构体包含驱动在设备模型核心中的内部状态.
*/
struct driver_private *priv;
int error = 0;

if (!sp)
return -EINVAL;

/*
* 此刻 sp 中的引用计数已经被增加, 它将在驱动从总线移除时被减少.
*/
pr_debug("bus: '%s': add driver %s\n", sp->bus->name, drv->name);

/*
* 为驱动的私有数据分配内存.
*/
priv = kzalloc(sizeof(*priv), GFP_KERNEL);
if (!priv) {
error = -ENOMEM;
goto out_put_bus;
}
/*
* 初始化一个klist, 它将用于链接所有绑定到此驱动的设备.
*/
klist_init(&priv->klist_devices, NULL, NULL);
/*
* 在私有结构和公开的驱动结构之间建立双向链接.
*/
priv->driver = drv;
drv->p = priv;
/*
* 设置此驱动 kobject 所属的 kset. 它的父目录将是总线的 'drivers' 目录.
*/
priv->kobj.kset = sp->drivers_kset;
/*
* 初始化并添加驱动的 kobject. 这会在 /sys/bus/<bus_name>/drivers/ 下创建名为 <drv->name> 的目录.
*/
error = kobject_init_and_add(&priv->kobj, &driver_ktype, NULL,
"%s", drv->name);
if (error)
goto out_unregister;

/*
* 将驱动添加到总线的驱动 klist 链表的尾部. 从此刻起, 驱动在总线上可见.
*/
klist_add_tail(&priv->knode_bus, &sp->klist_drivers);
/*
* 如果总线允许自动探测.
*/
if (sp->drivers_autoprobe) {
/*
* 调用 driver_attach(), 尝试将这个新驱动与总线上所有未绑定的设备进行匹配.
*/
error = driver_attach(drv);
if (error)
goto out_del_list;
}
/*
* 如果驱动属于一个内核模块, 建立模块和驱动之间的 sysfs 链接.
*/
error = module_add_driver(drv->owner, drv);
if (error) {
printk(KERN_ERR "%s: failed to create module links for %s\n",
__func__, drv->name);
goto out_detach;
}

/*
* 为驱动创建标准的 "uevent" 属性文件.
*/
error = driver_create_file(drv, &driver_attr_uevent);
if (error) {
printk(KERN_ERR "%s: uevent attr (%s) failed\n",
__func__, drv->name);
}
/*
* 为驱动添加由总线定义的通用驱动属性文件组.
*/
error = driver_add_groups(drv, sp->bus->drv_groups);
if (error) {
/* 出现这种错误很难恢复, 只能放弃并打印错误. */
printk(KERN_ERR "%s: driver_add_groups(%s) failed\n",
__func__, drv->name);
}

/*
* 如果驱动没有禁止 "bind/unbind" 属性文件.
*/
if (!drv->suppress_bind_attrs) {
/*
* 创建 "bind" 和 "unbind" sysfs 文件, 允许用户空间手动绑定/解绑设备.
*/
error = add_bind_files(drv);
if (error) {
/* 同上, 很难恢复. */
printk(KERN_ERR "%s: add_bind_files(%s) failed\n",
__func__, drv->name);
}
}

return 0;

/* --- 错误处理回滚路径 --- */
out_detach:
/* 如果模块链接失败, 先将驱动从所有设备上分离. */
driver_detach(drv);
out_del_list:
/* 如果附加设备失败, 将驱动从总线的 klist 中移除. */
klist_del(&priv->knode_bus);
out_unregister:
/* 如果添加到klist失败, 注销 kobject (这会删除 sysfs 目录并释放priv). */
kobject_put(&priv->kobj);
/* drv->p 在 kobject 的 release 回调函数 driver_release 中被释放, 这里先清空. */
drv->p = NULL;
out_put_bus:
/* 最后, 减少对总线 subsys_private 的引用计数. */
subsys_put(sp);
return error;
}

subsys_register: 注册一个子系统并为其创建根设备的核心实现

此函数是真正的执行者。它不仅注册了总线类型,还创建了一个与之同名的“根设备”,并将其放置在指定的父目录下。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
/*
* @subsys: 指向要注册的总线类型 (bus_type) 的指针.
* @groups: 一个属性组指针数组, 这些属性将被添加到创建的根设备上.
* @parent_of_root: 指向一个kobject, 它将作为根设备的父目录.
*/
static int subsys_register(const struct bus_type *subsys,
const struct attribute_group **groups,
struct kobject *parent_of_root)
{
/*
* 定义一个指向 subsys_private 的指针. 这是与总线关联的私有数据结构.
*/
struct subsys_private *sp;
/*
* 定义一个指向 device 的指针. 这将是我们动态创建的"假"根设备.
*/
struct device *dev;
/*
* 定义一个整型变量 err, 用于存储函数调用的返回值.
*/
int err;

/*
* 第一步: 调用 bus_register() 注册总线类型.
* 这会在sysfs中创建 /sys/bus/<subsys->name> 目录.
*/
err = bus_register(subsys);
if (err < 0)
return err;

/*
* 从总线获取其内嵌的私有数据结构.
*/
sp = bus_to_subsys(subsys);
if (!sp) {
err = -EINVAL;
goto err_sp; // 如果失败, 跳转到错误处理标签.
}

/*
* 第二步: 为"假"根设备分配内存.
* kzalloc 分配内存并将其清零. GFP_KERNEL 表示在分配时当前任务可以休眠.
*/
dev = kzalloc(sizeof(struct device), GFP_KERNEL);
if (!dev) {
err = -ENOMEM;
goto err_dev;
}

/*
* 使用总线的名称来命名这个新设备.
*/
err = dev_set_name(dev, "%s", subsys->name);
if (err < 0)
goto err_name;

/*
* 设置此设备的父kobject, 决定了它在sysfs设备树中的位置.
*/
dev->kobj.parent = parent_of_root;
/*
* 将传入的属性组关联到这个设备. 这些属性将作为文件出现在设备的sysfs目录下.
*/
dev->groups = groups;
/*
* 设置一个release函数. 当此设备的最后一个引用被释放时, 内核会调用此函数来释放设备内存.
* 这是防止内存泄漏的关键.
*/
dev->release = system_root_device_release;

/*
* 第三步: 调用 device_register() 正式注册这个新创建的设备.
* 这会使其出现在sysfs中.
*/
err = device_register(dev);
if (err < 0)
goto err_dev_reg;

/*
* 将这个新创建的根设备保存到总线的私有数据中, 以便将来可以找到它.
*/
sp->dev_root = dev;
/*
* 减少对私有数据的引用计数 (因为 bus_to_subsys 增加了它).
*/
subsys_put(sp);
return 0;

/*
* 这是标准的内核错误处理流程, 使用 goto 进行清理.
* 如果注册过程中的某一步失败, 程序会跳转到对应的标签,
* 并按相反的顺序执行所有已完成步骤的清理操作.
*/
err_dev_reg:
put_device(dev); // 撤销设备注册.
dev = NULL;
err_name:
kfree(dev); // 释放为设备分配的内存.
err_dev:
subsys_put(sp); // 减少私有数据的引用计数.
err_sp:
bus_unregister(subsys); // 撤销总线注册.
return err;
}

subsys_system_register: 在/sys/devices/system/下注册子系统的封装函数

这是一个导出的、供其他内核模块使用的API。它是一个简单的封装器,其唯一目的是调用subsys_register,并明确指定根设备的父目录为系统的system_kset,即/sys/devices/system/

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
/**
* subsys_system_register - 在 /sys/devices/system/ 目录下注册一个子系统
* @subsys: 要注册的系统子系统
* @groups: 根设备的默认属性
*
* 所有'系统'子系统都有一个名为 /sys/devices/system/<name> 的根设备.
* 这个根设备可以携带子系统范围的属性. 所有注册的设备都位于这个根设备之下,
* 并以子系统的名称加上一个简单的枚举编号来命名.
*
* 不要将此接口用于任何新事物, 它的存在只是为了兼容过去的坏主意.
* 新的子系统应该使用普通的子系统; 子系统范围的属性应该被添加到子系统目录
* (/sys/bus/<name>) 本身, 而不是创建一个位于 /sys/devices/system/<name> 的假根设备.
*/
int subsys_system_register(const struct bus_type *subsys,
const struct attribute_group **groups)
{
/*
* 调用核心实现函数, 并将第三个参数硬编码为 &system_kset->kobj.
* system_kset 是内核中代表 /sys/devices/system/ 目录的kset对象.
* 它的kobj成员就代表了这个目录本身.
*/
return subsys_register(subsys, groups, &system_kset->kobj);
}
/*
* 将此函数符号导出, 以便其他GPL许可证的内核模块(如CPU驱动)可以调用它.
*/
EXPORT_SYMBOL_GPL(subsys_system_register);

subsys_virtual_register 在/sys/devices/virtual/下注册子系统的封装函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
/**
* subsys_virtual_register - 在 /sys/devices/virtual/ 注册一个子系统
* @subsys:虚拟子系统
* @groups:根设备的默认属性
*
* 所有“虚拟”子系统都有一个 /sys/devices/system/<name> 根设备
* 替换为子系统的名称。 根设备可以在子系统范围内传输
*属性。 所有注册设备都位于此单个根设备下方。
* 设备命名没有限制。 这是针对内核软件的
* 需要 sysfs 接口的构造。
*/
int subsys_virtual_register(const struct bus_type *subsys,
const struct attribute_group **groups)
{
struct kobject *virtual_dir;

virtual_dir = virtual_device_parent();
if (!virtual_dir)
return -ENOMEM;

return subsys_register(subsys, groups, virtual_dir);
}
EXPORT_SYMBOL_GPL(subsys_virtual_register);

bus_for_each_dev 遍历总线上的所有设备

本代码片段定义了Linux设备模型中的一个基础且关键的功能函数:bus_for_each_dev。其核心作用是提供一个通用的机制,用于安全地遍历挂载在特定总线类型(如I2C, SPI, USB等)上的所有设备,并对每个设备执行一个由调用者提供的回调函数。这是一个核心的辅助函数,被各种子系统(例如I2C核心)用来实现更高级的功能,如将新注册的驱动与总线上所有已存在的设备进行匹配。

实现原理分析

此函数的实现依赖于内核中的klist数据结构,这是一种专门为安全遍历而设计的链表,即使在遍历过程中有节点被移除,遍历器也不会失效。

  1. 获取设备链表: 函数首先通过bus_to_subsysbus_type结构体中获取其私有数据subsys_private。这个私有结构体中包含了一个名为klist_devicesklist,它维护着所有注册到该总线上的设备。
  2. 初始化迭代器: 使用klist_iter_init_node来初始化一个klist迭代器i。如果调用者提供了start设备,迭代将从该设备的下一个节点开始;否则,从链表的头部开始。
  3. 循环遍历: while循环是遍历的核心。
    • 在每次循环中,调用辅助函数next_devicenext_device内部调用klist_next(&i)来获取klist中的下一个节点(klist_node)。
    • next_device接着将klist_node指针转换为其宿主结构体device_private的指针,并最终从中提取出struct device指针返回。
    • 如果next_device返回一个有效的设备指针dev,则调用用户传入的回调函数fn(dev, data)
  4. 提前终止: bus_for_each_dev会检查回调函数fn的返回值。如果返回值不为0,循环将立即终止,并将该返回值作为bus_for_each_dev的最终返回值。这为查找特定设备等场景提供了高效的提前退出机制。
  5. 资源清理: 遍历结束后(无论是正常完成还是提前终止),klist_iter_exit被调用来释放迭代器所持有的资源(如内部锁)。subsys_put则递减subsys_private的引用计数。

代码分析

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
// next_device: 从klist迭代器中获取下一个设备指针。
// @i: 指向klist迭代器的指针。
// 返回值: 成功则返回下一个 struct device 的指针,到达链表末尾则返回NULL。
static struct device *next_device(struct klist_iter *i)
{
// 获取迭代器指向的下一个klist节点。
struct klist_node *n = klist_next(i);
struct device *dev = NULL;
struct device_private *dev_prv;

// 如果节点存在(即未到链表末尾)。
if (n) {
// 将klist_node指针转换为其宿主结构体device_private的指针。
dev_prv = to_device_private_bus(n);
// 从device_private结构体中获取实际的device结构体指针。
dev = dev_prv->device;
}
return dev;
}

// bus_for_each_dev: 设备迭代器函数。
// @bus: 要遍历的总线类型。
// @start: 迭代的起始设备(如果为NULL,则从头开始)。
// @data: 传递给回调函数的私有数据。
// @fn: 将为每个设备调用的回调函数。
// 描述:
// 遍历 @bus 的设备列表,并为每个设备调用 @fn 函数。
// 如果 @start 不为NULL,则从该设备的下一个设备开始遍历。
// 每次都会检查 @fn 的返回值,如果返回任何非0值,则中断遍历并返回该值。
int bus_for_each_dev(const struct bus_type *bus, struct device *start,
void *data, device_iter_t fn)
{
struct subsys_private *sp = bus_to_subsys(bus);
struct klist_iter i;
struct device *dev;
int error = 0;

// 如果总线没有对应的私有子系统数据,则为无效参数。
if (!sp)
return -EINVAL;

// 初始化klist迭代器。如果提供了start设备,则从其节点开始;否则从头开始。
klist_iter_init_node(&sp->klist_devices, &i,
(start ? &start->p->knode_bus : NULL));
// 循环直到出错或遍历完所有设备。
// 在循环的条件判断中调用next_device来获取下一个设备。
while (!error && (dev = next_device(&i)))
// 为获取到的设备调用回调函数,并将其返回值赋给error。
error = fn(dev, data);
// 遍历结束,清理并释放迭代器。
klist_iter_exit(&i);
// 递减子系统私有数据的引用计数。
subsys_put(sp);
// 返回错误码(如果回调函数提前退出)或0(如果遍历完成)。
return error;
}
// 导出符号,使得内核其他部分可以使用此函数。
EXPORT_SYMBOL_GPL(bus_for_each_dev);