直接内存访问(DMA)

1. 什么是DMA

直接内存访问是一种硬件机制,它允许外围设备和主内存之间直接传输它们的I/O数据,而不需要系统处理器的参与。使用这种机制可以大大提高与设备通信的吞吐量。

 

2. DMA数据传输

有两种方式引发数据传输:

第一种情况:软件对数据的请求

1. 当进程调用read,驱动程序函数分配一个DMA缓冲区,并让硬件将数据传输到这个缓冲区中。进程处于睡眠状态。

2. 硬件将数据写入到DMA缓冲区中,当写入完毕,产生一个中断

3. 中断处理程序获取输入的数据,应答中断,并唤起进程,该进程现在即可读取数据

第二种情况发生在异步使用DMA时。

1. 硬件产生中断,宣告新数据的到来

2. 中断处理程序分配一个缓冲区,并且告诉硬件向哪里传输数据

3. 外围设备将数据写入数据区,完成后,产生另外一个中断

4.处理程序分发新数据,唤醒任何相关进程,然后执行清理工作

 

高效的DMA处理依赖于中断报告。

 

3. 分配DMA缓冲区

使用DMA缓冲区的主要问题是:当大于一页时,它们必须占据连续的物理页,因为设备使用ISAPCI系统总线传输数据,而这两种方式使用的都是物理地址。

使用get_free_pasges可以分配多大几M字节的内存(MAX_ORDER11),但是对于较大数量(即使是远小于128KB)的请求,通常会失败,这是因为系统内存充满了内存碎片。

解决方法之一就是在引导时分配内存,或者为缓冲区保留顶部物理内存。

例子:在系统引导时,向内核传递参数“mem=value”的方法保留顶部的RAM。比如系统有256内存,参数“mem=255M”,使内核不能使用顶部的1M字节。随后,模块可以使用下面代码获得该内存的访问权:

dmabuf=ioremap(0XFF00000/**255M/, 0X100000/*1M/*);

解决方法之二是使用GPF_NOFAIL分配标志为缓冲区分配内存,但是该方法为内存管理子系统带来了相当大的压力。

解决方法之三十设备支持分散/聚集I/O,这可以将缓冲区分配成多个小块,设备会很好地处理它们。

 

4. 通用DMA

DMA操作最终会分配缓冲区,并将总线地址传递给设备。内核提高了一个与总线——体系结构无关的DMA层。强烈建议在编写驱动程序时,为DMA操作使用该层。使用这些函数的头文件是<linux/dmamapping.h>

int dma_set_mask(struct device *dev, u64 mask);

该掩码显示该设备能寻址能力对应的位。比如说,设备受限于24位寻址,则mask应该是0x0FFFFFF

5. DMA映射

IOMMU在设备可访问的地址范围内规划了物理内存,使得物理上分散的缓冲区对设备来说成连续的。对IOMMU的运用需要使用到通用DMA层,而vir_to_bus函数不能完成这个任务。但是,x86平台没有对IOMMU的支持。

解决之道就是建立回弹缓冲区,然后,必要时会将数据写入或者读出回弹缓冲区。缺点是降低系统性能。

根据DMA缓冲区期望保留的时间长短,PCI代码区分两种类型的DMA映射:

一是一致性DMA映射,存在于驱动程序生命周期中,一致性映射的缓冲区必须可同时被CPU和外围设备访问。一致性映射必须保存在一致性缓存中。建立和使用一致性映射的开销是很大的。

二是流式DMA映射,内核开发者建议尽量使用流式映射,原因:一是在支持映射寄存器的系统中,每个DMA映射使用总线上的一个或多个映射寄存器,而一致性映射生命周期很长,长时间占用这些这些寄存器,甚至在不使用他们的时候也不释放所有权;二是在一些硬件中,流式映射可以被优化,但优化的方法对一致性映射无效。

6. 建立一致性映射

驱动程序可调用pci_alloc_consistent函数建立一致性映射:

void *dma_alloc_coherent(struct device *dev, size_t size, dma_addr_t *dma_handle, int falg);

该函数处理了缓冲区的分配和映射,前两个参数是device结构和所需的缓冲区的大小。函数在两处返回DMA映射的结果:函数的返回值是缓冲区的内核虚拟地址,可以被驱动程序使用;而与其相关的总线地址保存在dma_handle中。

当不再需要缓冲区时,调用下函数:

void dma_free_conherent(struct device *dev, size_t size, void *vaddr, dma_addr_t *dma_handle);

7. DMA

DMA池是一个生成小型,一致性DMA映射的机制。调用dma_alloc_coherent函数获得的映射,可能其最小大小为单个页。如果设备需要的DMA区域比这还小,就是用DMA池。在<linux/dmapool.h>中定义了DMA池函数:

struct dma_pool *dma_pool_create(const char *name, struct device *dev, size_t size, size_t align, size_t allocation);

void dma_pool_destroy(struct dma_pool *pool);

nameDMA池的名字,devdevice结构,size是从该池中分配的缓冲区的大小,align是该池分配操作所必须遵守的硬件对齐原则(用字节表示),如果allocation不为零,表示内存边界不能超越allocation。比如说传入的allocation4K,表示从该池分配的缓冲区不能跨越4KB的界限。

在销毁之前必须向DMA池返回所有分配的内存。

void * dma_pool_alloc(sturct dma_pool *pool, int mem_flags, dma_addr_t *handle);

void dma_pool_free(struct dma_pool *pool, void *addr, dma_addr_t addr);

8. 建立流式DMA映射       

在某些体系结构中,流式映射也能够拥有多个不连续的页和多个“分散/聚集”缓冲区。建立流式映射时,必须告诉内核数据流动的方向。

DMA_TO_DEVICE

DEVICE_TO_DMA

如果数据被发送到设备,使用DMA_TO_DEVICE;而如果数据被发送到CPU,则使用DEVICE_TO_DMA

DMA_BIDIRECTTONAL

如果数据可双向移动,则使用该值

DMA_NONE

该符号只是出于调试目的。

当只有一个缓冲区要被传输的时候,使用下函数映射它:

dma_addr_t dma_map_single(struct device *dev, void *buffer, size_t size, enum dma_data_direction direction);

返回值是总线地址,可以把它传递给设备;如果执行错误,返回NULL

当传输完毕后,使用下函数删除映射:

void dma_unmap_single(struct device *dev, dma_addr_t dma_addr, size_t size, enum dma-data_direction direction);

使用流式DMA的原则:

一是缓冲区只能用于这样的传送,即其传送方向匹配与映射时给定的方向值;

二是一旦缓冲区被映射,它将属于设备,不是处理器。直到缓冲区被撤销映射前,驱动程序不能以任何方式访问其中的内容。只用当dma_unmap_single函数被调用后,显示刷新处理器缓存中的数据,驱动程序才能安全访问其中的内容。

三是在DMA出于活动期间内,不能撤销对缓冲区的映射,否则会严重破坏系统的稳定性。             

如果要映射的缓冲区位于设备不能访问的内存区段(高端内存),怎么办?一些体系结构只产生一个错误,但是其他一些系统结构件创建一个回弹缓冲区。回弹缓冲区就是内存中的独立区域,它可被设备访问。如果使用DMA_TO_DEVICE标志映射缓冲区,并且需要使用回弹缓冲区,则在最初缓冲区中的内容作为映射操作的一部分被拷贝。很明显,在拷贝后,最初缓冲区内容的改变对设备不可见。同样DEVICE_TO_DMA回弹缓冲区被dma_unmap_single函数拷贝回最初的缓冲区中,也就是说,直到拷贝操作完成,来自设备的数据才可用。

有时候,驱动程序需要不经过撤销映射就访问流式DMA缓冲区的内容,为此内核提供了如下调用:

void dma_sync_single_for_cpu(struct device *dev, dma_handle_t bus_addr, size_t size, enum dma_data_directction direction);

应该在处理器访问流式DMA缓冲区前调用该函数。一旦调用了该函数,处理器将“拥有”DMA缓冲区,并可根据需要对它进行访问。然后在设备访问缓冲区前,应该调用下面的函数将所有权交还给设备:

void dma_sync_single_for_device(struct device *dev, dma_handle_t bus_addr, size_t size, enum dma_data_direction direction);

再次强调,处理器在调用该函数后,不能再访问DMA缓冲区了。

你可能感兴趣的:(直接内存访问(DMA))