[TOC]
lib/devres.c
devm_ioremap_resource: 安全、自动管理的IO内存映射
此函数是Linux内核中用于设备驱动程序开发的一个至关重要的辅助函数。它的核心作用是将一个从设备树(Device Tree)或平台数据中获取的、描述硬件寄存器物理地址的资源(struct resource
), 安全地映射到内核可以访问的虚拟地址空间, 并且这个过程是完全由设备资源管理(devm
)框架自动管理的。
该函数将一个典型的三步操作封装成了一个单一的、原子的API调用:
- 验证与请求: 它首先检查传入的资源是否是一个有效的内存区域(
IORESOURCE_MEM
)。然后, 它调用devm_request_mem_region
向内核的资源仲裁系统”宣告”:”本驱动将要使用这段物理内存”。这可以防止两个不同的驱动程序意外地同时操作同一块硬件寄存器区域, 从而避免冲突。 - 内存映射: 在成功”占有”该物理内存区域后, 它调用底层的
__devm_ioremap
函数, 将这段物理地址映射为内核虚拟地址。这个返回的虚拟地址(类型为void __iomem *
)就是驱动程序后续用来访问硬件寄存器的句柄。 - 自动资源管理与回滚: 这是
devm_
系列函数的核心价值。所有分配的资源(包括内存区域名、请求的内存区域、映射的虚拟地址)都被注册到设备的资源管理链表中。- 错误回滚: 如果在三步操作中的任何一步失败(例如,
ioremap
失败), 该函数会自动撤销并释放所有已经成功分配的资源(例如, 它会自动调用devm_release_mem_region
来释放之前请求的内存区域), 从而保证系统状态的一致性, 防止资源泄漏。 - 自动释放: 当驱动程序被卸载或其
probe
函数因其他原因失败返回时, 内核的设备模型会自动遍历并释放所有通过devm_
函数注册的资源。驱动开发者无需编写任何手动的清理代码。
- 错误回滚: 如果在三步操作中的任何一步失败(例如,
在STM32H750这样**无MMU(内存管理单元)**的MCU上, ioremap
的主要作用并非建立复杂的虚拟地址到物理地址的页表映射(因为地址通常是直接或简单偏移对应的), 而是:
- 提供一个标准的、跨平台可移植的API来访问IO内存。
- 通过返回一个特殊的
__iomem
指针类型来提醒开发者, 这段内存不能像普通RAM一样被直接解引用, 必须使用readl
/writel
等专用的IO访问函数。 - 确保对该内存区域应用正确的缓存策略(通常是”非缓存的”或”写合并的”), 以保证对硬件寄存器的读写操作能够立即生效, 而不会被CPU的缓存干扰。
__devm_ioremap_resource
: 核心实现函数
1 | /* |
devm_ioremap_resource
: 便捷的API封装函数
1 | /** |
__devm_ioremap: 自动管理的IO内存映射核心实现
此函数是Linux设备资源管理(devm
)框架中, 负责执行实际IO内存映射操作的底层核心函数。它被更高层的devm_ioremap_resource
等函数在内部调用。它的核心原理是: 先为将要返回的映射地址分配一个”管理席位”, 然后调用相应缓存策略的ioremap
函数执行实际映射, 如果映射成功, 就将返回的地址登记到那个”管理席位”并将其激活; 如果映射失败, 就撤销那个”管理席位”。
这整个过程确保了ioremap
操作的生命周期与设备驱动的生命周期完全绑定, 实现了iounmap
的自动化, 从而极大地提高了驱动代码的健壮性和简洁性。
其工作流程如下:
- 分配管理记录: 函数首先调用
devres_alloc_node
。这会分配一小块内存(ptr
), 这块内存本身就是一个受管资源, 并且它与一个特定的释放函数devm_ioremap_release
相关联。ptr
被设计用来存放最终ioremap
成功后返回的虚拟地址。 - 根据类型执行映射: 函数使用一个
switch
语句, 根据调用者传入的type
参数, 来选择调用不同缓存策略的底层ioremap
函数:ioremap
: 标准映射, 通常是设备无关的、非缓存的。ioremap_uc
: 强制非缓存(Un-Cacheable)映射。ioremap_wc
: 写合并(Write-Combining)映射, 允许多个连续的写操作在总线上合并成一次传输, 可以提高显存等设备的性能。ioremap_np
: 非提交写(Non-Posted)映射, 保证写操作完成前CPU会等待。
- 条件性激活:
- 如果底层
ioremap_*
函数成功并返回了一个有效的虚拟地址addr
, 函数就会将这个地址存入之前分配的管理记录中(*ptr = addr
), 然后调用devres_add
将这个管理记录正式加入到设备的资源链表中。从此刻起, 内核就承诺会在驱动卸载时自动调用devm_ioremap_release
来iounmap
这个地址。 - 如果底层
ioremap_*
函数失败并返回NULL
, 函数会调用devres_free
来释放第一步分配的、未使用的管理记录, 防止内存泄漏。
- 如果底层
对于STM32H750这样的**无MMU(内存管理单元)但通常有MPU(内存保护单元)**的系统, ioremap
的意义依然重大:
- 缓存一致性: 这是最主要的目的。STM32H7系列带有数据缓存, 直接访问物理地址可能会导致对硬件寄存器的读写操作被CPU缓存, 从而无法立即反映到硬件上, 造成灾难性后果。
ioremap
(特别是ioremap_uc
)会通过配置MPU, 将这段地址区域标记为”非缓存的”(non-cacheable), 确保每一次读写都直达物理总线。 - API标准化: 它提供了标准的、可移植的API来访问硬件IO内存。
- 类型安全: 返回的
void __iomem *
指针会阻止程序员意外地对其进行直接的指针解引用, 强制使用readl
/writel
等专用的、保证了内存屏障和访问顺序的函数。
1 | /* |
drivers/base/devres.c 设备资源管理(Device Resource Management) 简化驱动程序资源管理的自动化机制
drivers/base/devres.c
是Linux内核驱动核心(Driver Core)中一个至关重要的组件。它实现了一个名为“设备资源管理”(Device Resource Management,简称devres)的框架。该框架的核心目标是自动化设备驱动程序中的资源分配与释放,通过提供一系列以 devm_
为前缀的API函数,极大地简化了驱动程序的编写,并从根本上消除了常见的资源泄漏(Resource Leak)问题。
历史与背景
这项技术是为了解决什么特定问题而诞生的?
这项技术是为了解决在设备驱动程序开发中一个长期存在的痛点:繁琐且极易出错的手动资源管理。
- 复杂的错误处理路径:在一个驱动的
.probe()
初始化函数中,通常需要按顺序申请多种资源,如内存(kmalloc
)、中断(request_irq
)、I/O内存映射(ioremap
)、时钟(clk_get
)等。如果在初始化过程的任何一步失败,驱动程序必须编写一段“回滚”代码,按照与申请时相反的顺序,精确地释放所有已经成功申请的资源。这导致了代码中充满了复杂的goto
跳转或深层嵌套的if
判断,这种代码难以阅读、维护,并且非常容易出错。 - 资源泄漏:在上述复杂的手动清理逻辑中,开发者只要遗漏了任何一个错误路径下的释放操作,就会导致资源泄漏。这种泄漏会随着驱动的加载和卸载不断累积,最终可能导致系统内存耗尽或其他资源枯竭,严重影响系统的稳定运行。
- 冗余的释放代码:驱动的
.remove()
函数(在驱动卸载或设备移除时调用)必须手动释放所有在.probe()
中成功申请的资源,这在功能上与.probe()
中的错误处理路径代码是重复的。
devres
框架的诞生,就是为了用一个自动化的机制来取代这种手动、重复且易错的资源管理模式。
它的发展经历了哪些重要的里程碑或版本迭代?
devres
框架是随着Linux设备模型的成熟而逐步引入和完善的。它最初只覆盖了几种最核心的资源,如内存分配(devm_kmalloc
)。由于其带来的巨大好处,社区迅速将其推广,为绝大多数内核中常见的、与设备生命周期绑定的资源都添加了相应的 devm_
托管版本。重要的扩展包括:
- I/O内存映射 (
devm_ioremap
) - 中断请求 (
devm_request_irq
) - 时钟、电源、GPIO、Pin control等硬件描述资源 (
devm_clk_get
,devm_regulator_get
,devm_gpiod_get
,devm_pinctrl_get
) - I2C、SPI等总线设备的实例化 (
devm_i2c_new_device
,devm_spi_alloc_device
)
如今,devm_*
接口已经成为Linux内核驱动开发的事实标准。
目前该技术的社区活跃度和主流应用情况如何?
devres
是一个非常成熟、稳定且被普遍应用的核心内核框架。它不是一个可选的辅助功能,而是现代Linux设备驱动程序编写的基本规范。任何提交到主线内核的新驱动程序,如果可以使用的场景下没有使用 devm_*
接口,而采用了老旧的手动资源管理方式,几乎肯定会在代码审查阶段被要求修改。它被应用于内核中几乎所有的设备驱动中。
核心原理与设计
它的核心工作原理是什么?
devres
的核心原理是为每个设备(struct device
)关联一个专门用于管理资源的链表,并在驱动绑定和解绑的生命周期关键点进行自动化操作。
- 资源列表创建:当驱动核心准备调用一个驱动的
.probe()
函数来绑定一个设备时,它会为该设备初始化一个空的devres
资源链表。 - 托管式资源申请:驱动程序调用一个
devm_*
函数,例如devm_kzalloc()
。这个函数在内部会执行两个步骤:- 首先,它调用底层的、非托管的函数(如
kzalloc()
)来实际分配资源。 - 如果分配成功,它会额外分配一个小型的
devres
元数据结构。这个结构体中存储了指向刚刚分配的资源的指针,以及一个指向用于释放该类型资源的函数指针(例如,kfree
)。
- 首先,它调用底层的、非托管的函数(如
- 添加到列表:这个新创建的
devres
元数据结构被添加到该设备资源链表的头部。 - 自动化释放:当未来这个设备与驱动解绑时(例如,模块被卸载
rmmod
,或设备被热拔除),驱动核心会自动遍历该设备的所有devres
链表项。对于链表中的每一项,它会调用其中存储的释放函数,并传入相应的资源指针来释放资源。由于链表是后进先出(LIFO)的顺序,资源的释放顺序正好与申请顺序相反,完美符合依赖关系。
它的主要优势体现在哪些方面?
- 极大地简化代码:驱动的
.probe()
函数可以写成一段清晰的、线性的代码,不再需要复杂的错误处理跳转。任何失败都会导致函数直接返回错误码,而资源的清理工作由框架自动完成。 - 根除资源泄漏:通过自动化清理机制,从根本上杜绝了因忘记释放资源而导致的泄漏问题,显著提高了驱动的健壮性。
- 简化
.remove()
函数:由于资源释放是自动的,驱动的.remove()
函数通常会变得非常简单,甚至为空。它只需要处理那些与资源分配无关的逻辑操作,如硬件状态的关闭。 - 保证正确的释放顺序:自动以后进先出(LIFO)的顺序释放资源,天然地解决了资源间的依赖问题(例如,必须先释放中断,才能禁用时钟)。
它存在哪些已知的劣势、局限性或在特定场景下的不适用性?
- 严格的生命周期绑定:
devres
管理的资源生命周期严格地与设备和驱动的绑定关系绑定。如果某个资源需要跨越这个生命周期(例如,在驱动解绑后仍然需要存在),那么就不能使用devres
。这种情况在驱动开发中极为罕见,且通常意味着不佳的设计。 - 微小的性能开销:与直接的手动管理相比,
devres
存在一点微小的开销。这包括为每个资源分配元数据结构所占用的内存,以及在申请和释放时管理链表所花费的CPU时间。然而,这种开销在绝大多数情况下都是完全可以忽略不计的,用它换来的代码简洁性和健壮性是完全值得的。 - 并非万能:虽然框架覆盖了绝大多数常用资源,但并非所有内核资源都有对应的
devm_*
封装。对于没有现成封装的资源,开发者可以使用通用的devm_add_action_or_reset()
来注册一个自定义的清理函数。
使用场景
在哪些具体的业务或技术场景下,它是首选解决方案?请举例说明。
在任何设备驱动的 .probe()
函数中,只要需要申请的资源其生命周期应与该驱动和设备的绑定关系一致,devres
就是唯一且首选的解决方案。
- 例一:内存分配
一个平台设备驱动需要为其私有数据结构分配内存。- 推荐做法:
struct my_priv *priv = devm_kzalloc(&pdev->dev, sizeof(*priv), GFP_KERNEL);
- 效果:当该驱动被卸载时,这块内存会被自动
kfree
。
- 推荐做法:
- 例二:中断请求
一个SPI触摸屏驱动需要注册一个中断处理函数。- 推荐做法:
ret = devm_request_threaded_irq(&spi->dev, irq, NULL, my_irq_handler, IRQF_ONESHOT, "my-touchscreen", priv);
- 效果:当中断申请失败时,驱动直接
return ret
即可。当驱动卸载时,该中断会被自动free_irq
。
- 推荐做法:
- 例三:获取上游资源
一个音频解码器驱动需要获取并使能它的核心供电电源。- 推荐做法:
struct regulator *reg = devm_regulator_get(&pdev->dev, "vdd-core");
- 效果:
regulator
句柄会在驱动卸载时被自动regulator_put
。注意,devres
通常只管理资源的“获取”与“释放”,而“使能/禁用”(如regulator_enable
/disable
)这类状态操作通常仍需在.probe
和.remove
中手动配对调用。
- 推荐做法:
是否有不推荐使用该技术的场景?为什么?
- 生命周期不匹配的资源:如果一个资源是在设备文件被
open()
时创建,在close()
时销毁,那么它的生命周期是和文件句柄绑定的,而不是和设备驱动绑定。这种场景下不应使用devres
,而应使用标准的手动管理方式。 - 需要全局共享的资源:如果一个资源由多个设备驱动共享,并且其生命周期独立于任何单个设备,那么它也不应由
devres
管理。
对比分析
请将其 与 其他相似技术 进行详细对比。
devres
的主要对比对象是传统的、完全手动的资源管理方法。
特性 | devres (托管式, devm_* API) |
手动资源管理 (传统方式) |
---|---|---|
实现方式 | 驱动调用devm_* 函数,资源被自动添加到设备的资源链表中。释放操作由驱动核心在驱动解绑时自动触发。 |
驱动直接调用kmalloc , request_irq 等函数。必须在所有可能的错误路径和.remove 函数中显式地编写释放代码。 |
代码复杂度 | 低。.probe 函数逻辑清晰,呈线性结构。错误处理极其简单。.remove 函数也大大简化。 |
高。.probe 函数中充满了goto 标签和错误处理代码块,难以阅读和维护。.remove 中需要重复的释放逻辑。 |
健壮性 | 非常高。通过自动化机制从设计上防止了资源泄漏。释放顺序得到保证。 | 低。极易因人为疏忽而引入资源泄漏的bug。维护和修改时容易引入新的问题。 |
性能开销 | 有轻微开销。每个资源都需要一个额外的元数据结构,并涉及链表操作。 | 无额外开销。直接调用底层函数,性能最高。 |
开发效率 | 高。开发者可以专注于驱动的核心逻辑,而不用花费大量精力在资源管理的样板代码上。 | 低。需要编写和调试大量繁琐的资源清理代码。 |
适用场景 | 绝大多数设备驱动中的资源管理,是现代内核开发的首选和标准。 | 仅适用于资源生命周期与设备-驱动绑定关系不一致的罕见场景。 |
add_dr
: 将资源节点添加到设备链表
这是一个静态内联函数, 负责执行将资源节点实际链接到设备资源链表中的原子操作。
1 | /* |
devres_add
: 注册设备资源
这是一个暴露给内核其他部分使用的API函数。它负责准备工作和加锁, 然后调用add_dr
来完成资源的添加。
1 | /** |