[TOC]

drivers/base

drivers/base 是Linux内核源代码中的一个核心目录,它并非一项独立的技术,而是内核设备模型(Linux Device Model)的基础实现。这个模型是一个统一的、抽象的框架,用于管理系统中的所有设备、驱动程序以及它们之间的关系。理解drivers/base就是理解现代Linux内核如何看待和管理硬件。

历史与背景

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

在Linux内核2.5版本之前,内核中没有一个统一的结构来表示设备和驱动程序。驱动程序的编写和管理方式较为混乱,存在诸多问题:

  1. 电源管理困难:没有统一的设备层级结构,很难以正确的顺序对设备进行挂起(suspend)或恢复(resume)操作。例如,USB主控制器必须在USB设备之前被挂起。
  2. 代码冗余:每个子系统(如PCI, USB)都需要自己实现一套管理设备和驱动的机制,导致了大量的重复代码,尤其是在对象生命周期管理(如引用计数)和列表管理方面。
  3. 系统信息不透明:缺乏一种简洁、一致的方式从用户空间查看系统中所有设备的拓扑结构和状态。当时的 /proc 文件系统虽然提供了信息,但结构不够清晰和规范。

为了解决这些问题,Patrick Mochel在2.5开发系列内核中引入了统一设备模型。

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

  • 内核 2.5 系列(约2002-2003年):设备模型被设计和引入,这是其最重要的里程碑。kobjectkset 等核心数据结构和 sysfs 文件系统在此时诞生,为整个模型奠定了基础。
  • 内核 2.6 系列:设备模型得到大规模应用和完善,几乎所有的设备驱动都迁移到了这个新模型上。它成为了内核驱动开发的标准方式,并在此后的版本中不断进行优化和扩展。
  • 后续版本至今:设备模型本身已经非常成熟和稳定,后续的迭代主要是围绕它进行功能增强,例如改进即插即用(hotplug)机制、优化电源管理框架、引入设备链接(device links)以更好地处理设备依赖关系等。

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

drivers/base 中的代码是Linux内核最核心和稳定的部分之一,其社区活跃度体现在内核的方方面面。任何新的总线、设备或驱动的开发都必须基于这个模型。它不是一个可选的“应用”,而是编写现代Linux驱动程序的强制性框架。从嵌入式设备到超级计算机,所有运行Linux的系统都依赖于这个模型来管理硬件。

核心原理与设计

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

设备模型的核心思想是建立一个由多个核心数据结构组成的层次化对象系统,来抽象描述硬件拓扑和驱动管理。

  1. kobject (Kernel Object):是设备模型中最基本的构建块。它本身不做太多事情,主要提供通用功能,包括:

    • 引用计数:通过 kref 结构管理对象的生命周期,当引用计数降为零时,对象可以被安全释放。
    • 父子关系:通过一个指向父kobject的指针,将所有对象组织成一个层次化的树状结构。
    • 与sysfs的关联:每个kobject都可以在sysfs文件系统中表现为一个目录。
  2. kset (Kernel Object Set):是一个kobject的集合,用于将相关的kobject分组。kset本身也内嵌了一个kobject,因此它在sysfs中也表现为一个目录,其包含的kobject则成为该目录下的子目录。

  3. 核心结构体:基于kobjectkset,设备模型定义了几个关键的结构体:

    • struct device:代表一个具体的设备(如一个U盘、一个网络接口)。它内嵌了一个kobject,包含了设备的通用信息,如它所属的总线、驱动程序等。
    • struct device_driver:代表一个驱动程序。它定义了驱动能做什么,比如probe(探测设备)和remove(移除设备)等操作。
    • struct bus_type:代表一条总线(如PCI, USB, I2C)。它起着将devicedevice_driver撮合起来的作用,定义了如何匹配设备和驱动的规则(match函数)。
    • struct class:提供一种更高层次的设备视图,将功能相似的设备分组,而不关心它们挂载在哪条总线上。例如,所有的输入设备(鼠标、键盘)都属于input类,用户可以通过/sys/class/input/找到它们。

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

  • 统一的硬件视图:为内核和用户空间提供了系统硬件拓扑的一致性视图,这个视图通过 /sys 文件系统直观地展现出来。
  • 简化的驱动开发:通过提供通用的API和数据结构,减少了驱动开发中的样板代码,让开发者可以专注于硬件相关的逻辑。
  • 精确的电源管理:层次化的设备树使得系统可以按照正确的依赖顺序对设备进行挂起和恢复。
  • 代码复用和维护性:消除了大量冗余代码,使得内核的驱动部分更加简洁和易于维护。
  • 动态设备管理:与ueventudev等用户空间机制结合,实现了强大的即插即用功能。

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

  • 复杂性:对于初学者来说,设备模型的概念(如kobject、ktype、kset之间的关系)比较抽象,学习曲线较陡峭。不正确的引用计数操作很容易导致内存泄漏或系统崩溃。
  • 灵活性带来的风险sysfs允许用户空间直接读写内核对象的属性,这虽然强大但也带来了安全风险。如果驱动没有对来自用户空间的输入进行严格验证,可能导致内核漏洞。
  • 并非万能:虽然设备模型覆盖了绝大多数场景,但对于一些非常简单或者纯虚拟的、不需要电源管理和sysfs表示的内核对象,直接使用此模型可能会带来不必要的开销。

使用场景

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

在Linux内核中,只要是编写与硬件交互的驱动程序,使用设备模型就是唯一且标准的解决方案

  • PCI设备驱动:网卡、显卡等PCI设备。当内核启动时,PCI总线驱动会扫描PCI总线,为发现的每个设备创建一个struct device对象并注册。当一个匹配的PCI驱动模块加载时,其struct device_driver会被注册,总线核心的match逻辑会调用驱动的probe函数,完成设备和驱动的绑定。
  • USB设备驱动:U盘、USB摄像头等。原理与PCI类似,USB主控制器驱动会枚举连接的设备,为其创建device对象。USB设备驱动通过USB设备ID来匹配,并进行绑定。
  • 平台设备(Platform Device):主要用于嵌入式系统中,那些无法被总线自动枚举的、集成在SoC(System on Chip)上的设备。开发者通常在板级支持文件(Board Support Package)中硬编码定义platform_device,然后由匹配的platform_driver来驱动。

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

基本上没有不推荐使用该模型的驱动开发场景。即使是纯软件的虚拟设备,如果希望它能被用户空间工具(如udev)识别和管理,或者需要与其他设备进行统一的电源管理,也应该使用设备模型。绕过设备模型意味着放弃了内核提供的所有现代驱动基础设施。

对比分析

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

在这里,我们将设备模型与“前设备模型时代”的驱动注册方式以及Windows的驱动模型(WDM)进行对比。

与“前设备模型时代”的Linux驱动

特性 设备模型 (drivers/base) 前设备模型时代
实现方式 统一的 struct devicestruct driver 结构,通过总线进行匹配和绑定。 驱动直接使用 register_chrdev()register_blkdev() 等函数注册,没有统一的设备抽象。
资源占用 引入了kobject等数据结构,有轻微的内存开销,但通过代码复用,整体上可能更优。 每个驱动或子系统自管理,数据结构分散。
隔离级别 明确分离了设备(数据)和驱动(逻辑),以及总线(匹配),层次清晰。 设备和驱动逻辑紧密耦合。
系统视图 通过 sysfs 提供清晰的设备树层次结构。 主要通过 /proc,信息分散,不成体系。
电源管理 内建支持,可以进行有序的挂起/恢复。 难以实现全局、有序的电源管理。

与 Windows 驱动模型 (WDM)

特性 Linux 设备模型 Windows 驱动模型 (WDM)
实现方式 驱动作为内核的一部分(或动态模块),直接与内核API交互,代码开源。 分层模型(总线驱动、功能驱动、过滤驱动),通过I/O请求包(IRP)进行通信。
API/ABI 稳定性 没有稳定的内核内部API/ABI。驱动必须随内核版本更新重新编译。这保证了内核可以快速演进和优化。 提供稳定的二进制接口(ABI),理论上一个驱动可以用于多个Windows版本,但实际上每次大版本更新也常需重写。
开发模型 鼓励驱动代码进入内核主线,由社区共同维护。 驱动通常由硬件厂商独立开发和分发,代码闭源。
性能开销 直接函数调用,通信路径相对较短,开销较小。 基于IRP的层级间通信,涉及更多的数据结构和处理步骤,开销相对较大。
隔离级别 逻辑隔离清晰,但所有驱动都运行在内核空间(Ring 0),一个有缺陷的驱动可以导致整个系统崩溃。 WDM的层次化结构提供了一定的隔离,但同样运行在内核空间。

入门实践 (Hands-on Practice)

“可以提供一个简单的入门教程或关键命令列表吗?例如,如何安装、启动第一个服务等。”

drivers/base是内核的一部分,无法“安装”。入门实践主要是学习如何编写一个使用设备模型的简单驱动。以下是一个极简的平台设备驱动示例框架:

  1. 编写驱动代码 (my_driver.c):

    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
    #include <linux/module.h>
    #include <linux/platform_device.h>

    // 当驱动与设备匹配时调用
    static int my_probe(struct platform_device *pdev)
    {
    // pdev->dev 提供了通用的device结构
    dev_info(&pdev->dev, "my_probe() called!\n");
    // 此处添加获取硬件资源、初始化设备的代码
    return 0;
    }

    // 当设备被移除或驱动卸载时调用
    static int my_remove(struct platform_device *pdev)
    {
    dev_info(&pdev->dev, "my_remove() called!\n");
    // 此处添加释放资源的代码
    return 0;
    }

    // 定义平台驱动结构
    static struct platform_driver my_pdrv = {
    .probe = my_probe,
    .remove = my_remove,
    .driver = {
    .name = "my-platform-device", // 这个名字必须与设备匹配
    },
    };

    // 模块加载时注册驱动
    module_platform_driver(my_pdrv);

    MODULE_LICENSE("GPL");
    MODULE_AUTHOR("Your Name");
    MODULE_DESCRIPTION("A simple platform driver");
  2. 编写设备定义代码 (my_device.c 或在板级文件中)

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    #include <linux/module.h>
    #include <linux/platform_device.h>

    static struct platform_device *my_pdev;

    static int __init my_device_init(void)
    {
    // 注册一个平台设备
    my_pdev = platform_device_register_simple("my-platform-device", -1, NULL, 0);
    return 0;
    }

    static void __exit my_device_exit(void)
    {
    platform_device_unregister(my_pdev);
    }

    module_init(my_device_init);
    module_exit(my_device_exit);
    MODULE_LICENSE("GPL");
  3. 编译和加载

    • 为每个文件创建简单的 Makefile
    • 使用 make 编译生成 .ko 文件(内核模块)。
    • 使用 sudo insmod my_device.ko 加载设备模块。
    • 使用 sudo insmod my_driver.ko 加载驱动模块。
    • 使用 dmesg 查看内核日志,应该能看到 “my_probe() called!” 的消息。
    • 使用 sudo rmmod my_driversudo rmmod my_device 卸载模块。

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

  • 引用计数错误:忘记在使用kobjectdevice后调用put_device()kobject_put(),导致资源无法释放。
  • 并发问题:驱动中的函数(如probe, remove, sysfsstore/show)可能被并发调用,必须使用锁(如mutex)来保护共享数据。
  • 与用户空间的数据交换:从sysfsstore函数或ioctl中接收用户数据时,必须使用copy_from_user(),向用户空间写入时必须使用copy_to_user(),直接访问用户空间指针是危险的。
  • 资源清理不彻底:在probe函数中申请的所有资源(内存、中断、IO端口等),都必须在probe失败的错误路径和remove函数中被彻底释放。使用devm_*系列的资源管理函数可以极大地简化这一点。
  • 在错误的时间注册:在驱动初始化早期(early_init)阶段,设备模型的某些部分可能还未就绪,需要注意注册时机。

安全考量 (Security Aspects)

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

  • Sysfs接口漏洞:这是最主要的安全风险来源。如果驱动创建了一个可写的sysfs属性,但没有对用户写入的数据进行严格的长度和内容校验,就可能导致缓冲区溢出、整数溢出或其他类型的内存破坏,从而可能引发本地权限提升。
  • 信息泄露:通过sysfs暴露的属性可能会泄露内核内存地址或其他敏感信息,这会帮助攻击者绕过KASLR(内核地址空间布局随机化)等安全机制。
  • 拒绝服务(DoS):一个设计不佳的sysfs处理函数可能会在处理某些输入时进入死循环或执行非常耗时的操作,使得用户空间程序可以通过访问该sysfs文件来使内核线程或整个系统失去响应。

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

  • 最小权限原则:只通过sysfs暴露绝对必要的信息和控制点。文件权限应尽可能设为只读。
  • 严格的输入验证:对所有来自用户空间(通过sysfsstore函数)的输入进行严格的边界检查和合法性验证。
  • 使用标准接口:尽可能使用内核提供的标准子系统(如gpio, leds, regulator)而不是自己创建自定义的sysfs接口。这些子系统通常已经过了充分的审查。
  • 代码审查:驱动代码,尤其是处理用户输入的sysfs部分,是内核安全审查的重点。
  • 静态分析工具:使用如SparseCoverity等工具可以帮助发现一些潜在的编程错误。
  • 模糊测试(Fuzzing):使用syzkaller等内核模糊测试工具可以有效地发现sysfs接口中的漏洞。

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

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

  • sysfs:它是设备模型在用户空间的直接体现,是生态的核心部分。
  • udev/systemd-udevd:用户空间的核心守护进程,它监听内核发出的uevent事件(当设备添加或移除时产生),并根据规则在/dev目录下创建设备节点、加载驱动模块或执行其他配置任务。
  • lsusb, lspci, lscpu:这些命令行工具通过读取sysfs中的信息来展示USB、PCI和CPU等设备的详细信息。
  • debugfs:一个专门用于调试的虚拟文件系统,驱动开发者可以用它来输出内部状态信息,比printk更灵活。
  • perf:强大的性能分析工具,可以用来分析驱动程序中的性能瓶颈。

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

  • 官方文档:Linux内核源码树中的 Documentation/driver-api/driver-model/ 目录是关于设备模型最权威的文档。
  • 书籍:《Linux Device Drivers, 3rd Edition》 (LDD3) by Jonathan Corbet, Alessandro Rubini, and Greg Kroah-Hartman。虽然有些年头,但其关于设备模型的章节依然是经典。
  • 在线资源:LWN.net 网站上有大量关于内核开发和设备模型的深度文章。
  • 社区:Linux内核邮件列表(LKML)是所有内核开发讨论的中心。特定子系统(如USB, PCI)也有各自的邮件列表,是提问和获取帮助的最佳场所。

性能与监控 (Performance & Monitoring)

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

drivers/base本身作为框架,其性能开销通常很小,并被视为内核的整体开销。对其本身的监控并不常见。监控的重点在于它所管理的具体设备驱动的性能。

  • 使用 perf:可以用perf来剖析特定驱动中的函数调用,找出热点路径。例如,分析网络驱动的中断处理函数或数据包收发函数的CPU占用率。
  • ftrace:内核内置的跟踪工具,可以用来跟踪驱动中特定函数的调用延迟、调用栈等,对于调试性能问题非常有用。
  • 通过sysfs暴露的统计信息:许多驱动会通过sysfs暴露性能计数器,例如网络驱动会暴露收发包数、错误数等。可以监控这些文件的变化。

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

性能调优通常针对具体的驱动而非drivers/base框架本身。

  • 中断处理:将耗时的工作从中断上半部(hardirq)移到下半部(softirq, tasklet, workqueue),以减少关中断的时间,提高系统响应性。
  • DMA(直接内存访问):尽可能使用DMA来传输数据,将CPU从繁重的内存拷贝工作中解放出来。
  • 内存管理:避免在原子上下文(如中断处理或持有自旋锁时)进行可能睡眠的内存分配(如kmalloc)。使用内存池(mempool)来预分配对象,减少分配延迟。
  • 并发与锁:选择合适的锁机制。在高性能场景下,避免使用大的全局锁,尝试使用更细粒度的锁或无锁数据结构。

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

设备模型已经非常成熟,其核心不太可能有颠覆性变化。未来的发展主要集中在以下方面:

  • 异步化:为了加速系统启动,内核社区一直在推动设备和驱动的异步探测(probe)。这意味着设备的初始化可以并行进行,而不是串行等待。
  • 安全性增强:随着对内核安全性的日益重视,对sysfs等用户-内核接口的审查和加固会持续进行。
  • 与新技术的集成:随着CXL(Compute Express Link)等新总线技术的出现,设备模型会不断扩展以支持这些新技术。此外,将Rust语言引入内核开发,也可能在未来影响驱动的编写方式,带来更高的内存安全性。

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

目前没有替代Linux设备模型的讨论。它的设计非常成功和稳固,已经成为Linux内核的基石。任何改进都将是在现有框架内的演进,而非替代。一些微内核架构的操作系统(如Fuchsia的Zircon)使用了不同的驱动模型,但这与Linux的宏内核设计哲学不同,不构成直接的竞争或替代关系。

总结

drivers/base所实现的Linux设备模型是内核驱动开发的支柱。它通过一套优雅的抽象(kobject、device、driver、bus、class)解决了早期内核在电源管理、代码冗余和系统可见性方面的核心痛点。

关键特性总结

  • 统一抽象:为所有设备和驱动提供统一的数据结构和生命周期管理。
  • 层次化结构:以树状结构表示系统硬件拓扑。
  • Sysfs集成:将内核对象模型直接映射到/sys文件系统,提供给用户空间。
  • 强大的电源管理和即插即用:为现代操作系统核心功能提供基础。

学习该技术的要点建议

  1. 理解核心概念:首先要牢固掌握kobject的引用计数和父子关系,这是理解一切的基础。
  2. 阅读源码:直接阅读drivers/base/下的源码,以及一个简单总线(如platform总线)的实现,是最好的学习方式。
  3. 动手实践:从编写一个简单的平台设备驱动开始,亲自体验注册devicedriver,以及它们如何通过probe函数绑定的过程。
  4. 学习sysfs:尝试在你的驱动中创建只读和可读写的sysfs属性,并理解其背后的安全 implications。
  5. 掌握资源管理:熟练使用devm_*系列的接口,它们能让你的驱动代码更简洁、更安全。

include/linux/fwnode.h

fwnode_init

1
2
3
4
5
6
7
static inline void fwnode_init(struct fwnode_handle *fwnode,
const struct fwnode_operations *ops)
{
fwnode->ops = ops;
INIT_LIST_HEAD(&fwnode->consumers);
INIT_LIST_HEAD(&fwnode->suppliers);
}

drivers/base/init.c

driver_init 初始化驱动模型

  • 此函数是内核启动早期被调用的一个关键的聚合初始化函数。它的职责不是初始化任何具体的设备驱动,而是 构建起整个Linux设备模型(Linux Driver Model)的核心基础设施。这个模型是内核中用于表示设备、驱动以及它们之间关系的一整套数据结构和机制,它通过sysfs文件系统(挂载于/sys)向用户空间提供了一个层次分明的视图。
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
/**
* driver_init - 初始化驱动模型。
*
* 调用驱动模型的初始化函数来初始化它们的子系统。
* 在init/main.c中被早期调用。
*/
// `__init` 宏指示编译器将此函数放入".init.text"内存段,
// 内核启动完成后会释放此段内存,以节约RAM。
void __init driver_init(void)
{
/* 这些是核心中的核心组件 */

// 初始化“后备设备信息”(Backing Device Info)子系统。
// 它与块设备的I/O调度和脏页回写策略相关。
// `noop_backing_dev_info` 是一个空操作的默认BDI实例,
// 为系统提供一个基础的、安全的默认状态。
bdi_init(&noop_backing_dev_info);

// 初始化devtmpfs,这是一个在内核中实现的临时文件系统,
// 它的作用是在驱动程序发现并注册设备时,自动在/dev目录下创建设备节点。
// 这在嵌入式系统中极为重要,避免了手动创建设备文件。
devtmpfs_init();

// 初始化设备核心,创建sysfs文件系统的根目录 /sys/devices。
// 这是整个设备模型层次结构的顶级入口点。
devices_init();

// 初始化总线子系统,创建 /sys/bus 目录。
// 内核中不同类型的总线(如platform, i2c, spi)会在此目录下注册,
// 这是实现驱动与设备分离和绑定的关键。
buses_init();

// 初始化类子系统,创建 /sys/class 目录。
// 它提供了一个设备的用户视图,将不同总线上的同类设备(如gpio, net, rtc)
// 归纳在一起,方便用户访问。
classes_init();

// 初始化固件加载器接口。它提供了 request_firmware() API,
// 允许驱动程序从文件系统请求并加载固件文件到设备中。
// 对于需要加载固件的复杂外设(如外挂的WiFi模块)是必需的。
firmware_init();

// 初始化虚拟机监视器(Hypervisor)接口。
// ARMv7-M架构不支持硬件虚拟化,因此在为STM32编译的内核中,
// 此函数通常是一个空存根(stub),不执行任何有效操作。
hypervisor_init();

/*
* 下面这些也是核心组件,但必须在上述“核心中的核心”组件之后执行。
* 这暗示了它们之间存在初始化顺序的依赖关系。
*/

// 初始化一个“伪总线”(faux bus),用于管理那些不属于任何
// 物理总线的软件定义设备。
faux_bus_init();

// 初始化Open Firmware(OF)核心,即设备树(Device Tree)支持。
// 这在STM32H750平台上至关重要。此函数负责解析由引导加载程序
// 传递给内核的设备树二进制文件(.dtb),并将硬件描述信息转换为
// 内核中的设备节点对象,供驱动程序使用。
of_core_init();

// 初始化平台总线(Platform Bus)。这是SoC(片上系统)中绝大多数
// 片上外设(如定时器、ADC、DMA)所挂载的虚拟总线。
// `of_core_init`之后执行,是因为平台设备通常是在解析设备树后被创建的。
platform_bus_init();

// 初始化辅助总线(Auxiliary Bus)。它用于在主设备和其辅助设备
// 之间建立一对一的强关联,是一种特殊的设备管理机制。
// auxiliary_bus_init();

// 初始化内存设备,创建 /sys/devices/system/memory 目录。
// 用于表示系统中的物理内存块及其状态。
// memory_dev_init();

// 初始化节点设备,创建 /sys/devices/system/node 目录。
// "节点"(Node)是NUMA(非一致性内存访问)架构中的概念。
// STM32H750是UMA架构,非NUMA,此函数在编译时为空存根。
// node_dev_init();

// 初始化CPU设备,创建 /sys/devices/system/cpu 目录。
// 对于单核的STM32,它会创建 /sys/devices/system/cpu/cpu0,
// 允许用户空间查看CPU状态并进行管理(如频率调节)。
cpu_dev_init();

// 初始化容器设备(Container Device),与内存控制组(cgroups)相关。
// 在资源受限的STM32系统上,cgroups功能通常被关闭以节省内存,
// 此时该函数可能为空存根。
container_dev_init();
}

drivers/base/core.c 设备核心(Device Core) 设备模型的运转中枢

drivers/base/core.c 是Linux内核统一设备模型的心脏。它不是一个孤立的功能,而是整个设备模型框架的中央实现和调度器。文件中包含了设备注册与注销、设备与驱动的绑定与解绑、设备生命周期管理、sysfs 核心接口以及电源管理回调等最核心的逻辑。可以说,内核中任何设备的任何状态变化,都离不开core.c中代码的直接或间接执行。

历史与背景

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

在统一设备模型被构想出来时,需要一个集中的地方来实现所有设备和驱动都必须遵循的通用规则和流程。core.c正是为了这个目的而创建的,它解决了以下核心问题:

  1. 通用设备管理:提供一个单一、标准的接口(device_register, device_unregister)来处理系统中所有类型设备的添加和移除,避免每个子系统(PCI, USB等)重复造轮子。
  2. 驱动绑定协调:建立一个通用的机制,在设备或驱动被添加时,主动触发匹配过程,并在匹配成功后,以标准的顺序调用驱动的probe函数来初始化设备。
  3. Sysfs 视图生成:作为kobjectsysfs的直接用户,core.c负责为每个注册的struct device/sys/devices下创建对应的目录和基础属性文件(如uevent, power子目录)。
  4. 生命周期与引用计数:实现围绕struct device的引用计数机制(get_device, put_device),确保在有内核代码或用户空间文件正在使用设备时,该设备的数据结构不会被过早释放,这是维持系统稳定性的关键。
  5. 电源管理框架集成:提供调用设备驱动中电源管理回调函数(suspend, resume)的中心调度点。

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

core.c的发展史就是设备模型本身的发展史。

  • 内核 2.5 系列:与设备模型一同诞生,实现了设备注册、kobject集成和基础的sysfs文件创建。
  • 内核 2.6 系列:功能大规模完善,与udev机制紧密结合的uevent机制在这里实现。probe/remove的调用逻辑变得更加健壮。
  • 异步探测的引入:为了解决多核时代系统启动速度的瓶颈,core.c中加入了对设备异步探测的支持。这允许内核在满足依赖关系的前提下,并行地初始化多个设备,是重要的性能里程碑。
  • 设备链接(Device Links)的实现:为解决复杂的跨总线设备依赖问题(例如,一个I2C设备依赖于某个GPIO控制器先完成初始化),core.c中加入了管理设备链接的逻辑,这使得内核可以更精确地控制设备的探测顺序。
  • 持续的健壮性改进:多年来,core.c中的锁机制、错误处理和资源清理路径一直在不断地被审查和优化,以应对日益复杂的硬件和并发场景。

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

core.c是内核驱动子系统中最核心、最活跃的文件之一。任何对设备模型核心行为的修改、对电源管理的改进、对启动性能的优化,几乎都会涉及到对这个文件的修改。它不是一个可选的应用,而是Linux内核强制性的基础设施,所有设备驱动的运行都依赖于它。

核心原理与设计

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

core.c的核心是围绕struct device的生命周期管理和状态转换。

  1. 设备注册 (device_add):当一个驱动调用如platform_device_add等高级接口时,最终会调用到device_add。这个函数执行一系列关键操作:

    • 初始化内嵌的kobject,并将其添加到设备模型的层次结构中(设置其parent指针)。
    • sysfs中创建对应的目录,例如 /sys/devices/platform/serial8250/tty/ttyS0
    • 创建默认的sysfs属性文件,如uevent
    • 将设备添加到其所属总线的设备列表(bus->p->klist_devices)中。
    • 触发驱动匹配:调用bus_probe_device(dev),这是最关键的一步。此举会遍历总线上的所有驱动,尝试为这个新设备寻找一个“主人”。
  2. 驱动与设备的绑定 (driver_attach -> device_bind_driver -> really_probe)

    • bus_probe_device在总线上找到一个match成功的驱动后,会调用driver_attach
    • driver_attach最终会调用really_probe,该函数负责实际的绑定工作。
    • 它会检查设备的电源状态,确保设备处于可用状态。
    • 调用驱动的 probe 函数ret = drv->probe(dev)。这是驱动获得设备控制权、进行硬件初始化的入口点。
    • 如果probe成功(返回0),则将dev->driver指针指向该驱动,完成绑定。设备和驱动的sysfs目录下会创建符号链接,相互指向对方。
  3. 设备注销 (device_del)

    • 首先,检查设备是否已绑定驱动。如果绑定了,就调用该驱动的remove函数,让驱动释放硬件资源。
    • dev->driver指针设为NULL,解除绑定。
    • 从总线的设备列表中移除该设备。
    • sysfs中移除对应的目录和文件。
    • 从设备模型的父子关系中脱离。
  4. 引用计数

    • get_device(dev)会增加设备内嵌kobject的引用计数。
    • put_device(dev)会减少引用计数。当计数减至零时,会触发一个释放函数(device_release),该函数负责释放struct device本身占用的内存。这套机制确保了只要有任何地方还在“使用”一个设备,它的数据结构就不会消失。

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

  • 逻辑集中化:将所有设备共有的复杂逻辑(注册、绑定、sysfs管理、电源管理)集中实现,极大地降低了驱动开发的复杂性。
  • 强制统一模型:确保了所有驱动都遵循相同的生命周期和状态模型,使得整个驱动子系统行为一致、可预测。
  • 解耦:将设备的枚举(由总线驱动完成)、驱动的匹配(由总线match函数完成)和设备的初始化(由驱动probe函数完成)清晰地分离开。
  • 强大的基础:为sysfsudev、电源管理等高级功能提供了坚实的基础。

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

core.c本身没有“不适用”的场景,因为它是强制性的。其主要特点是复杂性

  • 陡峭的学习曲线:要完全理解core.c中的锁交互(如device_hotplug_lock)、异步执行流程和复杂的错误处理路径,需要对内核有深入的了解。
  • 调试困难:由于其核心地位,core.c中的一个微小bug或竞态条件都可能导致难以复现的系统崩溃,调试起来非常困难。
  • 性能关键点device_addreally_probe的执行路径直接影响系统启动速度和热插拔响应时间,是内核性能优化的重点和难点。

使用场景

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

core.c中的函数是底层API,驱动开发者通常不直接调用它们,而是调用特定总线或子系统提供的封装函数。但理解其场景至关重要。

  • 所有设备驱动开发:无论是编写一个PCI网卡驱动、USB鼠标驱动还是一个嵌入式系统的I2C传感器驱动,其注册、探测、移除的整个生命周期都由core.c中的逻辑来驱动和管理。

    • 例子:当你在驱动中调用pci_register_driver()时,这个函数内部会将你的驱动结构注册到PCI总线,然后遍历总线上所有未被驱动的设备,依次调用driver_attach()来尝试绑定,最终触发你的probe函数。
  • 内核子系统开发:任何需要管理一组虚拟或物理设备的内核子系统,都会基于core.c提供的struct device来进行构建。例如,Linux的输入子系统、块设备层、DRM(Direct Rendering Manager)图形子系统等。

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

没有。在现代Linux内核中,任何需要被表示为一个“设备”的实体,都必须通过core.c提供的这套机制进行管理。绕过它意味着你将失去sysfs、电源管理、热插拔、驱动绑定等所有现代内核特性,这是不可接受的。

对比分析

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

由于core.c是内核内部的核心实现,无法与另一个Linux技术对比。有意义的对比是将其代表的Linux设备模型核心实现与其他操作系统的驱动模型核心进行比较。

特性 Linux (drivers/base/core.c) Windows Driver Model (WDM) Apple macOS (I/O Kit)
实现方式 C语言,过程式。设备和驱动是数据结构,通过函数指针回调进行交互。代码开源。 C语言,分层模型。通过I/O请求包(IRP)在驱动栈之间传递消息。 C++的子集,面向对象。设备和驱动都是对象,通过方法调用和消息传递交互。
绑定/探测 core.c中的bus_probe_devicereally_probe主动发起,直接调用驱动的probe函数。 由即插即用(PnP)管理器负责构建设备栈,并向各层驱动发送IRP_MN_START_DEVICE请求。 基于“个性字典”(personality dictionary)进行匹配。匹配成功后,I/O Kit会实例化驱动对象并调用其start方法。
API/ABI 无稳定内部API/ABI。驱动必须随内核一同编译。这使得core.c可以随时进行优化和重构。 提供稳定的二进制接口(ABI),理论上驱动可跨版本使用,但实践中常需更新。 提供相对稳定的C++ API,但大版本更新也常需驱动适配。
灵活性 极高。驱动可以直接访问内核的任何部分(这也带来了风险)。core.c的逻辑相对直接。 较高。分层过滤驱动模型提供了强大的扩展性,但IRP的流转也带来了开销和复杂性。 很高。面向对象的设计提供了优雅的抽象,但模型也相对复杂。

入门实践 (Hands-on Practice)

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

开发者不直接使用core.c的函数。实践的关键是理解它的影响,以及使用它的上层API。

关键函数(驱动开发者应知晓其存在和作用)

  • get_device(struct device *dev): 获取对一个设备的引用,增加其引用计数。当你需要在一个异步任务中或者在超出设备生命周期范围的地方安全地使用dev指针时,必须先调用此函数。
  • put_device(struct device *dev): 释放对设备的引用。必须与get_device成对出现,否则会导致资源泄漏。
  • dev_name(const struct device *dev): 安全地获取设备的名字。
  • dev_set_drvdata(struct device *dev, void *data) / dev_get_drvdata(struct device *dev): 在设备的probe函数中,将驱动的私有数据结构与struct device关联起来,方便在其他回调函数(如中断处理、remove)中获取。

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

  • 引用计数错误:这是最常见、最致命的错误。忘记put_device会导致设备无法被移除,内存泄漏;错误地多调用put_device会导致悬空指针和use-after-free漏洞。
  • probe中的错误处理probe函数中申请了多种资源(内存、中断、I/O等),如果在中途失败,必须将所有已成功申请的资源按申请的逆序精确释放。使用devm_*系列的资源管理函数可以极大地简化这一点。
  • probe返回-EPROBE_DEFER:当你的设备依赖的另一个设备(如一个时钟或电源regulator)尚未准备好时,probe函数应该返回-EPROBE_DEFERcore.c中的逻辑会捕获这个特定的错误码,并在稍后自动重试探测你的设备。如果不这样做,你的设备将永远无法工作。
  • 并发与锁probe/remove函数和设备的sysfs属性回调函数可能在不同上下文中并发执行。必须使用锁来保护驱动的内部状态。

安全考量 (Security Aspects)

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

core.c本身是内核安全审查的重点,其代码被认为是高度可信的。安全风险主要体现在驱动程序如何与core.c提供的机制进行交互。

  • Use-After-Free:由于引用计数错误或removesysfs回调之间的竞态条件,驱动可能访问已经被core.c释放的struct device或其私有数据,这是最严重的安全漏洞之一。
  • Sysfs 接口漏洞:虽然sysfs文件的创建由core.c发起,但文件的内容和行为由驱动定义。驱动中的show/store回调函数如果存在缓冲区溢出或信息泄露,将直接导致内核漏洞。
  • 竞态条件:在热插拔或驱动绑定/解绑的瞬间,是竞态条件的高发区。例如,一个uevent已经发往用户空间,用户空间程序尝试操作设备,而此时remove函数正在清理资源,就可能导致崩溃。core.c使用device_hotplug_lock来序列化这些操作,但驱动内部仍需保持警惕。

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

  • 使用devm_*资源管理:这些函数将资源的生命周期与设备的生命周期绑定,在设备解绑时自动释放资源,极大地减少了错误处理代码和泄漏风险。
  • 内核静态/动态分析:使用sparse进行编译时检查,使用KASAN(Kernel Address Sanitizer)等动态工具来检测use-after-free和内存越界。
  • 代码审查:遵循内核编码规范,对锁的使用、引用计数和错误处理路径进行严格的同行评审。
  • Fuzzing:使用syzkaller等工具对驱动的sysfs接口和ioctl进行模糊测试,以发现潜在漏洞。

生态系统与社区、性能与监控、未来趋势

这三个方面对于core.c来说,与整个设备模型是高度一致的。

  • 生态系统与社区
    • 核心工具udev/systemd-udevdsysfs文件系统。
    • 社区:由内核驱动子系统维护者Greg Kroah-Hartman领导,在Linux内核邮件列表(LKML)上进行讨论。
  • 性能与监控
    • 关键指标:设备探测时间,直接影响系统启动速度。
    • 监控工具systemd-analyzeftraceperf
    • 调优技巧:核心是异步探测(Asynchronous Probing)。通过将驱动标记为可异步探测,可以让core.c的调度逻辑将其与其他不相关的驱动并行初始化。
  • 未来趋势
    • 发展方向:持续优化启动时间和热插拔性能,特别是在拥有数千个设备的服务器和虚拟化环境中。不断增强设备链接等依赖管理机制,以适应更复杂的硬件设计。
    • 替代方案没有core.c所代表的统一设备模型是Linux内核的基石,所有的发展都将是在此基础上的演进和增强。

总结

drivers/base/core.c是Linux设备模型的引擎和大脑。它不为任何特定类型的设备服务,而是为所有设备提供了一个统一的、强制性的管理框架。它负责设备的注册、驱动的绑定、生命周期的维护、sysfs的呈现以及与电源管理的交互。

关键特性总结

  • 集中化控制:所有设备生命周期中的关键节点都由core.c调度。
  • 标准执行流程:定义了标准的probe/remove调用时机和顺序。
  • 引用计数:通过get/put_device提供了健壮的内存和资源管理基础。
  • 性能优化:支持异步探测,是优化系统启动时间的关键。

学习该技术的要点建议

  1. 从上层开始:不要一头扎进core.c的源码。先熟练掌握如何使用一种总线(如platformpci)编写驱动。
  2. 理解生命周期:清晰地画出设备从注册到注销,驱动从加载到卸载,两者之间proberemove被调用的完整流程图。
  3. 掌握核心API:深刻理解devm_*系列函数、dev_get/set_drvdataget/put_device的正确用法和必要性。
  4. 跟踪与调试:当你对上层API熟悉后,使用ftrace等工具来跟踪一个设备注册的完整内核调用栈,看看它是如何一步步执行到core.c中的核心函数的。这是连接抽象概念和具体实现的桥梁。

dev_uevent_filter: 设备uevent事件过滤器

此函数作为一个过滤器, 用于在 uevent 事件发送前进行检查, 判断一个代表设备的 kobject 是否应该生成 uevent. 只有当设备关联到了一个总线(bus)或一个类别(class)时, 它才允许事件继续处理。如果一个设备既没有总线也没有类别, 那么它在 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
48
49
/*
* 静态函数声明: dev_uevent_filter
* 此函数作为 kset_uevent_ops.filter 的实现.
* @kobj: 指向一个 const struct kobject 的指针, 代表正在被检查的内核对象.
* @return: 如果允许 uevent 事件继续, 返回 1; 如果应该被过滤掉, 返回 0.
*/
static int dev_uevent_filter(const struct kobject *kobj)
{
/*
* 定义一个指向 const struct kobj_type 的指针 ktype.
* kobj_type 结构体描述了一类 kobject 的通用属性和操作.
* get_ktype(kobj) 函数用于获取 kobj 关联的 kobj_type.
*/
const struct kobj_type *ktype = get_ktype(kobj);

/*
* 检查此 kobject 的类型是否为 device_ktype.
* device_ktype 是所有 struct device 对象内嵌的 kobject 所使用的标准 kobj_type.
* 这确保了我们只处理代表设备的 kobject.
*/
if (ktype == &device_ktype) {
/*
* 将 const struct kobject 指针转换为 const struct device 指针.
* kobj_to_dev 是一个宏, 利用了 container_of 技巧从内嵌的 kobject 成员找到其容器 device 结构体的地址.
*/
const struct device *dev = kobj_to_dev(kobj);
/*
* 检查设备是否附加到了一个总线 (bus). 如果 dev->bus 指针有效, 说明它是一个总线设备.
*/
if (dev->bus)
/*
* 返回 1, 表示事件通过过滤, 应该被发送.
*/
return 1;
/*
* 如果设备没有附加到总线, 检查它是否属于一个设备类别 (class). 如果 dev->class 指针有效.
*/
if (dev->class)
/*
* 返回 1, 表示事件通过过滤, 应该被发送.
*/
return 1;
}
/*
* 如果 kobject 类型不是 device_ktype, 或者设备既不属于总线也不属于类别, 则返回 0.
* 返回 0 会导致 uevent 事件被丢弃.
*/
return 0;
}

dev_uevent_name: 获取设备uevent的子系统名称

此函数用于确定设备 uevent 事件中的 SUBSYSTEM 环境变量的值。它会检查设备所属的总线或类别, 并将它们的名称作为子系统名称返回。总线名称的优先级高于类别名称。这个子系统名称对于用户空间的 udev/mdev 规则匹配至关重要。

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
/*
* 静态函数声明: dev_uevent_name
* 此函数作为 kset_uevent_ops.name 的实现.
* @kobj: 指向一个 const struct kobject 的指针, 代表正在处理的设备对象.
* @return: 返回一个代表子系统名称的字符串. 如果无法确定, 返回 NULL.
*/
static const char *dev_uevent_name(const struct kobject *kobj)
{
/*
* 将 const struct kobject 指针转换为 const struct device 指针.
*/
const struct device *dev = kobj_to_dev(kobj);

/*
* 检查设备是否附加到了一个总线.
*/
if (dev->bus)
/*
* 如果是, 返回总线的名称 (dev->bus->name) 作为子系统名称.
*/
return dev->bus->name;
/*
* 如果设备没有总线, 检查它是否属于一个设备类别.
*/
if (dev->class)
/*
* 如果是, 返回类别的名称 (dev->class->name) 作为子系统名称.
*/
return dev->class->name;
/*
* 如果既没有总线也没有类别, 返回 NULL.
*/
return NULL;
}

dev_driver_uevent: 为设备uevent添加驱动程序信息

此函数负责向 uevent 的环境变量中添加 DRIVER=<driver_name> 键值对。由于设备的驱动绑定和解绑可能与 uevent 的发送产生并发竞争, 此函数必须小心处理。

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
/*
* dev_driver_uevent: 尝试为设备的 uevent 填充 "DRIVER=<名称>" 环境变量.
*
* @dev: 指向 const struct device 的指针, 代表目标设备.
* @env: 指向 struct kobj_uevent_env 的指针, uevent 的环境变量将添加到这里.
*/
static void dev_driver_uevent(const struct device *dev, struct kobj_uevent_env *env)
{
/*
* 获取设备总线对应的 subsys_private 结构体指针.
* 这个结构体包含了总线的私有数据, 包括其下的驱动程序列表和锁.
*/
struct subsys_private *sp = bus_to_subsys(dev->bus);

/*
* 检查 sp 是否有效 (即设备是否真的有总线).
*/
if (sp) {
/*
* 使用 scoped_guard 宏来自动管理自旋锁. 这是一种现代的C语言写法, 模拟了C++的RAII.
* 它会在进入代码块时获取自旋锁, 在退出代码块时(无论正常退出还是通过goto、return退出)自动释放锁.
* &sp->klist_drivers.k_lock 是总线驱动链表的锁.
* 在单核系统上, 获取这个自旋锁会禁用内核抢占, 保证了花括号内的代码在执行期间不会被其他任务打断.
*/
scoped_guard(spinlock, &sp->klist_drivers.k_lock) {
/*
* 使用 READ_ONCE 宏来安全地读取 dev->driver 指针.
* READ_ONCE 确保了这是一次原子的读取操作, 防止编译器进行不安全的优化(如重新加载或撕裂读),
* 保证我们得到的是一个在某个时间点上确切的指针值, 而不是一个可能被并发修改过程破坏的值.
*/
struct device_driver *drv = READ_ONCE(dev->driver);
/*
* 检查获取到的驱动指针是否有效.
* 因为获取了总线的驱动锁, 如果此刻 drv 不为NULL, 我们可以确保这个驱动结构体不会被释放.
* 这是因为驱动注销过程也需要获取同一个锁.
*/
if (drv)
/*
* 如果驱动存在, 则调用 add_uevent_var 将 "DRIVER=<驱动名>" 添加到环境变量中.
*/
add_uevent_var(env, "DRIVER=%s", drv->name);
} /* 此处 scoped_guard 自动释放锁, 如果之前禁用了抢占, 则会重新启用 */

/*
* 减少对总线子系统的引用计数, 与前面的 bus_to_subsys 调用配对.
*/
subsys_put(sp);
}
}

dev_uevent: 为设备uevent添加核心属性

此函数是设备 uevent 处理的核心, 作为 kset_uevent_ops.uevent 的实现。它负责将代表设备的最重要的一些属性添加到 uevent 环境变量中, 例如设备号(MAJOR, MINOR), 设备节点名(DEVNAME)和权限(DEVMODE)等。这些信息是用户空间 mdev 等工具创建 /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
/*
* 静态函数声明: dev_uevent
* 此函数作为 kset_uevent_ops.uevent 的实现.
* @kobj: 指向一个 const struct kobject 的指针, 代表正在处理的设备对象.
* @env: 指向 struct kobj_uevent_env 的指针, 所有的环境变量都将添加到这里.
* @return: 如果成功返回 0, 如果发生错误 (如内存不足) 返回错误码.
*/
static int dev_uevent(const struct kobject *kobj, struct kobj_uevent_env *env)
{
/*
* 将 const struct kobject 指针转换为 const struct device 指针.
*/
const struct device *dev = kobj_to_dev(kobj);
/*
* 定义一个整型变量 retval, 并初始化为 0 (成功).
*/
int retval = 0;

/*
* 如果设备存在设备节点属性, 则添加它们.
* MAJOR(dev->devt) 宏用于从 dev_t 类型的设备号中提取主设备号.
* 主设备号不为0通常意味着这是一个字符设备或块设备, 应该在/dev下有对应的节点.
*/
if (MAJOR(dev->devt)) {
/*
* 定义一些临时变量来接收 device_get_devnode 返回的属性.
*/
const char *tmp;
const char *name;
umode_t mode = 0; // 文件模式 (权限)
kuid_t uid = GLOBAL_ROOT_UID; // 用户ID
kgid_t gid = GLOBAL_ROOT_GID; // 组ID

/*
* 调用 add_uevent_var 添加 "MAJOR=<主设备号>" 到环境变量.
*/
add_uevent_var(env, "MAJOR=%u", MAJOR(dev->devt));
/*
* 调用 add_uevent_var 添加 "MINOR=<次设备号>" 到环境变量.
*/
add_uevent_var(env, "MINOR=%u", MINOR(dev->devt));
/*
* 调用 device_get_devnode 函数.
* 这个函数会查询设备驱动或类, 以确定它在 /dev 目录下的期望名称、权限和所有权.
* 结果会填充到 name, mode, uid, gid 等变量中.
*/
name = device_get_devnode(dev, &mode, &uid, &gid, &tmp);
/*
* 如果成功获取到设备节点名称.
*/
if (name) {
/*
* 添加 "DEVNAME=<设备名>" 到环境变量. 例如 "DEVNAME=sda1".
*/
add_uevent_var(env, "DEVNAME=%s", name);
/*
* 如果设备指定了特殊的权限模式 (mode不为0).
*/
if (mode)
/*
* 添加 "DEVMODE=<八进制权限>" 到环境变量. 例如 "DEVMODE=0660".
*/
add_uevent_var(env, "DEVMODE=%#o", mode & 0777);
/*
* 如果设备指定了非 root 的用户ID.
*/
if (!uid_eq(uid, GLOBAL_ROOT_UID))
/*
* 添加 "DEVUID=<用户ID>" 到环境变量.
*/
add_uevent_var(env, "DEVUID=%u", from_kuid(&init_user_ns, uid));
/*
* [代码片段缺失] 接下来应该会检查 gid 并添加 DEVUID.
*/

devices_init 设备初始化

  • 此函数是内核设备模型(Driver Model)初始化的入口点之一。它的核心任务是在sysfs虚拟文件系统中,构建起用于表示和管理系统中所有设备所必需的基础目录结构。这个结构是现代Linux内核中驱动、设备和用户空间交互的基石。
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
static const struct kset_uevent_ops device_uevent_ops = {
.filter = dev_uevent_filter,
.name = dev_uevent_name,
.uevent = dev_uevent,
};

// `__init`宏指示编译器将此函数放入特殊的".init.text"段。
// 内核启动后会释放此段内存,这对内存有限的STM32系统是关键优化。
int __init devices_init(void)
{
// 创建一个名为"devices"的kset,并将其添加到sysfs的根目录。
// kset是一组kobject的集合,它会在sysfs中表现为一个目录。
// 此行代码的效果是创建了 /sys/devices/ 目录。
// `&device_uevent_ops` 为这个kset关联了处理uevent(内核事件)的回调函数。
devices_kset = kset_create_and_add("devices", &device_uevent_ops, NULL);
// 如果kset创建失败(通常因为内存不足),则返回错误。
if (!devices_kset)
return -ENOMEM;

// 创建一个名为"dev"的kobject,并将其添加到sysfs的根目录。
// kobject是sysfs中目录项的基本表示。
// 此行代码的效果是创建了 /sys/dev/ 目录。
dev_kobj = kobject_create_and_add("dev", NULL);
if (!dev_kobj)
// 如果失败,则跳转到错误处理标签,以撤销已成功的操作。
goto dev_kobj_err;

// 创建一个名为"block"的kobject,并将其作为`dev_kobj`的子节点。
// 此行代码的效果是创建了 /sys/dev/block/ 目录。
// 该目录将用于存放指向块设备的符号链接。
sysfs_dev_block_kobj = kobject_create_and_add("block", dev_kobj);
if (!sysfs_dev_block_kobj)
goto block_kobj_err;

// 创建一个名为"char"的kobject,并将其作为`dev_kobj`的子节点。
// 此行代码的效果是创建了 /sys/dev/char/ 目录。
// 该目录将用于存放指向字符设备的符号链接。
sysfs_dev_char_kobj = kobject_create_and_add("char", dev_kobj);
if (!sysfs_dev_char_kobj)
goto char_kobj_err;

// 分配一个工作队列(workqueue)。
// 工作队列是一种将工作推迟到内核线程上下文中执行的机制。
// 此队列"device_link_wq"专门用于处理设备链接的异步更新。
// 在单核STM32上,这个工作会被调度到唯一的内核工作线程上执行。
device_link_wq = alloc_workqueue("device_link_wq", 0, 0);
if (!device_link_wq)
goto wq_err;

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

// --- 以下是错误处理的展开(unwind)路径 ---
// 这种goto模式是内核中标准、高效的资源清理方式。
wq_err:
// 如果工作队列分配失败,则释放char kobject。
kobject_put(sysfs_dev_char_kobj);
char_kobj_err:
// 如果char kobject创建失败,则释放block kobject。
kobject_put(sysfs_dev_block_kobj);
block_kobj_err:
// 如果block kobject创建失败,则释放dev kobject。
kobject_put(dev_kobj);
dev_kobj_err:
// 如果dev kobject创建失败,则注销devices kset。
kset_unregister(devices_kset);
// 返回内存不足错误。
return -ENOMEM;
}

device_add 设备添加

  • device_add 函数是 device_register 的第二步,也是核心步骤。它的主要作用是将一个已经通过 device_initialize 准备好的 struct device 对象正式添加到系统中。这个“添加”过程是多方面的,它包括:
    1. 命名设备: 为设备确定一个唯一的名称。
    2. 加入 sysfs: 在 /sys/devices/ 层次结构中为该设备创建对应的目录和属性文件,使其对用户空间可见。
    3. 建立链接: 将设备连接到其父设备、所属的总线和设备类。
    4. 通知系统: 向内核其他部分和用户空间(通过 uevent)宣告新设备的存在。
    5. 触发驱动探测: 最关键的一步,启动总线逻辑来为这个新设备寻找一个匹配的驱动程序并进行绑定(probe)。
  • 一旦 device_add 成功返回,这个设备就被认为是“活的”(live),并完全参与到内核的设备管理、电源管理和驱动模型中
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
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
/*
* device_add - 将设备添加到设备层次结构中
* @dev: 指向要添加的设备结构体
*
* 这是 device_register() 的第二部分,也可以在 device_initialize() 之后单独调用。
* 此函数通过 kobject_add() 将设备添加到 kobject 层次结构中,将其添加到
* 全局和同级的设备列表中,然后将其添加到驱动模型的其他相关子系统中。
*
* 注意:此函数执行成功后,应该调用 device_del() 来移除设备。如果此函数
* 执行失败,则 *只能* 使用 put_device() 来减少引用计数。
*/
int device_add(struct device *dev)
{
struct subsys_private *sp; // 指向类(class)子系统私有数据的指针
struct device *parent; // 指向父设备的指针
struct kobject *kobj; // 指向父 kobject 的指针
struct class_interface *class_intf; // 用于遍历类接口的指针
int error = -EINVAL; // 默认的错误码,EINVAL 表示无效参数
struct kobject *glue_dir = NULL; // 用于 class/device 符号链接的粘合目录

dev = get_device(dev); // 增加设备的引用计数,防止在操作过程中被意外释放
if (!dev) // 如果传入的 dev 为空,则直接出错返回
goto done;

// 如果设备的私有数据部分(dev->p)尚未分配,则进行初始化。
// dev->p 用于存放驱动模型核心内部使用的数据。
if (!dev->p) {
error = device_private_init(dev);
if (error)
goto done;
}

/*
* 对于静态分配的设备,需要初始化其名称。
* dev->init_name 通常在编译时由驱动程序设置。
* dev_set_name 会将 init_name 复制到动态分配的 dev->kobj.name 中。
*/
if (dev->init_name) {
error = dev_set_name(dev, "%s", dev->init_name);
dev->init_name = NULL; // 使用后清空,防止重复使用
}

// 检查设备名称是否已成功设置。
// dev_name(dev) 返回 dev->kobj.name。
if (dev_name(dev)) {
error = 0; // 名字已存在,清除错误码
} else if (dev->bus && dev->bus->dev_name) {
// 如果设备没有名字,但其所属的总线有命名规则(如 "spidev"),
// 则使用总线名和设备ID来生成一个唯一的名字,例如 "spidev1"。
error = dev_set_name(dev, "%s%u", dev->bus->dev_name, dev->id);
} else {
// 如果以上条件都不满足,则无法为设备命名,这是一个致命错误。
error = -EINVAL;
}
if (error)
goto name_error; // 如果命名失败,跳转到错误处理

// 打印一条调试信息,显示正在添加的设备名称。
pr_debug("device: '%s': %s\n", dev_name(dev), __func__);

// 获取父设备的引用。
parent = get_device(dev->parent);
// 确定本设备在 sysfs 中的父目录 kobject。通常就是 parent->kobj。
kobj = get_device_parent(dev, parent);
if (IS_ERR(kobj)) { // 检查 get_device_parent 是否返回了错误指针
error = PTR_ERR(kobj);
goto parent_error;
}
if (kobj)
dev->kobj.parent = kobj; // 设置本设备的 kobject 的父对象

// 如果本设备没有指定 NUMA 节点,则继承父设备的 NUMA 节点。
// 在 STM32 上,这总是 NUMA_NO_NODE。
if (parent && (dev_to_node(dev) == NUMA_NO_NODE))
set_dev_node(dev, dev_to_node(parent));

/*
* 核心步骤:将本设备的 kobject 添加到内核对象层次结构中。
* 这会在 sysfs 中创建对应的目录,例如 /sys/devices/platform/soc/40013000.spi。
* 此时设备在 sysfs 中变为可见。
*/
error = kobject_add(&dev->kobj, dev->kobj.parent, NULL);
if (error) {
glue_dir = kobj; // 保存父 kobj 指针用于错误清理
goto Error;
}

// 通知平台代码(如 ACPI 或特定固件),一个新设备已被添加。
device_platform_notify(dev);

// 在设备的 sysfs 目录下创建一个名为 "uevent" 的属性文件。
// 向此文件写入内容可以触发内核向用户空间发送 uevent 事件。
error = device_create_file(dev, &dev_attr_uevent);
if (error)
goto attrError;

// 如果设备属于某个类 (class),则在 /sys/class/ 目录下创建指向本设备的符号链接。
error = device_add_class_symlinks(dev);
if (error)
goto SymlinkError;
// 添加由设备驱动定义的默认属性文件到 sysfs 目录。
error = device_add_attrs(dev);
if (error)
goto AttrsError;
// 将设备注册到其所属的总线上。这会将设备添加到总线的设备链表中。
error = bus_add_device(dev);
if (error)
goto BusError;
// 为设备添加电源管理 (DPM) 相关的 sysfs 文件,如 /sys/.../power/control。
// error = dpm_sysfs_add(dev);
if (error)
goto DPMError;
// 将设备添加到电源管理子系统的活动设备列表中。
// device_pm_add(dev);

// 如果设备有关联的设备号 (dev_t),表示它是一个字符设备或块设备。
if (MAJOR(dev->devt)) {
// 在 sysfs 中创建名为 "dev" 的属性文件,内容为 "主设备号:次设备号"。
error = device_create_file(dev, &dev_attr_dev);
if (error)
goto DevAttrError;

// 在 /sys/dev/char/ 或 /sys/dev/block/ 下创建符号链接。
error = device_create_sys_dev_entry(dev);
if (error)
goto SysEntryError;

// 在 /dev 目录下创建设备节点文件 (例如 /dev/ttySTM0)。
devtmpfs_create_node(dev);
}

// 向总线上的所有其他设备驱动发出 "ADD_DEVICE" 通知。
bus_notify(dev, BUS_NOTIFY_ADD_DEVICE);
// 最终,向用户空间发送一个 KOBJ_ADD 类型的 uevent,通知 udev 等程序。
kobject_uevent(&dev->kobj, KOBJ_ADD);

/*
* 处理设备链接 (device link)。
* 如果本设备(supplier)被其他设备(consumer)所等待,现在就建立链接。
* 这对于处理设备树中定义的依赖关系至关重要。
*/
if (dev->fwnode && !dev->fwnode->dev) {
dev->fwnode->dev = dev;
fw_devlink_link_device(dev);
}

/*
* 关键步骤:触发总线为这个新设备寻找并探测匹配的驱动程序。
* 内核会遍历该总线上的所有驱动,调用它们的 match 函数,
* 如果匹配成功,就调用该驱动的 probe 函数。
*/
bus_probe_device(dev);

// ... (以下是更高级的设备链接和类相关的处理) ...

// 如果有父设备,将本设备添加到父设备的子设备链表中。
if (parent)
klist_add_tail(&dev->p->knode_parent,
&parent->p->klist_children);

// 如果设备属于一个类 (class),将其添加到该类的设备列表中。
sp = class_to_subsys(dev->class);
if (sp) {
mutex_lock(&sp->mutex);
klist_add_tail(&dev->p->knode_class, &sp->klist_devices);

// 通知所有注册到该类的接口,有一个新设备添加了。
list_for_each_entry(class_intf, &sp->interfaces, node)
if (class_intf->add_dev)
class_intf->add_dev(dev);
mutex_unlock(&sp->mutex);
subsys_put(sp);
}
done:
// 在函数开始时增加了引用计数,在这里减少它。
// 如果函数成功,对象的引用计数仍然 > 0。如果失败,这可能是最后一次 put,
// 可能会触发设备的释放。
put_device(dev);
return error; // 返回最终的错误码 (成功时为 0)

/*
* 错误处理回滚栈:
* 从这里开始是层层递进的错误处理代码。如果某一步失败,程序会跳转到
* 对应的标签,然后像剥洋葱一样,反向执行所有已经成功的步骤的逆操作,
* 以保证系统状态的一致性。
*/
SysEntryError:
if (MAJOR(dev->devt))
device_remove_file(dev, &dev_attr_dev);
DevAttrError:
device_pm_remove(dev);
dpm_sysfs_remove(dev);
DPMError:
device_set_driver(dev, NULL);
bus_remove_device(dev);
BusError:
device_remove_attrs(dev);
AttrsError:
device_remove_class_symlinks(dev);
SymlinkError:
device_remove_file(dev, &dev_attr_uevent);
attrError:
device_platform_notify_remove(dev);
kobject_uevent(&dev->kobj, KOBJ_REMOVE);
glue_dir = get_glue_dir(dev);
kobject_del(&dev->kobj); // 从 sysfs 中移除
Error:
cleanup_glue_dir(dev, glue_dir);
parent_error:
put_device(parent); // 释放对父设备的引用
name_error:
kfree(dev->p); // 释放私有数据
dev->p = NULL;
goto done; // 跳转到最后的 put_device
}
EXPORT_SYMBOL_GPL(device_add); // 导出符号给GPL模块使用

device_register 设备注册

  • device_initialize 函数的作用是对一个已经分配了内存的 struct device 结构体进行内部初始化。它并不将设备注册到内核或使其在 sysfs 中可见,而是为后续的注册和使用做准备。
    可以将其理解为 device_register 的第一步。执行完此函数后,这个设备结构体就变成了一个功能完备的内核对象(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
static const struct kobj_type device_ktype = {
.release = device_release,
.sysfs_ops = &dev_sysfs_ops,
.namespace = device_namespace,
.get_ownership = device_get_ownership,
};


/*
* 这是一个内核文档注释 (KernelDoc),用于自动生成文档。
* 它解释了函数的功能、参数和使用时的注意事项。
* 核心内容:
* - 功能: 初始化一个 device 结构体,为后续使用做准备。
* - 参数: @dev,指向要被初始化的设备结构体。
* - 注意: 调用者必须保证 @dev 指向的结构体已被清零 (例如使用 kzalloc 分配)。
* - 注意: 一旦调用此函数,必须使用 put_device() 来释放对设备的引用,而不能直接 free() 内存。
*/
void device_initialize(struct device *dev)
{
/*
* 将此设备的 kobject 归属到 `devices_kset`。
* `devices_kset` 代表 sysfs 中的 `/sys/devices/` 目录。
* 这一步设定了该设备在 sysfs 层次结构中的父目录,但此时还不会创建文件节点。
*/
dev->kobj.kset = devices_kset;

/*
* 初始化设备内嵌的 kobject。kobject 是设备在内核对象模型中的核心。
* kobject_init() 会初始化 kobject 的引用计数为 1,并设置其类型为 `device_ktype`。
* `device_ktype` 定义了这类 kobject 的通用属性,最重要的是定义了 release 函数,
* 该函数会在 kobject 的引用计数降为 0 时被调用,以释放 `struct device` 结构体本身。
*/
kobject_init(&dev->kobj, &device_ktype);

/*
* 初始化一个双向链表头。此链表用于管理与该设备关联的 DMA 内存池。
* 每个池用于分配特定大小的、对 DMA 友好的内存块。
*/
INIT_LIST_HEAD(&dev->dma_pools);

/*
* 初始化设备结构体内的互斥锁。
* 这个互斥锁 (`dev->mutex`) 用于保护整个设备结构体在“睡眠”上下文中的并发访问。
* 例如,在绑定/解绑驱动程序时,需要持有此锁。
* 在单核系统中,这主要用于防止因内核抢占而导致的竞态条件。
*/
mutex_init(&dev->mutex);

/*
* 这是内核锁依赖验证器 (lockdep) 的一个辅助函数。
* 它为这个特定的锁实例设置一个验证类别,用于调试和防止死锁。
* 在常规操作中,此行对功能没有影响,主要用于开发和调试阶段。
*/
lockdep_set_novalidate_class(&dev->mutex);

/*
* 初始化设备资源 (`devres`) 链表的自旋锁。
* devres 机制用于自动管理资源的生命周期。这个自旋锁保护 `devres_head` 链表的并发访问。
* 自旋锁用于在可能被中断上下文访问的临界区进行保护,因为它不会引起睡眠。
*/
spin_lock_init(&dev->devres_lock);

/*
* 初始化设备资源 (`devres`) 链表的头节点。
* 所有通过 `devm_*` 系列函数(如 `devm_kmalloc`)为该设备分配的资源,
* 都会被记录在这个链表中。
*/
INIT_LIST_HEAD(&dev->devres_head);

/*
* 初始化设备结构体中的电源管理相关字段。
* 这为设备参与内核的运行时电源管理 (Runtime PM) 和系统级的睡眠/唤醒 (Suspend/Resume) 做好准备。
*/
device_pm_init(dev);

/*
* 设置设备的 NUMA (非统一内存访问架构) 节点 ID。
* STM32H750 是一个单片机,不具备 NUMA 架构。因此,这里将其设置为 `NUMA_NO_NODE`,
* 这是一个表示“无特定节点”或“不适用”的宏。
* 这体现了 Linux 内核代码的通用性,能够在不同硬件架构下工作。
*/
// set_dev_node(dev, NUMA_NO_NODE);

/* 初始化用于设备链接 (device links) 的三个链表头。*/
/* `consumers` 链表: 记录了哪些其他设备依赖于本设备 (本设备是它们的 "supplier")。*/
INIT_LIST_HEAD(&dev->links.consumers);
/* `suppliers` 链表: 记录了本设备依赖于哪些其他设备 (本设备是它们的 "consumer")。*/
INIT_LIST_HEAD(&dev->links.suppliers);
/* `defer_sync` 链表: 用于在驱动探测期间临时存放需要延迟同步的链接。*/
INIT_LIST_HEAD(&dev->links.defer_sync);

/*
* 设置设备链接的初始状态。
* `DL_DEV_NO_DRIVER` 表示设备当前没有驱动程序与之绑定,
* 这是设备初始化后的默认状态。
*/
dev->links.status = DL_DEV_NO_DRIVER;

/*
* 这是一个条件编译块,其内容是否被编译取决于内核的配置。
* 这些配置项与特定架构是否支持或需要为设备或CPU进行DMA同步操作有关。
*/
#if defined(CONFIG_ARCH_HAS_SYNC_DMA_FOR_DEVICE) || \
defined(CONFIG_ARCH_HAS_SYNC_DMA_FOR_CPU) || \
defined(CONFIG_ARCH_HAS_SYNC_DMA_FOR_CPU_ALL)
/*
* 如果架构需要,则初始化 `dma_coherent` 标志。
* `dma_default_coherent` 是一个全局变量,表示系统默认的 DMA 内存是否是“一致性”的。
* 在 ARM 架构(如STM32H750)上,这意味着CPU缓存和DMA操作之间是否能自动保持数据同步。
* 这个标志会影响 DMA API 的行为。
*/
dev->dma_coherent = dma_default_coherent;
#endif

/*
* 初始化与 SWIOTLB (软件IO转译后备缓冲区) 相关的设备字段。
* SWIOTLB 是一个为不兼容的 DMA 操作提供“bounce buffer”(反弹缓冲区)的软件层。
* 在没有 IOMMU 的 STM32H750 上,如果外设的 DMA 单元只能访问特定地址范围的内存,
* SWIOTLB 就会非常有用。它会从一个低地址的公共缓冲区分配内存,并在真实缓冲区和
* 这个公共缓冲区之间进行数据拷贝,从而实现 DMA。
*/
// swiotlb_dev_init(dev);
}

/*
* 将 `device_initialize` 函数导出,使其可以被内核模块调用。
* `_GPL` 后缀表示只有遵循 GPL 兼容许可证的模块才能使用此函数。
*/
EXPORT_SYMBOL_GPL(device_initialize);
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
/*
* KernelDoc 文档注释。
* 核心内容:
* - 功能: 向系统中注册一个设备。
* - 参数: @dev,指向要注册的设备结构体。
* - 描述: 它清晰地分为两步:初始化设备,然后将其添加到系统中。
* 这是最简单和最常见的用法。
* - 注意: 无论此函数成功还是失败,都绝不能直接释放 @dev。
* 必须使用 put_device() 来放弃在此函数中初始化的引用。
*/
int device_register(struct device *dev)
{
/*
* 第一步:调用 `device_initialize` 对设备结构体进行内部初始化。
* 执行完这一步后,`dev` 成为一个合法的、引用计数的内核对象,
* 但它对系统的其他部分(特别是用户空间)仍然是不可见的。
*/
device_initialize(dev);

/*
* 第二步:调用 `device_add` 将设备正式添加到系统中。
* `device_add` 的主要工作包括:
* 1. 为设备分配一个唯一的设备名 (如果尚未设置)。
* 2. 在 sysfs 的 `/sys/devices/...` 目录下创建对应的目录和属性文件。
* 3. 将设备添加到各种内核链表(如总线上的设备列表)中。
* 4. 发出一个 "add" 类型的 uevent 事件,通知用户空间的 udev 等程序有新设备加入。
* 5. 触发总线为这个新设备寻找并探测(probe)匹配的驱动程序。
* `device_add` 会返回一个整数结果,0表示成功,负数表示错误码。
* `device_register` 直接返回 `device_add` 的结果。
*/
return device_add(dev);
}

/*
* 同样地,将 `device_register` 函数导出给 GPL 兼容的内核模块使用。
*/
EXPORT_SYMBOL_GPL(device_register);

get_device_parent 被添加到系统中的设备 (dev) 确定其在 sysfs 文件系统中的父目

  • get_device_parent 函数的核心作用是为即将被添加到系统中的设备 (dev) 确定其在 sysfs 文件系统中的父目录。这个父目录由一个 kobject(内核对象)表示
  • 需要明确一个关键概念:设备的逻辑父子关系(由 dev->parent 指针定义)和它在 sysfs 中的目录结构并不总是一一对应的。get_device_parent 的任务就是根据设备的类型(是否属于一个类 class)、其逻辑父设备以及总线的规则,来智能地决定它在 /sys/devices/ 下应该挂载到哪个目录下。
  • 其主要目的是为了创建一个清晰、无冲突、易于管理的 sysfs 命名空间。它通过创建所谓的“粘合目录”(glue directories) 来实现这一目标,避免了不同子系统之间的命名冲突。
    • 对于属于 class 的设备:
      1. 它首先确定一个逻辑上的父 kobject (这可能是实际的父设备, 或是一个虚拟目录)。
      2. 然后, 它会在这个逻辑父 kobject 下查找或创建一个名为 “glue” (粘合) 的目录。这个 “glue” 目录的名称与设备的 class 名称相同, 作用是将所有属于同一个 class 的子设备组织在一起。
      3. 例如, 如果一个父设备下有多个 “net” 类的子设备, 此函数会确保它们都位于父设备的 net/ 子目录下。为了保证线程安全, 这个查找和创建过程由互斥锁 (mutex) 和自旋锁 (spinlock) 保护。
    • 对于不属于 class 的设备:
      1. 如果设备属于一个总线 (bus) 并且没有指定父设备, 它会尝试使用该总线的根设备作为父设备。
      2. 如果明确指定了父设备 (parent), 则直接使用该父设备的 kobject 作为父对象。
      3. 如果以上条件都不满足, 则该设备没有父对象。
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
/*
* get_device_parent - 获取设备的 sysfs 父 kobject
* @dev: 需要确定父目录的设备
* @parent: 设备的逻辑父设备 (可能为 NULL)
*
* 返回:
* 指向父 kobject 的指针,该指针的引用计数已被增加。
* 如果发生错误,则返回一个 ERR_PTR() 编码的错误指针。
*/
static struct kobject *get_device_parent(struct device *dev,
struct device *parent)
{
// sp 指向设备所属的类(class)的私有数据结构。
struct subsys_private *sp = class_to_subsys(dev->class);
struct kobject *kobj = NULL;

// 主要决策分支:判断设备是否属于一个类。
if (sp) {
struct kobject *parent_kobj; // 用来存放最终确定的父 kobject
struct kobject *k;

/*
* 如果设备没有逻辑父节点,那么它将存在于 "virtual" 目录下。
* 如果一个类设备(dev)的父设备(parent)不属于任何类,那么为了防止
* 命名空间冲突,这个类设备将存在于一个“粘合”目录中。
*/
if (parent == NULL)
// 父设备为空,则获取虚拟设备目录 (/sys/devices/virtual) 作为父目录。
parent_kobj = virtual_device_parent();
else if (parent->class && !dev->class->ns_type) {
// 优化:如果父设备和子设备都属于类,且子设备的类没有特殊命名空间要求,
// 那么子设备可以直接挂在父设备的 kobject 下,无需创建粘合目录。
subsys_put(sp); // 释放对类私有数据的引用
return &parent->kobj;
} else {
// 默认情况:父 kobject 就是逻辑父设备的 kobject。
parent_kobj = &parent->kobj;
}

// 全局锁,保护粘合目录的查找和创建过程,防止并发冲突。
mutex_lock(&gdp_mutex);

// 在这个类的粘合目录列表(sp->glue_dirs)中查找是否已存在一个父目录为 parent_kobj 的目录。
spin_lock(&sp->glue_dirs.list_lock);
list_for_each_entry(k, &sp->glue_dirs.list, entry)
if (k->parent == parent_kobj) {
kobj = kobject_get(k); // 如果找到,增加其引用计数
break;
}
spin_unlock(&sp->glue_dirs.list_lock);

if (kobj) {
// 如果找到了已经存在的粘合目录,直接返回它。
mutex_unlock(&gdp_mutex);
subsys_put(sp);
return kobj;
}

// 如果没有找到,则在父设备下创建一个新的代表该类的粘合目录。
k = class_dir_create_and_add(sp, parent_kobj);
mutex_unlock(&gdp_mutex);
subsys_put(sp);
return k; // 返回新创建的目录
}

/*
* 对于不属于任何类的设备:
* 某些子系统(总线)可以为它们的设备指定一个默认的根目录。
*/
if (!parent && dev->bus) {
// 如果设备没有逻辑父节点,但它属于一个总线...
struct device *dev_root = bus_get_dev_root(dev->bus);

if (dev_root) {
// ...并且该总线定义了一个根设备,则使用该根设备的 kobject 作为父目录。
kobj = &dev_root->kobj;
put_device(dev_root); // 释放对根设备的引用
return kobj;
}
}

// 最简单的情况:如果设备有逻辑父节点,直接使用父设备的 kobject 作为父目录。
if (parent)
return &parent->kobj;

// 如果以上所有情况都不满足,则设备没有父目录,它将被添加到 /sys/devices/ 的顶层。
return NULL;
}

virtual_device_parent (/sys/devices/virtual)

1
2
3
4
5
6
7
8
9
10
struct kobject *virtual_device_parent(void)
{
static struct kobject *virtual_dir = NULL;

if (!virtual_dir)
virtual_dir = kobject_create_and_add("virtual",
&devices_kset->kobj);

return virtual_dir;
}

此代码片段定义了一组只读的sysfs属性文件, 用于从用户空间查询一个device_link实例的内部状态和配置标志。device_link是内核中用于表示两个设备之间依赖关系(一个“供应者”和一个“消费者”)的机制。这些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
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
/*
* status_show: 'status' sysfs 文件的读操作处理函数.
* 它将设备链接的内部状态枚举值转换成人类可读的字符串.
*
* @dev: 指向代表设备链接的 device 结构体.
* @attr: 指向属性描述符.
* @buf: 用于存放输出字符串的用户缓冲区.
* @return: 写入缓冲区的字节数.
*/
static ssize_t status_show(struct device *dev,
struct device_attribute *attr, char *buf)
{
/*
* 定义一个字符指针, 用于指向最终输出的字符串.
*/
const char *output;

/*
* 使用 switch 语句检查 to_devlink(dev)->status 的值.
* to_devlink(dev) 从通用device指针获取到具体的 device_link 指针.
* .status 成员是一个枚举, 代表了链接当前所处的生命周期状态.
*/
switch (to_devlink(dev)->status) {
case DL_STATE_NONE:
output = "not tracked"; /* 状态: 未追踪 */
break;
case DL_STATE_DORMANT:
output = "dormant"; /* 状态: 休眠 (供应者存在, 消费者不存在) */
break;
case DL_STATE_AVAILABLE:
output = "available"; /* 状态: 可用 (供应者已探测成功) */
break;
case DL_STATE_CONSUMER_PROBE:
output = "consumer probing"; /* 状态: 消费者正在探测 */
break;
case DL_STATE_ACTIVE:
output = "active"; /* 状态: 活跃 (消费者已探测成功) */
break;
case DL_STATE_SUPPLIER_UNBIND:
output = "supplier unbinding"; /* 状态: 供应者正在解绑 */
break;
default:
output = "unknown"; /* 状态: 未知 */
break;
}

/*
* 使用 sysfs_emit 安全地将选定的字符串和换行符写入用户缓冲区.
*/
return sysfs_emit(buf, "%s\n", output);
}
/*
* DEVICE_ATTR_RO 是一个辅助宏, 用于快速定义一个名为 'status' 的只读(RO) sysfs 文件.
* 它会自动创建 device_attribute 实例并将其 .show 回调设置为 status_show.
*/
static DEVICE_ATTR_RO(status);

/*
* auto_remove_on_show: 'auto_remove_on' sysfs 文件的读操作处理函数.
* 它显示了在何种条件下这个设备链接会被自动移除.
*/
static ssize_t auto_remove_on_show(struct device *dev,
struct device_attribute *attr, char *buf)
{
struct device_link *link = to_devlink(dev);
const char *output;

/*
* 使用 device_link_test 检查链接是否设置了 DL_FLAG_AUTOREMOVE_SUPPLIER 标志.
*/
if (device_link_test(link, DL_FLAG_AUTOREMOVE_SUPPLIER))
output = "supplier unbind"; /* 当供应者解绑时自动移除 */
/*
* 检查链接是否设置了 DL_FLAG_AUTOREMOVE_CONSUMER 标志.
*/
else if (device_link_test(link, DL_FLAG_AUTOREMOVE_CONSUMER))
output = "consumer unbind"; /* 当消费者解绑时自动移除 */
else
output = "never"; /* 永不自动移除 */

return sysfs_emit(buf, "%s\n", output);
}
/*
* 定义一个名为 'auto_remove_on' 的只读 sysfs 文件.
*/
static DEVICE_ATTR_RO(auto_remove_on);

/*
* runtime_pm_show: 'runtime_pm' sysfs 文件的读操作处理函数.
* 它显示了这个设备链接是否会影响运行时电源管理(Runtime PM).
*/
static ssize_t runtime_pm_show(struct device *dev,
struct device_attribute *attr, char *buf)
{
struct device_link *link = to_devlink(dev);

/*
* device_link_test(link, DL_FLAG_PM_RUNTIME) 会在标志被设置时返回1, 否则返回0.
* sysfs_emit 将这个整数(1或0)格式化为字符串写入缓冲区.
*/
return sysfs_emit(buf, "%d\n", device_link_test(link, DL_FLAG_PM_RUNTIME));
}
/*
* 定义一个名为 'runtime_pm' 的只读 sysfs 文件.
*/
static DEVICE_ATTR_RO(runtime_pm);

/*
* sync_state_only_show: 'sync_state_only' sysfs 文件的读操作处理函数.
* 它显示了这个链接是否只同步驱动状态, 而不强制探测顺序.
*/
static ssize_t sync_state_only_show(struct device *dev,
struct device_attribute *attr, char *buf)
{
struct device_link *link = to_devlink(dev);

/*
* 检查 DL_FLAG_SYNC_STATE_ONLY 标志, 并将结果(1或0)写入缓冲区.
*/
return sysfs_emit(buf, "%d\n", device_link_test(link, DL_FLAG_SYNC_STATE_ONLY));
}
/*
* 定义一个名为 'sync_state_only' 的只读 sysfs 文件.
*/
static DEVICE_ATTR_RO(sync_state_only);

/*
* 定义一个静态的 struct attribute 指针数组.
* 这个数组包含了所有我们希望在 devlink 设备目录下创建的文件的属性定义.
*/
static struct attribute *devlink_attrs[] = {
&dev_attr_status.attr,
&dev_attr_auto_remove_on.attr,
&dev_attr_runtime_pm.attr,
&dev_attr_sync_state_only.attr,
NULL, /* 数组必须以 NULL 结尾. */
};
/*
* ATTRIBUTE_GROUPS 是一个宏, 用于将属性数组包装成一个或多个属性组.
* 这里我们只定义了一个组, 名为 devlink_groups.
* 这个组可以被一次性注册到 sysfs 中.
*/
ATTRIBUTE_GROUPS(devlink);

此函数是一个回调函数, 在内核注册一个新的device_link实例时被调用。它的核心作用是在sysfs文件系统中创建一组共四个符号链接(symlinks), 用于清晰地展示一个“供应者”(supplier)设备和一个“消费者”(consumer)设备之间的依赖关系。这使得用户和系统工具可以方便地通过文件系统导航来查看和理解设备之间的连接。

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
/*
* 这是一个静态函数, 作为devlink类接口的 .add_dev 回调.
* 当一个代表设备链接的device(dev)被添加到devlink类时, 此函数会被调用.
*
* @dev: 指向被添加的设备. 这个设备本身就代表了一个 "链接".
* @return: 0 表示成功, 负值错误码表示失败.
*/
static int devlink_add_symlinks(struct device *dev)
{
/*
* 定义两个字符指针, 用于存储动态生成的符号链接名称.
* __free(kfree) 是一个GCC扩展属性(__cleanup__), 它能确保在这两个指针离开作用域时,
* 自动调用 kfree() 来释放它们指向的内存. 这是一个现代的C语言技巧, 用于自动资源管理, 能有效防止内存泄漏.
*/
char *buf_con __free(kfree) = NULL, *buf_sup __free(kfree) = NULL;
/*
* ret: 用于存储函数调用的返回值.
*/
int ret;
/*
* to_devlink 是一个宏, 用于从通用的 struct device 指针中获取其所属的 struct device_link 结构体指针.
* struct device_link 包含了关于这个链接的详细信息.
*/
struct device_link *link = to_devlink(dev);
/*
* 从 link 结构体中获取指向 "供应者" 设备的指针.
*/
struct device *sup = link->supplier;
/*
* 从 link 结构体中获取指向 "消费者" 设备的指针.
*/
struct device *con = link->consumer;

/*
* 链接1: 在代表链接的设备目录下, 创建一个名为 "supplier" 的符号链接, 指向供应者设备.
* 路径示例: /sys/class/devlink/devlink0/supplier -> ../../devices/platform/soc/4000e000.i2c
*/
ret = sysfs_create_link(&link->link_dev.kobj, &sup->kobj, "supplier");
if (ret)
goto out; // 如果失败, 直接跳转到末尾退出.

/*
* 链接2: 在代表链接的设备目录下, 创建一个名为 "consumer" 的符号链接, 指向消费者设备.
* 路径示例: /sys/class/devlink/devlink0/consumer -> ../../devices/platform/soc/i2c-1/1-0050
*/
ret = sysfs_create_link(&link->link_dev.kobj, &con->kobj, "consumer");
if (ret)
goto err_con; // 如果失败, 跳转到err_con标签, 清理已创建的链接1.

/*
* 使用 kasprintf 动态地构建一个字符串, 作为即将创建的符号链接的名称.
* GFP_KERNEL 表示这是一个常规的、可能会睡眠的内核内存分配.
* 字符串格式为 "consumer:<总线名称>:<设备名称>", 例如 "consumer:i2c:1-0050".
* 这种命名方式确保了链接名称的唯一性和可读性.
*/
buf_con = kasprintf(GFP_KERNEL, "consumer:%s:%s", dev_bus_name(con), dev_name(con));
if (!buf_con) {
ret = -ENOMEM; // 如果内存分配失败
goto err_con_dev; // 跳转去清理链接1和2.
}

/*
* 链接3: 在供应者设备的目录下, 创建一个以刚才生成的字符串命名的符号链接, 指向代表链接的设备.
* 路径示例: /sys/devices/platform/soc/4000e000.i2c/consumer:i2c:1-0050 -> ../../../class/devlink/devlink0
*/
ret = sysfs_create_link(&sup->kobj, &link->link_dev.kobj, buf_con);
if (ret)
goto err_con_dev; // 如果失败, 跳转去清理链接1和2.

/*
* 动态构建另一个字符串, 用于从消费者指向链接的符号链接名称.
* 格式为 "supplier:<总线名称>:<设备名称>", 例如 "supplier:platform:4000e000.i2c".
*/
buf_sup = kasprintf(GFP_KERNEL, "supplier:%s:%s", dev_bus_name(sup), dev_name(sup));
if (!buf_sup) {
ret = -ENOMEM; // 如果内存分配失败
goto err_sup_dev; // 跳转去清理链接1,2,3.
}

/*
* 链接4: 在消费者设备的目录下, 创建一个以刚才生成的字符串命名的符号链接, 指向代表链接的设备.
* 路径示例: /sys/devices/platform/soc/i2c-1/1-0050/supplier:platform:4000e000.i2c -> ../../../../class/devlink/devlink0
*/
ret = sysfs_create_link(&con->kobj, &link->link_dev.kobj, buf_sup);
if (ret)
goto err_sup_dev; // 如果失败, 跳转去清理链接1,2,3.

/*
* 所有链接都已成功创建, 跳转到末尾退出.
*/
goto out;

/*
* 这是标准的C语言错误处理流程, 使用 goto 来确保在任何步骤失败时,
* 都能按相反的顺序精确地撤销所有已成功的操作.
*/
err_sup_dev:
sysfs_remove_link(&sup->kobj, buf_con); // 清理链接3
err_con_dev:
sysfs_remove_link(&link->link_dev.kobj, "consumer"); // 清理链接2
err_con:
sysfs_remove_link(&link->link_dev.kobj, "supplier"); // 清理链接1
out:
/*
* 返回最终的结果. 如果成功, ret为0; 如果失败, ret为导致失败的错误码.
* buf_con 和 buf_sup 指向的内存会因为 __free 属性而在这里被自动释放.
*/
return ret;
}

此代码片段的作用是在内核中注册 devlink 设备类和其关联的类接口。这个过程会在sysfs中创建一个名为/sys/class/devlink/的目录, 并建立一个机制, 使得每当有设备被添加到这个devlink类时, 都会自动触发预定义的回调函数, 以执行诸如创建符号链接等操作。

devlink 是一个相对较新的内核框架, 旨在为各种复杂的网络设备 (如智能网卡、交换机芯片) 提供一个统一的、与具体总线无关的管理接口。它用于处理那些不适合放在传统网络驱动模型中的功能, 例如固件更新、设备诊断和资源报告等。

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
static const struct class devlink_class = {
.name = "devlink",
.dev_groups = devlink_groups,
.dev_release = devlink_dev_release,
};

/*
* 定义一个静态的 struct class_interface 实例, 名为 devlink_class_intf.
* class_interface 提供了一种机制, 可以在一个类的所有设备上自动执行某些操作.
*/
static struct class_interface devlink_class_intf = {
/*
* .class: 指向此接口所属的类的指针. 在这里, 它指向 devlink_class.
* (devlink_class 的定义不在此代码段中, 但可以推断它是一个 struct class 实例, .name 为 "devlink").
*/
.class = &devlink_class,
/*
* .add_dev: 一个函数指针, 指向一个回调函数.
* 每当有任何设备被添加到 devlink_class 时, 内核都会自动调用 devlink_add_symlinks 这个函数.
* 它的作用是为新添加的设备创建一些有用的符号链接(symlinks)在sysfs中, 以方便用户导航和管理.
*/
.add_dev = devlink_add_symlinks,
/*
* .remove_dev: 一个函数指针, 指向一个回调函数.
* 每当有任何设备从 devlink_class 中被移除时, 内核都会自动调用 devlink_remove_symlinks 这个函数.
* 它的作用是清理之前由 .add_dev 创建的符号链接.
*/
.remove_dev = devlink_remove_symlinks,
};

/*
* devlink_class_init: devlink 类的初始化函数.
* 标记为 __init, 表示此函数仅在内核启动期间执行, 其占用的内存之后可以被回收.
*/
static int __init devlink_class_init(void)
{
/*
* 定义一个整型变量 ret, 用于存储函数调用的返回值.
*/
int ret;

/*
* 步骤1: 注册 devlink 设备类.
* 调用 class_register() 将 devlink_class 注册到内核中.
* 这会在 sysfs 中创建 /sys/class/devlink/ 目录.
*/
ret = class_register(&devlink_class);
/*
* 检查注册是否成功. 如果失败(ret不为0), 直接返回错误码.
*/
if (ret)
return ret;

/*
* 步骤2: 注册 devlink 类的接口.
* 调用 class_interface_register() 将我们上面定义的 devlink_class_intf 注册到内核.
* 从此刻起, 添加/移除 devlink 设备就会自动触发回调.
*/
ret = class_interface_register(&devlink_class_intf);
/*
* 检查接口注册是否成功.
*/
if (ret)
/*
* 如果接口注册失败, 我们必须执行清理操作,
* 将刚刚成功注册的 devlink_class 注销掉, 以避免系统状态不一致.
* 这是一种良好的错误处理实践.
*/
class_unregister(&devlink_class);

/*
* 返回最终的操作结果. 如果成功, ret为0; 如果失败, ret为导致失败的错误码.
*/
return ret;
}
/*
* postcore_initcall 将 devlink_class_init 函数注册为一个初始化调用.
* 这个时机确保了在任何设备驱动尝试注册 devlink 实例之前, devlink 类本身已经准备就绪.
*/
postcore_initcall(devlink_class_init);

设备链接sync_state回调的延迟执行框架

此代码片段揭示了Linux内核设备模型中一个相当高级且精妙的内部机制: 设备链接sync_state回调的暂停、延迟和批量处理框架。它的核心作用是在系统进行大规模设备创建(例如, 在启动时从设备树填充平台设备)的阶段, 暂时”暂停”一个名为sync_state的设备状态同步回调的执行, 将所有本应触发的回调”延迟”并收集起来, 直到”暂停”状态结束后, 再对收集到的设备进行一次性的、批量的状态同步

这个框架的根本原理是避免”回调风暴”(callback storm)并确保依赖关系完整性sync_state回调函数通常在设备之间的依赖关系(即”链接”)建立或改变时被调用, 以便设备可以根据其”供应商”(supplier)的状态来调整自身。如果在of_platform_populate期间每创建一个设备链接就立即触发一次回调, 将会导致成百上千次低效的、可能是过早的函数调用。此框架通过引入”暂停/恢复”机制, 将这些回调合并成一次在更合适时机(通常是所有设备都已创建后)的批量执行, 从而极大地提高了启动效率和系统的健壮性。


核心组件与工作流程

这两个函数是该框架的主控制开关of_platform_populate在开始工作前会调用pause(), 在结束后调用resume()

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
/*
* device_links_supplier_sync_state_pause: 暂停sync_state回调的执行.
*/
void device_links_supplier_sync_state_pause(void)
{
device_links_write_lock(); // 获取全局设备链接写锁.
/*
* defer_sync_state_count 是一个引用计数器.
* 这允许多个调用者嵌套地请求暂停.
*/
defer_sync_state_count++;
device_links_write_unlock();
}

/*
* device_links_supplier_sync_state_resume: 恢复sync_state回调的执行.
*/
void device_links_supplier_sync_state_resume(void)
{
struct device *dev, *tmp;
/* 定义一个临时的本地链表头. */
LIST_HEAD(sync_list);

device_links_write_lock();
if (!defer_sync_state_count) {
WARN(true, "Unmatched sync_state pause/resume!"); // 匹配错误警告.
goto out;
}
defer_sync_state_count--; // 减少引用计数.
if (defer_sync_state_count)
goto out; // 如果计数还不为0, 说明外层还有暂停请求, 直接返回.

/*
* 计数器归零! 开始处理被延迟的设备.
* 遍历全局的 deferred_sync 链表.
*/
list_for_each_entry_safe(dev, tmp, &deferred_sync, links.defer_sync) {
/*
* 将设备从 deferred_sync 链表中移除, 并调用 __device_links_queue_sync_state
* 来重新评估它是否已准备好被同步, 如果是, 则将其加入到临时的 sync_list 中.
*/
list_del_init(&dev->links.defer_sync);
__device_links_queue_sync_state(dev, &sync_list);
}
out:
device_links_write_unlock(); // 释放全局锁.

/* 在没有持有任何全局锁的情况下, 刷新临时的 sync_list, 执行真正的回调. */
device_links_flush_sync_list(&sync_list, NULL);
}

此函数是一个过滤器和队列管理器。它负责判断一个设备当前是否满足被同步的条件, 如果满足, 就将其加入到一个待处理列表中。

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
/*
* __device_links_queue_sync_state: 将一个设备排队等待sync_state()回调.
* @dev: 目标设备.
* @list: 要加入的链表头.
*/
static void __device_links_queue_sync_state(struct device *dev,
struct list_head *list)
{
struct device_link *link;

/* 检查1: 设备驱动是否实现了 sync_state 回调? */
if (!dev_has_sync_state(dev))
return;
/* 检查2: 设备是否已被标记为"已同步"或"已在队列中"? 防止重复添加. */
if (dev->state_synced)
return;

/*
* 关键检查3: 遍历此设备的所有消费者(consumer)链接.
* 一个设备的状态同步通常依赖于其所有消费者都已就绪.
*/
list_for_each_entry(link, &dev->links.consumers, s_node) {
if (!device_link_test(link, DL_FLAG_MANAGED))
continue;
/* 如果有任何一个消费者链接尚未激活, 那么现在同步还为时过早. */
if (link->status != DL_STATE_ACTIVE)
return;
}

/* 所有检查通过! 准备将其加入队列. */
/* 设置标志位, 防止在本次批量处理中被重复添加. */
dev->state_synced = true;

if (WARN_ON(!list_empty(&dev->links.defer_sync)))
return;

get_device(dev); // 增加设备引用计数, 防止在被处理前意外释放.
list_add_tail(&dev->links.defer_sync, list); // 加入到待处理列表.
}

此函数是最终的执行者。它会遍历一个已准备就绪的设备列表, 并为它们一一调用sync_state回调。

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
/*
* device_links_flush_sync_list: 对一个设备列表调用sync_state().
* @list: 包含待处理设备的链表.
* @dont_lock_dev: 一个可选的设备指针, 指示调用者已持有该设备的锁.
*/
static void device_links_flush_sync_list(struct list_head *list,
struct device *dont_lock_dev)
{
struct device *dev, *tmp;

/* 安全地遍历列表, 因为我们会在循环中修改它. */
list_for_each_entry_safe(dev, tmp, list, links.defer_sync) {
/* 从待处理列表中移除. */
list_del_init(&dev->links.defer_sync);

/*
* 为保证线程安全, 在调用sync_state之前, 必须获取该设备自身的锁.
* (除非调用者已告知它持有了该锁).
*/
if (dev != dont_lock_dev)
device_lock(dev);

/* 核心操作: 调用dev_sync_state(), 内部会调用驱动的sync_state回调. */
/*
static inline void dev_sync_state(struct device *dev)
{
if (dev->bus->sync_state)
dev->bus->sync_state(dev);
else if (dev->driver && dev->driver->sync_state)
dev->driver->sync_state(dev);
}
*/
dev_sync_state(dev);

if (dev != dont_lock_dev)
device_unlock(dev);

put_device(dev); // 释放之前get_device()获取的引用.
}
}

4. sync_state_resume_initcall: 最终的保险措施

这是一个late_initcall, 意味着它会在内核启动过程的非常后期被调用。它的作用是一个”保险”, 确保即使有任何pause()调用没有对应的resume()调用, 在启动的最后阶段, 所有被延迟的同步操作也一定会被执行一次。

1
2
3
4
5
6
static int sync_state_resume_initcall(void)
{
device_links_supplier_sync_state_resume();
return 0;
}
late_initcall(sync_state_resume_initcall);

device_shutdown: 关闭系统中的所有设备

此函数是内核关机流程的核心组成部分, 位于kernel_shutdown_prepare之后。它的核心原理是以一种安全、健壮、且遵循依赖关系的方式, 遍历系统中所有已注册的设备, 并调用其驱动程序提供的.shutdown()回调函数, 以执行特定于硬件的最终关闭操作

这个函数的设计体现了对健壮性的极致追求, 其关键机制如下:

  1. 同步与稳定化: 在进入主循环之前, 它首先调用wait_for_device_probe()等待所有正在进行的设备探测完成, 然后调用device_block_probing()禁止任何新的设备探测。这确保了它即将处理的设备列表是一个稳定、不再增加的集合。
  2. 反向顺序遍历: 这是最关键的原则。函数从全局设备链表(devices_kset->list)的尾部向前遍历。由于设备通常是按父子依赖顺序注册的(父设备先注册), 这种反向遍历天然地保证了子设备会在其父设备之前被关闭。例如, 一个USB存储设备会被在其所连接的USB集线器之前关闭, 而USB集线器又会在USB主控制器之前关闭。这个顺序对于避免硬件状态错误和数据损坏至关重要。
  3. 精妙的锁与引用计数管理: 为了在遍历一个全局链表的同时安全地执行可能休眠的shutdown操作, 它采用了一种复杂的”锁-取-删-解锁-处理-重锁”模式。
    • 它首先获取保护全局链表的自旋锁。
    • 然后从链表中摘下一个设备, 并立即释放全局自旋锁。
    • 在释放全局锁之前, 它通过get_device()增加了该设备及其父设备的引用计数。这可以防止在处理当前设备时, 另一个线程(或中断)意外地移除并释放了它的父设备, 从而避免了悬空指针(use-after-free)错误。
    • 接着, 它获取该设备及其父设备的私有互斥锁(device_lock), 以防止与该设备自身的probe/release路径发生竞争。
    • 在所有锁都就绪后, 它才安全地调用驱动的.shutdown()方法。
    • 处理完毕后, 它以相反的顺序释放所有锁和引用计数。
  4. 调用层级: 它会按照class->shutdown_pre -> bus->shutdowndriver->shutdown的顺序尝试调用回调。这提供了一个分层的关闭机制, 允许从更通用(类别)到更具体(驱动)的层面执行清理操作。
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
/**
* device_shutdown - 在每个要关闭的设备上调用 ->shutdown().
*/
void device_shutdown(void)
{
struct device *dev, *parent;

/* 步骤1: 等待所有正在进行的异步设备探测完成. 确保我们有一个完整的设备列表. */
wait_for_device_probe();
/* 步骤2: 阻止任何新的设备探测被启动. 稳定设备列表, 防止在关机时有新设备加入. */
device_block_probing();

/* 步骤3: 暂停CPU频率调节. 在关机期间保持CPU频率稳定, 避免潜在的问题. */
cpufreq_suspend();

/* 步骤4: 获取保护全局设备链表(devices_kset->list)的自旋锁. */
spin_lock(&devices_kset->list_lock);
/*
* 主循环: 反向遍历设备链表, 依次关闭每个设备.
* 需要注意的是, 即使在系统关机期间, 设备的拔出事件也可能开始将设备脱机.
*/
while (!list_empty(&devices_kset->list)) {
/*
* 从链表的尾部(prev)获取设备. 这是为了保证子设备在父设备之前被关闭,
* 维持正确的依赖关系.
*/
dev = list_entry(devices_kset->list.prev, struct device,
kobj.entry);

/*
* 持有设备父节点的引用计数, 以防止在持有父节点锁时, 父节点被释放.
*/
parent = get_device(dev->parent);
/* 持有当前设备的引用计数, 确保在本轮循环中, dev指针始终有效. */
get_device(dev);
/*
* 将设备从kset链表中移除, 以防其驱动的shutdown()方法没有移除它.
* 这是一个安全措施. list_del_init会将节点从链表中删除并重新初始化.
*/
list_del_init(&dev->kobj.entry);
/*
* 释放全局链表锁. 因为后续的shutdown()调用可能休眠,
* 而持有自旋锁是绝对不能休眠的.
*/
spin_unlock(&devices_kset->list_lock);

/* 持有设备私有锁, 以避免与该设备的probe/release路径发生竞争. */
if (parent)
device_lock(parent); /* 先锁父设备, 再锁子设备, 保证一致的锁顺序以避免死锁. */
device_lock(dev);

/* 禁止任何新的运行时挂起(runtime suspend)操作. */
pm_runtime_get_noresume(dev);
/* 等待任何正在进行的运行时电源管理(Runtime PM)操作完成. */
pm_runtime_barrier(dev);

/* 如果设备所属的类别(class)定义了shutdown_pre回调, 则调用它. */
if (dev->class && dev->class->shutdown_pre) {
if (initcall_debug)
dev_info(dev, "shutdown_pre\n");
dev->class->shutdown_pre(dev);
}
/* 如果设备所属的总线(bus)定义了shutdown回调, 则调用它. */
if (dev->bus && dev->bus->shutdown) {
if (initcall_debug)
dev_info(dev, "shutdown\n");
dev->bus->shutdown(dev);
/* 否则, 如果设备绑定的驱动(driver)定义了shutdown回调, 则调用它. */
} else if (dev->driver && dev->driver->shutdown) {
if (initcall_debug)
dev_info(dev, "shutdown\n");
dev->driver->shutdown(dev);
}

/* 以与加锁相反的顺序解锁. */
device_unlock(dev);
if (parent)
device_unlock(parent);

/* 释放之前获取的引用计数. */
put_device(dev);
put_device(parent);

/* 重新获取全局链表锁, 准备处理下一个设备. */
spin_lock(&devices_kset->list_lock);
}
/* 循环结束, 释放最后的全局链表锁. */
spin_unlock(&devices_kset->list_lock);
}

drivers/base/driver.c 驱动程序核心(Driver Core) 驱动模型的抽象与管理

历史与背景

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

drivers/base/driver.c 文件及其相关的驱动核心(Driver Core)代码是为了解决Linux内核在发展过程中面临的驱动管理混乱、代码冗余和缺乏统一抽象的问题而诞生的。

  • 缺乏统一结构:在2.6内核系列之前,不同类型的驱动(如字符设备、块设备、网络设备)有各自独立、互不相通的管理方式。每一种总线(如PCI, USB)也都有自己一套发现设备、匹配驱动的逻辑,导致了大量的重复代码。
  • 无序的初始化:没有一个集中的机制来管理驱动和设备的注册与注销,使得系统初始化和关闭过程变得复杂且容易出错。
  • 电源管理困难:在没有统一设备模型的情况下,要实现一个全局的、有序的电源管理策略(如系统休眠/唤醒)极其困难,因为无法以一种通用的方式遍历和控制系统中的所有设备。
  • 信息无法导出:内核中的设备和驱动关系对用户空间是完全不透明的。管理员无法方便地查看哪些设备正在被哪个驱动程序所控制,也无法进行手动的干预。

为了解决这些问题,Linux内核在2.5开发系列中引入了统一的设备模型(Unified Device Model),而 drivers/base/driver.c 正是这个模型中负责抽象和管理“驱动程序”这一侧的核心组件

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

drivers/base/driver.c 的发展与整个Linux设备模型的演进紧密相连。

  • 2.5/2.6内核 - 统一设备模型诞生:这是最重大的里程碑。struct device_driver 作为一个通用的驱动程序描述符被引入,与 struct devicestruct bus_type 共同构成了设备模型的三大核心结构。driver_register()driver_unregister() 等核心API在此期间被建立起来。
  • sysfs的整合:设备模型的建立与sysfs虚拟文件系统的出现是相辅相成的。driver.c 中的逻辑负责将已注册的驱动程序以目录和文件的形式呈现在 /sys/bus/<bus_name>/drivers/ 路径下,极大地增强了系统的可观测性和可管理性。
  • 手动绑定/解绑:随着sysfs接口的成熟,内核增加了通过用户空间写入sysfs文件来手动将一个设备从其驱动上解绑(unbind),或将一个没有驱动的设备绑定(bind)到指定驱动的功能。这为驱动调试和在同一硬件上切换不同驱动提供了极大的灵活性。
  • 异步探测的支持:为了加速系统启动,设备模型引入了驱动异步探测(Asynchronous Probing)机制。struct device_driver 中也增加了相应的字段来支持这一特性,允许没有依赖关系的驱动并行初始化。

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

drivers/base/driver.c 中的代码是Linux驱动核心的基石,极其稳定且成熟。它不是一个应用层的功能,而是所有内核驱动程序开发都必须依赖的底层框架。所有内核开发者在编写任何类型的设备驱动时,无论是PCI驱动、USB驱动还是平台设备驱动,最终都会通过其总线特有的注册函数(如 pci_driver_register)间接地调用 driver_register(),将其驱动纳入到这个统一的框架中进行管理。

核心原理与设计

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

drivers/base/driver.c 的核心是围绕 struct device_driver 结构体,提供驱动程序的注册、注销和与设备进行匹配(即“绑定”)的通用逻辑。

  1. 驱动抽象:内核定义了通用的 struct device_driver 结构体,其中包含了驱动的名称、所属的总线类型、以及一系列标准的回调函数指针(如 .probe, .remove, .suspend, .resume 等)。 各种具体的驱动(如pci_driver, usb_driver)都会内嵌一个struct device_driver实例。
  2. 驱动注册:当一个驱动模块被加载时,它会调用 driver_register() 函数(通常由总线特定的封装函数如 platform_driver_register 间接调用)。 该函数主要做两件事:
    • 将该驱动程序添加到一个全局的、按总线类型组织的驱动列表中。
    • 在sysfs中,于对应的总线目录下(/sys/bus/<bus_type>/drivers/)创建以该驱动命名的目录,并导出其属性。
  3. 触发设备匹配:驱动的注册会触发一个核心动作:尝试将这个新驱动与系统里所有尚未绑定驱动的、且属于同一总线的设备进行匹配。它会遍历该总线上的设备列表,对每个设备调用总线定义的 .match() 函数。
  4. 绑定过程:如果 .match() 函数返回成功(表示该驱动支持此设备),驱动核心就会执行“绑定”(Binding)操作。绑定的核心是调用该驱动的 .probe() 函数。在 .probe() 函数中,驱动会执行所有针对该设备的初始化工作(申请资源、映射寄存器、注册中断等)。如果 .probe() 成功,这个设备和驱动就正式绑定在一起了。
  5. 驱动注销:当驱动模块被卸载时,driver_unregister() 被调用。它会首先遍历所有与该驱动绑定的设备,并对每个设备调用驱动的 .remove() 函数来解除绑定、释放资源。然后,它会将驱动从全局驱动列表中移除,并删除其在sysfs中的目录。

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

  • 统一抽象:为所有类型的驱动程序提供了一个统一的生命周期模型和接口(probe, remove, suspend, resume等),大大降低了驱动开发的复杂性。
  • 代码复用:将驱动与设备的匹配、绑定、电源管理等通用逻辑集中在驱动核心中,避免了在每个总线驱动中重复实现。
  • 解耦:将驱动的注册与设备的注册完全解耦。驱动和设备可以在任何时间点、以任何顺序出现在系统中,驱动核心会自动处理它们的匹配,这完美地支持了热插拔(Hotplug)设备。
  • 可观测性和可管理性:通过sysfs,清晰地向用户空间展示了驱动和设备的关系,并提供了运行时的管理接口。

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

drivers/base/driver.c 作为内核的基础框架,本身没有明显的“劣势”,其设计是现代内核的基石。其局限性主要体M現为,它只提供了一个通用的框架,具体的匹配逻辑和设备交互行为仍然需要由总线和设备驱动自身去实现。例如,它不关心PCI驱动是如何通过Vendor/Device ID来匹配设备的,也不关心I2C驱动是如何通过设备地址来通信的,这些都由具体的总线层来定义。

使用场景

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

driver.c 提供的框架是Linux内核中编写任何与物理或虚拟总线关联的设备驱动时的唯一标准方案

  • 编写USB设备驱动:开发者会定义一个 struct usb_driver,其中包含了 .probe.disconnect 函数,以及一个 id_table 来声明该驱动支持哪些USB设备的Vendor/Product ID。当调用 usb_register() 时,其内部会调用 driver_register(),将驱动注册到USB总线上。当一个匹配的USB设备插入时,USB总线会使用驱动核心的框架来调用该驱动的 .probe() 函数。
  • 编写平台设备驱动(Platform Device Driver):对于嵌入式SoC中的IP核,通常被抽象为平台设备。开发者会定义一个 struct platform_driver,通过设备树(Device Tree)中的 compatible 字符串进行匹配。调用 platform_driver_register() 会将驱动注册到平台总线(”platform” bus),后续的匹配和绑定流程完全由 driver.c 及其关联的驱动核心代码来调度。
  • 驱动调试:当怀疑一个设备工作不正常时,管理员可以通过sysfs接口手动将其从当前驱动 unbind,然后再尝试 bind 到一个不同版本或调试版本的驱动上,而无需卸载整个驱动模块(这可能会影响其他同类正常工作的设备)。

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

该框架不适用于那些不与具体“设备”实例打交道的纯软件或逻辑性驱动。例如:

  • 文件系统驱动:像EXT4、XFS这样的文件系统,它们虽然也是驱动,但它们操作的是块设备,而不是直接管理块设备硬件本身。它们有自己独立的注册机制。
  • 纯算法模块:如内核中的加密算法模块,它们只提供API供其他内核代码调用,不与任何硬件设备绑定,因此不会使用 struct device_driver 进行注册。

对比分析

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

driver.c 中实现的 driver_register 机制,其对比对象应该是那些更古老或更底层的驱动注册方式,例如经典的字符设备注册。

特性 统一设备模型 (driver_register 框架) 传统字符设备注册 (register_chrdev)
抽象层次 。这是一个通用的、面向“设备-驱动”绑定的模型,位于具体设备类型(字符、块)之上。 。这是一个具体的、面向“主/次设备号-文件操作”映射的接口。
核心实体 struct device_driver,代表一个驱动程序实体。 struct file_operations,代表一组对设备文件的操作方法。
注册目的 将驱动程序告知内核,使其能够被自动匹配和绑定到兼容的物理或虚拟设备上。 为一个驱动程序预留一段主设备号(Major Number)范围,并将该范围内的文件操作请求导向该驱动定义的 file_operations
与硬件关系 紧密。框架的核心就是管理驱动与硬件设备(struct device)的生命周期关系。 间接register_chrdev 本身不关心硬件,它只建立设备号和函数指针的映射。驱动需要自己管理硬件。
Sysfs集成 深度集成。自动在 /sys/bus/... 下创建丰富的目录结构,展示驱动和设备的关系。 无直接关系。需要配合 device_create 等函数才能在 /sys/class/... 下创建节点,并由udev在 /dev 下创建设备文件。
现代用法 所有现代总线设备(PCI, USB, Platform, I2C, SPI等)驱动的基础。 仍然用于实现没有总线概念的、简单的或虚拟的字符设备,但通常会与设备模型API(cdev_alloc, device_create)结合使用,而不是单独使用。

driver_register: 向总线注册一个驱动程序

此函数是驱动程序开发者使用的标准接口, 用于将其驱动程序注册到内核的设备模型中, 并将其附加到一个特定的总线上。这是使驱动程序能够被内核识别并用于探测匹配设备的第一步。

工作原理:

  1. 验证和前置检查: 函数首先执行一系列严格的检查, 以确保注册操作的有效性。它会验证驱动程序试图绑定的总线是否已经被注册, 并检查该驱动程序的名称是否已在该总线上被其他驱动占用, 防止重名冲突。
  2. 添加到总线: 核心工作被委托给 bus_add_driver() 函数。这个函数负责在 sysfs 中, 在对应的总线目录下 (/sys/bus/<bus_name>/drivers/) 创建以驱动程序命名的目录, 并将该驱动程序添加到总线的内部驱动程序链表(klist)中。
  3. 创建属性文件: 接着, 它调用 driver_add_groups() 来创建由驱动程序自身定义的、额外的 sysfs 属性文件组。这允许驱动程序向用户空间暴露一些可配置的参数或状态信息。
  4. 错误回滚: 该函数具有健壮的错误处理机制。如果在添加属性文件时失败, 它会调用 bus_remove_driver() 来撤销之前的 bus_add_driver() 操作, 保证系统状态的一致性。
  5. 通知用户空间: 成功将驱动程序添加到内核并创建好 sysfs 条目后, 它会调用 kobject_uevent() 发送一个 KOBJ_ADD 事件。这个事件会通知用户空间的守护进程(如 mdevudev), 有一个新的驱动程序可用。
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
/**
* driver_register - 向总线注册驱动程序
* @drv: 需要被注册的驱动程序
*
* 我们将大部分工作传递给 bus_add_driver() 调用,
* 因为我们必须做的大多数事情都与总线的数据结构有关.
*/
int driver_register(struct device_driver *drv)
{
int ret;
struct device_driver *other;

/*
* 调用 bus_is_registered() 检查此驱动想要绑定的总线(drv->bus)是否已经被注册.
* 这是一个基本的前提条件, 驱动不能注册到一个不存在的总线上.
*/
if (!bus_is_registered(drv->bus)) {
/*
* 如果总线未注册, 打印一条详细的错误日志, 包括驱动名和总线名.
*/
pr_err("Driver '%s' was unable to register with bus_type '%s' because the bus was not initialized.\n",
drv->name, drv->bus->name);
/*
* 返回 -EINVAL (无效参数) 错误.
*/
return -EINVAL;
}

/*
* 这是一个向后兼容性检查. 旧的驱动可能会直接在 drv 结构体中定义 probe/remove 方法.
* 现代的内核期望这些方法由总线(bus_type)统一提供. 如果驱动还在使用旧方法, 打印一条警告.
*/
if ((drv->bus->probe && drv->probe) ||
(drv->bus->remove && drv->remove) ||
(drv->bus->shutdown && drv->shutdown))
pr_warn("Driver '%s' needs updating - please use "
"bus_type methods\n", drv->name);

/*
* 调用 driver_find() 在指定总线上查找是否已存在同名的驱动.
*/
other = driver_find(drv->name, drv->bus);
/*
* 如果找到了 (other 不为 NULL), 说明驱动已注册.
*/
if (other) {
/*
* 打印错误信息, 指出驱动已注册, 中止本次操作.
*/
pr_err("Error: Driver '%s' is already registered, "
"aborting...\n", drv->name);
/*
* 返回 -EBUSY (设备或资源忙) 错误.
*/
return -EBUSY;
}

/*
* 调用 bus_add_driver(), 这是实际执行添加操作的核心函数.
* 它负责创建 sysfs 目录, 并将驱动添加到总线的驱动列表中.
*/
ret = bus_add_driver(drv);
/*
* 如果 bus_add_driver() 失败, 直接返回其错误码.
*/
if (ret)
return ret;
/*
* 调用 driver_add_groups() 来创建驱动自定义的 sysfs 属性文件组.
*/
ret = driver_add_groups(drv, drv->groups);
/*
* 如果添加属性组失败.
*/
if (ret) {
/*
* 执行回滚操作: 调用 bus_remove_driver() 来撤销刚才成功的 bus_add_driver() 操作.
*/
bus_remove_driver(drv);
/*
* 返回添加属性组时产生的错误码.
*/
return ret;
}
/*
* 注册成功后, 发送一个 "add" uevent 事件到用户空间.
* drv->p 是驱动的私有数据(driver_private), 它包含了驱动的 kobject.
*/
kobject_uevent(&drv->p->kobj, KOBJ_ADD);
/*
* 调用此函数来可能会延长延迟探测的超时时间.
* 当有新驱动注册时, 意味着之前一些因为缺少驱动而探测失败的设备可能现在可以成功了,
* 所以给它们多一点时间来重新探测是合理的.
*/
deferred_probe_extend_timeout();

/*
* 返回最终结果 (成功时为 0).
*/
return ret;
}
/*
* 将 driver_register 函数导出, 使其对其他GPL兼容的内核模块可用.
*/
EXPORT_SYMBOL_GPL(driver_register);

drivers/base/swnode.c 软件节点(Software Nodes) 为非固件设备提供统一属性接口

历史与背景

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

drivers/base/swnode.c 文件实现了内核的“软件节点”(Software Nodes)框架。这项技术的诞生是为了解决一个核心问题:如何为那些并非由固件(如设备树Device Tree或ACPI)描述的设备,提供一个与固件设备相兼容的、统一的属性访问接口。

在现代Linux内核中,驱动程序被鼓励使用一套统一的设备属性API(如device_property_read_u32())来获取其配置信息(如中断号、寄存器地址、特定配置参数等)。这套API的后端可以是设备树(device_node)或ACPI。但对于以下类型的设备,它们没有来自固件的描述节点:

  • 多功能设备(Multi-Function Devices, MFDs)的子功能:一个物理芯片可能包含多个独立的功能(如一个音频编解码器芯片同时集成了PMIC功能)。主驱动会为这个芯片注册一个设备,然后需要为每个子功能(codec, pmic)创建独立的、逻辑上的子设备。
  • 程序化实例化的设备:例如,通过I2C或SPI总线手动实例化的设备(在没有设备树描述的情况下,通过i2c_new_client_device()创建)。
  • 纯虚拟或测试设备:完全由软件模拟,用于测试或提供逻辑功能的设备。

swnode出现之前,为这些设备传递配置信息的方式是混乱且非标准化的,最常见的是使用 platform_dataplatform_data 只是一个 void * 指针,驱动需要进行强制类型转换,并且每种设备都定义自己独特的结构体,缺乏统一性。这导致驱动中需要充斥着 #ifdef CONFIG_OF 这样的条件编译,以及两套完全不同的代码路径:一套用于从设备树获取属性,另一套用于解析 platform_data

swnode 的诞生就是为了彻底解决这个问题,它创建了一个纯软件的、在内存中构建的“节点”,这个节点可以像设备树节点一样附加到设备上,并响应统一的设备属性API调用。

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

swnode 的发展是内核统一设备属性API演进过程中的关键一步。

  • platform_data 时代:早期的主要方式,被认为是一种不良实践并被逐步弃用。
  • 统一设备属性API的出现:内核引入了 device_property_read_*() 系列函数,旨在提供一个与后端无关的属性读取接口。
  • fwnode 句柄的抽象struct device 中增加了一个名为 fwnodestruct fwnode_handle * 成员。这个句柄可以指向一个设备树节点(device_node)、一个ACPI句柄,或者一个swnode。这层抽象是实现API统一的关键。
  • swnode 的实现swnode 作为 fwnode_handle 的第三种后端被正式实现。它允许驱动程序在运行时动态地创建属性集,并将其附加到设备上,使得 device_property_read_*() API也能无缝地工作在这些纯软件设备上。

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

swnode 已经成为现代Linux内核驱动开发中的一项核心基础设施,非常稳定和成熟。它被广泛认为是取代 platform_data 的标准和最佳实践。其主要应用场景包括:

  • MFD驱动:这是 swnode 最典型的应用场景。
  • I2C/SPI核心:用于在没有固件描述时实例化设备。
  • 一些总线和框架驱动:用于创建具有特定属性的逻辑或辅助设备。

核心原理与设计

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

swnode 的核心原理是模拟一个固件节点(如设备树节点)的行为,并将其“挂”在 struct devicefwnode 句柄上,从而截获并响应统一属性API的调用。

  1. 属性定义:驱动程序首先在代码中定义一个 struct property_entry 数组。每个 property_entry 代表一个属性,包含属性名(如 "interrupts")、长度和值。
  2. 软件节点创建:驱动调用 swnode_create() 或相关API,并传入上述的属性数组。这个函数会在内存中分配并初始化一个 struct swnode 对象,并将属性数据与之关联。
  3. 附加到设备:当驱动程序创建子设备(例如,通过 platform_device_alloc())时,它可以将返回的 swnode 句柄赋值给新设备的 dev->fwnode 成员。
  4. 统一API调用:当这个子设备的驱动(或其他代码)调用 device_property_read_u32(dev, "my-property", &val) 时,这个API的内部实现会通过 dev->fwnode 找到关联的 swnode
  5. 请求分派:API接着会调用 swnode 注册的操作函数(swnode_property_read),这个函数会在驱动之前提供的 property_entry 数组中查找名为 "my-property" 的条目,并返回其值。

通过这个流程,调用者完全不知道属性是来自设备树还是一个内存中的 swnode,从而实现了接口的透明和统一。

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

  • 统一接口:允许驱动程序使用一套代码来处理来自固件和来自软件定义的设备属性,消除了大量的 #ifdef 和重复逻辑。
  • 代码清晰:用结构化的、带命名的属性(property_entry)取代了无类型的 void * 指针(platform_data),代码更易读、更安全。
  • 标准化:为 MFD 和其他程序化创建设备提供了一种标准的、通用的传递配置信息的方式。
  • 灵活性:允许在运行时动态构建设备的属性集。

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

  • 静态定义:属性是在驱动的C代码中静态定义的。对于需要在不重新编译内核的情况下修改设备属性的场景(例如由用户修改),设备树覆层(Device Tree Overlay)是更合适的解决方案。
  • 内存开销:与 platform_data 相比,swnodeproperty_entry 结构体在内存中会占用稍多的空间,但这通常是可以忽略不计的。
  • 非硬件描述swnode 的设计目标是为软件创建的设备服务,它不应该被用来描述真实的、应该在设备树或ACPI中描述的硬件。

使用场景

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

swnode 是为程序化创建的、需要向其驱动传递结构化配置信息的子设备或逻辑设备提供的首选解决方案。

  • 场景一:MFD驱动
    一个PMIC芯片(如max77620)通过I2C连接,它内部集成了稳压器(regulator)、实时时钟(RTC)、onkey等多个功能。
    1. max77620 的主I2C驱动在 .probe() 函数中被调用。
    2. 主驱动会为每个子功能(regulator, rtc)在代码中定义一个 property_entry 数组,描述该子功能的特定属性(如regulator的ID,RTC的中断等)。
    3. 主驱动为每个子功能创建一个 swnode,然后创建相应的 platform_device,并将 swnode 附加到这些 platform_device 上。
    4. 当内核为这些新的 platform_device 匹配到各自的驱动(如通用的regulator驱动、rtc驱动)时,这些驱动就可以通过标准的 device_property_read_*() API来获取自己的配置,而无需知道自己是被一个MFD驱动创建的。
  • 场景二:无设备树的I2C设备实例化
    在一些没有设备树的老旧系统或特定配置中,可能需要在启动脚本或另一个驱动中手动创建一个I2C设备。这时可以先创建一个swnode来描述这个I2C设备的属性(如中断线),然后调用 i2c_new_client_device() 并将 swnode 与之关联。

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

  • 硬件设备描述:任何可以在设备树或ACPI中被静态描述的物理硬件,都必须使用设备树或ACPI。swnode 不能替代固件作为硬件的“单一事实来源”(single source of truth)。
  • 用户空间配置:如果希望由系统管理员或用户来配置设备参数,应该使用设备树覆层(DT Overlay)或内核模块参数,而不是 swnode,因为后者需要修改内核代码并重新编译。

对比分析

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

swnode 的核心是作为统一设备属性API的一个后端,因此它的主要对比对象是其他后端以及被它取代的旧技术。

特性 swnode (Software Node) Device Tree (device_node) ACPI (acpi_handle) platform_data (已废弃)
数据来源 C代码:由驱动程序在运行时于内存中构建。 固件:由bootloader从存储介质加载的 .dtb 二进制文件。 固件:BIOS/UEFI提供的ACPI表。 C代码:由父驱动或板级文件创建的一个自定义结构体。
访问API 统一属性API (device_property_read_*) 统一属性API (device_property_read_*) 统一属性API (device_property_read_*) 无统一API。需要强制类型转换并直接访问结构体成员。
数据结构 标准化的 struct property_entry 数组。 标准化的、树状的节点和属性结构。 标准化的ACPI对象和方法。 完全自定义struct,类型不安全。
设计用途 程序化创建的设备提供统一的属性接口。 描述物理硬件的拓扑和配置。 描述物理硬件的拓扑、配置和电源管理。 旧方式:为程序化创建的设备传递配置。
灵活性 定义在内核代码中,修改需重新编译。 灵活,可通过DT Overlay在运行时修改。 相对静态,由固件提供。 定义在内核代码中,修改需重新编译。
典型例子 MFD驱动为其子功能创建的逻辑设备。 SoC上的I2C控制器、GPIO控制器。 x86平台上的电源按钮、LID开关。 老旧MFD驱动或板级文件中用于传递IRQ号的代码。

software_node_init: 初始化软件节点子系统

此函数的作用是在内核启动期间, 在sysfs文件系统中创建/sys/kernel/software_nodes/目录。这个目录是“软件节点”(Software Nodes)层次结构的根, 为内核提供了一种完全在软件中以编程方式描述设备、属性和依赖关系的机制, 作为对基于硬件的设备树(Device Tree)的一种补充或替代。

在单核无MMU的STM32H750平台上的原理与作用

在STM32H750这样的嵌入式系统上, 绝大多数硬件外设都是通过设备树来描述的。然而, 软件节点机制依然具有重要作用。它允许驱动程序:

  1. 创建逻辑设备: 一个驱动可以聚合多个物理硬件资源(例如一个DMA通道、一个定时器和一个GPIO), 并通过创建一个软件节点来将它们统一表示成一个单一的、功能更高级的逻辑设备。
  2. 描述动态设备: 对于那些在系统运行时才被发现的设备(例如通过一个I2C或SPI总线探测到的传感器, 或者一个自定义总线上的设备), 可以为其动态创建软件节点来描述其属性和层次关系。
  3. 提供配置接口: 软件节点可以附加属性, 这些属性会以文件的形式出现在sysfs中, 为用户空间提供了一种配置和交互的标准化接口。

software_node_init函数本身的行为在任何架构上都是一致的, 它仅仅是创建了一个目录。但在STM32平台上, 它为驱动程序的开发者提供了一个强大的、灵活的工具, 使其能够在静态的设备树模型之外, 构建更复杂的、动态的设备拓扑结构。postcore_initcall确保了这个基础目录在任何可能需要使用它的驱动程序被初始化之前就已经准备就绪。

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
/*
* 这是一个静态的初始化函数, 用于在内核启动时建立软件节点子系统.
* __init 宏告诉编译器和链接器, 这个函数及其引用的数据仅在初始化阶段使用,
* 在内核启动完成后, 其占用的内存可以被回收, 以节省宝贵的RAM.
* @return: 0 表示成功, 负值错误码表示失败.
*/
static int __init software_node_init(void)
{
/*
* 调用 kset_create_and_add() 函数, 这是在 sysfs 中创建一个新目录(kset)的核心API.
* 一个 kset 是一个 kobject 的集合, 它在 sysfs 中表现为一个目录.
*
* 参数1, "software_nodes":
* 这是一个字符串, 定义了将在 sysfs 中创建的目录的名称.
*
* 参数2, NULL:
* 这个参数是用于指定 uevent 操作的回调函数集(kset_uevent_ops).
* 设置为 NULL 意味着这个 kset 将使用默认的 uevent 处理行为.
*
* 参数3, kernel_kobj:
* 这是一个指向父 kobject 的指针. kernel_kobj 是内核导出的一个全局变量, 它代表了 /sys/kernel/ 这个目录.
* 因此, 这行代码的完整作用是在 /sys/kernel/ 目录下创建一个名为 "software_nodes" 的子目录.
*
* swnode_kset:
* 这是一个全局(或文件静态)指针, 用于保存 kset_create_and_add() 成功后返回的 kset 对象的句柄.
* 其他需要在此目录下创建节点的代码将会使用这个句柄.
*/
swnode_kset = kset_create_and_add("software_nodes", NULL, kernel_kobj);
/*
* 检查 kset_create_and_add() 的返回值.
* 如果函数执行失败, 它会返回 NULL. 这通常是由于内存不足导致的.
*/
if (!swnode_kset)
/*
* 如果创建失败, 返回 -ENOMEM 错误码.
* ENOMEM 是标准的内核错误码, 意为 "Error, No Memory" (内存不足).
*/
return -ENOMEM;
/*
* 如果函数执行成功, 返回 0. 这是内核中表示函数成功执行的标准方式.
*/
return 0;
}
/*
* postcore_initcall 是一个宏, 用于将 software_node_init 函数注册为一个内核初始化回调.
* 内核初始化过程被分成了多个阶段. "postcore" 阶段意味着这个函数将在非常核心的子系统
* (如内存管理, 进程调度) 初始化完成之后, 但在大多数设备驱动程序开始探测和初始化之前被调用.
* 这个时机是恰当的, 因为它确保了 "software_nodes" 这个基础框架在任何驱动程序可能需要使用它之前就已经准备好了.
*/
postcore_initcall(software_node_init);