[TOC]

kernel/resource.c 硬件资源管理(Hardware Resource Management) 防止硬件资源冲突的仲裁者

历史与背景

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

kernel/resource.c 中的代码实现了一个基础性的内核框架,用于管理和仲裁对物理硬件资源的独占式访问。这项技术的诞生是为了解决一个在任何操作系统中都存在的根本性问题:防止多个设备驱动程序同时访问和控制同一块硬件资源,从而导致数据损坏、系统崩溃和不可预测的行为。

这些被管理的硬件资源主要是指基于地址的资源,包括:

  1. I/O内存(Memory-Mapped I/O, MMIO):设备寄存器被映射到CPU的物理地址空间的一部分。
  2. 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。

  1. 核心数据结构 struct resource

    • 它代表了一段连续的硬件资源(如0xFEBE0000到0xFEBEFFFF的MMIO区域)。
    • 关键成员包括:start(起始地址)、end(结束地址)、name(拥有该资源的驱动名)、flags(资源类型,如IORESOURCE_MEM, IORESOURCE_IO,以及状态IORESOURCE_BUSY表示已被占用)。
    • parent, sibling, child 指针:用于构建资源树。
  2. 两大资源树

    • 内核维护了两个全局的根资源:iomem_resourceioport_resource
    • 所有I/O内存资源最终都作为iomem_resource的子孙节点存在。
    • 所有I/O端口资源最终都作为ioport_resource的子孙节点存在。
  3. 工作流程

    • 注册(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标志,使其可供其他驱动使用。

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

  • 冲突预防:这是其最核心的价值,从根本上保证了系统硬件资源的有序访问。
  • 可见性与可调试性:通过/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

  • 在计算机领域,PCIPeripheral Component Interconnect 的缩写,表示 外设部件互连。它是一种计算机总线标准,用于将外部硬件设备(如网卡、声卡、显卡等)连接到计算机的主板上。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
struct resource ioport_resource = {
.name = "PCI IO",
.start = 0,
.end = IO_SPACE_LIMIT,
.flags = IORESOURCE_IO,
};
EXPORT_SYMBOL(ioport_resource);

struct resource iomem_resource = {
.name = "PCI mem",
.start = 0,
.end = -1,
.flags = IORESOURCE_MEM,
};
EXPORT_SYMBOL(iomem_resource);

request_resource 请求并保留 I/O 或内存资源

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
/* 如果无法请求,则返回冲突条目 */
static struct resource * __request_resource(struct resource *root, struct resource *new)
{
resource_size_t start = new->start;
resource_size_t end = new->end;
struct resource *tmp, **p;
//检查是否与父节点有范围冲突
if (end < start)
return root;
if (start < root->start)
return root;
if (end > root->end)
return root;

p = &root->child;
for (;;) {
tmp = *p;
//当前子节点为空(!tmp)或当前子节点的起始地址大于新资源的结束地址(tmp->start > end),说明新资源可以插入到当前子节点之前
if (!tmp || tmp->start > end) {
new->sibling = tmp;
*p = new;
new->parent = root;
return NULL;
}
p = &tmp->sibling;
if (tmp->end < start)
continue;
return tmp;
}
}

/**
* request_resource_conflict - 请求并保留 I/O 或内存资源
* @root:根资源描述符
* @new:调用方所需的资源描述符
*
* 成功时返回 0,出错时返回 Conflict 资源。
*/
struct resource *request_resource_conflict(struct resource *root, struct resource *new)
{
struct resource *conflict;

write_lock(&resource_lock);
conflict = __request_resource(root, new);
write_unlock(&resource_lock);
return conflict;
}

/**
* request_resource - 请求并保留 I/O 或内存资源
* @root:根资源描述符
* @new:调用方所需的资源描述符
*
* 成功时返回 0,出错时返回负错误代码。
*/
int request_resource(struct resource *root, struct resource *new)
{
struct resource *conflict;

conflict = request_resource_conflict(root, new);
return conflict ? -EBUSY : 0;
}

__region_intersects: 遍历资源树以检查内存区域的相交情况

此函数是 region_intersects 的内部核心实现。它的主要职责是递归地遍历一个给定的父资源(parent)下的所有子资源, 以判断一个指定的内存区域(start, size)是否与特定类型(flags, desc)的资源发生重叠, 并精确地分类这种重叠的性质。

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

在STM32H750平台上, 系统的物理内存布局由设备树(Device Tree)描述, 内核在启动时会根据设备树构建一个静态的资源树, 其根节点是 iomem_resource。这个树清晰地描绘了每一块内存的归属, 例如哪些地址范围是SRAM, 哪些是Flash, 哪些是USART1的寄存器, 哪些是为DMA预留的内存等等。

__region_intersects 函数就是在这个静态的、代表了MCU整个物理地址空间的资源树上进行工作的。

  • 遍历机制: 它通过 parent->childp->sibling 指针, 对资源树进行深度优先的遍历。对于STM32系统来说, 这就是逐一检查设备树中定义的内存资源(reg属性)和预留内存(reserved-memory)。
  • 核心逻辑: 它的逻辑比简单的重叠检查要复杂。当它发现一个与目标区域重叠的资源 p 时, 如果 p 的类型不匹配, 它并不会立即将其计为“其它类型重叠”。而是会进一步检查 p 的所有子资源, 看这些子资源是否是匹配的类型, 并且是否能“完全覆盖”掉 p 与目标区域重叠的部分。这对于处理嵌套资源(例如, 一个大的I/O内存窗口内包含了一小块系统RAM)至关重要。
  • 安全性保障: 这个函数是内核内存安全模型的基石之一。例如, 当memremap函数被调用以映射一块DMA内存时, 它会使用此函数来验证这块内存确实是预留给DMA的(匹配 IORESOURCE_MEMIORES_DESC_RESERVED), 并且没有意外地与其它关键外设(如中断控制器)的寄存器区域发生重叠。这在没有MMU硬件进行地址隔离的STM32平台上, 是防止驱动程序之间内存踩踏和系统崩溃的关键软件机制。
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
/*
* 这是一个内部静态函数, 执行实际的区域相交检查.
*
* @parent: 指向父资源节点的指针, 函数将从此节点的子节点开始遍历.
* @start: 要检查的内存区域的起始物理地址.
* @size: 要检查的内存区域的大小.
* @flags: 要匹配的资源标志位 (例如 IORESOURCE_MEM).
* @desc: 要匹配的资源描述符 (例如 IORES_DESC_RESERVED), IORES_DESC_NONE 表示不关心描述符.
* @return: 返回 REGION_DISJOINT, REGION_INTERSECTS, 或 REGION_MIXED.
*/
static int __region_intersects(struct resource *parent, resource_size_t start,
size_t size, unsigned long flags,
unsigned long desc)
{
/*
* type: 一个计数器, 用于记录与"目标类型"资源(由flags和desc定义)发生重叠的次数.
* other: 一个计数器, 用于记录与"非目标类型"资源发生重叠的次数.
*/
int type = 0; int other = 0;
/*
* p: 用于在主循环中遍历父节点的所有直接子节点 (兄弟节点链表).
* dp: 用于在内层循环中遍历一个节点的所有后代节点.
*/
struct resource *p, *dp;
/*
* res: 用于存储由 start 和 size 定义的目标内存区域.
* o: 用于存储两个区域实际发生重叠(相交)的部分.
*/
struct resource res, o;
/*
* 一个布尔标志, 用于追踪一个非匹配的父资源区域是否被其匹配类型的后代资源完全覆盖.
*/
bool covered;

/*
* 使用 DEFINE_RES 宏来初始化 res 结构体, 使其代表 [start, start + size - 1] 这个区域.
*/
res = DEFINE_RES(start, size, 0);

/*
* 遍历 parent 节点的所有直接子节点. p 从第一个子节点开始, 然后通过 sibling 指针移动到下一个兄弟节点.
*/
for (p = parent->child; p ; p = p->sibling) {
/*
* 调用 resource_intersection 检查子资源 p 和我们的目标区域 res 是否有重叠.
* 如果没有重叠, 该函数返回 false, 我们就继续检查下一个兄弟节点.
* 如果有重叠, 重叠的部分会被写入到 o 结构体中, 函数返回 true.
*/
if (!resource_intersection(p, &res, &o))
continue;
/*
* 调用 is_type_match 检查发生重叠的资源 p 的类型是否与我们正在寻找的类型(flags, desc)相匹配.
*/
if (is_type_match(p, flags, desc)) {
/*
* 如果类型匹配, 增加 type 计数器, 然后继续检查下一个兄弟节点.
*/
type++;
continue;
}
/*
* 如果代码执行到这里, 说明我们找到了一个与目标区域重叠的资源 p, 但它的类型不匹配.
* 我们不能立即将其计为 "other", 因为它可能是一个容器, 其内部包含了我们想要寻找的匹配类型的资源.
*/

/*
* 初始化 covered 标志为 false.
*/
covered = false;
/*
* 使用 for_each_resource 宏遍历 p 的所有后代资源 (包括p自己, 但我们已经检查过p了, 这里应从其子节点开始).
* 这个循环的目的是检查 p 的内部是否包含能够"覆盖"掉重叠区域 o 的匹配类型资源.
*/
for_each_resource(p, dp, false) {
/*
* 检查后代资源 dp 是否与我们的目标区域 res 有重叠. 如果没有, 就没必要继续处理.
*/
if (!resource_overlaps(dp, &res))
continue;
/*
* 检查这个后代资源的类型是否匹配.
*/
if (is_type_match(dp, flags, desc)) {
/*
* 如果后代资源类型匹配, 增加 type 计数器.
*/
type++;
/*
* 如果这个匹配的后代资源的起始地址大于重叠区域的起始地址,
* 意味着重叠区域 o 的开头部分没有被覆盖, 所以这仍然是一个 "mixed" 情况. 退出内层循环.
*/
if (dp->start > o.start)
break;
/*
* 如果这个匹配的后代资源的结束地址覆盖了整个重叠区域 o 的剩余部分.
*/
if (dp->end >= o.end) {
/*
* 那么我们认为重叠区域被完全覆盖了. 设置 covered 为 true, 并退出内层循环.
*/
covered = true;
break;
}
/*
* 如果这个后代资源只覆盖了重叠区域 o 的一部分, 我们就从 o 中 "减去" 已被覆盖的部分,
* 方法是将 o 的起始地址移到该后代资源的末尾之后, 以便在下一次循环中检查 o 的剩余部分.
*/
o.start = max(o.start, dp->end + 1);
}
}
/*
* 在检查完 p 的所有后代之后, 如果 covered 标志仍然是 false,
* 这意味着与 p 重叠的部分没有被任何匹配类型的后代资源完全覆盖.
* 因此, 我们必须将其计为一个 "other" 类型的重叠.
*/
if (!covered)
other++;
}

/*
* 在遍历完所有兄弟节点后, 根据计数器的值返回最终结果.
* 如果 type 计数器为0, 说明没有找到任何匹配类型的重叠.
*/
if (type == 0)
return REGION_DISJOINT;

/*
* 如果 type 大于0, 并且 other 为0, 说明所有重叠都发生在匹配类型的资源上.
*/
if (other == 0)
return REGION_INTERSECTS;

/*
* 如果 type 和 other 都大于0, 说明重叠既发生在匹配类型上, 也发生在非匹配类型上.
*/
return REGION_MIXED;
}

region_intersects: 判断一个内存区域是否与特定类型的系统资源相交

此函数的核心作用是检查一个给定的物理内存区域(由起始地址 start 和大小 size 定义)是否与内核资源树中特定类型(由 flags 定义)和特定描述符(由 desc 定义)的资源区域存在交集。它被内核其他子系统(如 memremap)调用, 以确保操作的合法性, 例如防止用户意外地以不正确的缓存属性重新映射主系统RAM。

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

在STM32H750这样的嵌入式平台上, 内核通过解析设备树(Device Tree)来了解系统的硬件布局。所有外设的寄存器地址范围、片上SRAM和Flash、以及通过reserved-memory节点定义的专用内存(如DMA内存池), 都会被注册到以 iomem_resource 为根的全局资源树中。

region_intersects 函数就是在这个已建立的资源树上进行查询的接口。它的工作流程如下:

  1. 锁定资源树:它首先获取一个读写锁(rwlock)的“读锁”。在单核抢占式(preemptible)内核中, 这个锁的作用不是防止多核并发访问, 而是防止在读取资源树的过程中, 当前任务被另一个可能修改资源树的任务抢占。例如, 当一个驱动模块被卸载时, 它会从资源树中移除自己的资源, 这是一个写操作。read_lock确保了region_intersects在执行期间看到的是一个一致、完整的资源树快照。
  2. 遍历与比较:它调用内部函数 __region_intersects 来遍历资源树的节点。对于树中的每一个资源, 它会检查其 flagsdesc 是否与查询的参数匹配, 如果匹配, 再判断其地址范围是否与 [start, start + size) 有重叠。
  3. 返回结果:根据遍历比较的结果, 它返回一个状态码, 明确指出查询的区域是完全不相交(REGION_DISJOINT)、仅与目标类型的资源相交(REGION_INTERSECTS), 还是与多种不同类型的资源都有相交(REGION_MIXED)。

这个函数是保证内核内存操作安全性和正确性的基础构件, 确保了对底层物理内存的操作不会与已注册的硬件资源发生意外冲突。


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
/*
* 定义一个全局的 struct resource 实例, 名为 iomem_resource.
* struct resource 是内核中用于描述一段连续硬件资源 (如内存或I/O端口) 的标准结构.
* 这个实例是所有 "I/O Memory" 资源的根节点, 构成了资源树的顶部.
*/
struct resource iomem_resource = {
/*
* .name: 资源的名称, 用于在调试信息(如/proc/iomem)中显示.
*/
.name = "PCI mem",
/*
* .start: 资源的起始地址. 初始化为0.
*/
.start = 0,
/*
* .end: 资源的结束地址. 初始化为-1 (即最大可能的地址), 表示这个根资源覆盖了整个物理地址空间.
*/
.end = -1,
/*
* .flags: 资源的标志位. IORESOURCE_MEM 表明这是一个内存类型的资源.
*/
.flags = IORESOURCE_MEM,
};

/*
* 使用 DEFINE_RWLOCK 宏来静态地定义并初始化一个读写锁, 名为 resource_lock.
* 这个锁用于保护对全局资源树 (iomem_resource 和 ioport_resource) 的并发访问.
* 在单核抢占式内核中, 它防止任务在读写资源树时被其他任务抢占而导致数据不一致.
*/
static DEFINE_RWLOCK(resource_lock);
/*
* 使用 EXPORT_SYMBOL 宏将 iomem_resource 符号导出.
* 这使得其他内核模块可以直接访问这个全局的资源树根节点.
*/
EXPORT_SYMBOL(iomem_resource);
/**
* region_intersects() - 判断一个区域是否与已知的资源区域相交
* @start: 区域的起始地址.
* @size: 区域的大小.
* @flags: 资源的标志位 (用于在资源树中匹配).
* @desc: 资源的描述符 (用于进一步匹配), 或者使用 IORES_DESC_NONE 表示不关心描述符.
*
* 检查指定的区域是部分重叠还是完全包含一个由 @flags 和 @desc (可选) 标识的资源.
* 如果区域与 @flags/@desc 不重叠, 返回 REGION_DISJOINT.
* 如果区域与 @flags/@desc 重叠, 并且还与其他类型的资源重叠, 返回 REGION_MIXED.
* 如果区域只与 @flags/@desc 重叠 (不与任何其他已定义资源重叠), 返回 REGION_INTERSECTS.
* 注意, 当指定区域与RAM和未定义的内存空洞重叠时, 也会返回 REGION_INTERSECTS.
*
* region_intersects() 被内存重映射函数使用, 以确保用户不会重新映射RAM,
* 并且它比逐页遍历资源表要快得多.
*/
int region_intersects(resource_size_t start, size_t size, unsigned long flags,
unsigned long desc)
{
/*
* 定义一个整型变量 ret, 用于存储内部函数的返回值.
*/
int ret;

/*
* 对全局的 resource_lock 加读锁.
* 这允许多个读取者同时进入临界区, 但会阻止任何写入者.
* 在单核抢占式内核上, 这确保了在遍历资源树期间, 不会有其他任务(通过写操作)修改树的结构.
*/
read_lock(&resource_lock);
/*
* 调用内部核心函数 __region_intersects 来执行实际的遍历和比较逻辑.
* @ &iomem_resource: 从 I/O 内存资源的根节点开始搜索.
* @ start, size, flags, desc: 传递查询参数.
* 其返回值被赋给 ret.
*/
ret = __region_intersects(&iomem_resource, start, size, flags, desc);
/*
* 释放之前获取的读锁.
*/
read_unlock(&resource_lock);

/*
* 返回从内部函数获得的最终结果.
*/
return ret;
}
/*
* 使用 EXPORT_SYMBOL_GPL 将 region_intersects 函数的符号导出.
* 这使得其他遵循GPL许可证的内核模块可以调用此函数.
*/
EXPORT_SYMBOL_GPL(region_intersects);

__request_resource: 在资源树中查找冲突并插入新节点

此函数是Linux内核资源管理子系统中最底层的冲突检测与插入算法。它被__request_region_locked调用, 在一个已经锁定的、安全的上下文中执行。它的核心原理是在一个按起始地址排序的资源链表中, 高效地查找一个可以插入新资源节点的”空隙”。如果找到空隙, 它就执行插入并返回成功(NULL); 如果在查找过程中发现任何地址重叠, 它会立即停止并返回指向冲突节点的指针。

这是一个精巧且高效的链表操作算法, 其关键在于:

  1. 边界检查: 它首先进行快速的边界检查, 确保要插入的新资源new完全包含在父资源root的范围之内。如果new的范围超出了root的范围, 这本身就是一种冲突, 函数会返回root作为冲突节点。
  2. 排序链表遍历: 它假设root的子节点链表(root->child)是一个已经按照start地址从小到大排好序的链表。这是整个算法高效运行的基础。
  3. 指针的指针 (**p): 这是理解此函数最关键的部分。它不使用传统的previouscurrent指针来遍历链表, 而是使用一个”指向指针的指针”p
    • p最初指向root->child这个指针本身。
    • 在循环中, p会依次指向前一个节点的sibling指针。
    • 这种技巧的妙处在于, 当找到插入点时, 只需一条*p = new;语句就可以完成链接。*p修改的可能是root->child指针(如果在头部插入), 也可能是前一个节点的sibling指针, 无需区分这两种情况, 代码非常简洁。
  4. 冲突与插入逻辑: 循环中的逻辑非常清晰:
    • 情况A (无重叠): 如果当前节点tmp的结束地址小于new的起始地址(tmp->end < start), 说明两者没有重叠, 可以安全地继续检查下一个兄弟节点。
    • 情况B (找到插入点): 如果当前节点tmp的起始地址已经大于new的结束地址(tmp->start > end), 或者已经到达链表末尾(!tmp), 说明我们找到了一个可以容纳new的空隙。此时执行插入操作, 并返回NULL表示成功。
    • 情况C (冲突): 如果既不满足情况A也不满足情况B, 那么唯一的可能性就是newtmp的地址范围有重叠。此时, tmp就是冲突节点, 函数立即将其返回。

这个函数是纯粹的、与硬件无关的数据结构操作算法, 是构建整个资源仲裁系统的基石。它不涉及任何锁或硬件访问, 只是高效地在内存中维护一个有序的、无重叠的地址区间集合。

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
/*
* __request_resource: 尝试请求资源, 如果无法请求, 则返回冲突的条目.
* @root: 要在其中请求资源的父/根资源.
* @new: 代表要请求的新资源的描述符.
*/
static struct resource * __request_resource(struct resource *root, struct resource *new)
{
/* 获取新资源的起始和结束地址. */
resource_size_t start = new->start;
resource_size_t end = new->end;
/* tmp 用于遍历, p 是一个指向"resource指针"的指针, 这是实现高效插入的关键. */
struct resource *tmp, **p;

/* 合法性检查: 结束地址不能小于起始地址. 如果是, 认为与根节点冲突. */
if (end < start)
return root;
/* 边界检查: 新资源的范围必须完全在父资源的范围内. */
if (start < root->start)
return root;
if (end > root->end)
return root;

/* 初始化p, 使其指向 root 的 child 指针. *p 的值就是 root->child. */
p = &root->child;
/* 进入无限循环来遍历 root 的子节点链表. */
for (;;) {
/* tmp 指向当前要检查的子节点. 第一次循环时, tmp = root->child. */
tmp = *p;
/*
* 这是插入条件判断:
* 1. !tmp: 是否已经到达链表末尾?
* 2. tmp->start > end: 当前子节点的起始地址是否在新资源的结束地址之后?
* (因为链表是排序的, 这意味着新资源与当前及后续所有节点都没有冲突).
* 如果满足任一条件, 说明我们找到了插入点.
*/
if (!tmp || tmp->start > end) {
/* 将新节点的 sibling 指向当前节点 tmp (可能是NULL, 如果在末尾插入). */
new->sibling = tmp;
/*
* 关键的插入操作: 修改 p 所指向的指针的值, 使其指向 new.
* 这可能是修改 root->child, 也可能是修改前一个节点的 sibling.
*/
*p = new;
/* 设置新节点的父节点指针. */
new->parent = root;
/* 返回 NULL 表示插入成功, 没有冲突. */
return NULL;
}

/*
* 如果没找到插入点, 我们需要继续向后遍历.
* 更新 p, 使其指向当前节点 tmp 的 sibling 指针.
*/
p = &tmp->sibling;

/*
* 这是无冲突条件判断:
* 当前节点 tmp 的结束地址是否在新资源的起始地址之前?
* 如果是, 说明两者之间没有重叠, 可以安全地继续检查下一个节点.
*/
if (tmp->end < start)
continue;

/*
* 如果代码执行到这里, 说明既不满足插入条件, 也不满足无冲突条件.
* 这意味着 new 和 tmp 的地址范围必然有重叠.
* 因此, tmp 就是冲突的资源, 将其返回.
*/
return tmp;
}
}

__request_region_locked: 在锁定下执行资源区域请求的核心逻辑

此函数是Linux资源管理子系统的最深层、最核心的执行者。它的命名后缀_locked明确地告知调用者, 此函数必须在一个已经获取了resource_lock写锁定的上下文中被调用。它的核心原理是: 在一个无限循环中, 尝试将一个代表新区域的资源节点插入到全局资源树中。如果插入成功, 则函数完成; 如果遇到冲突, 它会根据冲突的类型, 尝试两种高级策略——“向下深入”或”睡眠等待”——来解决冲突。如果所有策略都失败, 才最终宣告失败。

这个函数的复杂性完全体现在其精妙的冲突解决机制上:

  1. 初始化与尝试: 函数首先填充传入的res结构体, 标记其为IORESOURCE_BUSY, 然后进入一个无限循环(for(;;)), 在循环中调用__request_resource尝试将其插入资源树。
  2. 成功路径: 如果__request_resource返回NULL, 表示没有冲突, 资源被成功插入。函数立即跳出循环并返回成功。
  3. 冲突处理路径: 如果__request_resource返回一个非NULLconflict指针, 意味着发生了地址重叠。此时, 函数会依次尝试:
    • 策略一: 向下深入(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错误。

在STM32H750这样的单核抢占式系统上, 虽然不存在多核并发, 但resource_lock依然是必需的。它能防止一个正在修改资源树的任务被另一个也想修改资源树的任务或中断抢占, 从而保证了全局资源树数据结构的绝对一致性和完整性。IORESOURCE_MUXED这种特性在典型的MCU嵌入式开发中较少使用, 但内核的通用框架必须包含这种逻辑以支持所有类型的硬件。

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
/*
* __request_region_locked: 在已持有锁的情况下, 创建一个新的繁忙资源区域.
* @res: 一个已分配但未填充的 resource 结构体.
* @parent: 父资源描述符.
* @start: 资源起始地址.
* @n: 资源区域大小.
* @name: 调用者的ID字符串.
* @flags: IO资源标志.
*/
static int __request_region_locked(struct resource *res, struct resource *parent,
resource_size_t start, resource_size_t n,
const char *name, int flags)
{
/* 声明一个等待队列项, 并将其与当前进程关联. 用于下面的 "睡眠等待" 逻辑. */
DECLARE_WAITQUEUE(wait, current);

/* 填充传入的 resource 结构体的基本信息. */
res->name = name;
res->start = start;
res->end = start + n - 1; /* end 是包含的, 所以要减1. */

/* 进入一个无限循环, 用于重试请求操作. */
for (;;) {
struct resource *conflict;

/* 设置资源标志: 从父节点继承类型, 并标记为 BUSY 和传入的 flags. */
res->flags = resource_type(parent) | resource_ext_type(parent);
res->flags |= IORESOURCE_BUSY | flags;
res->desc = parent->desc;

/* 调用 __request_resource 尝试将 res 插入到 parent 的子节点中. */
conflict = __request_resource(parent, res);
/* 如果返回 NULL, 说明没有冲突, 插入成功. */
if (!conflict)
break; /* 跳出循环, 函数成功. */

/* 这是一个针对特定内存管理(HMM)的调试警告, 在一般驱动中不常见. */
if (parent == &iomem_resource &&
conflict->desc == IORES_DESC_DEVICE_PRIVATE_MEMORY) {
pr_warn("Unaddressable device %s %pR conflicts with %pR\n",
conflict->name, conflict, res);
}

/* 如果冲突的资源不是父资源本身 (即, 是一个子节点). */
if (conflict != parent) {
/* 并且, 这个冲突的子节点本身没有被标记为 BUSY. */
if (!(conflict->flags & IORESOURCE_BUSY)) {
/* 这意味着我们找到了一个更具体的 "容器", 我们应该在它内部进行申请. */
parent = conflict; /* 将父节点更新为这个更具体的容器. */
continue; /* 继续循环, 在新的父节点下重试. */
}
}

/*
* 如果冲突的资源和我们请求的资源都标记为 IORESOURCE_MUXED (可复用).
* 这是一种可以等待的资源.
*/
if (conflict->flags & flags & IORESOURCE_MUXED) {
add_wait_queue(&muxed_resource_wait, &wait); /* 将自己加入等待队列. */
write_unlock(&resource_lock); /* 释放锁, 准备去睡眠. */
set_current_state(TASK_UNINTERRUPTIBLE); /* 设置进程状态为不可中断睡眠. */
schedule(); /* 调用调度器, 让出CPU. */
remove_wait_queue(&muxed_resource_wait, &wait); /* 被唤醒后, 移出等待队列. */
write_lock(&resource_lock); /* 重新获取锁. */
continue; /* 继续循环, 再次尝试申请. */
}
/* 如果以上特殊情况都不满足, 那就是一个无法解决的冲突. */
return -EBUSY; /* 返回 "设备或资源繁忙" 错误. */
}

/* 成功跳出循环, 返回0. */
return 0;
}

__request_region: 预留一个硬件资源区域的核心实现

此函数是Linux内核资源管理子系统的底层核心。它不是一个devm(设备资源管理)函数, 而是被devm_request_region系列函数在内部调用的最终执行者。它的核心原理是: 在一个全局的、被读写锁保护的资源树中, 检查一段指定的物理地址范围是否可用; 如果可用, 就为其创建一个新的”资源”节点并插入到树中, 以此声明对该地址范围的所有权, 防止其他驱动程序再次申请, 从而实现对硬件资源的仲裁和冲突避免。

整个函数的执行流程体现了对并发访问和数据一致性的严谨处理:

  1. 分配资源描述符: 函数首先调用alloc_resource在内存中分配一个struct resource结构体。这就像是申请一张空白的表格, 准备用来记录将要被预留的这块区域的信息。
  2. 获取写锁定: 这是最关键的一步。它调用write_lock来获取一个全局的读写锁resource_lock
    • 为什么是读写锁? 因为内核中可能会有很多地方需要读取资源树(例如, 用户通过cat /proc/iomem查看内存布局), 这些读取操作可以安全地并发进行。然而, 写入操作(添加或删除一个区域)是互斥的, 在写入时必须阻止任何其他的读或写操作, 以保证数据树的完整性。读写锁完美地满足了这种”多读单写”的场景。
    • 在STM32H750这样的单核抢占式系统上, 获取这个锁能有效防止当前任务在修改资源树的中间过程时, 被另一个也想操作资源树的任务抢占, 从而保证了操作的原子性。
  3. 执行核心操作 (在锁定状态下): 在获得锁之后, 它调用__request_region_locked。这个内部函数负责执行真正的逻辑: 遍历指定的父资源树(例如iomem_resource), 检查请求的范围(startstart+n)是否与任何已存在的区域重叠。如果没有重叠, 它就将第一步分配的空白resource结构体填充好信息并插入到树中。
  4. 错误处理: 如果__request_region_locked因为地址冲突等原因失败(返回错误码), 那么第一步分配的空白resource结构体就没用了。函数会立即调用free_resource将其释放, 防止内存泄漏, 然后返回NULL表示失败。
  5. 撤销用户空间映射 (仅IO内存): 如果请求成功, 并且操作的是IO内存(parent == &iomem_resource), 它会调用revoke_iomem。这是一个重要的安全和正确性措施, 它的作用是检查是否有任何用户空间进程(例如通过/dev/memuio驱动)已经映射了这段即将被内核驱动接管的物理内存。如果有, revoke_iomem会强制使这些用户空间的映射失效, 确保只有内核驱动能够独占地、安全地访问这些硬件寄存器。在无MMU的系统上, 此函数的功能可能会有所简化, 但其意图——确保内核的独占控制权——保持不变。
  6. 返回成功: 所有步骤都成功后, 函数返回指向新创建并插入到资源树中的resource结构体的指针。这个指针本身就是一种”所有权证明”, 虽然大部分驱动并不直接使用它, 而是将其视为操作成功的标志。
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
/**
* __request_region - 创建一个新的繁忙资源区域
* @parent: 父资源描述符 (例如 &iomem_resource)
* @start: 资源起始地址
* @n: 资源区域大小
* @name: 预留该区域的调用者的ID字符串
* @flags: IO资源标志 (通常为0)
*/
struct resource *__request_region(struct resource *parent,
resource_size_t start, resource_size_t n,
const char *name, int flags)
{
/* 定义一个指向 resource 结构体的指针 res. */
struct resource *res = alloc_resource(GFP_KERNEL);
/* 定义一个整型变量 ret 用于存储返回值. */
int ret;

/*
* 步骤1: 尝试分配一个新的 resource 结构体实例. 这只是一个数据容器.
*/
if (!res)
return NULL; /* 如果内存不足, 分配失败, 返回NULL. */

/*
* 步骤2: 获取全局资源锁的写锁定.
* 这会阻塞所有其他尝试读取或写入资源树的操作, 直到我们释放锁.
*/
write_lock(&resource_lock);
/*
* 步骤3: 调用带锁的核心函数, 执行实际的冲突检查和插入操作.
* 这个函数会填充 res 结构体并将其插入到 parent 的资源树中.
*/
ret = __request_region_locked(res, parent, start, n, name, flags);
/*
* 操作完成, 释放写锁定, 允许其他读写者继续.
*/
write_unlock(&resource_lock);

/*
* 步骤4: 检查核心操作是否失败.
*/
if (ret) {
/*
* 如果失败了(例如, 地址冲突), 那么我们之前分配的 res 容器就没用了.
* 调用 free_resource 将其释放, 防止内存泄漏.
*/
free_resource(res);
/*
* 返回 NULL, 表示请求失败.
*/
return NULL;
}

/*
* 步骤5: 如果请求的是IO内存区域, 并且成功了.
*/
if (parent == &iomem_resource)
/*
* 调用 revoke_iomem, 撤销任何可能存在的用户空间对这段物理内存的映射.
* 确保内核驱动的独占访问权.
*/
revoke_iomem(res);

/*
* 成功预留了该区域, 返回指向新创建的资源描述符的指针.
*/
return res;
}
/* 将此函数导出, 使其对内核其他部分(主要是devm框架)可用. */
EXPORT_SYMBOL(__request_region);

devm_request_region / devm_request_mem_region: 自动管理的资源区域请求

此代码片段展示了Linux内核中用于请求(request)或预留(reserve)一段物理地址范围devm(设备资源管理)接口。它的核心原理是为设备驱动程序提供一个安全、便捷的API来声明其对某段硬件地址(无论是内存映射的IO寄存器还是传统的IO端口)的所有权, 并确保这个”所有权”声明在驱动程序卸载时会被自动撤销

这套机制是解决两个基本问题的关键:

  1. 资源冲突仲裁: 内核维护着一个全局的资源树(可以在/proc/iomem/proc/ioports中看到其状态)。当一个驱动调用devm_request_mem_region时, 内核会检查其请求的地址范围是否已经与一个已存在的预留区域重叠。如果重叠, 请求就会失败。这从根本上防止了两个不同的驱动程序意外地同时访问和操作同一组硬件寄存器, 从而避免了难以诊断的硬件冲突和系统不稳定。
  2. 资源生命周期管理: 这是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
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
/*
* __devm_request_region: 实际执行资源请求的内部函数.
* @dev: 请求资源的设备.
* @parent: 指向要从中分配资源的父资源树 (例如 &iomem_resource).
* @start: 请求的起始地址.
* @n: 请求的地址范围大小.
* @name: 为这个区域指定的名字, 会显示在 /proc/iomem 中.
* @return: 成功时返回一个指向新分配的资源结构体的指针, 失败时返回 NULL.
*/
struct resource *
__devm_request_region(struct device *dev, struct resource *parent,
resource_size_t start, resource_size_t n, const char *name)
{
/* 定义一个指向 devres 管理结构体的指针 dr. */
struct region_devres *dr = NULL;
/* 定义一个指向实际资源结构体的指针 res. */
struct resource *res;

/*
* 步骤1: 分配 devres 管理结构体.
* devm_region_release 是在驱动卸载时将被调用的清理函数.
*/
dr = devres_alloc(devm_region_release, sizeof(struct region_devres),
GFP_KERNEL);
if (!dr)
return NULL; /* 内存分配失败 */

/*
* 步骤2: 将请求的参数保存到管理结构体中.
* 这样做是为了让未来的清理函数 devm_region_release 知道要释放哪个区域.
*/
dr->parent = parent;
dr->start = start;
dr->n = n;

/*
* 步骤3: 调用底层的、非devm的 __request_region 函数来执行实际的预留操作.
* 这个函数会检查地址冲突, 如果没有冲突, 就在父资源树中标记这段区域为已占用.
*/
res = __request_region(parent, start, n, name, 0);
if (res)
/*
* 步骤4a: 如果预留成功 (res不为NULL), 则调用 devres_add.
* 这会将管理结构体 dr 链接到设备 dev 的资源列表中, "激活"自动清理机制.
*/
devres_add(dev, dr);
else
/*
* 步骤4b: 如果预留失败 (例如地址已被占用), 那么我们就不需要管理结构体了.
* 调用 devres_free 将其释放, 防止内存泄漏.
*/
devres_free(dr);

/*
* 返回底层 __request_region 的结果.
*/
return res;
}
/* 将此内部函数导出, 以便在内核其他地方(如 devm_ioremap_resource)使用. */
EXPORT_SYMBOL(__devm_request_region);

便捷宏封装

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/*
* 这是一个宏, 为请求 IO 端口区域提供了简洁的 API.
* 它会自动将 &ioport_resource (全局IO端口资源树的根) 作为父资源传递给 __devm_request_region.
* 在 ARM 架构上基本不会使用.
*/
#define devm_request_region(dev,start,n,name) \
__devm_request_region(dev, &ioport_resource, (start), (n), (name))

/*
* 这是一个宏, 为请求内存映射 IO 区域提供了简洁的 API.
* 它会自动将 &iomem_resource (全局IO内存资源树的根) 作为父资源传递给 __devm_request_region.
* 这是在 STM32 和其他 ARM 系统上使用的标准宏.
*/
#define devm_request_mem_region(dev,start,n,name) \
__devm_request_region(dev, &iomem_resource, (start), (n), (name))

I/O内存撤销框架初始化:创建用于访问控制的伪文件系统与全局Inode

本代码片段的功能是为内核的I/O内存(iomem)访问控制机制建立基础。其核心作用是创建一个专门的、内存中的伪文件系统(pseudo filesystem),并在此文件系统中生成一个全局唯一的匿名inode(iomem_inode)。这个inode将作为内核中“已撤销”(revoked)I/O内存区域的标志物。当一个驱动程序或子系统决定禁止通过/dev/mem等接口直接访问某段物理I/O内存时,它会将该内存区域与这个全局inode关联起来。后续的访问检查逻辑就可以通过判断一个地址是否与此inode关联,来决定是允许还是拒绝访问。

实现原理分析

此代码的实现巧妙地利用了Linux的虚拟文件系统(VFS)来管理一个非文件类的内核资源。

  1. 启动参数配置 (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)。这提供了一个灵活的策略控制。
  2. 伪文件系统定义 (iomem_fs_type):

    • 代码定义了一个名为 “iomem” 的file_system_type。这是一个“伪”文件系统,因为它不与任何块设备(如硬盘)关联,其所有对象都只存在于内存中。
    • 其目的是为了能够合法地创建一个超级块(superblock)对象,而超级块是创建inode的前提。
  3. 初始化与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
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
// strict_iomem: 内核启动参数 "iomem=" 的处理函数。
// @str: 启动参数 "iomem=" 后面的字符串值。
static int __init strict_iomem(char *str)
{
// 如果参数包含 "relaxed",则全局变量 strict_iomem_checks 设为 0 (禁用严格检查)。
if (strstr(str, "relaxed"))
strict_iomem_checks = 0;
// 如果参数包含 "strict",则设为 1 (启用严格检查)。
if (strstr(str, "strict"))
strict_iomem_checks = 1;
return 1; // 返回1表示参数处理成功。
}

// iomem_fs_init_fs_context: "iomem" 伪文件系统的挂载上下文初始化函数。
static int iomem_fs_init_fs_context(struct fs_context *fc)
{
// 使用 init_pseudo 辅助函数来初始化一个伪文件系统。
// DEVMEM_MAGIC 是一个唯一的魔数,用于标识此文件系统类型。
return init_pseudo(fc, DEVMEM_MAGIC) ? 0 : -ENOMEM;
}

// 定义 "iomem" 文件系统类型。
static struct file_system_type iomem_fs_type = {
.name = "iomem", // 文件系统在内核中的内部名称。
.owner = THIS_MODULE,
.init_fs_context = iomem_fs_init_fs_context, // 指定挂载初始化函数。
.kill_sb = kill_anon_super, // 指定卸载时销毁超级块的函数。
};

// iomem_init_inode: iomem 撤销框架的核心初始化函数。
static int __init iomem_init_inode(void)
{
static struct vfsmount *iomem_vfs_mount; // 用于保存伪文件系统的挂载点。
static int iomem_fs_cnt; // 用于“钉住”(pin)文件系统的引用计数器。
struct inode *inode;
int rc;

// “钉住”一个iomem_fs_type的实例,即在内部挂载它并保持挂载状态。
rc = simple_pin_fs(&iomem_fs_type, &iomem_vfs_mount, &iomem_fs_cnt);
if (rc < 0) {
pr_err("Cannot mount iomem pseudo filesystem: %d\n", rc);
return rc;
}

// 在刚刚挂载的伪文件系统的超级块上,分配一个匿名的inode。
inode = alloc_anon_inode(iomem_vfs_mount->mnt_sb);
if (IS_ERR(inode)) {
rc = PTR_ERR(inode);
pr_err("Cannot allocate inode for iomem: %d\n", rc);
// 如果分配inode失败,则释放之前“钉住”的文件系统。
simple_release_fs(&iomem_vfs_mount, &iomem_fs_cnt);
return rc;
}

/*
* 发布 iomem 撤销inode已初始化的事实。
* 与 revoke_iomem() 中的 smp_load_acquire() 配对。
*/
// 使用带有 "release" 语义的存储操作,安全地将新inode发布到全局变量。
smp_store_release(&iomem_inode, inode);

return 0;
}

// 将 iomem_init_inode 注册为文件系统级别的初始化调用。
fs_initcall(iomem_init_inode);

// 注册 "iomem=" 启动参数的处理函数。
__setup("iomem=", strict_iomem);