[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)。
- 数据结构
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()得到的vaddr与dma基址,并挂入page_list。struct dma_block:每个 block 的“头部”,至少包含next_block和该 block 的dma地址。实现里要求size >= sizeof(struct dma_block)。
- 创建:
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 属性文件。
- 扩容:
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_boundary与if (offset + size > next_boundary) offset = next_boundary; next_boundary += boundary;跳转 offset,保证“块不跨越指定 boundary”。
- 分配/释放:
dma_pool_alloc()/dma_pool_free()
dma_pool_alloc():- 先拿自旋锁,从空闲链表 pop;
- 若没有空闲块,会先释放自旋锁,再去
pool_alloc_page()(代码里明确标注“可能 sleep”),成功后再加锁初始化页面并重新 pop。 - 返回
block的虚拟地址,并通过handle返回对应dma地址。
dma_pool_free():加锁,做错误检查后 push 回空闲链表,并更新nr_active。
- 销毁:
dma_pool_destroy()
- 从
dev->dma_pools删除;如果这是最后一个 pool,则移除 sysfs 属性;若发现nr_active != 0会报错并认为 busy。 - 非 busy 时逐个
dma_free_coherent()释放每个dma_page的 coherent 内存并释放元数据。
- managed 版本:
dmam_pool_create/destroy()
- 用
devres保存指针,设备解绑时走dmam_pool_release()自动调用dma_pool_destroy()。
主要优势体现在哪些方面?
- 小块 coherent 分配的效率与碎片控制:一次 coherent 分配后切分复用,典型情况下分配/释放只是链表操作+锁。
- 硬件约束表达能力:
align与boundary直接编码到切块逻辑里,适合“不能跨 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。
使用场景
首选场景举例
- 大量小块 coherent 对象:如 DMA 描述符、硬件队列元素、控制结构等(CPU/设备共同访问,要求一致性)。 ([Linux内核文档][2])
- 需要边界限制的对象:例如硬件要求单次 DMA 传输不跨 4KB。接口注释明确把它作为典型用途。
- 希望简化资源释放的驱动:优先用
dmam_pool_create(),把销毁绑到 device 生命周期。
下面是一个“驱动侧典型用法”的最小骨架(示意):
1 | /** |
不推荐使用的场景(原因)
- 大块连续缓冲区:例如几十 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();分配在缺块时会走可能睡眠的扩容路径。
学习要点建议(按读源码的顺序)
- 先读
dma_pool_create_node()的参数约束:你会理解size/allocation/boundary三者的关系。 - 再读
pool_initialise_page():边界控制与切块逻辑都在这里。 - 最后读
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 | /** |
你先回答我一个问题(只答一句):
为什么 dma_pool_alloc() 在空闲栈为空时,必须先释放 pool->lock 再调用 pool_alloc_page()?(提示:考虑“可能睡眠”的语义约束。)








