[TOC]
kernel/resource.c 硬件资源管理(Hardware Resource Management) 防止硬件资源冲突的仲裁者
历史与背景
这项技术是为了解决什么特定问题而诞生的?
kernel/resource.c
中的代码实现了一个基础性的内核框架,用于管理和仲裁对物理硬件资源的独占式访问。这项技术的诞生是为了解决一个在任何操作系统中都存在的根本性问题:防止多个设备驱动程序同时访问和控制同一块硬件资源,从而导致数据损坏、系统崩溃和不可预测的行为。
这些被管理的硬件资源主要是指基于地址的资源,包括:
- I/O内存(Memory-Mapped I/O, MMIO):设备寄存器被映射到CPU的物理地址空间的一部分。
- I/O端口(I/O Ports):在x86等架构上,存在一个独立于主内存地址空间的、较小的I/O地址空间。
在没有一个集中管理者的情况下,每个驱动程序都可能硬编码它认为自己应该使用的地址。如果两个不同的驱动(或者同一个驱动的两个实例)尝试使用重叠的地址范围,就会发生冲突。resource.c
提供了一个中央注册和分配系统,确保任何一块已被声明的资源,在被释放之前都不能被其他任何驱动再次声明。
它的发展经历了哪些重要的里程碑或版本迭代?
资源管理框架是内核设备模型的基础,其演进与硬件的发展紧密相关。
- 基本注册与冲突检测:最初的实现提供了一个简单的机制,允许驱动程序“声明”它们使用的I/O端口和内存范围,并检查这些范围是否与其他已声明的范围重叠。
- 分层树状结构(Hierarchical Tree Structure):这是一个决定性的里程碑。
struct resource
被设计成一个可以形成父子关系的树状结构。这完美地映射了现代硬件的拓扑结构。例如,一个PCI总线可以声明一个巨大的内存区域作为父资源,然后连接在该总线上的各个PCI设备可以从这个父资源中请求分配出属于自己的、较小的子资源。这不仅能有效管理资源,还能清晰地表达硬件间的从属关系。 - 与硬件发现机制的集成:随着Plug-and-Play、PCI、ACPI和Device Tree等技术的普及,硬件资源的定义从驱动中的硬编码,转移到了由固件或硬件描述文件(如Device Tree)提供。总线驱动程序(如PCI驱动)负责在设备枚举时解析这些信息,并调用
resource.c
中的函数,将这些资源注册到全局的资源树中。 - 通过/proc文件系统的可见性:内核将全局的I/O内存和I/O端口资源树分别暴露在
/proc/iomem
和/proc/ioports
文件中。这为开发者和系统管理员提供了一个极其重要的工具,用于查看当前系统中所有硬件资源的分配情况,是调试资源冲突问题的关键。
目前该技术的社区活跃度和主流应用情况如何?
kernel/resource.c
是内核中一个极其稳定、成熟和基础的组件。它不是一个可选功能,而是所有与物理硬件直接交互的设备驱动程序都必须依赖的基石。所有总线驱动(PCI, USB, Platform Bus等)和设备驱动在初始化时,第一步几乎都是请求和映射它们需要的硬件资源,这些操作最终都由resource.c
提供的框架来仲裁。
核心原理与设计
它的核心工作原理是什么?
resource.c
的核心是两个全局的、树状的资源管理器,以及一组用于操作这些树的API。
核心数据结构
struct resource
:- 它代表了一段连续的硬件资源(如0xFEBE0000到0xFEBEFFFF的MMIO区域)。
- 关键成员包括:
start
(起始地址)、end
(结束地址)、name
(拥有该资源的驱动名)、flags
(资源类型,如IORESOURCE_MEM
,IORESOURCE_IO
,以及状态IORESOURCE_BUSY
表示已被占用)。 parent
,sibling
,child
指针:用于构建资源树。
两大资源树:
- 内核维护了两个全局的根资源:
iomem_resource
和ioport_resource
。 - 所有I/O内存资源最终都作为
iomem_resource
的子孙节点存在。 - 所有I/O端口资源最终都作为
ioport_resource
的子孙节点存在。
- 内核维护了两个全局的根资源:
工作流程:
- 注册(Registration):在系统启动或设备热插拔时,总线驱动会从ACPI或Device Tree中读取设备的资源信息,并调用
insert_resource()
等函数,将这些资源作为其父资源(通常是总线自身的资源)的子节点插入到相应的资源树中。 - 请求/声明(Requesting):设备驱动程序在
probe()
函数中,会调用request_mem_region()
或request_region()
(它们是__request_region()
的封装)来声明自己需要使用的资源。 __request_region()
会在资源树中查找请求的范围。如果该范围完全空闲(没有被标记为IORESOURCE_BUSY
),函数就会将该范围标记为BUSY
,并将其name
设置为当前驱动的名字,然后返回成功。- 如果请求的范围与任何一个已标记为
BUSY
的区域有重叠,请求就会失败。 - 释放(Releasing):当驱动程序被卸载时,它会在
.remove()
函数中调用release_mem_region()
或release_region()
来释放它所占用的资源,清除BUSY
标志,使其可供其他驱动使用。
- 注册(Registration):在系统启动或设备热插拔时,总线驱动会从ACPI或Device Tree中读取设备的资源信息,并调用
它的主要优势体现在哪些方面?
- 冲突预防:这是其最核心的价值,从根本上保证了系统硬件资源的有序访问。
- 可见性与可调试性:通过
/proc
接口,使得整个系统的硬件资源布局一目了然。 - 硬件拓扑映射:树状结构能够清晰地反映硬件总线和设备之间的层级关系。
- 动态性:能够很好地支持热插拔设备的资源动态分配和回收。
它存在哪些已知的劣势、局限性或在特定场景下的不适用性?
- 依赖于正确的硬件描述:该框架的有效性完全依赖于ACPI表或Device Tree提供的资源信息是准确无误的。如果硬件描述本身就是错误的(例如,两个设备被错误地赋予了重叠的地址),
resource.c
无法检测到这一点,仍然会导致冲突。 - 模型简单:它主要管理基于地址的、可独占的资源。对于更复杂的资源类型,如中断(IRQ)或DMA通道,虽然它们有时也会被表示为
struct resource
,但它们有自己更复杂的专用管理子系统。 - 静态性:一旦资源被注册到树中,其范围通常是固定的。该框架不适合用于管理那些大小和位置在运行时会频繁、剧烈变化的资源。
使用场景
在哪些具体的业务或技术场景下,它是首选解决方案?
resource.c
提供的机制是内核驱动程序申请独占访问MMIO和I/O端口地址范围的唯一标准方案。
- PCI设备驱动:一个网卡驱动在初始化时,会通过PCI核心代码获取其BAR(Base Address Register)中定义的MMIO地址范围,然后调用
request_mem_region()
来独占这块区域。 - SoC中的平台设备:一个嵌入式系统中的I2C控制器驱动,会从Device Tree中获取其寄存器基地址和大小,并调用
platform_get_resource()
来获得struct resource
,然后请求它。 - 任何直接与硬件寄存器通信的驱动:无论是显卡、声卡、SATA控制器还是USB主控制器,只要它需要通过读写特定物理地址来与硬件交互,就必须使用这个框架。
是否有不推荐使用该技术的场景?为什么?
- 软件内存分配:此框架严禁用于分配通用RAM给软件使用。这是
kmalloc
/vmalloc
等内存分配器的任务。resource.c
管理的是与硬件直接关联的物理地址空间。 - 中断、DMA等复杂资源:如前所述,IRQ和DMA有其自己的、功能更丰富的管理框架(
kernel/irq
,dma-mapping
API)。不应直接使用request_resource
来管理它们。
对比分析
请将其 与 其他相似技术 进行详细对比。
在内核中,resource.c
是管理特定类型资源的基础设施。最适合的对比是与其他管理不同类型硬件资源的子系统进行比较。
特性 | kernel/resource.c (IOMEM/IOPORT) |
kernel/irq (中断管理) |
DMA Mapping API | gpiolib (GPIO管理) |
---|---|---|---|---|
管理对象 | 连续的物理地址范围。 | 中断号 (IRQ numbers)。 | DMA通道和内存一致性。 | 单个GPIO引脚。 |
核心抽象 | struct resource (start, end, flags)。 |
struct irq_desc , irq_chip 。 |
struct dma_chan , dma_addr_t 。 |
struct gpio_desc 。 |
主要操作 | request_region , release_region (声明所有权)。 |
request_irq , free_irq (注册处理函数)。 |
dma_alloc_coherent , dma_map_single (分配/映射DMA缓冲区)。 |
gpiod_get , gpiod_direction_output (获取引脚, 设置方向/值)。 |
主要目的 | 防止地址冲突,提供独占访问。 | 事件处理,将硬件中断路由到正确的处理函数。 | 数据传输,协调CPU和外设对内存的访问。 | 通用I/O控制,读取输入状态或设置输出电平。 |
复杂性 | 较低。主要是对地址范围的预定和检查。 | 非常高。涉及中断控制器、亲和性、线程化中断等复杂概念。 | 高。涉及IOMMU、缓存一致性、scatter-gather list等。 | 中等。涉及引脚复用、防抖、开漏等多种状态。 |
kernel/resource.c
ioport_resource
- 在计算机领域,PCI 是 Peripheral Component Interconnect 的缩写,表示 外设部件互连。它是一种计算机总线标准,用于将外部硬件设备(如网卡、声卡、显卡等)连接到计算机的主板上。
1 | struct resource ioport_resource = { |
request_resource 请求并保留 I/O 或内存资源
1 | /* 如果无法请求,则返回冲突条目 */ |
__region_intersects: 遍历资源树以检查内存区域的相交情况
此函数是 region_intersects
的内部核心实现。它的主要职责是递归地遍历一个给定的父资源(parent
)下的所有子资源, 以判断一个指定的内存区域(start
, size
)是否与特定类型(flags
, desc
)的资源发生重叠, 并精确地分类这种重叠的性质。
- 在单核无MMU的STM32H750平台上的原理与作用
在STM32H750平台上, 系统的物理内存布局由设备树(Device Tree)描述, 内核在启动时会根据设备树构建一个静态的资源树, 其根节点是 iomem_resource
。这个树清晰地描绘了每一块内存的归属, 例如哪些地址范围是SRAM, 哪些是Flash, 哪些是USART1的寄存器, 哪些是为DMA预留的内存等等。
__region_intersects
函数就是在这个静态的、代表了MCU整个物理地址空间的资源树上进行工作的。
- 遍历机制: 它通过
parent->child
和p->sibling
指针, 对资源树进行深度优先的遍历。对于STM32系统来说, 这就是逐一检查设备树中定义的内存资源(reg
属性)和预留内存(reserved-memory
)。 - 核心逻辑: 它的逻辑比简单的重叠检查要复杂。当它发现一个与目标区域重叠的资源
p
时, 如果p
的类型不匹配, 它并不会立即将其计为“其它类型重叠”。而是会进一步检查p
的所有子资源, 看这些子资源是否是匹配的类型, 并且是否能“完全覆盖”掉p
与目标区域重叠的部分。这对于处理嵌套资源(例如, 一个大的I/O内存窗口内包含了一小块系统RAM)至关重要。 - 安全性保障: 这个函数是内核内存安全模型的基石之一。例如, 当
memremap
函数被调用以映射一块DMA内存时, 它会使用此函数来验证这块内存确实是预留给DMA的(匹配IORESOURCE_MEM
和IORES_DESC_RESERVED
), 并且没有意外地与其它关键外设(如中断控制器)的寄存器区域发生重叠。这在没有MMU硬件进行地址隔离的STM32平台上, 是防止驱动程序之间内存踩踏和系统崩溃的关键软件机制。
1 | /* |
region_intersects: 判断一个内存区域是否与特定类型的系统资源相交
此函数的核心作用是检查一个给定的物理内存区域(由起始地址 start
和大小 size
定义)是否与内核资源树中特定类型(由 flags
定义)和特定描述符(由 desc
定义)的资源区域存在交集。它被内核其他子系统(如 memremap
)调用, 以确保操作的合法性, 例如防止用户意外地以不正确的缓存属性重新映射主系统RAM。
- 在单核无MMU的STM32H750平台上的原理与作用
在STM32H750这样的嵌入式平台上, 内核通过解析设备树(Device Tree)来了解系统的硬件布局。所有外设的寄存器地址范围、片上SRAM和Flash、以及通过reserved-memory
节点定义的专用内存(如DMA内存池), 都会被注册到以 iomem_resource
为根的全局资源树中。
region_intersects
函数就是在这个已建立的资源树上进行查询的接口。它的工作流程如下:
- 锁定资源树:它首先获取一个读写锁(rwlock)的“读锁”。在单核抢占式(preemptible)内核中, 这个锁的作用不是防止多核并发访问, 而是防止在读取资源树的过程中, 当前任务被另一个可能修改资源树的任务抢占。例如, 当一个驱动模块被卸载时, 它会从资源树中移除自己的资源, 这是一个写操作。
read_lock
确保了region_intersects
在执行期间看到的是一个一致、完整的资源树快照。 - 遍历与比较:它调用内部函数
__region_intersects
来遍历资源树的节点。对于树中的每一个资源, 它会检查其flags
和desc
是否与查询的参数匹配, 如果匹配, 再判断其地址范围是否与[start, start + size)
有重叠。 - 返回结果:根据遍历比较的结果, 它返回一个状态码, 明确指出查询的区域是完全不相交(
REGION_DISJOINT
)、仅与目标类型的资源相交(REGION_INTERSECTS
), 还是与多种不同类型的资源都有相交(REGION_MIXED
)。
这个函数是保证内核内存操作安全性和正确性的基础构件, 确保了对底层物理内存的操作不会与已注册的硬件资源发生意外冲突。
1 | /* |
__request_resource: 在资源树中查找冲突并插入新节点
此函数是Linux内核资源管理子系统中最底层的冲突检测与插入算法。它被__request_region_locked
调用, 在一个已经锁定的、安全的上下文中执行。它的核心原理是在一个按起始地址排序的资源链表中, 高效地查找一个可以插入新资源节点的”空隙”。如果找到空隙, 它就执行插入并返回成功(NULL
); 如果在查找过程中发现任何地址重叠, 它会立即停止并返回指向冲突节点的指针。
这是一个精巧且高效的链表操作算法, 其关键在于:
- 边界检查: 它首先进行快速的边界检查, 确保要插入的新资源
new
完全包含在父资源root
的范围之内。如果new
的范围超出了root
的范围, 这本身就是一种冲突, 函数会返回root
作为冲突节点。 - 排序链表遍历: 它假设
root
的子节点链表(root->child
)是一个已经按照start
地址从小到大排好序的链表。这是整个算法高效运行的基础。 - 指针的指针 (
**p
): 这是理解此函数最关键的部分。它不使用传统的previous
和current
指针来遍历链表, 而是使用一个”指向指针的指针”p
。p
最初指向root->child
这个指针本身。- 在循环中,
p
会依次指向前一个节点的sibling
指针。 - 这种技巧的妙处在于, 当找到插入点时, 只需一条
*p = new;
语句就可以完成链接。*p
修改的可能是root->child
指针(如果在头部插入), 也可能是前一个节点的sibling
指针, 无需区分这两种情况, 代码非常简洁。
- 冲突与插入逻辑: 循环中的逻辑非常清晰:
- 情况A (无重叠): 如果当前节点
tmp
的结束地址小于new
的起始地址(tmp->end < start
), 说明两者没有重叠, 可以安全地继续检查下一个兄弟节点。 - 情况B (找到插入点): 如果当前节点
tmp
的起始地址已经大于new
的结束地址(tmp->start > end
), 或者已经到达链表末尾(!tmp
), 说明我们找到了一个可以容纳new
的空隙。此时执行插入操作, 并返回NULL
表示成功。 - 情况C (冲突): 如果既不满足情况A也不满足情况B, 那么唯一的可能性就是
new
和tmp
的地址范围有重叠。此时,tmp
就是冲突节点, 函数立即将其返回。
- 情况A (无重叠): 如果当前节点
这个函数是纯粹的、与硬件无关的数据结构操作算法, 是构建整个资源仲裁系统的基石。它不涉及任何锁或硬件访问, 只是高效地在内存中维护一个有序的、无重叠的地址区间集合。
1 | /* |
__request_region_locked: 在锁定下执行资源区域请求的核心逻辑
此函数是Linux资源管理子系统的最深层、最核心的执行者。它的命名后缀_locked
明确地告知调用者, 此函数必须在一个已经获取了resource_lock
写锁定的上下文中被调用。它的核心原理是: 在一个无限循环中, 尝试将一个代表新区域的资源节点插入到全局资源树中。如果插入成功, 则函数完成; 如果遇到冲突, 它会根据冲突的类型, 尝试两种高级策略——“向下深入”或”睡眠等待”——来解决冲突。如果所有策略都失败, 才最终宣告失败。
这个函数的复杂性完全体现在其精妙的冲突解决机制上:
- 初始化与尝试: 函数首先填充传入的
res
结构体, 标记其为IORESOURCE_BUSY
, 然后进入一个无限循环(for(;;)
), 在循环中调用__request_resource
尝试将其插入资源树。 - 成功路径: 如果
__request_resource
返回NULL
, 表示没有冲突, 资源被成功插入。函数立即跳出循环并返回成功。 - 冲突处理路径: 如果
__request_resource
返回一个非NULL
的conflict
指针, 意味着发生了地址重叠。此时, 函数会依次尝试:- 策略一: 向下深入(Dive Down)。如果冲突的区域
conflict
本身没有被标记为BUSY
, 这通常意味着conflict
是一个更大的”容器”区域(例如, 一个为整个PCIe设备预留的总线地址窗口)。此时, 函数会将conflict
设置为新的parent
, 然后continue
继续循环, 尝试在那个更具体的容器区域内重新申请。这实现了在资源树中自动寻找最精确插入点的逻辑。 - 策略二: 睡眠等待(Wait for Muxed Resource)。如果冲突的区域被标记为
IORESOURCE_MUXED
, 这是一种特殊的可共享资源, 意味着虽然它已被占用, 但未来可能会被释放。此时, 函数会执行一个经典的”解锁-睡眠-重锁”模式:
a. 将当前进程加入到该复用资源的等待队列muxed_resource_wait
中。
b. 临时释放resource_lock
写锁。这是至关重要的一步, 因为持有自旋锁是绝对不能睡眠的, 否则会造成系统死锁。
c. 调用schedule()
将当前进程投入睡眠, 让出CPU。
d. 当其他代码释放了这个复用资源并唤醒等待队列后, 当前进程会从schedule()
返回。
e. 重新获取resource_lock
写锁, 并continue
继续循环, 再次尝试申请。 - 最终失败: 如果以上两种特殊策略都不适用(即, 这是一个普通的、确实繁忙的资源冲突), 函数将放弃尝试, 返回
-EBUSY
错误。
- 策略一: 向下深入(Dive Down)。如果冲突的区域
在STM32H750这样的单核抢占式系统上, 虽然不存在多核并发, 但resource_lock
依然是必需的。它能防止一个正在修改资源树的任务被另一个也想修改资源树的任务或中断抢占, 从而保证了全局资源树数据结构的绝对一致性和完整性。IORESOURCE_MUXED
这种特性在典型的MCU嵌入式开发中较少使用, 但内核的通用框架必须包含这种逻辑以支持所有类型的硬件。
1 | /* |
__request_region: 预留一个硬件资源区域的核心实现
此函数是Linux内核资源管理子系统的底层核心。它不是一个devm
(设备资源管理)函数, 而是被devm_request_region
系列函数在内部调用的最终执行者。它的核心原理是: 在一个全局的、被读写锁保护的资源树中, 检查一段指定的物理地址范围是否可用; 如果可用, 就为其创建一个新的”资源”节点并插入到树中, 以此声明对该地址范围的所有权, 防止其他驱动程序再次申请, 从而实现对硬件资源的仲裁和冲突避免。
整个函数的执行流程体现了对并发访问和数据一致性的严谨处理:
- 分配资源描述符: 函数首先调用
alloc_resource
在内存中分配一个struct resource
结构体。这就像是申请一张空白的表格, 准备用来记录将要被预留的这块区域的信息。 - 获取写锁定: 这是最关键的一步。它调用
write_lock
来获取一个全局的读写锁resource_lock
。- 为什么是读写锁? 因为内核中可能会有很多地方需要读取资源树(例如, 用户通过
cat /proc/iomem
查看内存布局), 这些读取操作可以安全地并发进行。然而, 写入操作(添加或删除一个区域)是互斥的, 在写入时必须阻止任何其他的读或写操作, 以保证数据树的完整性。读写锁完美地满足了这种”多读单写”的场景。 - 在STM32H750这样的单核抢占式系统上, 获取这个锁能有效防止当前任务在修改资源树的中间过程时, 被另一个也想操作资源树的任务抢占, 从而保证了操作的原子性。
- 为什么是读写锁? 因为内核中可能会有很多地方需要读取资源树(例如, 用户通过
- 执行核心操作 (在锁定状态下): 在获得锁之后, 它调用
__request_region_locked
。这个内部函数负责执行真正的逻辑: 遍历指定的父资源树(例如iomem_resource
), 检查请求的范围(start
到start+n
)是否与任何已存在的区域重叠。如果没有重叠, 它就将第一步分配的空白resource
结构体填充好信息并插入到树中。 - 错误处理: 如果
__request_region_locked
因为地址冲突等原因失败(返回错误码), 那么第一步分配的空白resource
结构体就没用了。函数会立即调用free_resource
将其释放, 防止内存泄漏, 然后返回NULL
表示失败。 - 撤销用户空间映射 (仅IO内存): 如果请求成功, 并且操作的是IO内存(
parent == &iomem_resource
), 它会调用revoke_iomem
。这是一个重要的安全和正确性措施, 它的作用是检查是否有任何用户空间进程(例如通过/dev/mem
或uio
驱动)已经映射了这段即将被内核驱动接管的物理内存。如果有,revoke_iomem
会强制使这些用户空间的映射失效, 确保只有内核驱动能够独占地、安全地访问这些硬件寄存器。在无MMU的系统上, 此函数的功能可能会有所简化, 但其意图——确保内核的独占控制权——保持不变。 - 返回成功: 所有步骤都成功后, 函数返回指向新创建并插入到资源树中的
resource
结构体的指针。这个指针本身就是一种”所有权证明”, 虽然大部分驱动并不直接使用它, 而是将其视为操作成功的标志。
1 | /** |
devm_request_region / devm_request_mem_region: 自动管理的资源区域请求
此代码片段展示了Linux内核中用于请求(request)或预留(reserve)一段物理地址范围的devm
(设备资源管理)接口。它的核心原理是为设备驱动程序提供一个安全、便捷的API来声明其对某段硬件地址(无论是内存映射的IO寄存器还是传统的IO端口)的所有权, 并确保这个”所有权”声明在驱动程序卸载时会被自动撤销。
这套机制是解决两个基本问题的关键:
- 资源冲突仲裁: 内核维护着一个全局的资源树(可以在
/proc/iomem
和/proc/ioports
中看到其状态)。当一个驱动调用devm_request_mem_region
时, 内核会检查其请求的地址范围是否已经与一个已存在的预留区域重叠。如果重叠, 请求就会失败。这从根本上防止了两个不同的驱动程序意外地同时访问和操作同一组硬件寄存器, 从而避免了难以诊断的硬件冲突和系统不稳定。 - 资源生命周期管理: 这是
devm
框架的核心价值。- 函数首先分配一个微小的管理结构(
region_devres
), 并将请求的参数(起始地址, 大小等)保存在其中。 - 然后, 它调用底层的
__request_region
来执行实际的仲裁和预留操作。 - 只有当底层预留成功时, 它才会调用
devres_add
将那个小小的管理结构链接到设备的资源列表中。 - 当驱动被卸载时, 内核会自动遍历该设备的资源列表, 并调用与每个管理结构关联的释放函数(此处为
devm_region_release
), 该释放函数会使用之前保存的参数来精确地释放对应的资源预留。
- 函数首先分配一个微小的管理结构(
对于像STM32H750这样的ARM架构微控制器, 它没有独立的IO端口地址空间, 所有的硬件外设寄存器都是内存映射的。因此, 驱动程序总是会使用devm_request_mem_region
这个宏。这个函数是stm32_pctl_probe
中调用的devm_ioremap_resource
函数的内部组成部分, 是在执行ioremap
之前必须成功完成的先决条件。它确保了在对pinctrl或GPIO寄存器进行任何读写之前, 该驱动已经合法地声明了对那片物理内存的所有权。
核心实现函数: __devm_request_region
1 | /* |
便捷宏封装
1 | /* |
I/O内存撤销框架初始化:创建用于访问控制的伪文件系统与全局Inode
本代码片段的功能是为内核的I/O内存(iomem
)访问控制机制建立基础。其核心作用是创建一个专门的、内存中的伪文件系统(pseudo filesystem),并在此文件系统中生成一个全局唯一的匿名inode(iomem_inode
)。这个inode将作为内核中“已撤销”(revoked)I/O内存区域的标志物。当一个驱动程序或子系统决定禁止通过/dev/mem
等接口直接访问某段物理I/O内存时,它会将该内存区域与这个全局inode关联起来。后续的访问检查逻辑就可以通过判断一个地址是否与此inode关联,来决定是允许还是拒绝访问。
实现原理分析
此代码的实现巧妙地利用了Linux的虚拟文件系统(VFS)来管理一个非文件类的内核资源。
启动参数配置 (
strict_iomem
,__setup
):- 内核提供了一个启动参数
iomem=
,允许系统管理员在启动时选择I/O内存访问检查的严格程度。 __setup("iomem=", strict_iomem)
宏将strict_iomem
函数注册为iomem=
参数的回调。strict_iomem
函数解析参数值,如果包含 “relaxed”,则禁用严格检查(strict_iomem_checks = 0
);如果包含 “strict”,则启用严格检查(strict_iomem_checks = 1
)。这提供了一个灵活的策略控制。
- 内核提供了一个启动参数
伪文件系统定义 (
iomem_fs_type
):- 代码定义了一个名为 “iomem” 的
file_system_type
。这是一个“伪”文件系统,因为它不与任何块设备(如硬盘)关联,其所有对象都只存在于内存中。 - 其目的是为了能够合法地创建一个超级块(superblock)对象,而超级块是创建inode的前提。
- 代码定义了一个名为 “iomem” 的
初始化与Inode创建 (
iomem_init_inode
):fs_initcall
确保此函数在VFS子系统初始化之后执行。simple_pin_fs
是一个关键的辅助函数。它负责“挂载”这个名为 “iomem” 的伪文件系统,并获取其vfsmount
和超级块(mnt_sb
)对象。pin
意味着这个挂载会被“钉住”,防止被意外卸载。alloc_anon_inode
在刚刚挂载的伪文件系统的超级块上,分配一个“匿名”inode。这个inode不对应任何目录项(dentry),它仅仅是一个独立的、存在于内存中的VFS inode对象。smp_store_release(&iomem_inode, inode)
是一个带有内存屏障的原子写操作。它将新创建的inode的地址安全地发布到全局指针iomem_inode
。_release
语义确保了在此之前对inode的所有初始化操作,对于所有其他CPU来说,都在它们看到iomem_inode
的新值之前完成。这是保证多处理器系统一致性的关键。
代码分析
1 | // strict_iomem: 内核启动参数 "iomem=" 的处理函数。 |