[Linux]学习笔记系列 -- [drivers][dma]dmapool


title: dmapool

categories:

  • linux
  • drivers
  • dma
    tags:
  • linux
  • drivers
  • dma
    abbrlink: d8c38d13
    date: 2025-10-21 14:12:36

https://github.com/wdfk-prog/linux-study

文章目录

[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 生命周期。

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

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();分配在缺块时会走可能睡眠的扩容路径。

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

  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 大页申请开销。


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()?(提示:考虑"可能睡眠"的语义约束。)

相关推荐
电饭叔2 小时前
Tkinter Button 括号内的核心参数详解
python·学习
玄〤2 小时前
Java 大数据量输入输出优化方案详解:从 Scanner 到手写快读(含漫画解析)
java·开发语言·笔记·算法
goxingman3 小时前
在 Linux 中查看磁盘运行占用(I/O 使用率)
linux·运维·chrome
STCNXPARM3 小时前
Linux camera之Media子系统
linux·camera·v4l2·media子系统
小天源3 小时前
XShell一台控制多台操作详情
linux·运维·服务器
xu_yule3 小时前
网络和Linux网络-13(高级IO+多路转接)五种IO模型+select编程
linux·网络·c++·select·i/o
闵帆3 小时前
反演学习器面临的鸿沟
人工智能·学习·机器学习
夜流冰3 小时前
编程参考 - Linux kernel代码查看
linux·运维·服务器
xu_yule3 小时前
网络和Linux网络-14(IO多路转接)poll和epoll编程-服务器
linux·运维·服务器·epoll·poll