DMA是硬件的一种能力,具备这种能力的硬件可以直接从主存中读写数据,也就是它可以直接使用主存进行I/O而不需要处理器的干预,这可以节省处理器资源并提高整个系统的IO吞吐量,因为IO操作相对来说是较慢的,如果每个IO都要使用处理器资源,则毫无以为会耗费大量CPU时间在单个IO上,最终导致系统IO性能下降。
一、DMA工作方式
对于I/O来说,在输入端存在两种工作模式:- 软件发起读请求,然后硬件响应该请求(存储器多用该方式)
- 硬件产生输入事件,然后硬件处理(网卡多用该方式)
由于DMA是一种I/O的方式,因而这样是它工作的场景。
DMA的工作方式是:
- 在输入时,软件准备一块内存区(DMA缓冲区),然后告知硬件,硬件通过DMA的方式将数据写入这部分区域
- 在输出时,软件准备好一块包含输出数据的内存区(DMA缓冲区),然后告知硬件,硬件通过DMA的方式获得这部分数据并输出出去。
二、分配DMA缓存
当使用DMA时,需要注意如果DMA缓冲区的大小大于一页,则它们必须占据连续的物理内存页,因为设备进行I/O时需要通过它所连接的总线(典型的总线就是PCI总线)进行,也就是说设备需要通过总线来访问这部分地址,在有些架构上总线需要使用物理地址,因而为了可移植性,总好总使用物理地址。分配缓冲区的机制可以是在系统启动时,也可以是在系统运行时,驱动的实现者需要根据自己的情形做出选择。驱动必须保证自己分配了正确的缓冲区(分配标记GFP_DMA可以帮助从DMA区域分配内存)。
编入内核的内核部件可以在系统启动时为自己预留大块的内存,但是如果一个编译为内核模块的内核部件需要使用大块内存时,该方式就不适用了,这个时候可以用另外一种方式:假设系统总共有4G内存,一个内核模块想要为自己预留100M大小的内存区域,则可以在系统启动时,设置启动参数mem=3.9G,这样内核将不使用最后的100M,然后该内核部件可以使用ioremap来获得最后的100M的内存。
由于总线使用物理地址,而程序使用的是虚拟地址,因而二者之间需要进行转换,内核提供了如下两个函数在二者之间进行转换:
unsigned long virt_to_bus(volatile void *address);
void *bus_to_virt(unsigned long address);
这里说的分配DMA缓冲区的方式以及总线地址和虚拟地址之间转换的方式都是低level的一些接口,使用这些接口时,使用者需要对硬件以及该结构非常熟悉,也就说说驱动编写者要确保自己知道所有的东西。实际上有更好的方案:为了方便使用DMA,内核提供了通用的DMA层,最好使用通用DMA层的接口,这样可以简化驱动的编写工作。
三、通用DMA层
不同架构上总线的连接工作方式,内存分配组织方式,处理缓存一致性的方式都有可能有所不同,内核提供了独立于总线和体系架构的DMA层,它隐藏了大多数的问题,因而它应该是使用DMA时的首选。3.1 设置硬件DMA能力
通用DMA层假设设备都能在32位地址上执行DMA,如果一个设备不能再32位地址上执行DMA,则它应该调用int dma_set_mask(struct device *dev, u64 mask);
来设置自己可以进行DMA的地址能力,比如如果设备只能在16位地址上进行DMA,则应该设置mask为0xffff。
该函数的返回值表明内核是否支持在指定的掩码上进行DMA,如果返回非0,则表明内核支持这样的DMA,如果返回0,则内核不支持在这样的DMA,设备就无法再进行DMA操作了。
如果设备支持在32位地址上进行DMA,则不必执行该函数。
3.2 DMA映射
DMA映射将要分配的DMA缓冲区的虚拟地址和为该设备生成的、设备可用的地址(即总线地址)关联了起来。前边提到用virt_to_bus可以将虚拟地址转变成总线地址,但是它并不总是正确的,因为有的架构支持IOMMU,支持IOMMU的硬件为总线提供了一套映射寄存器。IOMMU在设备可访问的地址空间范围内管理物理内存,该机制使得物理上分散的缓冲区对设备来说可能是连续的了。在这种方式下virt_to_bus是无法工作的。而通用DMA层则包括了对IOMMU的使用支持。因而使用通用DMA层更简单,更不易错。
DMA映射必须解决缓存一致性的问题,这是所有涉及到低级内存访问的操作都需要考虑的问题,因为处理器会缓存最近被使用的内存,如果该缓存和它对应的主存的数据不一致就可能导致问题。通用DMA层会完成这个工作。
DMA映射使用数据结构dma_addr_t来代表总线地址。它由总线使用,驱动不应使用它。
根据DMA缓冲区的生命周期,存在两种类型的DMA映射:
3.2.1 一致DMA映射
该类型的映射存在周期和驱动的生命周期一样。这种映射的缓冲区必须同时可以被CPU和外设访问。因此一致性映射必须建立在一致性缓存中,该类型的映射的建立和使用开销比较大。通过dma_alloc_coherent可以建立一致性映射,其原型如下:
void * dmam_alloc_coherent(struct device *dev, size_t size, dma_addr_t *dma_handle, gfp_t gfp);
它完成缓冲区的分配和映射。各参数的含义:
- dev:设备device结构
- size:以字节为单位的缓冲区大小
- dma_handle:与该缓冲区相关的总线地址
- gfp:分配标记
当使用完后,需要使用dmam_free_coherent来释放DMA缓冲区,其原型如下:
void dmam_free_coherent(struct device *dev, size_t size, void *vaddr,dma_addr_t dma_handle);
各参数含义和分配时的相同。
除了以上两个API外,内核还提供了一个生成小型、一致性DMA映射的机制—DMA池。它可以生成较小的一致性DMA缓冲区。
使用DMA池中的缓冲区时,需要首先创建DMA池,DMA池用dma_pool_create来创建,用dma_pool_destory来释放。其原型分别如下:
struct dma_pool *dma_pool_create(const char *name, struct device *dev,size_t size, size_t align, size_t boundary);
各参数含义如下:
- name:DMA池的名字
- dev:设备数据结构指针
- size:从该DMA池中分配的缓冲区的大小
- align:从该池分配时所遵循的对其原则
- boundary:如果它不为0,则从该DMA池返回的内存不能越过2的boundary次方的边界。
在使用时,需要从DMA池中分配DMA缓存,从DMA池分配DMA缓存使用函数dma_pool_alloc,其原型如下:
void *dma_pool_alloc(struct dma_pool *pool, gfp_t mem_flags, dma_addr_t *handle);
各参数含义如下:
- pool:从其中进行分配的DMA池
- mem_flags:分配标记
- handle:该缓冲区对应的总线地址
当使用完从DMA池分配的DMA缓冲区时,需要使用dma_pool_free来释放。其原型如下:
void dma_pool_free(struct dma_pool *pool, void *vaddr, dma_addr_t dma);其参数含义同dma_pool_alloc
3.2.2 流式DMA映射
- 通常为单独的DMA操作建立流式DMA 映射。在一些架构上,流式DMA映射被优化了,当然这需要遵循严格的访问规则。在使用DMA映射时,应该优先选择流式DMA,原因在于:
- 在支持映射寄存器的系统上,每个DMA 映射需要在总线上使用一个或多个的映射寄存器。一致映射具有很长的声明周期,因而会长期占用这些宝贵的资源,这有时候是一种浪费。
- 在某些硬件上,流式映射可以使用一致映射中无法使用的方式进行优化。
3.2.2.1 建立流式DMA 映射
相对于一致性映射,流式映射的接口比较复杂,这是因为:- 流式映射应该能与已经由驱动分配的缓冲区一起工作,因而不得不处理那些不是它们所选择的地址(但是已经被驱动分配的)。
- 某些架构上,流式映射能够拥有多个不连续的页和多个“分散/聚集”缓冲区。
- DMA_TO_DEVICE
- DMA_FROM_DEVICE
- DMA_BIDIRECTIONAL
- DMA_NONE
驱动不应该总是使用DMA_BIDIRECTIONAL,因为在某些架构,这可能导致性能急剧下降。
当只有一个缓冲区要传输时,使用函数dma_map_single来映射它,其原型如下:
dma_addr_t dma_map_single(struct device *dev, void *ptr, size_t size, enum dma_data_direction direction);
它将建立一个流式映射,并将内核虚拟地址和总线地址关联起来。在这一步完成后,内核会保证缓冲区所包含的所有数据都已经进入主存而不是在CPU缓存(即cache)中。各个参数含义如下:
- dev:设备数据结构指针
- ptr:指向DMA缓冲区的指针
- size:大小
- direction:数据流动的方向
void dma_unmap_single(struct device *dev, dma_addr_t dma_addr, size_t size, enum dma_data_direction direction);
它删除指定的映射,参数含义同建立映射的。
流式DMA的规则:
- 缓冲区只能用于direction指定的数据传输
- 一旦缓冲被映射,它就属于设备,而不属于处理器。在该映射被删除前,驱动不能以任何方式访问该缓冲区。
- 在DMA活动期间,即设备还在使用该缓冲区时,不能删除这个映射。
void dma_sync_single_for_cpu(struct device *dev, dma_addr_t dma_handle, size_t size, enum dma_data_direction direction);
void dma_sync_single_for_device(struct device *dev, dma_addr_t addr, size_t size, enum dma_data_direction dir);
3.2.2.2 单页流映射
通用DMA框架也提供了对单页进行DMA映射以及取消映射的API,相关的API如下:
dma_addr_t dma_map_page(struct device *dev, struct page *page, size_t offset, size_t size, enum dma_data_direction dir);
参数含义如下:
- dev:设备数据结构指针
- page:指向作为DAM缓冲区的page指针
- offset:映射从page的何处开始
- size:映射区域的大小
- dir:数据流动方向
void dma_unmap_page(struct device *dev, dma_addr_t addr, size_t size, enum dma_data_direction dir);
该函数用于取消映射
3.2.2.3 发散/汇聚映射
通用DMA框架还提供了一种特殊类型的流DMA映射机制--发散/汇聚映射。该机制允许一次为多个缓冲区创建DMA映射。其原型如下:int dma_map_sg(struct device *dev, struct scatterlist *sg, int nents, enum dma_data_direction direction);
各参数含义如下:
- dev:设备数据结构指针
- sg:缓冲区列表的第一个缓冲区的指针
- nets:sg中有多少个缓冲区
- direction:数据流动方向
数据结构scatterlist包含了每个缓冲区的信息,其定义如下:
struct scatterlist {
#ifdef CONFIG_DEBUG_SG
unsigned long sg_magic;
#endif
unsigned long page_link;
unsigned int offset;
unsigned int length;
dma_addr_t dma_address;
#ifdef CONFIG_NEED_SG_DMA_LENGTH
unsigned int dma_length;
#endif
};
注意如果sg已经映射过了,则不能再对其进行映射,再次映射会损坏sg中的信息。对于sg中的每个缓冲,该函数会正确的为其产生设备总线地址,驱动应该使用该总线地址,内核提供了两个相关的宏:
dma_addr_t sg_dma_address(struct scatterlist *sg);
用于从scatterlist返回总线( DMA )地址.
unsigned int sg_dma_len(struct scatterlist *sg);
用于返回这个缓冲的长度.
void dma_unmap_sg(struct device *dev, struct scatterlist *list, int nents, enum dma_data_direction direction);
该函数用于取消发散/汇聚映射。netns必须等于传给dma_map_sg的值,而不是dma_map_sg返回的值。
类似于单一映射,如果CPU必须访问已经映射了的缓冲区,则必须先让CPU获取这些缓冲区,对应的API如下:
void dma_sync_sg_for_cpu(struct device *dev, struct scatterlist *sg, int nents, enum dma_data_direction direction);
void dma_sync_sg_for_device(struct device *dev, struct scatterlist *sg, int nents, enum dma_data_direction direction);
四、DMA控制器(DMAC)
DMA控制器拥有关于DMA传送的信息,比如传送的方向,内存地址,传送数据的大小。它还包含了一个计数器来跟踪进行中的传送的状态。当控制器收到一个DMA请求信号时,它会获得总线的控制权,并驱动信号线以便设备可以读写数据。当外设想要传送数据时,它必须首先激活DMA请求线,实际的传输由DMAC管理。当DMA控制器选中设备时,即设备可以访问总线时,它就在总线上进行读写,当读写完成时,设备常常通过中断来进行通知。外设的驱动负责向DMAC提供传输的方向,总线地址以及传送数据的大小。同时外设的驱动还要负责准备传送的数据并且在DMA结束时响应中断。
DMA控制器包括了多个(4个)DMA通道,每个通道都与一组DMA寄存器相关联,这些寄存器用于保存进行DMA操作所需要的信息,因此DAM通道数目决定了可以同时由DMA控制器管理的DMA的数目。每次DMA传输的大小保存在DMA控制器中,表示每次传输需要多少个总线周期,总线周期*总线宽带即可得到每次所传输的数据大小。DMA控制器是一个系统范围的资源,并且DMA资源以通道的形式存在。内核提供了一套API来管理这个资源。
4.1 注册 DMA
类似于中断线,内核提供了一个API用于申请试用DMA通道。相应的API如下:int request_dma(unsigned int chan, const char *dev_id);
各参数含义:
chan:请求的通道号。是一个小于MAX_DMA_CHANNELS的值
dev_id:用于标识谁在请求DMA通道资源。
函数成功时返回0
void free_dma(unsigned int channel);
该函数用于释放DMA通道资源。
一般情况下,如果DMA也需要用到中断,则建议先申请中断资源,后申请DMA资源;先释放DMA资源,后释放中断资源。
4.2 设置DMA控制器
在申请了DMA资源后,如果要使用DMA(比如要进行DMA读或者DMA写时),设备驱动就需要正确的设置DMA控制器以使得它可以工作。DMA 控制器是一个共享的资源,并且它不支持并发的设置,因而DMA控制器由一个自旋锁dma_spin_lock来进行保护。设备驱动可以使用如下两个函数来使用该自旋锁:
unsigned long claim_dma_lock( );
它用于获取DMA自旋锁,其返回值必须在释放DMA自旋锁时被传递给释放DMA自旋锁的函数。
void release_dma_lock(unsigned long flags);
它用于释放DMA自旋锁。
自旋锁用于保护DMA控制器,因而当一个驱动对DMA控制器进行设置时,必须持有自旋锁。对DMA控制器进行设置的API包括:
void set_dma_mode(unsigned int channel, char mode);
设置DMA通道channel的传输模式。
void set_dma_addr(unsigned int channel, unsigned int addr);
该函数用于设置DMA通道channel的总线地址
void set_dma_count(unsigned int channel, unsigned int count);
该函数用于设置DMA通道channel所要传输的字节数。
void enable_dma(unsigned int channel);
该函数用于使能指定的DMA通道
void disable_dma(unsigned int channel);
该函数用于关闭指定的DMA通道
更多的API详见相关BSP的dma.h