title: dmapool
categories:
- linux
- drivers
- dma
tags: - linux
- drivers
- dma
abbrlink: d8c38d13
date: 2025-10-21 14:12:36

文章目录
- [[mm/dmapool.c] [DMA 池分配器(dma_pool)] [为指定 device 提供"小块、一致性(coherent)可 DMA"的池化分配/释放接口]](#[mm/dmapool.c] [DMA 池分配器(dma_pool)] [为指定 device 提供“小块、一致性(coherent)可 DMA”的池化分配/释放接口])
- [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 小块内存池的页面分配、块分配/回收与 devres 托管(pool_alloc_page / dma_pool_alloc / dma_pool_free / dma_pool_destroy / dmam_pool_create / dmam_pool_destroy))
[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 生命周期。
下面是一个"驱动侧典型用法"的最小骨架(示意):
c
/**
* @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();分配在缺块时会走可能睡眠的扩容路径。
学习要点建议(按读源码的顺序)
- 先读
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 大页申请开销。
c
/**
* @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()?(提示:考虑"可能睡眠"的语义约束。)