[toc]

[mm/dmapool.c] [DMA 池分配器(dma_pool)] [为指定 device 提供“小块、一致性(coherent)可 DMA”的池化分配/释放接口]

介绍

dma_pool 的目标很明确:给驱动提供固定大小的小块 DMA 一致性内存,避免频繁用 dma_alloc_coherent() 做“小块分配”带来的浪费与开销;实现方式是:先用 dma_alloc_coherent() 一次拿一段(通常至少一页)一致性内存,然后切成等大小 block,用空闲链表管理。源码文件开头注释把这个设计讲得很直接:从页分配器拿 coherent page,再拆分成 blocks,并用跨页的单链表跟踪空闲块。


历史与背景

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

驱动里常见“很多很小、但必须设备可直接 DMA 访问且无需显式 cache flush”的对象(例如描述符、队列元素等)。直接 dma_alloc_coherent() 去分配这些小对象会导致:

  • 粒度偏大:coherent 分配往往以页或更大粒度管理,小对象会产生明显内部碎片;
  • 频繁分配/释放开销:每次都走 coherent 分配路径。
    dma_pool 用“先集中分配再切块”的方式,专门优化这一类场景。

重要里程碑或迭代点(从该文件可见的“能力演进”)

不强行绑定到某个具体内核版本号(需要查 git 历史才精确),但从当前主线实现能看到这些关键能力点:

  • 对齐/边界约束能力align 必须是 2 的幂;boundary 也是 2 的幂且不能小于块大小,并且默认会被收敛到 allocation 范围内。
  • NUMA 节点支持dma_pool_create_node() 允许把元数据结构按 node 分配。
  • devres 托管(managed)接口dmam_pool_create()/dmam_pool_destroy() 绑定设备生命周期,驱动 detach 时自动清理,降低泄漏风险。
  • 调试填充(poison)与一致性检查:在特定配置下对 free/alloc 做填充与破坏检测。
  • sysfs 可观测性:首次给某设备创建 pool 时创建只读属性 pools,导出 pool 名称与计数信息。

社区活跃度和主流应用情况

dma_pool 属于内核 DMA API 的标准组成部分,仍在 docs.kernel.org 的 DMA API 文档中持续维护与更新,说明它是主线长期支持的接口。 ([Linux内核文档][2])
同时它位于 torvalds/linux 主仓(整体活跃度极高),属于大量驱动可复用的公共设施。 ([GitHub][3])


核心原理与设计

核心工作原理是什么?

可以把它拆成 4 个动作:创建 → 扩容(按页申请)→ 分配(pop)→ 释放(push)

  1. 数据结构
  • struct dma_pool:维护 page_list(已分配的 coherent 区块集合)、next_block(全局空闲 block 单链表头)、计数(nr_blocks/nr_active/nr_pages)、参数(size/allocation/boundary/node)以及 dev
  • struct dma_page:记录某次 dma_alloc_coherent() 得到的 vaddrdma 基址,并挂入 page_list
  • struct dma_block:每个 block 的“头部”,至少包含 next_block 和该 block 的 dma 地址。实现里要求 size >= sizeof(struct dma_block)
  1. 创建:dma_pool_create_node()
  • 参数校验:align 为 0 则置 1;否则必须是 2 的幂;size 不能为 0 且不能过大;并强制 size >= sizeof(struct dma_block);随后按对齐做 ALIGN(size, align)
  • allocation = max(size, PAGE_SIZE):确保至少按页规模切分(或在 size > PAGE_SIZE 时一块就可能占掉整个 allocation)。
  • boundary:若为 0 则默认为 allocation;否则必须是 2 的幂且 boundary >= size,并最终 boundary = min(boundary, allocation)
  • 将 pool 挂到 dev->dma_pools;若这是该 device 的第一个 pool,则创建 sysfs 属性文件。
  1. 扩容:pool_alloc_page() + pool_initialise_page()
  • pool_alloc_page():先 kmalloc_node 分配 struct dma_page 元数据,再 dma_alloc_coherent(dev, allocation, &page->dma, flags) 分配真正 coherent 内存。
  • pool_initialise_page():从 offset=0 开始按 pool->size 切分;每个 block 的 dma = page->dma + offset,并串成链;最后把新链表拼接到 pool->next_block,并把该 dma_page 加入 page_list
  • 边界约束实现点:通过 next_boundaryif (offset + size > next_boundary) offset = next_boundary; next_boundary += boundary; 跳转 offset,保证“块不跨越指定 boundary”。
  1. 分配/释放:dma_pool_alloc() / dma_pool_free()
  • dma_pool_alloc()

    • 先拿自旋锁,从空闲链表 pop;
    • 若没有空闲块,会先释放自旋锁,再去 pool_alloc_page()(代码里明确标注“可能 sleep”),成功后再加锁初始化页面并重新 pop。
    • 返回 block 的虚拟地址,并通过 handle 返回对应 dma 地址。
  • dma_pool_free():加锁,做错误检查后 push 回空闲链表,并更新 nr_active

  1. 销毁:dma_pool_destroy()
  • dev->dma_pools 删除;如果这是最后一个 pool,则移除 sysfs 属性;若发现 nr_active != 0 会报错并认为 busy。
  • 非 busy 时逐个 dma_free_coherent() 释放每个 dma_page 的 coherent 内存并释放元数据。
  1. managed 版本:dmam_pool_create/destroy()
  • devres 保存指针,设备解绑时走 dmam_pool_release() 自动调用 dma_pool_destroy()

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

  • 小块 coherent 分配的效率与碎片控制:一次 coherent 分配后切分复用,典型情况下分配/释放只是链表操作+锁。
  • 硬件约束表达能力alignboundary 直接编码到切块逻辑里,适合“不能跨 4KB”等限制。
  • 可观测/可管理:device 侧 sysfs pools 能看到 pool 计数;managed 接口能减少驱动资源管理错误。

已知劣势、局限性、不适用性

  • 占用的是 coherent DMA 内存:这类内存资源通常更紧张/代价更高,不适合“把它当通用小对象分配器”。(官方文档也明确这些块都是 coherent mapping。) ([Linux内核文档][2])
  • 按固定块大小工作:pool 创建后 size 固定,变长对象需要多个 pool 或改用其他方案。
  • 扩容路径可能睡眠:当 free list 为空时需要 pool_alloc_page(),源码注释明确“might sleep”,因此不能把“必定不睡眠”当成接口保证。
  • 没有“自动回收空页”的逻辑:该实现只在 destroy 时释放 dma_page,长生命周期 pool 可能长期持有 nr_pages

使用场景

首选场景举例

  1. 大量小块 coherent 对象:如 DMA 描述符、硬件队列元素、控制结构等(CPU/设备共同访问,要求一致性)。 ([Linux内核文档][2])
  2. 需要边界限制的对象:例如硬件要求单次 DMA 传输不跨 4KB。接口注释明确把它作为典型用途。
  3. 希望简化资源释放的驱动:优先用 dmam_pool_create(),把销毁绑到 device 生命周期。

下面是一个“驱动侧典型用法”的最小骨架(示意):

1
2
3
4
5
6
7
8
9
10
/**
* @brief 初始化 DMA descriptor pool(示例)
* @param dev 目标设备
* @return 成功返回 pool 指针,失败返回 NULL
*/
static struct dma_pool *desc_pool_init(struct device *dev)
{
/* 64B 描述符,64B 对齐,不跨 4KB */
return dmam_pool_create("desc_pool", dev, 64, 64, 4096);
}

不推荐使用的场景(原因)

  • 大块连续缓冲区:例如几十 KB/MB 的 buffer,更适合直接 dma_alloc_coherent()/CMA 等;用 pool 只会让 “allocation=max(size,PAGE_SIZE)” 的策略变得不经济。
  • 每次 I/O 都映射/解除映射的 streaming 模型更合适的场景:比如用普通缓存内存承载数据、只在 DMA 时临时 map/unmap;这类场景不需要长期 coherent 常驻。

对比分析

对比对象我选 3 类最常见的“替代/相邻方案”:

  • dma_pool(本文件实现)
  • 直接 dma_alloc_coherent()(每次分配一个 coherent buffer)
  • “普通内存 + streaming DMA map/unmap”(例如 kmalloc + dma_map_single / dma_unmap_single
维度 dma_pool 直接 dma_alloc_coherent 普通内存 + streaming map/unmap
实现方式 coherent 大块切分成固定 size block;free list 管理 每次走 coherent 分配/释放 buffer 来自可缓存内存;每次 DMA 前后做 map/unmap(可能含 cache 维护) ([Linux内核官网][4])
性能开销 热路径通常是锁+链表;冷路径需要再申请 coherent page,可能睡眠 每次都在 coherent 分配路径,频繁小块时开销更集中 每次 I/O 都要 map/unmap;但内存本身是可缓存的,CPU 访问效率通常更好(取决于平台) ([Linux内核官网][4])
资源占用 长期占用 coherent 页;不自动回收空页,适合长期复用 按需占用 coherent;频繁分配会导致碎片/管理成本 占用普通内存;DMA 时付出映射与一致性维护成本
隔离级别 每个 pool 绑定一个 dev;block 的 dma 来自该 dev 的 coherent 区域 同上(但没有“池”的复用结构) DMA 地址由映射接口生成,强调“DMA 地址空间与 CPU 地址空间可能不同” ([Linux内核官网][4])
启动/首次使用速度 第一次可能触发 coherent 页分配与切分;之后很快 每次都类似“首次成本” 每次 I/O 都要映射;但不需要长期预热

总结

关键特性

  • 固定块大小的小对象 coherent DMA 分配器:用 coherent 大块切分、空闲链表复用。
  • 对齐/边界约束内建align/boundary 直接影响切分与返回对象。
  • 可观测与可托管:sysfs pools 输出计数;dmam_* 自动随设备释放。
  • 注意上下文:创建接口标注 not in_interrupt();分配在缺块时会走可能睡眠的扩容路径。

学习要点建议(按读源码的顺序)

  1. 先读 dma_pool_create_node() 的参数约束:你会理解 size/allocation/boundary 三者的关系。
  2. 再读 pool_initialise_page():边界控制与切块逻辑都在这里。
  3. 最后读 dma_pool_alloc/free/destroy:重点看锁、计数、以及 “缺块扩容会先放锁” 的原因。

DMA Pool:一致性 DMA 小块内存池的页面分配、块分配/回收与 devres 托管(pool_alloc_page / dma_pool_alloc / dma_pool_free / dma_pool_destroy / dmam_pool_create / dmam_pool_destroy)

DMA pool 用于为设备驱动提供大量小而固定大小的“DMA 一致性(coherent)”内存块,典型用途是硬件描述符(例如你前面 MDMA 的 stm32_mdma_hwdesc)。它把底层的 dma_alloc_coherent() 按页(pool->allocation)批量申请,再把页切成等大小 block,通过栈/空闲链表快速分配与回收,避免频繁的 coherent 大页申请开销。


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
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
/**
* @brief 为 DMA pool 申请一页 coherent 内存并返回封装对象
* @param[in] pool DMA pool 对象,提供 device、页大小、NUMA 节点等信息
* @param[in] mem_flags GFP 标志,决定分配行为(是否可睡眠、是否可回收等)
* @return 成功返回 dma_page 指针;失败返回 NULL
*/
static struct dma_page *pool_alloc_page(struct dma_pool *pool, gfp_t mem_flags)
{
/** page:记录该页 coherent 内存的元数据对象 */
struct dma_page *page;

/** 为 page 元数据按 pool->node 进行节点感知分配 */
page = kmalloc_node(sizeof(*page), mem_flags, pool->node);
if (!page) /* 元数据分配失败直接返回 */
return NULL;

/**
* 为该 pool 申请一段 coherent DMA 内存:
* - 返回 CPU 可访问虚拟地址 page->vaddr
* - 同时返回设备侧可用的 DMA 地址 page->dma
* - 大小为 pool->allocation(通常是“一页”或 pool 设计的批量粒度)
*/
page->vaddr = dma_alloc_coherent(pool->dev, pool->allocation,
&page->dma, mem_flags);
if (!page->vaddr) { /* coherent 内存分配失败需要回滚元数据 */
kfree(page);
return NULL;
}

/** 成功返回包含 vaddr/dma 的 page */
return page;
}

/**
* @brief 销毁一个 DMA pool(调用者必须保证 pool 中不再有任何在用 block)
* @param[in] pool 需要销毁的 DMA pool
*
* 约束:
* - 该接口要求非中断上下文(可能睡眠/持 mutex)
* - 调用者保证:不会再有人使用该 pool,且 pool 中的 block 不再被设备/驱动访问
*/
void dma_pool_destroy(struct dma_pool *pool)
{
/** page/tmp:遍历 pool->page_list 使用的当前节点与临时节点 */
struct dma_page *page, *tmp;

/** empty:该设备是否已无任何 pool;busy:销毁时是否仍有活跃 block */
bool empty, busy = false;

/** pool 指针为空则直接返回(防御式处理) */
if (unlikely(!pool))
return;

/**
* 从全局注册与设备属性视图中移除该 pool:
* pools_reg_lock/pools_lock 用于保护 pool 注册链表与设备属性文件状态。
*/
mutex_lock(&pools_reg_lock);
mutex_lock(&pools_lock);
list_del(&pool->pools); /* 从设备的 pool 链表摘除当前 pool */
empty = list_empty(&pool->dev->dma_pools); /* 检查该 device 是否还剩其它 pool */
mutex_unlock(&pools_lock);
if (empty) /* 若该 device 不再有 pool,则移除 sysfs 属性文件 */
device_remove_file(pool->dev, &dev_attr_pools);
mutex_unlock(&pools_reg_lock);

/**
* 检查是否仍有活跃 block:
* nr_active 表示当前从 pool 分配出去但尚未归还的 block 数。
* 若非 0,说明调用者违反“销毁前必须全部归还”的约束。
*/
if (pool->nr_active) {
dev_err(pool->dev, "%s %s busy\n", __func__, pool->name);
busy = true;
}

/**
* 遍历并释放 pool 中所有页:
* - 若不 busy:释放 coherent 页本体(dma_free_coherent)
* - 无论 busy 与否:都释放 page 元数据并从链表移除
*
* 设计意图:
* - busy 时不释放 coherent 页,避免设备仍在 DMA 访问时释放底层内存导致数据破坏/总线错误
* - 但仍然清理元数据与链表,尽量避免进一步使用(属于错误恢复路径)
*/
list_for_each_entry_safe(page, tmp, &pool->page_list, page_list) {
if (!busy)
dma_free_coherent(pool->dev, pool->allocation,
page->vaddr, page->dma);
list_del(&page->page_list); /* 从页链表摘除 */
kfree(page); /* 释放页元数据 */
}

/** 最后释放 pool 对象本体 */
kfree(pool);
}
EXPORT_SYMBOL(dma_pool_destroy);

/**
* @brief 从 DMA pool 分配一个 coherent block
* @param[in] pool 目标 DMA pool
* @param[in] mem_flags GFP 标志
* @param[out] handle 返回该 block 的 DMA 地址
* @return 成功返回该 block 的 CPU 虚拟地址;失败返回 NULL
*/
void *dma_pool_alloc(struct dma_pool *pool, gfp_t mem_flags,
dma_addr_t *handle)
{
/** block:从 pool 中弹出的空闲 block(其起始地址即返回的 vaddr) */
struct dma_block *block;

/** page:当 pool 为空时用于扩展的新页 */
struct dma_page *page;

/** flags:自旋锁保存/恢复中断状态用 */
unsigned long flags;

/** 调试/静态检查:提示该路径可能进行内存分配 */
might_alloc(mem_flags);

/**
* 先在锁内尝试弹出空闲 block:
* pool->lock 保护空闲结构、nr_active 等共享状态。
*/
spin_lock_irqsave(&pool->lock, flags);
block = pool_block_pop(pool);
if (!block) {
/**
* 空闲列表为空:
* 由于 pool_alloc_page() 可能睡眠,因此必须先放锁,
* 否则会在自旋锁持有期间睡眠导致严重错误。
*/
spin_unlock_irqrestore(&pool->lock, flags);

/** 申请新页时去掉 __GFP_ZERO:页初始化由 pool 自己控制 */
page = pool_alloc_page(pool, mem_flags & (~__GFP_ZERO));
if (!page) /* 新页申请失败则直接返回 */
return NULL;

/**
* 重新加锁并初始化新页:
* pool_initialise_page() 会把 page 切成 block 并压入空闲结构,
* 然后再次从空闲结构弹出一个 block。
*/
spin_lock_irqsave(&pool->lock, flags);
pool_initialise_page(pool, page);
block = pool_block_pop(pool);
}
spin_unlock_irqrestore(&pool->lock, flags);

/** 返回该 block 的 DMA 地址给调用者 */
*handle = block->dma;

/**
* 进行一致性/越界等检查:
* pool_check_block() 通常用于调试验证块是否属于该 pool、是否满足对齐等约束。
*/
pool_check_block(pool, block, mem_flags);

/**
* 若分配标志要求“分配时清零”,则对 block 内容做 memset:
* want_init_on_alloc() 由内核策略决定是否需要初始化。
*/
if (want_init_on_alloc(mem_flags))
memset(block, 0, pool->size);

/** 返回 CPU 虚拟地址(block 起始地址) */
return block;
}
EXPORT_SYMBOL(dma_pool_alloc);

/**
* @brief 将一个 coherent block 归还到 DMA pool
* @param[in] pool 目标 DMA pool
* @param[in] vaddr block 的 CPU 虚拟地址
* @param[in] dma block 的 DMA 地址
*
* 约束:调用者保证该 block 不会再被设备/驱动触碰,除非再次分配获得。
*/
void dma_pool_free(struct dma_pool *pool, void *vaddr, dma_addr_t dma)
{
/** block:把 vaddr 解释为 pool 内部的 dma_block 结构 */
struct dma_block *block = vaddr;

/** flags:自旋锁保存/恢复中断状态用 */
unsigned long flags;

/** 锁内归还,避免与并发 alloc/free 破坏空闲结构 */
spin_lock_irqsave(&pool->lock, flags);

/**
* 校验 vaddr/dma 是否匹配该 pool 的 block:
* 通过 pool_block_err() 拦截明显错误的释放(例如地址不属于该 pool)。
*/
if (!pool_block_err(pool, vaddr, dma)) {
/** 将 block 压回空闲结构,并维护活跃计数 */
pool_block_push(pool, block, dma);
pool->nr_active--;
}

/** 释放锁并恢复中断状态 */
spin_unlock_irqrestore(&pool->lock, flags);
}
EXPORT_SYMBOL(dma_pool_free);

/**
* @brief devres 托管释放回调:用于驱动解绑时自动销毁 DMA pool
* @param[in] dev 关联的设备对象
* @param[in] res devres 资源记录,内容是 struct dma_pool* 的指针
*/
static void dmam_pool_release(struct device *dev, void *res)
{
/** pool:从 devres 记录中取出的 DMA pool 指针 */
struct dma_pool *pool = *(struct dma_pool **)res;

/** 释放动作就是销毁 pool */
dma_pool_destroy(pool);
}

/**
* @brief devres 匹配回调:用于在 devres 中定位特定 pool
* @param[in] dev 关联的设备对象
* @param[in] res devres 资源记录
* @param[in] match_data 需要匹配的目标指针(即 pool)
* @return 匹配返回非 0,否则返回 0
*/
static int dmam_pool_match(struct device *dev, void *res, void *match_data)
{
/** 仅当 devres 中记录的 pool 指针等于 match_data 时认为匹配 */
return *(struct dma_pool **)res == match_data;
}

/**
* @brief 创建一个 devres 托管的 DMA pool(驱动解绑时自动销毁)
* @param[in] name pool 名称(用于诊断输出)
* @param[in] dev 执行 DMA 的设备
* @param[in] size 每个 block 的大小
* @param[in] align block 对齐要求(必须是 2 的幂)
* @param[in] allocation block 不得跨越的边界(0 表示不做边界限制)
* @return 成功返回 DMA pool 指针;失败返回 NULL
*/
struct dma_pool *dmam_pool_create(const char *name, struct device *dev,
size_t size, size_t align, size_t allocation)
{
/** ptr:devres 记录,用于保存一个 struct dma_pool* 并绑定释放回调 */
struct dma_pool **ptr;

/** pool:最终创建出的 DMA pool */
struct dma_pool *pool;

/**
* 为 devres 分配记录:
* - 绑定释放回调 dmam_pool_release
* - 记录大小为 sizeof(*ptr),用于存放 pool 指针
*/
ptr = devres_alloc(dmam_pool_release, sizeof(*ptr), GFP_KERNEL);
if (!ptr)
return NULL;

/** 创建非托管的 DMA pool,并把指针写入 devres 记录 */
pool = *ptr = dma_pool_create(name, dev, size, align, allocation);

/**
* 创建成功则把 devres 记录挂到设备上;
* 失败则释放 devres 记录本身,避免泄漏。
*/
if (pool)
devres_add(dev, ptr);
else
devres_free(ptr);

/** 返回创建结果 */
return pool;
}
EXPORT_SYMBOL(dmam_pool_create);

/**
* @brief 销毁一个 devres 托管的 DMA pool
* @param[in] pool 需要销毁的 pool
*
* 该接口通过 devres_release 触发 dmam_pool_release,最终调用 dma_pool_destroy。
*/
void dmam_pool_destroy(struct dma_pool *pool)
{
/** dev:该 pool 绑定的设备对象 */
struct device *dev = pool->dev;

/**
* 从 devres 中释放与 pool 匹配的记录:
* - dmam_pool_release 会被调用,进而销毁 pool
* - WARN_ON 用于提示 release 异常(例如未找到记录)
*/
WARN_ON(devres_release(dev, dmam_pool_release, dmam_pool_match, pool));
}
EXPORT_SYMBOL(dmam_pool_destroy);

你先回答我一个问题(只答一句):
为什么 dma_pool_alloc() 在空闲栈为空时,必须先释放 pool->lock 再调用 pool_alloc_page()?(提示:考虑“可能睡眠”的语义约束。)