[TOC]

lib/devres.c

devm_ioremap_resource: 安全、自动管理的IO内存映射

此函数是Linux内核中用于设备驱动程序开发的一个至关重要的辅助函数。它的核心作用是将一个从设备树(Device Tree)或平台数据中获取的、描述硬件寄存器物理地址的资源(struct resource), 安全地映射到内核可以访问的虚拟地址空间, 并且这个过程是完全由设备资源管理(devm)框架自动管理的

该函数将一个典型的三步操作封装成了一个单一的、原子的API调用:

  1. 验证与请求: 它首先检查传入的资源是否是一个有效的内存区域(IORESOURCE_MEM)。然后, 它调用devm_request_mem_region向内核的资源仲裁系统”宣告”:”本驱动将要使用这段物理内存”。这可以防止两个不同的驱动程序意外地同时操作同一块硬件寄存器区域, 从而避免冲突。
  2. 内存映射: 在成功”占有”该物理内存区域后, 它调用底层的__devm_ioremap函数, 将这段物理地址映射为内核虚拟地址。这个返回的虚拟地址(类型为void __iomem *)就是驱动程序后续用来访问硬件寄存器的句柄。
  3. 自动资源管理与回滚: 这是devm_系列函数的核心价值。所有分配的资源(包括内存区域名、请求的内存区域、映射的虚拟地址)都被注册到设备的资源管理链表中。
    • 错误回滚: 如果在三步操作中的任何一步失败(例如, ioremap失败), 该函数会自动撤销并释放所有已经成功分配的资源(例如, 它会自动调用devm_release_mem_region来释放之前请求的内存区域), 从而保证系统状态的一致性, 防止资源泄漏。
    • 自动释放: 当驱动程序被卸载或其probe函数因其他原因失败返回时, 内核的设备模型会自动遍历并释放所有通过devm_函数注册的资源。驱动开发者无需编写任何手动的清理代码。

在STM32H750这样**无MMU(内存管理单元)**的MCU上, ioremap的主要作用并非建立复杂的虚拟地址到物理地址的页表映射(因为地址通常是直接或简单偏移对应的), 而是:

  1. 提供一个标准的、跨平台可移植的API来访问IO内存。
  2. 通过返回一个特殊的__iomem指针类型来提醒开发者, 这段内存不能像普通RAM一样被直接解引用, 必须使用readl/writel等专用的IO访问函数。
  3. 确保对该内存区域应用正确的缓存策略(通常是”非缓存的”或”写合并的”), 以保证对硬件寄存器的读写操作能够立即生效, 而不会被CPU的缓存干扰。

__devm_ioremap_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
71
/*
* 这是一个内部静态函数, 是 devm_ioremap_resource 的核心实现.
* @dev: 指向设备结构体的指针.
* @res: 指向要被映射的资源结构体的指针.
* @type: ioremap 的类型 (如缓存策略).
*/
static void __iomem *
__devm_ioremap_resource(struct device *dev, const struct resource *res,
enum devm_ioremap_type type)
{
resource_size_t size;
void __iomem *dest_ptr;
char *pretty_name;
int ret;

/* 强力断言, 确保 dev 指针不为 NULL. 这是 devm 框架的基础. */
BUG_ON(!dev);

/* 检查资源指针是否有效, 以及资源类型是否为内存. */
if (!res || resource_type(res) != IORESOURCE_MEM) {
/* 如果资源无效, 打印一条可用于触发"推迟探测"的错误日志, 并返回错误. */
ret = dev_err_probe(dev, -EINVAL, "invalid resource %pR\n", res);
return IOMEM_ERR_PTR(ret);
}

/* 如果请求的是标准映射, 但资源本身带有"非提交写"标志, 则自动升级映射类型. */
if (type == DEVM_IOREMAP && res->flags & IORESOURCE_MEM_NONPOSTED)
type = DEVM_IOREMAP_NP;

/* 获取资源描述的内存区域大小. */
size = resource_size(res);

/*
* 为请求的内存区域生成一个易于阅读的名称, 用于调试(如 /proc/iomem).
* 如果资源本身有名字, 则格式为 "设备名 资源名".
*/
if (res->name)
pretty_name = devm_kasprintf(dev, GFP_KERNEL, "%s %s",
dev_name(dev), res->name);
else /* 否则只使用设备名. */
pretty_name = devm_kstrdup(dev, dev_name(dev), GFP_KERNEL);
if (!pretty_name) {
ret = dev_err_probe(dev, -ENOMEM, "can't generate pretty name for resource %pR\n", res);
return IOMEM_ERR_PTR(ret);
}

/*
* 关键步骤1: 请求内存区域. 这会检查该物理地址范围是否已被其他驱动占用.
* devm_request_mem_region 会自动管理这个请求, 在驱动卸载时释放.
*/
if (!devm_request_mem_region(dev, res->start, size, pretty_name)) {
/* 如果区域已被占用, 返回 -EBUSY 错误. */
ret = dev_err_probe(dev, -EBUSY, "can't request region for resource %pR\n", res);
return IOMEM_ERR_PTR(ret);
}

/* 关键步骤2: 执行实际的内存映射. __devm_ioremap 会自动管理映射的虚拟地址, 在驱动卸载时 unmap. */
dest_ptr = __devm_ioremap(dev, res->start, size, type);
if (!dest_ptr) {
/*
* 关键步骤3: 错误回滚. 如果 ioremap 失败, 必须释放刚刚成功请求的内存区域.
* devm_release_mem_region 会找到并释放之前由 devm_request_mem_region 占用的区域.
*/
devm_release_mem_region(dev, res->start, size);
ret = dev_err_probe(dev, -ENOMEM, "ioremap failed for resource %pR\n", res);
return IOMEM_ERR_PTR(ret);
}

/* 所有步骤成功, 返回映射后的虚拟地址指针. */
return dest_ptr;
}

devm_ioremap_resource: 便捷的API封装函数

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
/**
* devm_ioremap_resource() - 检查、请求并映射一个资源
* @dev: 处理该资源的通用设备
* @res: 要被处理的资源
*
* 此函数检查一个资源是否是有效的内存区域, 请求该内存区域, 然后进行 ioremap.
* 所有操作都由 devm 框架管理, 将在驱动程序分离(detach)时被自动撤销.
*
* 使用示例:
*
* res = platform_get_resource(pdev, IORESOURCE_MEM, 0);
* base = devm_ioremap_resource(&pdev->dev, res);
* if (IS_ERR(base))
* return PTR_ERR(base);
*
* 返回: 一个指向重映射后内存的指针, 如果失败则返回一个 ERR_PTR() 编码的错误码.
*/
void __iomem *devm_ioremap_resource(struct device *dev,
const struct resource *res)
{
/*
* 这是一个简单的封装, 它调用核心实现函数 __devm_ioremap_resource,
* 并为映射类型传递一个默认值 DEVM_IOREMAP.
* 这为驱动开发者提供了最常用、最简洁的API.
*/
return __devm_ioremap_resource(dev, res, DEVM_IOREMAP);
}
/* 将此函数导出, 使其对其他内核模块可用. */
EXPORT_SYMBOL(devm_ioremap_resource);

__devm_ioremap: 自动管理的IO内存映射核心实现

此函数是Linux设备资源管理(devm)框架中, 负责执行实际IO内存映射操作的底层核心函数。它被更高层的devm_ioremap_resource等函数在内部调用。它的核心原理是: 先为将要返回的映射地址分配一个”管理席位”, 然后调用相应缓存策略的ioremap函数执行实际映射, 如果映射成功, 就将返回的地址登记到那个”管理席位”并将其激活; 如果映射失败, 就撤销那个”管理席位”。

这整个过程确保了ioremap操作的生命周期与设备驱动的生命周期完全绑定, 实现了iounmap的自动化, 从而极大地提高了驱动代码的健壮性和简洁性。

其工作流程如下:

  1. 分配管理记录: 函数首先调用devres_alloc_node。这会分配一小块内存(ptr), 这块内存本身就是一个受管资源, 并且它与一个特定的释放函数devm_ioremap_release相关联。ptr被设计用来存放最终ioremap成功后返回的虚拟地址。
  2. 根据类型执行映射: 函数使用一个switch语句, 根据调用者传入的type参数, 来选择调用不同缓存策略的底层ioremap函数:
    • ioremap: 标准映射, 通常是设备无关的、非缓存的。
    • ioremap_uc: 强制非缓存(Un-Cacheable)映射。
    • ioremap_wc: 写合并(Write-Combining)映射, 允许多个连续的写操作在总线上合并成一次传输, 可以提高显存等设备的性能。
    • ioremap_np: 非提交写(Non-Posted)映射, 保证写操作完成前CPU会等待。
  3. 条件性激活:
    • 如果底层ioremap_*函数成功并返回了一个有效的虚拟地址addr, 函数就会将这个地址存入之前分配的管理记录中(*ptr = addr), 然后调用devres_add将这个管理记录正式加入到设备的资源链表中。从此刻起, 内核就承诺会在驱动卸载时自动调用devm_ioremap_releaseiounmap这个地址。
    • 如果底层ioremap_*函数失败并返回NULL, 函数会调用devres_free来释放第一步分配的、未使用的管理记录, 防止内存泄漏。

对于STM32H750这样的**无MMU(内存管理单元)但通常有MPU(内存保护单元)**的系统, ioremap的意义依然重大:

  • 缓存一致性: 这是最主要的目的。STM32H7系列带有数据缓存, 直接访问物理地址可能会导致对硬件寄存器的读写操作被CPU缓存, 从而无法立即反映到硬件上, 造成灾难性后果。ioremap (特别是ioremap_uc)会通过配置MPU, 将这段地址区域标记为”非缓存的”(non-cacheable), 确保每一次读写都直达物理总线。
  • API标准化: 它提供了标准的、可移植的API来访问硬件IO内存。
  • 类型安全: 返回的void __iomem *指针会阻止程序员意外地对其进行直接的指针解引用, 强制使用readl/writel等专用的、保证了内存屏障和访问顺序的函数。
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
/*
* __devm_ioremap: devm框架中执行ioremap的内部核心函数.
* @dev: 设备指针.
* @offset: 要映射的物理起始地址.
* @size: 映射的大小.
* @type: 映射的类型 (缓存策略).
*/
static void __iomem *__devm_ioremap(struct device *dev, resource_size_t offset,
resource_size_t size,
enum devm_ioremap_type type)
{
/*
* ptr 是一个指向 "void __iomem * 指针" 的指针. 它本身将作为devres的管理记录.
* addr 用于存储底层ioremap函数返回的实际映射地址.
*/
void __iomem **ptr, *addr = NULL;

/*
* 步骤1: 分配一个devres管理记录.
* 这个记录的大小是一个指针的大小. 它的释放函数被指定为 devm_ioremap_release.
* dev_to_node(dev) 用于NUMA系统, 确保在与设备亲和的内存节点上分配.
*/
ptr = devres_alloc_node(devm_ioremap_release, sizeof(*ptr), GFP_KERNEL,
dev_to_node(dev));
if (!ptr)
return NULL; /* 内存分配失败 */

/*
* 步骤2: 根据传入的映射类型, 调用对应的底层ioremap函数.
*/
switch (type) {
case DEVM_IOREMAP:
addr = ioremap(offset, size);
break;
case DEVM_IOREMAP_UC:
addr = ioremap_uc(offset, size);
break;
case DEVM_IOREMAP_WC:
addr = ioremap_wc(offset, size);
break;
case DEVM_IOREMAP_NP:
addr = ioremap_np(offset, size);
break;
}

/*
* 步骤3: 根据映射结果, 决定是激活管理记录还是丢弃它.
*/
if (addr) {
/*
* 如果映射成功 (addr不为NULL):
* 将返回的映射地址 addr 存入管理记录 ptr 中.
*/
*ptr = addr;
/*
* 调用 devres_add 将管理记录 ptr 正式添加到设备的资源列表中.
* 从此, 这个映射的生命周期就由devm框架管理了.
*/
devres_add(dev, ptr);
} else {
/*
* 如果映射失败 (addr为NULL):
* 调用 devres_free 释放我们之前分配的、未使用的管理记录 ptr, 防止内存泄漏.
*/
devres_free(ptr);
}

/*
* 返回底层ioremap函数的结果 (成功时是映射地址, 失败时是NULL).
*/
return addr;
}

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)关联一个专门用于管理资源的链表,并在驱动绑定和解绑的生命周期关键点进行自动化操作。

  1. 资源列表创建:当驱动核心准备调用一个驱动的 .probe() 函数来绑定一个设备时,它会为该设备初始化一个空的devres资源链表。
  2. 托管式资源申请:驱动程序调用一个 devm_* 函数,例如 devm_kzalloc()。这个函数在内部会执行两个步骤:
    • 首先,它调用底层的、非托管的函数(如 kzalloc())来实际分配资源。
    • 如果分配成功,它会额外分配一个小型的 devres 元数据结构。这个结构体中存储了指向刚刚分配的资源的指针,以及一个指向用于释放该类型资源的函数指针(例如,kfree)。
  3. 添加到列表:这个新创建的 devres 元数据结构被添加到该设备资源链表的头部。
  4. 自动化释放:当未来这个设备与驱动解绑时(例如,模块被卸载 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
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
/*
* 静态函数声明: add_dr
* 此函数仅在当前文件中可见, 是 devres_add 的一个辅助函数.
* @dev: 指向 struct device 的指针, 资源将被添加到这个设备上.
* @node: 指向 struct devres_node 的指针, 这是要被添加的实际资源节点.
*/
static void add_dr(struct device *dev, struct devres_node *node)
{
/*
* 调用 devres_log 打印一条调试日志, 表明正在为设备'dev'添加一个资源节点'node'.
* 这在内核开启了动态调试时非常有用, 在生产环境中通常被编译掉.
*/
devres_log(dev, node, "ADD");
/*
* BUG_ON 是一个强力断言宏. 如果其条件为真, 内核会立即停止运行(panic).
* !list_empty(&node->entry) 的意思是 "节点的链表入口不是空的".
* 因此, 这行代码的作用是: 断言即将被添加的节点尚未属于任何链表.
* 这是一个至关重要的完整性检查, 防止将一个已在链表中的节点重复添加, 这会破坏链表结构.
*/
BUG_ON(!list_empty(&node->entry));
/*
* 调用内核标准的链表操作函数 list_add_tail.
* 作用: 将 node->entry 这个链表节点添加到 dev->devres_head 这个链表的尾部.
* 添加到尾部确保了资源的释放顺序与分配顺序相反(后进先出 LIFO),
* 这对于处理资源之间的依赖关系至关重要(例如, 必须先释放内存, 再注销使用该内存的设备).
*/
list_add_tail(&node->entry, &dev->devres_head);
}

devres_add: 注册设备资源

这是一个暴露给内核其他部分使用的API函数。它负责准备工作和加锁, 然后调用add_dr来完成资源的添加。

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
/**
* devres_add - 注册设备资源
* @dev: 要添加资源的设备
* @res: 要注册的资源
*
* 将 devres 资源 @res 注册到设备 @dev. @res 应该是通过
* devres_alloc() 分配的. 在驱动程序分离(detach)时, 关联的释放
* 函数将被调用, 并且 devres 资源将被自动释放.
*/
void devres_add(struct device *dev, void *res)
{
/*
* 定义一个指向 devres 结构体的指针 dr.
* container_of 是一个宏, 它的作用是"根据结构体成员的地址, 找到整个结构体的地址".
* 在这里, 'res' 是指向 devres 结构体中 'data' 成员的指针 (void *类型).
* 这行代码的作用就是从用户提供的资源指针 res, 反向计算出包含它的 devres 管理结构的起始地址.
*/
struct devres *dr = container_of(res, struct devres, data);
/*
* 定义一个无符号长整型 flags, 用于保存中断状态.
*/
unsigned long flags;

/*
* 获取自旋锁, 并保存当前的中断状态.
* dev->devres_lock 是每个设备私有的, 用于保护其 devres_head 链表的自旋锁.
* spin_lock_irqsave 会:
* 1. 在获取锁之前, 禁用当前CPU核心的本地中断.
* 2. 将当前的中断状态(是开是关)保存到 flags 变量中.
* 3. 获取自旋锁.
* 在 STM32H750 这样的单核抢占式系统上, 这样做可以防止两种并发冲突:
* a) 防止当前任务在修改链表时被另一个任务抢占.
* b) 防止在修改链表时被一个中断处理程序打断(如果中断处理程序也可能访问该链表).
*/
spin_lock_irqsave(&dev->devres_lock, flags);
/*
* 调用辅助函数 add_dr, 将从 dr 中提取出的资源节点 (&dr->node) 添加到设备链表中.
* 由于此时持有锁, 这个添加操作是原子的.
*/
add_dr(dev, &dr->node);
/*
* 释放自旋锁, 并恢复之前保存的中断状态.
* spin_unlock_irqrestore 会:
* 1. 释放自旋锁.
* 2. 使用 flags 变量恢复锁定时保存的中断状态.
* 这样做可以确保如果之前中断是开启的, 那么现在它会被重新开启; 如果之前是关闭的, 它会保持关闭.
*/
spin_unlock_irqrestore(&dev->devres_lock, flags);
}
/*
* 将 devres_add 函数导出, 使其对其他遵循GPL许可证的内核模块可用.
*/
EXPORT_SYMBOL_GPL(devres_add);