恒美微站 Logo 恒美微站
  • 首页
  • 关于我们
  • 建站服务
  • 主题模板
  • 案例展示
  • 资讯中心
  • 联系我们

Linux内核dma-buf框架解析:多设备共享内存与驱动实现

  • 首页
  • 资讯中心
  • /
  • Linux内核dma-buf框架解析:多设备共享内存与驱动实现

相关资讯

SpringBoot+SSM二手交易系统实战:从建模到部署的完整复盘 2026/10/22 3:04:59
AI底层逻辑:数据、算法与算力的协同、调优及部署实践 2026/10/22 2:59:59
SpringBoot2+Vue3全栈开发实战:师生健康管理系统源码解析 2026/10/22 2:59:59

最新资讯

PJ85718DM与PIC32构建HVAC多点单总线温度监测系统
STM32+PJ85718DM单总线温度传感器:HVAC分布式测温软硬件实战
STM32L073RZ与PJ85718DM实现本地远程温度采集:I2C与Modbus RTU设计详解
基于STM32与PJ85718DM的HVAC本地及远程温度监测系统设计
一颗芯片测两路温度:STM32+PJ85718DM实现本地与远程二极管测温
HVAC温度采集实战:本地I2C与远程RS-485组网架构详解

今日推荐

电商智能运营体系从0到1:架构设计、落地步骤与避坑指南
压缩空气储能与热网热惯性联合优化调度:热电联产系统建模与求解
若依框架AI代码生成不稳定?用Agent Skill三层结构+五步法搞定

本周热门

基于GMM的语音识别系统原理与MATLAB实现详解
数字图像处理入门:反白、灰度化、调色板修改与导出txt实践
机器人大脑技术盘点:具身智能的感知、决策与数据闭环

本月精选

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频
Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证
2026 大模型集体涨价:用 Python 做企业 Token 成本测算与选型避坑(附配置)

Linux内核dma-buf框架解析:多设备共享内存与驱动实现

发布时间:2026/10/22 3:04:59
Linux内核dma-buf框架解析:多设备共享内存与驱动实现 写这篇笔记之前我在内核里翻dma-buf相关代码的时间比我写驱动的时间还长。不是说代码有多难而是这个框架的抽象层次比较高光看函数名很容易绕晕。等真正把一个exporter和一个importer跑通之后回头看整个设计才觉得确实是这么回事它解决的痛点是Linux图形、多媒体、异构计算场景里最基础也最麻烦的——多设备共享内存。这篇笔记不是源码逐行注释而是我从驱动开发者的角度把dma-buf的核心设计、API调用链、踩过的坑串起来的实操总结。适合正在看dma-buf代码、准备写驱动间共享buffer的朋友参考。1. 先说清楚dma-buf到底解决了什么问题内核里关于内存分配的接口并不少。kmalloc、alloc_pages、dma_alloc_coherent每个都有自己的适用场景。但这些接口都有一个共同点内存一旦分配出来默认就绑定在某个设备或某个子系统上了。而实际硬件场景中经常需要把同一块内存交给多个设备去访问。举一个最典型的例子摄像头采集到一帧图像需要交给GPU去做计算再送到显示控制器去扫描输出。这三方共享同一块物理内存但它们的DMA需求完全不同——摄像头是单向写入GPU要做随机读改写显示器是周期性读取。如果每一段都用memcpy把数据拷一遍带宽和延迟都扛不住尤其是4K60这种场景。dma-buf框架解决的就是这个跨设备共享问题。它把“一块DMA内存”抽象成一个内核对象提供一套标准的接口让buffer的**生产者exporter和消费者importer**能够安全地进行内存共享和访问同步。注意这里的关键词是“同步”——多设备并发访问同一个buffer不做同步是会出数据错乱的。dma-buf通过fence机制解决了这个问题这块后面细说。这个框架最早是为什么引入的可以追溯到图形栈里GPU和显示控制器共享显存的需求后来慢慢扩展到视频编解码、ISP、NPU等几乎所有需要设备间传数据的场景。在Android系统里大部分图形buffer的传递链路底层都是dma-buf在支撑。所以如果你写的驱动涉及以下场景基本就跑不掉要碰dma-buf设备需要把自己分配的内存分享给其他设备或用户态使用驱动需要访问其他驱动创建的内存对象需要把DMA buffer暴露给用户态做零拷贝读写需要在内核里对buffer做缓存一致性和同步管理2. 核心对象模型dma_buf、dma_buf_ops、dma_resv和sg_tabledma-buf框架的源码主要在drivers/dma-buf/目录下核心头文件是include/linux/dma-buf.h。理解这个框架第一步要分清里面几个核心结构体的分工。2.1 dma_buf对外暴露的抽象对象struct dma_buf是整个框架的门面。它代表一块可以共享的DMA内存用户态拿到fd之后通过这个结构体就能访问buffer。关键成员有file关联的struct filedma-buf最终通过一个anon_inode的fd暴露给用户态exp_nameexporter的名字通常用驱动名sizebuffer大小字节opsstruct dma_buf_ops所有底层操作都在这套回调里attachments挂载到该buffer上的设备列表resvstruct dma_resvbuffer的锁和fence管理注意dma_buf本身不直接管理物理页物理内存的管理在exporter那里。dma-buf更像是一个带引用计数的“票据”真正干活的是ops里的那些回调函数。2.2 dma_buf_opsexporter必须实现的操作集dma_buf_ops是exporter和框架之间的契约。内核文档里对每个回调的语义讲得比较细这里我只挑最重要的几个说struct dma_buf_ops { /* 判断buffer是否支持mmap */ bool (*mmap_capable)(struct dma_buf *); /* 将buffer映射到用户态地址空间 */ int (*mmap)(struct dma_buf *, struct vm_area_struct *); /* 释放buffer当最后一个引用被解除时调用 */ void (*release)(struct dma_buf *); /* Attach设备到buffer上返回attachment */ struct sg_table *(*map_dma_buf)(struct dma_buf_attachment *, enum dma_data_direction); void (*unmap_dma_buf)(struct dma_buf_attachment *, struct sg_table *, enum dma_data_direction); /* 缺页处理主要用于用户态mmap */ int (*vmap)(struct dma_buf *, struct iosys_map *); };map_dma_buf是核心当某个设备要把这块buffer变成自己可以访问的DMA地址时exporter需要返回一个sg_table里面填写设备视角的DMA地址。注意这里不是把物理内存直接给设备而是把物理页映射成设备I/O地址DMA地址IOMMU开没开这个地址的意义完全不同。2.3 dma_buf_attachment设备和buffer之间的绑定关系看内核代码时你会发现设备要访问dma-buf不会直接操作dma_buf而是先调用dma_buf_attach()得到一个dma_buf_attachment。这个attachment才是设备和buffer之间的绑定关系记录。每个attachment可以独立持有自己版本的sg_table通过dma_buf_map_attachment()获得因为不同的设备可能挂在不同的IOMMU域里同一块物理页在不同设备视角下DMA地址完全可能不一样。这个设计非常关键可以理解为“一份内存多份地址视图”。2.4 dma_resv共享和同步的基石struct dma_resv在老版本内核里叫dma_buf_resv名字改过但作用没变管理buffer上所有有读写依赖的fence为并发访问提供同步机制。struct dma_resv { struct ww_mutex lock; struct dma_resv_list *fence; struct dma_fence *fence_excl; };这里有两个概念要分清读fence列表shared允许多个读者并发写fenceexclusive同一时间只允许一个写者举个例子显示控制器在扫描buffer时会往resv里加一个读fence。GPU如果要对同一块buffer做渲染写入就必须等这个读fence信号完成。这个机制保证了“正在被显示的内存不会被GPU改到一半”。2.5 sg_tableDMA层面的物理描述struct sg_table是内核里描述分散内存的经典结构它把一组物理页组成一个“散列表”。在dma-buf语境下exporter的map_dma_buf回调负责把buffer的物理页组装成sg_table并映射到设备地址空间。一个常见的误区是把sg_table当成一个纯逻辑数据结构其实里面的dma_address字段已经在map阶段赋值好了它代表的是设备视角的地址。没有经过IOMMU映射时dma_address通常等于物理地址开了IOMMU后它可能是设备DMA域的虚拟地址。3. 核心API调用链从分配buffer到设备访问刚才讲的是静态结构现在串一条完整的动态调用链看看一个dma-buf从出生到被设备访问要经历哪些阶段。我以自己的理解分成五步每一步对应一组API。3.1 第一步Exporter创建bufferexporter调用dma_buf_export()把自己拥有的内存包装成dma-bufstruct dma_buf *dma_buf_export(const struct dma_buf_export_info *exp_info);dma_buf_export_info里有几个字段要特别注意struct dma_buf_export_info { const char *exp_name; /* 通常是驱动名 */ struct module *owner; /* THIS_MODULE */ const struct dma_buf_ops *ops; /* 回调集 */ size_t size; /* buffer字节数 */ int flags; /* 一般为O_RDWR或O_RDONLY */ struct dma_resv *resv; /* 外部传入的resv也可留空让框架分配 */ void *priv; /* exporter私有数据通常是自己的buffer结构 */ };实际开发中exporter创建的dma-buf往往和自身的buffer管理结构绑定。比如一个分配了CMA内存的设备驱动会把CMA的内存信息存在priv里等到map回调触发时再拿出来组装sg_table。这种解耦方式让dma-buf不关心底层内存到底来自哪个分配器。3.2 第二步用户态或内核态获取dma-bufdma-buf导出后的使用路径有两条一是内核态共享通过dma_buf_get()/dma_buf_fd()等接口在驱动间传递或者通过设备树和平台数据传递。二是用户态共享这更常见。dma_buf_export()内部会创建struct file并关联到一个anon_inode的fd上用户态通过这个fd就能访问buffer了。内核提供了DMA_BUF_IOCTL_SYNC这个ioctl用户态可以主动触发cache同步。我们经常在图形栈里看到sync_file、dma_fence这些fd它们和dma-buf fd是两个不同的东西fd标识不同前者是同步原语后者是内存对象。3.3 第三步设备attach到dma-buf当某个设备驱动importer需要访问这块buffer时第一步是调用struct dma_buf_attachment *dma_buf_attach(struct dma_buf *dmabuf, struct device *dev);这一步是在“注册访问关系”。attach之后importer还不能立刻拿到地址需要继续调用map接口。按照内核的语义attach表示“我可能需要用到这块内存了”map才是“我要真正访问了”。这个设计看起来绕了一层我刚开始也很不理解不能直接给地址吗后来做IOMMU场景才体会到attach可以理解为一次代价比较高的“准备动作”而map/unmap是高频操作。dma-buf框架允许在同一attachment上反复map/unmap避免每次都重建映射关系这非常契合驱动的使用习惯。3.4 第四步map获取设备DMA地址struct sg_table *dma_buf_map_attachment(struct dma_buf_attachment *attach, enum dma_data_direction direction);返回的是struct sg_table *里面每个scatterlist项的dma_address就是设备可用的DMA地址。之后设备DMA控制器执行搬运操作时使用的地址就是这个。注意参数direction的选择DMA_TO_DEVICE、DMA_FROM_DEVICE、DMA_BIDIRECTIONAL。这不仅是给DMA引擎看的方向描述map过程中可能还需要做cache clean或invalidate。方向设错了轻则多刷几次cache性能损耗重则读到脏数据。3.5 第五步访问完成unmap并detachDMA搬运结束之后importer要调dma_buf_unmap_attachment()把sg_table“还”给exporter再根据生命周期调dma_buf_detach()解除绑定关系。这里有一个很多人踩过的坑map/unmap的次数必须配对。dma-buf框架在debug配置下会检查leak如果map了没unmap内核日志会刷WARNING信息时间长了还会把sg_table占用的内存全部耗尽。我们自己开发时为了图省事曾经在hot path上反复map不unmap跑一趟压力测试直接内存飙到上限。unmap这件事看起来是个细节搞错一次排查成本很高。4. 实操写一个简单的exporter驱动上面讲的全是概念和API这一节直接撸一个最小可运行的exporter驱动。它的任务是分配一块CMA内存给使用者并将其包装成dma-buf最后交给用户态或另一个内核驱动访问。为了简化这个exporter只做map和mmap支持基本读写。4.1 驱动框架搭建先定义我们的私有buffer结构struct fake_buf { void *vaddr; /* 内核虚拟地址 */ unsigned long paddr; /* 物理地址 */ size_t size; struct sg_table sgt; /* 预先建好的sg_table */ };分配CMA内存这里不多展开直接假设从dma_alloc_coherent()或预留的CMA区域拿到了物理地址paddr和内核虚拟地址vaddr。很多驱动用dma_buf_export包装的对象底层走的就是自己的buffer管理。接着实现必要的dma_buf_opsstatic struct sg_table *fake_map_dma_buf(struct dma_buf_attachment *attach, enum dma_data_direction dir) { struct fake_buf *buf attach-dmabuf-priv; struct sg_table *sgt buf-sgt; /* 假设内存已经是连续的否则需要遍历pages逐项填充 */ sg_init_table(sgt-sgl, 1); sg_set_page(sgt-sgl, phys_to_page(buf-paddr), buf-size, offset_in_page(buf-paddr)); /* 把物理页映射成设备DMA地址 */ if (!dma_map_sg(attach-dev, sgt-sgl, sgt-nents, dir)) return ERR_PTR(-ENOMEM); return sgt; }这个map函数看起来很简单但有一个关键点值得展开dma_map_sg要做的事情不光是“算地址”还包括在需要时做cache相关操作。例如从CPU写到设备时map阶段会做cache clean从设备写到CPU时map阶段可能会做invalidate。如果exporter底层用的内存本身就不是常规页缓存比如使用dma_alloc_coherent分配的buffercache已经是no-allocate属性那这个map回调就可以做得非常简单甚至只需要填一下地址即可。4.2 实现mmap支持用户态经常需要直接读写这块buffer比如把一帧图像mmap出来做后处理。mmap回调的核心是建立用户虚拟地址到物理页的映射关系static int fake_mmap(struct dma_buf *dmabuf, struct vm_area_struct *vma) { struct fake_buf *buf dmabuf-priv; vma-vm_page_prot pgprot_writecombine(vma-vm_page_prot); return remap_pfn_range(vma, vma-vm_start, buf-paddr PAGE_SHIFT, vma-vm_end - vma-vm_start, vma-vm_page_prot); }这里我选用了remap_pfn_range它比较适合连续物理内存。如果buffer是离散的scatterlist就得改成逐页vm_insert_pfn或者在fault回调里按需映射。写combine的页属性是显示类应用常用的一种优化策略实际场景中有些硬件对memory type敏感需要根据实际情况调整。4.3 导出dma-buf并暴露fdprobe阶段完成以上初始化后调用exporter接口struct dma_buf_export_info exp_info { .exp_name fake_exporter, .owner THIS_MODULE, .ops fake_dma_buf_ops, .size buf-size, .flags O_RDWR, .priv buf, }; buf-dmabuf dma_buf_export(exp_info); if (IS_ERR(buf-dmabuf)) return PTR_ERR(buf-dmabuf); /* 获取一个fd交给用户态 */ int fd dma_buf_fd(buf-dmabuf, O_CLOEXEC);dma_buf_fd()会把dma-buf关联的struct file安装到进程的fd表里。这里注意fd的生命周期和dma_buf的引用计数是解耦的fd关闭不代表dma-buf立即释放只有当所有fd和内核引用都释放后release回调才会触发。如果你debug log发现release一直不打印大概率是内核里还握着某个dma_buf_get()的引用没放。5. 实操importer侧如何访问别人的dma-bufexporter写完再写一个consumer侧的例子。这个驱动负责从一个用户态fd获取dma-buf然后示意性地把它映射到自己的设备地址空间。5.1 从fd获取dma-buf如果importer在内核态常见的做法是通过dma_buf_get()从一个fd编号获取struct dma_buf *dmabuf dma_buf_get(fd); if (IS_ERR(dmabuf)) return PTR_ERR(dmabuf);注意拿到之后一定要配对调用dma_buf_put()。在Android/GStreamer这类多媒体框架里fd的传递往往跨进程、跨驱动引用计数管理是bug高发区。很多疑难内存泄漏问题最后追根溯源都是某个驱动的dma_buf引用没放。5.2 Attach和Map拿到struct dma_buf之后importer驱动做两件事struct dma_buf_attachment *attach; struct sg_table *sgt; attach dma_buf_attach(dmabuf, dev); if (IS_ERR(attach)) return PTR_ERR(attach); sgt dma_buf_map_attachment(attach, DMA_BIDIRECTIONAL); if (IS_ERR(sgt)) goto err_unmap;sg_table里的每个entry就是一段DMA地址了。假设我们只需要操作第一个entry可以把sg_dma_address()取出来配置到DMA控制器的描述符里把sg_dma_len()填到描述符长度里。这个描述符准备好之后就可以启动搬运了。在实际项目中importer往往不会在每次搬运时都attach/detach而是在设备probe阶段attach一次之后高频map/unmap。每个attach的设计目标是“一配一映射”但map可以多次这就是框架推荐的使用模式。5.3 同步与fence访问结束后importer可能还要考虑和别的设备做同步。标准流程是importer将自己的DMA操作封装成struct dma_fence通过dma_resv_add_fence()挂到buffer的resv上。这样其他设备的fence回调可以等待这个操作完成。fence机制比较复杂第一次接触时可以从“它就是DMA完成的一次性信号”角度来看。硬件DMA产生中断后驱动在中断处理里dma_fence_signal()这个fence等待这个fence的其他设备就会被唤醒继续干活。简单说dma-buf负责“共享什么”dma_fence负责“什么时候安全”。两者配合才能构成完整的多设备buffer流转。6. 常见问题排查与调试心得dma-buf这么多年用下来坑很多网上资料也零散。我把遇到过的和周围人问得最多的问题集中整理一下做个速查表现象可能原因排查手段用户态mmap后读到全0或脏数据cache未同步或buffer没被正确映射检查dma_buf_ops里的map/mmap实现确认cache sync方向用DMA_BUF_IOCTL_SYNC手动刷新验证DMA拿到的数据是乱的方向参数不对cache没有invalidata检查dma_buf_map_attachment()用的dma_data_direction必要时改成DMA_BIDIRECTIONAL验证release永远不回调引用计数泄漏/sys/kernel/debug/dma_buf/bufinfo逐个看active fd数用dma_buf_put配对检查attach/map在IOMMU场景报错sg_table组装不对IOMMU映射失败打开CONFIG_DMA_API_DEBUG看dma_map_sg返回长度和实际请求是否一致多设备并发数据错乱fence使用不当或没用fence确认写入端是否在resv上挂了exclusive fence读取端是否挂了shared fence并等待6.1 排查cache一致性问题的经验我调试过的一个案例exporter用dma_alloc_coherent分配了bufferexporter这边的cache本来就是consistent的但是importer设备要求显示控制器读取。刚开始数据偶尔错乱反复查了很久最后发现是importer侧使用DMA_BIDIRECTIONAL做map时在map阶段把cache清掉了但显示控制器本身读的是consistent buffercache清完之后数据写回来反而乱了。这类问题在逻辑上非常简单要搞清楚buffer的cache属性是谁定义的map回调里做cache操作是否多余。买一送一如果你在的硬件平台开了IOMMU还能遇到另一种情况——map回调里返回了物理地址但设备DMA域和CPU物理域不一样此时要么在dma_map_sg里完成映射要么走IOMMU映射千万别自己乱拼地址。6.2 sysfs和debugfs调试内核提供了几个非常实用的调试入口/sys/kernel/debug/dma_buf/bufinfo每个dma_buf的export name、size、refcount、attached devices/sys/kernel/debug/dma_buf/dump_bufdump特定buffer的信息CONFIG_DMABUF_DEBUG检查map/unmap配对和attach/detach配对内核驱动开发时养成习惯每次访问dma_buf后打一条trace配合debugfs查看refcount是否异常回收出现问题第一眼就能定位。6.3 附内核配置项建议在开发dma-buf相关驱动时推荐打开这些内核配置CONFIG_DMABUF_DEBUGy CONFIG_DMA_API_DEBUGy CONFIG_DEBUG_FSy CONFIG_CMAy前面三个是debug利器最后一个看具体场景。如果是做嵌入式图形或视频通路CMA几乎必开。7. 还有几个容易忽略的设计细节再补充几个容易被绕进去的细节这些在阅读源码时很容易跳过去但实际调bug的时候能救命。7.1 dma-buf的file和fddma_buf_export()会创建struct file但这个file的f_op是框架内建的一套操作集而不是exporter自己定义的。这意味着用户态的read、write调用通常会返回-EINVAL或-EOPNOTSUPPexporter自定义语义只能靠ioctl扩展。所以用户在用户态拿到fd后默认能做的就是mmap和ioctl sync不能把dma-buf当成一个普通文件来读写。7.2 UML用户态映射的cache管理dma_buf_vmap()/dma_buf_vunmap()接口专门为内核态CPU访问准备返回的是struct iosys_map。当驱动需要把dma-buf里的内容读出来做校验时用它比直接操纵用户态映射安全得多。iosys_map这个结构体是为了同时兼容“拥有直接内核虚拟地址的系统”和“只能通过ioremap访问的系统”而抽象出来的代码里看起来多包了一层实际是为了跨架构统一。7.3 dma_buf的互斥锁struct dma_resv内部用的是struct ww_mutexwound/wait mutex这是为多设备多buffer场景下防止死锁专门设计的。如果驱动里同时要锁多个buffer注意锁顺序问题尽量不要自己用普通mutex去包dma_resv的锁否则容易在复杂buffer流转中死锁。7.4 reserved memory和CMA注意事项很多使用dma-buf的导入导出流程其实底层buffer内存来自CMA区域。CMA区域的分配在系统内存压力大时可能阻塞如果I/O路径上采集或渲染线程拿不到buffer会产生明显的卡顿。做视频通路时通常会在系统启动早期预留一块专用内存而不是让每个buffer都去动态分配。这块虽然和dma-buf本身没直接关系但在项目实际落地时很重要——框架解决的是“共享”内存为什么充足得自己操心。8. 一个简单的自测流程写完exporter和importer驱动之后怎么快速验证功能没问题我用的方法比较土但很有效让exporter导出dma-buf转成fd交给用户态用户态通过mmap往buffer里写入一段特定的pattern让importer驱动对该buffer做一次DMA读取操作然后CPU读回来比对如果pattern一致说明mmap映射和DMA map链路都没问题再跑一个压测循环做1000次attach/map/访问/unmap/detach观察refcount和内存有没有泄漏用户态写pattern - mmap - importer DMA读 - CPU复核 - 一致则通不一致则查cache方向如果第3步数据不一致优先看两个方向一是map回调里dma_map_sg的方向参数二是用户态mmap后是否做过DMA_BUF_IOCTL_SYNC。我遇到的毫无例外的问题最后都能归到这两个点上。9. 我对dma-buf的整体把握把dma-buf的学习路线串下来它其实不是一个特别复杂的框架。核心理解就是两句话第一它把“内存共享”这件事标准化了。标准化的手段是定义了一套ops回调让每个exporter自己决定怎么提供buffer给其他人用。这样importer不需要知道buffer是从CMA来、还是从ION来甚至不需要知道底层内存管理细节。第二它把“共享之后的同步”也标准化了。通过dma_resv和dma_fence让多个设备能在同一块buffer上做生产者和消费者的配合避免互相污染数据。围绕这两句话再去理解attach、map、fence、cache sync这些概念基本不会跑偏。写驱动时核心思考路径永远是这么几个我的buffer谁分配的、物理页长什么样、谁要访问、访问方向是什么、是否需要和别的设备同步、cache怎么处理。一条条梳理清楚落实到dma-buf的API上就不容易出错。最后分享一个小技巧调试dma-buf相关问题时真的建议把CONFIG_DMABUF_DEBUG打开并且在每个attach/map操作点加上trace_printk或者dev_dbg。不要依赖“大概没问题”的感觉这类跨设备同步问题一旦出现往往是偶发的、需要特定时序才能复现的早发现问题早解决。

关于恒美微站

恒美微站专注于为个体商户、工作室提供极简自助建站服务,让每个人都能轻松拥有专业网站。

快速链接

  • 关于我们
  • 建站服务
  • 主题模板
  • 案例展示
  • 资讯中心

服务项目

  • 可视化建站
  • 拖拽编辑
  • 主题定制
  • SEO 优化
  • 网站托管

联系方式

  • 📍 地址:北京市朝阳区建国路 88 号
  • 📞 电话:400-888-8888
  • ✉️ 邮箱:info@hmyw.cn
  • 🕐 时间:周一至周日 9:00-18:00

© 2024 恒美微站 hmyw.cn 版权所有 | 京 ICP 备 12345678 号