DMA直接内存访问
DMA是一种硬件机制,用于支持在外设与主机内存之间直接进行数据读写,而无需CPU的参与。由于CPU全程没有参与,因此硬件需要通过中断信号通知CPU DMA的完成状态。当然CPU也可以选择轮询寄存器的方式获取DMA的完成状态,但这就有点两边都不讨好了,会让CPU和DMA控制器都处于繁忙状态,在重数据轻计算场景下可能会看到这种用法。
DMA传输使用什么地址?
外设和主机内存都在系统总线上,对于不同的系统总线,DMA传输使用的地址类型也不一样,对于PCIe总线而言,如果没有使用IOMMU机制,DMA使用的是物理地址,对于SBus而言,使用的虚拟地址。如果DMA传输支持虚拟地址,单次DMA传输大于一页时,不要求物理页地址跨页连续。
不是所有内存申请接口分配出来的内存buffer都适合用来进行DMA传输的,比如:如果分配出来的Buffer对应的内存物理地址(总线地址)高于4GB,而设备的DMA寻址能力只有32-bit,那这块Buffer就不能被这个设备用于DMA传输。
在使用kmalloc和get_free_pages时,如果指定了GFP_DMA
,申请的内存就在24-bit范围内。但是要注意,申请的内存大小不是无限制的,比如get_free_pages,MAX_ORDER为11,至多申请4MB。
当你需要传输很大的DMA Buffer时,有以下几种方式:
- 在系统boot阶段使用
mem=
参数指定保留部分高地址空间用于DMA传输 - 如果设备支持的话,使用scatter/gather IO方式进行传输
不同的架构有不同的DMA一致性限制,有些很容易就能实现一致性,有些很麻烦,Linux提供了DMA抽象层屏蔽了这些问题,常见的接口如下:
指定设备寻址能力
1 |
|
DMA映射
DMA映射做的事情是返回一个设备可用的总线地址,看起来virt_to_bus函数也许能做这件事,但实际上在特殊场景下会遇到一些问题,比如:
- 如果系统开启了IOMMU,需要把IOVA给硬件,硬件使用IOVA进行DMA传输
- 如果应用程序malloc了一块位于4GB以上的内存空间,但是硬件的寻址能力为32-bit,此时就需要使用bounce buffer,牺牲一点DMA的性能,保证DMA可以正常进行。
因此无法直接调用virt_to_bus把对应的总线地址给硬件,最好使用DMA通用接口进行DMA映射。
DMA映射做的另一件事情是保证缓存一致性,在一些架构上通过硬件就可以实现一致性,在一些架构上则需要软件配合。
有两种DMA映射方式:一致DMA映射、流式DMA映射。
一致DMA映射
1 | void *dma_alloc_coherent(struct device *dev, size_t size, dma_addr_t *dma_handle, int flag); |
此外,DMA pool用于小块的一致性DMA映射,dma_alloc_coherent至少会分配一页大小的DMA Buffer:
1 | struct dma_pool* dma_pool_create(const char *name, struct device *dev, size_t size, size_t align, size_t allocation); |
流式DMA映射
流式DMA映射必须指定数据传输方向,可以用于简化缓存一致性操作:
1 | enum dma_data_direction { |
针对不同的场景,使用不同的接口进行DMA映射:
1 | dma_addr_t dma_map_single(struct device *dev, void *buffer, size_t size, enum dma_data_direction); |
当dma_map_single被调用时,对应的数据可能还在缓存中,该函数会将其flush到内存;当数据地址无法被设备寻址时,dma_map_single会创建一个bounce buffer,如果DMA方向为双向的,在map、unmap时都需要拷贝一次数据,这是比较耗时的。
如果不想unmap又想访问DMA Buffer,需要调用:
1 | void dma_sync_single_for_cpu(struct device *dev, dma_addr_t bus_addr, size_t size, enum dma_data_direction); |