设备IO之二(DMA)

DMA是硬件的一种能力,具备这种能力的硬件可以直接从主存中读写数据,也就是它可以直接使用主存进行I/O而不需要处理器的干预,这可以节省处理器资源并提高整个系统的IO吞吐量,因为IO操作相对来说是较慢的,如果每个IO都要使用处理器资源,则毫无以为会耗费大量CPU时间在单个IO上,最终导致系统IO性能下降。

一、DMA工作方式

对于I/O来说,在输入端存在两种工作模式:
  1. 软件发起读请求,然后硬件响应该请求(存储器多用该方式)
  2. 硬件产生输入事件,然后硬件处理(网卡多用该方式)
在输出端,都是由软件主动发出写请求(这是显然的,因为输出的数据显然要由软件来准备)。
由于DMA是一种I/O的方式,因而这样是它工作的场景。
DMA的工作方式是:
  1. 在输入时,软件准备一块内存区(DMA缓冲区),然后告知硬件,硬件通过DMA的方式将数据写入这部分区域
  2. 在输出时,软件准备好一块包含输出数据的内存区(DMA缓冲区),然后告知硬件,硬件通过DMA的方式获得这部分数据并输出出去。
本质上这就是DMA的工作方式。不过在输入端,不同的I/O工作方式准备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次方的边界。
void dma_pool_destroy(struct dma_pool *pool);
在使用时,需要从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_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:数据流动的方向
在传输完毕后,要用dma_unmap_single来删除映射,其原型如下:
void dma_unmap_single(struct device *dev, dma_addr_t dma_addr, size_t size, enum dma_data_direction direction);
它删除指定的映射,参数含义同建立映射的。
流式DMA的规则:
  • 缓冲区只能用于direction指定的数据传输
  • 一旦缓冲被映射,它就属于设备,而不属于处理器。在该映射被删除前,驱动不能以任何方式访问该缓冲区。
  • 在DMA活动期间,即设备还在使用该缓冲区时,不能删除这个映射。
内核也提供了让驱动在撤销映射前就访问流式DMA缓冲区的内容的方式,做法时,首先调用dma_sync_single_for_cpu,调用完该函数后,CPU就拥有了该缓冲区,因此也就可以访问缓冲区了;在访问完毕后,需要调用dma_sync_single_for_device将缓冲区的控制权归还给设备。
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:数据流动方向
该函数返回地址为该缓冲区的虚拟地址。从参数可以看出可以指定只映射一个页的一部分,但是建议不这么做,因为page是内核管理物理内存的单位,内核也基于它来提供一致性控制,只映射一页可能会导致一致性问题。

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:数据流动方向
该函数的返回值是成功映射了多少个缓冲区。如果在分散/汇聚列表中一些缓冲的物理地址或虚拟地址相邻的,且IOMMU可以将它们映射成单个内存块,则返回值可能比输入值nents小。
数据结构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

猜你喜欢

转载自blog.csdn.net/goodluckwhh/article/details/16886553