☰
Linux内核dma-buf框架解析:多设备共享内存与驱动实现
2026/10/22 3:04:50 网站建设 项目流程

写这篇笔记之前,我在内核里翻dma-buf相关代码的时间,比我写驱动的时间还长。不是说代码有多难,而是这个框架的抽象层次比较高,光看函数名很容易绕晕。等真正把一个exporter和一个importer跑通之后,回头看整个设计,才觉得确实是这么回事:它解决的痛点,是Linux图形、多媒体、异构计算场景里最基础也最麻烦的——多设备共享内存。

这篇笔记不是源码逐行注释,而是我从驱动开发者的角度,把dma-buf的核心设计、API调用链、踩过的坑串起来的实操总结。适合正在看dma-buf代码、准备写驱动间共享buffer的朋友参考。

1. 先说清楚dma-buf到底解决了什么问题

内核里关于内存分配的接口并不少。kmalloc、alloc_pages、dma_alloc_coherent,每个都有自己的适用场景。但这些接口都有一个共同点:内存一旦分配出来,默认就绑定在某个设备或某个子系统上了。而实际硬件场景中,经常需要把同一块内存交给多个设备去访问。

举一个最典型的例子:摄像头采集到一帧图像,需要交给GPU去做计算,再送到显示控制器去扫描输出。这三方共享同一块物理内存,但它们的DMA需求完全不同——摄像头是单向写入,GPU要做随机读改写,显示器是周期性读取。如果每一段都用memcpy把数据拷一遍,带宽和延迟都扛不住,尤其是4K@60这种场景。

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_table

dma-buf框架的源码主要在drivers/dma-buf/目录下,核心头文件是include/linux/dma-buf.h。理解这个框架,第一步要分清里面几个核心结构体的分工。

2.1 dma_buf:对外暴露的抽象对象

struct dma_buf是整个框架的门面。它代表一块可以共享的DMA内存,用户态拿到fd之后,通过这个结构体就能访问buffer。关键成员有:

  • file:关联的struct file,dma-buf最终通过一个anon_inode的fd暴露给用户态
  • exp_name:exporter的名字,通常用驱动名
  • size:buffer大小(字节)
  • ops:struct dma_buf_ops,所有底层操作都在这套回调里
  • attachments:挂载到该buffer上的设备列表
  • resv:struct dma_resv,buffer的锁和fence管理

注意dma_buf本身不直接管理物理页,物理内存的管理在exporter那里。dma-buf更像是一个带引用计数的“票据”,真正干活的是ops里的那些回调函数。

2.2 dma_buf_ops:exporter必须实现的操作集

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):允许多个读者并发
  • 写fence(exclusive):同一时间只允许一个写者

举个例子:显示控制器在扫描buffer时,会往resv里加一个读fence。GPU如果要对同一块buffer做渲染写入,就必须等这个读fence信号完成。这个机制保证了“正在被显示的内存不会被GPU改到一半”。

2.5 sg_table:DMA层面的物理描述

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创建buffer

exporter调用dma_buf_export()把自己拥有的内存包装成dma-buf:

struct 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-buf

dma-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并detach

DMA搬运结束之后,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_ops:

static 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分配的buffer,cache已经是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并暴露fd

probe阶段完成以上初始化后,调用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-buf

exporter写完,再写一个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分配了buffer,exporter这边的cache本来就是consistent的,但是importer设备要求显示控制器读取。刚开始数据偶尔错乱,反复查了很久,最后发现是importer侧使用DMA_BIDIRECTIONAL做map时在map阶段把cache清掉了,但显示控制器本身读的是consistent buffer,cache清完之后数据写回来反而乱了。

这类问题在逻辑上非常简单:要搞清楚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_buf:dump特定buffer的信息
  • CONFIG_DMABUF_DEBUG:检查map/unmap配对和attach/detach配对

内核驱动开发时养成习惯:每次访问dma_buf后打一条trace,配合debugfs查看refcount是否异常回收,出现问题第一眼就能定位。

6.3 附:内核配置项建议

在开发dma-buf相关驱动时,推荐打开这些内核配置:

CONFIG_DMABUF_DEBUG=y CONFIG_DMA_API_DEBUG=y CONFIG_DEBUG_FS=y CONFIG_CMA=y

前面三个是debug利器,最后一个看具体场景。如果是做嵌入式图形或视频通路,CMA几乎必开。

7. 还有几个容易忽略的设计细节

再补充几个容易被绕进去的细节,这些在阅读源码时很容易跳过去,但实际调bug的时候能救命。

7.1 dma-buf的file和fd

dma_buf_export()会创建struct file,但这个file的f_op是框架内建的一套操作集,而不是exporter自己定义的。这意味着:用户态的read、write调用通常会返回-EINVAL或-EOPNOTSUPP,exporter自定义语义只能靠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_mutex(wound/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驱动之后,怎么快速验证功能没问题?我用的方法比较土但很有效:

  1. 让exporter导出dma-buf,转成fd交给用户态
  2. 用户态通过mmap往buffer里写入一段特定的pattern
  3. 让importer驱动对该buffer做一次DMA读取操作,然后CPU读回来比对
  4. 如果pattern一致,说明mmap映射和DMA map链路都没问题
  5. 再跑一个压测:循环做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。不要依赖“大概没问题”的感觉,这类跨设备同步问题一旦出现,往往是偶发的、需要特定时序才能复现的,早发现问题早解决。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询