核心内容摘要
葫芦兄弟:守护童年的英雄梦,唤醒内心的无限可能
title: dmapoolcategories:linuxdriversdmatags:linuxdriversdmaabbrlink: d8c38d13date:
14:12:36https://github.com/wdfk-prog/linux-study文章目录[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[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()从offset0开始按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 侧 sysfspools能看到 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 生命周期。
下面是一个“驱动侧典型用法”的最小骨架示意/** * brief 初始化 DMA descriptor pool示例 * param dev 目标设备 * return 成功返回 pool 指针失败返回 NULL */staticstructdma_pool*desc_pool_init(structdevice*dev){/* 64B 描述符64B 对齐不跨 4KB */returndmam_pool_create(desc_pool,dev,64,64,
;}不推荐使用的场景原因大块连续缓冲区例如几十 KB/MB 的 buffer更适合直接dma_alloc_coherent()/CMA 等用 pool 只会让 “allocationmax(size,PAGE_SIZE)” 的策略变得不经济。
每次 I/O 都映射/解除映射的 streaming 模型更合适的场景比如用普通缓存内存承载数据、只在 DMA 时临时 map/unmap这类场景不需要长期 coherent 常驻。
对比分析对比对象我选 3 类最常见的“替代/相邻方案”dma_pool本文件实现直接dma_alloc_coherent()每次分配一个 coherent buffer“普通内存 streaming DMA map/unmap”例如kmallocdma_map_single/dma_unmap_single维度dma_pool直接 dma_alloc_coherent普通内存 streaming map/unmap实现方式coherent 大块切分成固定 size blockfree list 管理每次走 coherent 分配/释放buffer 来自可缓存内存每次 DMA 前后做 map/unmap可能含 cache 维护 ([Linux内核官网][4])性能开销热路径通常是锁链表冷路径需要再申请 coherent page可能睡眠每次都在 coherent 分配路径频繁小块时开销更集中每次 I/O 都要 map/unmap但内存本身是可缓存的CPU 访问效率通常更好取决于平台 ([Linux内核官网][4])资源占用长期占用 coherent 页不自动回收空页适合长期复用按需占用 coherent频繁分配会导致碎片/管理成本占用普通内存DMA 时付出映射与一致性维护成本隔离级别每个 pool 绑定一个devblock 的dma来自该 dev 的 coherent 区域同上但没有“池”的复用结构DMA 地址由映射接口生成强调“DMA 地址空间与 CPU 地址空间可能不同” ([Linux内核官网][4])启动/首次使用速度第一次可能触发 coherent 页分配与切分之后很快每次都类似“首次成本”每次 I/O 都要映射但不需要长期预热
总结关键特性固定块大小的小对象 coherent DMA 分配器用 coherent 大块切分、空闲链表复用。
对齐/边界约束内建align/boundary直接影响切分与返回对象。
可观测与可托管sysfspools输出计数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_destroyDMA pool 用于为设备驱动提供大量小而固定大小的“DMA 一致性coherent”内存块典型用途是硬件描述符例如你前面 MDMA 的stm32_mdma_hwdesc。
它把底层的dma_alloc_coherent()按页pool-allocation批量申请再把页切成等大小 block通过栈/空闲链表快速分配与回收避免频繁的 coherent 大页申请开销。
/** * brief 为 DMA pool 申请一页 coherent 内存并返回封装对象 * param[in] pool DMA pool 对象提供 device、页大小、NUMA 节点等信息 * param[in] mem_flags GFP 标志决定分配行为是否可睡眠、是否可回收等 * return 成功返回 dma_page 指针失败返回 NULL */staticstructdma_page*pool_alloc_page(structdma_pool*pool,gfp_tmem_flags){/** page记录该页 coherent 内存的元数据对象 */structdma_page*page;/** 为 page 元数据按 pool-node 进行节点感知分配 */pagekmalloc_node(sizeof(*page),mem_flags,pool-node);if(!page)/* 元数据分配失败直接返回 */returnNULL;/** * 为该 pool 申请一段 coherent DMA 内存 * - 返回 CPU 可访问虚拟地址 page-vaddr * - 同时返回设备侧可用的 DMA 地址 page-dma * - 大小为 pool-allocation通常是“一页”或 pool 设计的批量粒度 */page-vaddrdma_alloc_coherent(pool-dev,pool-allocation,page-dma,mem_flags);if(!page-vaddr){/* coherent 内存分配失败需要回滚元数据 */kfree(page);returnNULL;}/** 成功返回包含 vaddr/dma 的 page */returnpage;}/** * brief 销毁一个 DMA pool调用者必须保证 pool 中不再有任何在用 block * param[in] pool 需要销毁的 DMA pool * * 约束 * - 该接口要求非中断上下文可能睡眠/持 mutex * - 调用者保证不会再有人使用该 pool且 pool 中的 block 不再被设备/驱动访问 */voiddma_pool_destroy(structdma_pool*pool){/** page/tmp遍历 pool-page_list 使用的当前节点与临时节点 */structdma_page*page,*tmp;/** empty该设备是否已无任何 poolbusy销毁时是否仍有活跃 block */bool empty,busyfalse;/** 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 */emptylist_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);busytrue;}/** * 遍历并释放 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(structdma_pool*pool,gfp_tmem_flags,dma_addr_t*handle){/** block从 pool 中弹出的空闲 block其起始地址即返回的 vaddr */structdma_block*block;/** page当 pool 为空时用于扩展的新页 */structdma_page*page;/** flags自旋锁保存/恢复中断状态用 */unsignedlongflags;/** 调试/静态检查提示该路径可能进行内存分配 */might_alloc(mem_flags);/** * 先在锁内尝试弹出空闲 block * pool-lock 保护空闲结构、nr_active 等共享状态。
*/spin_lock_irqsave(pool-lock,flags);blockpool_block_pop(pool);if(!block){/** * 空闲列表为空 * 由于 pool_alloc_page() 可能睡眠因此必须先放锁 * 否则会在自旋锁持有期间睡眠导致严重错误。
*/spin_unlock_irqrestore(pool-lock,flags);/** 申请新页时去掉 __GFP_ZERO页初始化由 pool 自己控制 */pagepool_alloc_page(pool,mem_flags(~__GFP_ZERO));if(!page)/* 新页申请失败则直接返回 */returnNULL;/** * 重新加锁并初始化新页 * pool_initialise_page() 会把 page 切成 block 并压入空闲结构 * 然后再次从空闲结构弹出一个 block。
*/spin_lock_irqsave(pool-lock,flags);pool_initialise_page(pool,page);blockpool_block_pop(pool);}spin_unlock_irqrestore(pool-lock,flags);/** 返回该 block 的 DMA 地址给调用者 */*handleblock-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 起始地址 */returnblock;}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 不会再被设备/驱动触碰除非再次分配获得。
*/voiddma_pool_free(structdma_pool*pool,void*vaddr,dma_addr_tdma){/** block把 vaddr 解释为 pool 内部的 dma_block 结构 */structdma_block*blockvaddr;/** flags自旋锁保存/恢复中断状态用 */unsignedlongflags;/** 锁内归还避免与并发 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* 的指针 */staticvoiddmam_pool_release(structdevice*dev,void*res){/** pool从 devres 记录中取出的 DMA pool 指针 */structdma_pool*pool*(structdma_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 */staticintdmam_pool_match(structdevice*dev,void*res,void*match_data){/** 仅当 devres 中记录的 pool 指针等于 match_data 时认为匹配 */return*(structdma_pool**)resmatch_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 */structdma_pool*dmam_pool_create(constchar*name,structdevice*dev,size_tsize,size_talign,size_tallocation){/** ptrdevres 记录用于保存一个 struct dma_pool* 并绑定释放回调 */structdma_pool**ptr;/** pool最终创建出的 DMA pool */structdma_pool*pool;/** * 为 devres 分配记录 * - 绑定释放回调 dmam_pool_release * - 记录大小为 sizeof(*ptr)用于存放 pool 指针 */ptrdevres_alloc(dmam_pool_release,sizeof(*ptr),GFP_KERNEL);if(!ptr)returnNULL;/** 创建非托管的 DMA pool并把指针写入 devres 记录 */pool*ptrdma_pool_create(name,dev,size,align,allocation);/** * 创建成功则把 devres 记录挂到设备上 * 失败则释放 devres 记录本身避免泄漏。
*/if(pool)devres_add(dev,ptr);elsedevres_free(ptr);/** 返回创建结果 */returnpool;}EXPORT_SYMBOL(dmam_pool_create);/** * brief 销毁一个 devres 托管的 DMA pool * param[in] pool 需要销毁的 pool * * 该接口通过 devres_release 触发 dmam_pool_release最终调用 dma_pool_destroy。
*/voiddmam_pool_destroy(structdma_pool*pool){/** dev该 pool 绑定的设备对象 */structdevice*devpool-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()提示考虑“可能睡眠”的语义约束。