[TOC]
drivers/base
drivers/base 是Linux内核源代码中的一个核心目录,它并非一项独立的技术,而是内核设备模型(Linux Device Model)的基础实现。这个模型是一个统一的、抽象的框架,用于管理系统中的所有设备、驱动程序以及它们之间的关系。理解drivers/base就是理解现代Linux内核如何看待和管理硬件。
历史与背景
这项技术是为了解决什么特定问题而诞生的?
在Linux内核2.5版本之前,内核中没有一个统一的结构来表示设备和驱动程序。驱动程序的编写和管理方式较为混乱,存在诸多问题:
- 电源管理困难:没有统一的设备层级结构,很难以正确的顺序对设备进行挂起(suspend)或恢复(resume)操作。例如,USB主控制器必须在USB设备之前被挂起。
- 代码冗余:每个子系统(如PCI, USB)都需要自己实现一套管理设备和驱动的机制,导致了大量的重复代码,尤其是在对象生命周期管理(如引用计数)和列表管理方面。
- 系统信息不透明:缺乏一种简洁、一致的方式从用户空间查看系统中所有设备的拓扑结构和状态。当时的
/proc文件系统虽然提供了信息,但结构不够清晰和规范。
为了解决这些问题,Patrick Mochel在2.5开发系列内核中引入了统一设备模型。
它的发展经历了哪些重要的里程碑或版本迭代?
- 内核 2.5 系列(约2002-2003年):设备模型被设计和引入,这是其最重要的里程碑。
kobject、kset等核心数据结构和sysfs文件系统在此时诞生,为整个模型奠定了基础。 - 内核 2.6 系列:设备模型得到大规模应用和完善,几乎所有的设备驱动都迁移到了这个新模型上。它成为了内核驱动开发的标准方式,并在此后的版本中不断进行优化和扩展。
- 后续版本至今:设备模型本身已经非常成熟和稳定,后续的迭代主要是围绕它进行功能增强,例如改进即插即用(hotplug)机制、优化电源管理框架、引入设备链接(device links)以更好地处理设备依赖关系等。
目前该技术的社区活跃度和主流应用情况如何?
drivers/base 中的代码是Linux内核最核心和稳定的部分之一,其社区活跃度体现在内核的方方面面。任何新的总线、设备或驱动的开发都必须基于这个模型。它不是一个可选的“应用”,而是编写现代Linux驱动程序的强制性框架。从嵌入式设备到超级计算机,所有运行Linux的系统都依赖于这个模型来管理硬件。
核心原理与设计
它的核心工作原理是什么?
设备模型的核心思想是建立一个由多个核心数据结构组成的层次化对象系统,来抽象描述硬件拓扑和驱动管理。
kobject (Kernel Object):是设备模型中最基本的构建块。它本身不做太多事情,主要提供通用功能,包括:
- 引用计数:通过
kref结构管理对象的生命周期,当引用计数降为零时,对象可以被安全释放。 - 父子关系:通过一个指向父
kobject的指针,将所有对象组织成一个层次化的树状结构。 - 与sysfs的关联:每个
kobject都可以在sysfs文件系统中表现为一个目录。
- 引用计数:通过
kset (Kernel Object Set):是一个
kobject的集合,用于将相关的kobject分组。kset本身也内嵌了一个kobject,因此它在sysfs中也表现为一个目录,其包含的kobject则成为该目录下的子目录。核心结构体:基于
kobject和kset,设备模型定义了几个关键的结构体:struct device:代表一个具体的设备(如一个U盘、一个网络接口)。它内嵌了一个kobject,包含了设备的通用信息,如它所属的总线、驱动程序等。struct device_driver:代表一个驱动程序。它定义了驱动能做什么,比如probe(探测设备)和remove(移除设备)等操作。struct bus_type:代表一条总线(如PCI, USB, I2C)。它起着将device和device_driver撮合起来的作用,定义了如何匹配设备和驱动的规则(match函数)。struct class:提供一种更高层次的设备视图,将功能相似的设备分组,而不关心它们挂载在哪条总线上。例如,所有的输入设备(鼠标、键盘)都属于input类,用户可以通过/sys/class/input/找到它们。
它的主要优势体现在哪些方面?
- 统一的硬件视图:为内核和用户空间提供了系统硬件拓扑的一致性视图,这个视图通过
/sys文件系统直观地展现出来。 - 简化的驱动开发:通过提供通用的API和数据结构,减少了驱动开发中的样板代码,让开发者可以专注于硬件相关的逻辑。
- 精确的电源管理:层次化的设备树使得系统可以按照正确的依赖顺序对设备进行挂起和恢复。
- 代码复用和维护性:消除了大量冗余代码,使得内核的驱动部分更加简洁和易于维护。
- 动态设备管理:与
uevent和udev等用户空间机制结合,实现了强大的即插即用功能。
它存在哪些已知的劣势、局限性或在特定场景下的不适用性?
- 复杂性:对于初学者来说,设备模型的概念(如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 device 和 struct 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是内核的一部分,无法“安装”。入门实践主要是学习如何编写一个使用设备模型的简单驱动。以下是一个极简的平台设备驱动示例框架:
编写驱动代码 (
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
// 当驱动与设备匹配时调用
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");编写设备定义代码 (
my_device.c或在板级文件中):1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
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");编译和加载:
- 为每个文件创建简单的
Makefile。 - 使用
make编译生成.ko文件(内核模块)。 - 使用
sudo insmod my_device.ko加载设备模块。 - 使用
sudo insmod my_driver.ko加载驱动模块。 - 使用
dmesg查看内核日志,应该能看到 “my_probe() called!” 的消息。 - 使用
sudo rmmod my_driver和sudo rmmod my_device卸载模块。
- 为每个文件创建简单的
“在初次使用时,有哪些常见的‘坑’或需要注意的配置细节?”
- 引用计数错误:忘记在使用
kobject或device后调用put_device()或kobject_put(),导致资源无法释放。 - 并发问题:驱动中的函数(如
probe,remove,sysfs的store/show)可能被并发调用,必须使用锁(如mutex)来保护共享数据。 - 与用户空间的数据交换:从
sysfs的store函数或ioctl中接收用户数据时,必须使用copy_from_user(),向用户空间写入时必须使用copy_to_user(),直接访问用户空间指针是危险的。 - 资源清理不彻底:在
probe函数中申请的所有资源(内存、中断、IO端口等),都必须在probe失败的错误路径和remove函数中被彻底释放。使用devm_*系列的资源管理函数可以极大地简化这一点。 - 在错误的时间注册:在驱动初始化早期(
early_init)阶段,设备模型的某些部分可能还未就绪,需要注意注册时机。
安全考量 (Security Aspects)
“使用这项技术时,需要注意哪些主要的安全风险?”
- Sysfs接口漏洞:这是最主要的安全风险来源。如果驱动创建了一个可写的
sysfs属性,但没有对用户写入的数据进行严格的长度和内容校验,就可能导致缓冲区溢出、整数溢出或其他类型的内存破坏,从而可能引发本地权限提升。 - 信息泄露:通过
sysfs暴露的属性可能会泄露内核内存地址或其他敏感信息,这会帮助攻击者绕过KASLR(内核地址空间布局随机化)等安全机制。 - 拒绝服务(DoS):一个设计不佳的
sysfs处理函数可能会在处理某些输入时进入死循环或执行非常耗时的操作,使得用户空间程序可以通过访问该sysfs文件来使内核线程或整个系统失去响应。
“业界有哪些增强其安全性的最佳实践或辅助工具?”
- 最小权限原则:只通过
sysfs暴露绝对必要的信息和控制点。文件权限应尽可能设为只读。 - 严格的输入验证:对所有来自用户空间(通过
sysfs的store函数)的输入进行严格的边界检查和合法性验证。 - 使用标准接口:尽可能使用内核提供的标准子系统(如
gpio,leds,regulator)而不是自己创建自定义的sysfs接口。这些子系统通常已经过了充分的审查。 - 代码审查:驱动代码,尤其是处理用户输入的
sysfs部分,是内核安全审查的重点。 - 静态分析工具:使用如
Sparse或Coverity等工具可以帮助发现一些潜在的编程错误。 - 模糊测试(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)来预分配对象,减少分配延迟。 - 并发与锁:选择合适的锁机制。在高性能场景下,避免使用大的全局锁,尝试使用更细粒度的锁或无锁数据结构。
未来趋势 (Future Trends)
“这项技术未来的发展方向是什么?”
设备模型已经非常成熟,其核心不太可能有颠覆性变化。未来的发展主要集中在以下方面:
- 异步化:为了加速系统启动,内核社区一直在推动设备和驱动的异步探测(
probe)。这意味着设备的初始化可以并行进行,而不是串行等待。 - 安全性增强:随着对内核安全性的日益重视,对
sysfs等用户-内核接口的审查和加固会持续进行。 - 与新技术的集成:随着CXL(Compute Express Link)等新总线技术的出现,设备模型会不断扩展以支持这些新技术。此外,将Rust语言引入内核开发,也可能在未来影响驱动的编写方式,带来更高的内存安全性。
“社区中是否有正在讨论的替代技术或下一代方案?”
目前没有替代Linux设备模型的讨论。它的设计非常成功和稳固,已经成为Linux内核的基石。任何改进都将是在现有框架内的演进,而非替代。一些微内核架构的操作系统(如Fuchsia的Zircon)使用了不同的驱动模型,但这与Linux的宏内核设计哲学不同,不构成直接的竞争或替代关系。
总结
drivers/base所实现的Linux设备模型是内核驱动开发的支柱。它通过一套优雅的抽象(kobject、device、driver、bus、class)解决了早期内核在电源管理、代码冗余和系统可见性方面的核心痛点。
关键特性总结:
- 统一抽象:为所有设备和驱动提供统一的数据结构和生命周期管理。
- 层次化结构:以树状结构表示系统硬件拓扑。
- Sysfs集成:将内核对象模型直接映射到
/sys文件系统,提供给用户空间。 - 强大的电源管理和即插即用:为现代操作系统核心功能提供基础。
学习该技术的要点建议:
- 理解核心概念:首先要牢固掌握
kobject的引用计数和父子关系,这是理解一切的基础。 - 阅读源码:直接阅读
drivers/base/下的源码,以及一个简单总线(如platform总线)的实现,是最好的学习方式。 - 动手实践:从编写一个简单的平台设备驱动开始,亲自体验注册
device和driver,以及它们如何通过probe函数绑定的过程。 - 学习
sysfs:尝试在你的驱动中创建只读和可读写的sysfs属性,并理解其背后的安全 implications。 - 掌握资源管理:熟练使用
devm_*系列的接口,它们能让你的驱动代码更简洁、更安全。
include/linux/fwnode.h
fwnode_init
1 | static inline void fwnode_init(struct fwnode_handle *fwnode, |
drivers/base/init.c
driver_init 初始化驱动模型
- 此函数是内核启动早期被调用的一个关键的聚合初始化函数。它的职责不是初始化任何具体的设备驱动,而是 构建起整个Linux设备模型(Linux Driver Model)的核心基础设施。这个模型是内核中用于表示设备、驱动以及它们之间关系的一整套数据结构和机制,它通过sysfs文件系统(挂载于/sys)向用户空间提供了一个层次分明的视图。
1 | /** |
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 device和struct 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 结构体,提供驱动程序的注册、注销和与设备进行匹配(即“绑定”)的通用逻辑。
- 驱动抽象:内核定义了通用的
struct device_driver结构体,其中包含了驱动的名称、所属的总线类型、以及一系列标准的回调函数指针(如.probe,.remove,.suspend,.resume等)。 各种具体的驱动(如pci_driver,usb_driver)都会内嵌一个struct device_driver实例。 - 驱动注册:当一个驱动模块被加载时,它会调用
driver_register()函数(通常由总线特定的封装函数如platform_driver_register间接调用)。 该函数主要做两件事:- 将该驱动程序添加到一个全局的、按总线类型组织的驱动列表中。
- 在sysfs中,于对应的总线目录下(
/sys/bus/<bus_type>/drivers/)创建以该驱动命名的目录,并导出其属性。
- 触发设备匹配:驱动的注册会触发一个核心动作:尝试将这个新驱动与系统里所有尚未绑定驱动的、且属于同一总线的设备进行匹配。它会遍历该总线上的设备列表,对每个设备调用总线定义的
.match()函数。 - 绑定过程:如果
.match()函数返回成功(表示该驱动支持此设备),驱动核心就会执行“绑定”(Binding)操作。绑定的核心是调用该驱动的.probe()函数。在.probe()函数中,驱动会执行所有针对该设备的初始化工作(申请资源、映射寄存器、注册中断等)。如果.probe()成功,这个设备和驱动就正式绑定在一起了。 - 驱动注销:当驱动模块被卸载时,
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: 向总线注册一个驱动程序
此函数是驱动程序开发者使用的标准接口, 用于将其驱动程序注册到内核的设备模型中, 并将其附加到一个特定的总线上。这是使驱动程序能够被内核识别并用于探测匹配设备的第一步。
工作原理:
- 验证和前置检查: 函数首先执行一系列严格的检查, 以确保注册操作的有效性。它会验证驱动程序试图绑定的总线是否已经被注册, 并检查该驱动程序的名称是否已在该总线上被其他驱动占用, 防止重名冲突。
- 添加到总线: 核心工作被委托给
bus_add_driver()函数。这个函数负责在sysfs中, 在对应的总线目录下 (/sys/bus/<bus_name>/drivers/) 创建以驱动程序命名的目录, 并将该驱动程序添加到总线的内部驱动程序链表(klist)中。 - 创建属性文件: 接着, 它调用
driver_add_groups()来创建由驱动程序自身定义的、额外的sysfs属性文件组。这允许驱动程序向用户空间暴露一些可配置的参数或状态信息。 - 错误回滚: 该函数具有健壮的错误处理机制。如果在添加属性文件时失败, 它会调用
bus_remove_driver()来撤销之前的bus_add_driver()操作, 保证系统状态的一致性。 - 通知用户空间: 成功将驱动程序添加到内核并创建好
sysfs条目后, 它会调用kobject_uevent()发送一个KOBJ_ADD事件。这个事件会通知用户空间的守护进程(如mdev或udev), 有一个新的驱动程序可用。
1 | /** |
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_data。platform_data 只是一个 void * 指针,驱动需要进行强制类型转换,并且每种设备都定义自己独特的结构体,缺乏统一性。这导致驱动中需要充斥着 #ifdef CONFIG_OF 这样的条件编译,以及两套完全不同的代码路径:一套用于从设备树获取属性,另一套用于解析 platform_data。
swnode 的诞生就是为了彻底解决这个问题,它创建了一个纯软件的、在内存中构建的“节点”,这个节点可以像设备树节点一样附加到设备上,并响应统一的设备属性API调用。
它的发展经历了哪些重要的里程碑或版本迭代?
swnode 的发展是内核统一设备属性API演进过程中的关键一步。
platform_data时代:早期的主要方式,被认为是一种不良实践并被逐步弃用。- 统一设备属性API的出现:内核引入了
device_property_read_*()系列函数,旨在提供一个与后端无关的属性读取接口。 fwnode句柄的抽象:struct device中增加了一个名为fwnode的struct 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 device 的 fwnode 句柄上,从而截获并响应统一属性API的调用。
- 属性定义:驱动程序首先在代码中定义一个
struct property_entry数组。每个property_entry代表一个属性,包含属性名(如"interrupts")、长度和值。 - 软件节点创建:驱动调用
swnode_create()或相关API,并传入上述的属性数组。这个函数会在内存中分配并初始化一个struct swnode对象,并将属性数据与之关联。 - 附加到设备:当驱动程序创建子设备(例如,通过
platform_device_alloc())时,它可以将返回的swnode句柄赋值给新设备的dev->fwnode成员。 - 统一API调用:当这个子设备的驱动(或其他代码)调用
device_property_read_u32(dev, "my-property", &val)时,这个API的内部实现会通过dev->fwnode找到关联的swnode。 - 请求分派:API接着会调用
swnode注册的操作函数(swnode_property_read),这个函数会在驱动之前提供的property_entry数组中查找名为"my-property"的条目,并返回其值。
通过这个流程,调用者完全不知道属性是来自设备树还是一个内存中的 swnode,从而实现了接口的透明和统一。
它的主要优势体现在哪些方面?
- 统一接口:允许驱动程序使用一套代码来处理来自固件和来自软件定义的设备属性,消除了大量的
#ifdef和重复逻辑。 - 代码清晰:用结构化的、带命名的属性(
property_entry)取代了无类型的void *指针(platform_data),代码更易读、更安全。 - 标准化:为 MFD 和其他程序化创建设备提供了一种标准的、通用的传递配置信息的方式。
- 灵活性:允许在运行时动态构建设备的属性集。
它存在哪些已知的劣势、局限性或在特定场景下的不适用性?
- 静态定义:属性是在驱动的C代码中静态定义的。对于需要在不重新编译内核的情况下修改设备属性的场景(例如由用户修改),设备树覆层(Device Tree Overlay)是更合适的解决方案。
- 内存开销:与
platform_data相比,swnode和property_entry结构体在内存中会占用稍多的空间,但这通常是可以忽略不计的。 - 非硬件描述:
swnode的设计目标是为软件创建的设备服务,它不应该被用来描述真实的、应该在设备树或ACPI中描述的硬件。
使用场景
在哪些具体的业务或技术场景下,它是首选解决方案?请举例说明。
swnode 是为程序化创建的、需要向其驱动传递结构化配置信息的子设备或逻辑设备提供的首选解决方案。
- 场景一:MFD驱动
一个PMIC芯片(如max77620)通过I2C连接,它内部集成了稳压器(regulator)、实时时钟(RTC)、onkey等多个功能。max77620的主I2C驱动在.probe()函数中被调用。- 主驱动会为每个子功能(regulator, rtc)在代码中定义一个
property_entry数组,描述该子功能的特定属性(如regulator的ID,RTC的中断等)。 - 主驱动为每个子功能创建一个
swnode,然后创建相应的platform_device,并将swnode附加到这些platform_device上。 - 当内核为这些新的
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这样的嵌入式系统上, 绝大多数硬件外设都是通过设备树来描述的。然而, 软件节点机制依然具有重要作用。它允许驱动程序:
- 创建逻辑设备: 一个驱动可以聚合多个物理硬件资源(例如一个DMA通道、一个定时器和一个GPIO), 并通过创建一个软件节点来将它们统一表示成一个单一的、功能更高级的逻辑设备。
- 描述动态设备: 对于那些在系统运行时才被发现的设备(例如通过一个I2C或SPI总线探测到的传感器, 或者一个自定义总线上的设备), 可以为其动态创建软件节点来描述其属性和层次关系。
- 提供配置接口: 软件节点可以附加属性, 这些属性会以文件的形式出现在
sysfs中, 为用户空间提供了一种配置和交互的标准化接口。
software_node_init函数本身的行为在任何架构上都是一致的, 它仅仅是创建了一个目录。但在STM32平台上, 它为驱动程序的开发者提供了一个强大的、灵活的工具, 使其能够在静态的设备树模型之外, 构建更复杂的、动态的设备拓扑结构。postcore_initcall确保了这个基础目录在任何可能需要使用它的驱动程序被初始化之前就已经准备就绪。
1 | /* |









