Linux-3.5 引入了一套 Contiguous Memory Allocator,简称 CMA,基于 DMA 映射框架为内核提供连续大块内存的申请和释放。CMA 主要思路是将预留内存纳入 DMA 映射管理,可以给系统内所有设备共享使用,这样就既解决了为 GPU、Camera、显示等图像处理类模块预留大块的连续内存,又解决了预留内存被空置的问题,提升内存使用率。
CMA 本身不是一套分配内存的算法,它的底层仍然要依赖伙伴算法系统来支持,可以理解为 CMA 是介于 DMA mapping 和内存管理之间的中间层。
CMA 的具体功能有:
-
在系统的启动过程中,根据内核编译配置、或者 DTS 配置将内存中某块区域用于 CMA,然后内核中其他模块可以通过 DMA 的接口 API 申请连续内存,这块区域我们称之为 CMA area。
-
提供 cma_alloc()和 cma_release()两个接口函数用于分配和释放 CMA pages。
-
记录和跟踪 CMA area 中各个 pages 的状态。
-
调用伙伴系统接口,进行真正的内存分配。
CMA 主要接口
-
CMA area 的声明
setup_bootmem() -> dma_contiguous_reserve(),定义在 kernel/dma/contiguous.c
-
CMA 初始化
见 mm/cma.c 中的 cma_init_reserved_areas() -> init_cma_reserved_pageblock(),其中会设置 page 属性为 MIGRATE_CMA。
-
申请 CMA
使用 DMA 标准接口 dma_alloc_coherent() 和 dma_alloc_wc(),会间接调用 dma_alloc_from_contiguous()。
-
释放 CMA
使用 DMA 标准接口 dma_free_coherent()。
CMA 解决的是预留内存空闲期间如何给其他设备共享的问题,DMA-BUF 解决的是使用期间多个设备共享的问题、以及内核态和用户态如何共享内存的问题。DMA-BUF 可减少多余的拷贝,提升系统运行效率。
DMA-BUF {BANNED}最佳初原型是 hrbuf,于 2011 年首次提出,实现了 “Buffer Sharing” 的概念验证。shrbuf 被社区重构变身为 DMA-BUF,2012 年合入 Linux-3.3 主线版本。
DMA-BUF 被广泛用在多媒体驱动中,尤其在 V4L2、DRM 子系统中经常用到。
工作机制
为了解决各个驱动之间的 buffer 共享问题,DMA-BUF 将 buffer 与 file 结合使用,让 DMA-BUF 既是一块物理 buffer,同时也是个 Linux 标准 file。
分配 buffer 的模块为 exporter,使用该 buffer 的模块为 importer。
DMA-BUF 支持连续物理内存、散列物理内存的 buffer 管理。ArtInChip 平台目前只支持连续物理内存的 DMA-BUF。
主要接口
-
内核空间
-
用户空间
通过 ioctl 来管理 DMA-BUF。
DMA-BUF 的引入为应用程序和驱动程序共享内存提供了一种通用的方法。DMA-BUF 本身由 DMA-BUF exporter 创建,DMA-BUF exporter 是一个驱动程序,它可以分配特定类型的内存,而且还为 kernel、user space、device 提供了多种回调函数来处理 buffer 的 map 和 unmap 问题。
DMA-BUF 的一般使用流程如下:
-
dma_buf_attach()
将 dma-buf attach 到一个 device 上(后续会使用该 buffer),exporter 驱动根据需要可以试着移动该 buffer,以确保新的 DMA 设备可以访问到它,或者直接返回错误。该 buffer 可以被 attach 到多个 device 上。
-
dma_buf_map_attachment()
将 buffer map 到一个 device (已经 attach 上)的设备地址空间里,该 buffer 可以被多个设备进行 map 操作。
-
dma_buf_unmap_attachment()
从 attach 的设备地址空间 unmap buffer
-
dma_buf_detach()
表示设备已完成 buffer 的使用,exporter 驱动可以在这里执行它们想要的清理工作。
如果我们以传统的 DMA API 的视角来看,我们可以认为 DMA-BUF 通常被 CPU 所拥有。只有在调用 dma_buf_map_attachment() 时,buffer 的所有权才会移交给 DMA 设备(并涉及 cache flush 操作)。然后在调用 dma_buf_unmap_attachment() 时,buffer 被 unmap,所有权又交还给 CPU(同样需要执行 cache invalidate 操作)。实际上 DMA-BUF exporter 驱动已经变相的成为遵循 DMA API 所有权规则的执行实体。
站在 DMA API 的角度来看:通过调用 dma_buf_map_attachment(),DMA-BUF 的所有权就转移到了 DMA 设备,通过调用 dma_buf_unmap_attachment() 又将所有权交还给 CPU,而每次调用这两个函数都会执行 cache 相关的操作。虽然这样的一个顺序操作能够确保 CPU Cache 处理的正确性,但是对于涉及多个 DMA 设备的 buffer pipeline 操作,CPU 实际上根本没有参与过对这些 buffer 的访问,而每次的 cache map 和 unmap 操作加起来则可能会导致明显的性能问题。
为了避免这些多余的 cache 操作,DMA-BUF 接口允许将 DMA API 的某些规则颠倒过来。需要注意的是,DMA API 假设 CPU 是所有 memory 的天然拥有者,且只有在 DMA 传输过程中(buffer 的所有权已明确转移给了 DMA 设备),才需要考虑这一反向规则。DMA-BUF 接口要求 CPU 在访问 DMA-BUF 之前需要先调用 dma_buf_begin_cpu_access(),在访问结束后调用 dma_buf_end_cpu_access()。如果 CPU 想从用户空间访问该 buffer,则可以使用 DMA_BUF_IOCTL_SYNC
ioctl() 命令来发起 begin/end cpu_access 的调用。
特殊接口:
-
dma_buf_begin_cpu_access()
通过该接口,exporter 驱动可以确保当前 buffer 只允许被 CPU 访问,这个过程中可能需要 allocate 或 swap-in 以及 pin(固定)后端存储。另外,exporter 驱动还需要确保 CPU 访问的方向与它所请求的方向是一致的。
-
dma_buf_end_cpu_access()
该接口在 importer 完成 CPU Access 时调用,exporter 可以在该接口中实现 cache flush 操作,并 unpin 之前在 dma_buf_begin_cpu_access() 中 pin 过的内存资源。
要实现一个 dma-buf exporter驱动,需要执行3个步骤:
dma_buf_ops
DEFINE_DMA_BUF_EXPORT_INFO
dma_buf_export()
注意: 其中 dma_buf_ops 的回调接口中,如下接口又是必须要实现的,缺少任何一个都将导致 dma_buf_export() 函数调用失败!
map_dma_buf
unmap_dma_buf
map
map_atomic
mmap
release
如下 dma-buf API 实现了 CPU 在内核空间对 dma-buf 内存的访问:
dma_buf_kmap()
dma_buf_kmap_atomic()
dma_buf_vmap()
(它们的反向操作分别对应各自的 unmap 接口)
通过 dma_buf_kmap() / dma_buf_vmap() 操作,就可以把实际的物理内存,映射到 kernel 空间,并转化成 CPU 可以连续访问的虚拟地址,方便后续软件直接读写这块物理内存。因此,无论这块 buffer 在物理上是否连续,在经过 kmap / vmap 映射后的虚拟地址一定是连续的。
上述的3个接口分别和 linux 内存管理子系统(MM)中的 kmap()、 kmap_atomic() 和 vmap() 函数一一对应,三者的区别如下:
函数
说明
kmap()
一次只能映射1个page,可能会睡眠,只能在进程上下文中调用
kmap_atomic()
一次只能映射1个page,不会睡眠,可在中断上下文中调用
vmap()
一次可以映射多个pages,且这些pages物理上可以不连续,只能在进程上下文中调用
DMA Access
dma-buf 提供给 DMA 硬件访问的 API 主要就两个:
dma_buf_attach()
dma_buf_map_attachment()
这两个接口调用有严格的先后顺序,必须先 attach,再 map attachment,因为后者的参数是由前者提供的,所以通常这两个接口形影不离。
与上面两个 API 相对应的反向操作接口为: dma_buf_dettach() 和dma_buf_unmap_attachment()。
dma_buf_attach()
该函数实际是 “dma-buf attach device” 的缩写,用于建立一个 dma-buf 与 device 的连接关系,这个连接关系被存放在新创建的 dma_buf_attachment 对象中,供后续调用 dma_buf_map_attachment() 使用。
该函数对应 dma_buf_ops 中的 attach 回调接口,如果 device 对后续的 map attachment 操作没有什么特殊要求,可以不实现。
dma_buf_map_attachment()
该函数实际是 “dma-buf map attachment into sg_table” 的缩写,它主要完成2件事情:
生成 sg_table
同步 Cache
选择返回 sg_table 而不是物理地址,是为了兼容所有 DMA 硬件(带或不带 IOMMU),因为 sg_table 既可以表示连续物理内存,也可以表示非连续物理内存。
同步 Cache 是为了防止该 buffer 事先被 CPU 填充过,数据暂存在 Cache 中而非 DDR 上,导致 DMA 访问的不是{BANNED}最佳新的有效数据。通过将 Cache 中的数据回写到 DDR 上可以避免此类问题的发生。同样的,在 DMA 访问内存结束后,需要将 Cache 设置为无效,以便后续 CPU 直接从 DDR 上(而非 Cache 中)读取该内存数据。通常我们使用如下流式 DMA 映射接口来完成 Cache 的同步:
dma_map_single() / dma_unmap_single()
dma_map_page() / dma_unmap_page()
dma_map_sg() / dma_unmap_sg()
为了方便应用程序能直接在用户空间读写 dma-buf 的内存,dma_buf_ops 为我们提供了一个 mmap 回调接口,可以把 dma-buf 的物理内存直接映射到用户空间,这样应用程序就可以像访问普通文件那样访问 dma-buf 的物理内存了。
在 Linux 设备驱动中,大多数驱动的 mmap 操作接口都是通过调用 remap_pfn_range()
函数来实现的,dma-buf 也不例外。
除了 dma_buf_ops 提供的 mmap 回调接口外,dma-buf 还为我们提供了 dma_buf_mmap() 内核 API,使得我们可以在其他设备驱动中就地取材,直接引用 dma-buf 的 mmap 实现,以此来间接的实现设备驱动的 mmap 文件操作接口。