DMA---直接内存访问
用来在设备内存与主存RAM之间直接进行数据交换,这个过程无需CPU干预,
对于系统中有大量数据交换的设备而言,如果能够充分利用DMA特性,可以大大提高系统性能。
1.内核中DMA层
--内核为设备驱动程序提供了统一的DMA接口,这些接口屏蔽了不同平台之间的差异。
--一致性映射类型的dma_alloc_coherent/流式映射类型的dma_map_single
不同的平台(X86/ARM)提供各自的struct dma_map_ops对象来实现对应的DMA映射。
2.物理地址和总线地址
物理地址是指cpu地址信号线上产生的地址。
总线地址可以认为从设备的角度看到的地址,不同类型的总线具有不同类型的总线地址。
DMA地址--用来在设备和主存之间寻址,虽然是总线地址,但是从内核代码的角度来看,被称为DMA地址,
与之相对应的数据结构是dma_addr_t(-->typedef u32 dma_addr_t;)。
3.DMA映射
3.1 基本原理
DMA映射主要为在设备与主存之间建立DMA数据传输通道时,在主存中为该DMA通道分配内存空间的行为,该内存空间
也称为DMA缓冲区。这个任务原本可以很简单,但是由于现代处理器cache的存在,使得事情变得复杂。
3.2 RAM与cache内容的一致性问题
1.出现问题原因
现代处理器为了提升系统性能,在CPU与RAM之间加入了高速缓存cache,
所以当在RAM中为一个DMA通道建立一段缓冲区时,
必须仔细考虑RAM与cache内容的一致性问题。
/*具体的分析*/
如果RAM与Device之间的一次数据交换改变了RAM中DMA缓冲区的内容,
而cache中缓存了DMA缓冲区对应的RAM中一段内存块。
如果没有机制保护cache中的内容被新的DMA缓冲区数据更新(或者无效),
那么cache和他对应的RAM中的一段内存块在内容上出现了不一致,
此时如果CPU去读取device传到RAM的DMA缓冲区中的数据,
它将直接从cache获得数据,这些数据显然不是它所期望的,
因为cache对应的RAM中的数据已经更新了。
2.解决问题--
就cache一致性问题,不同的体系架构有不同的策略,有些是在硬件层面予以保证(x86平台)
有些没有硬件支持而需要软件的参与(ARM品台)。
--Linux内核中的通用DMA尽力为设备驱动程序提供统一的接口来处理cache缓存一致性的问题,
而将大量品台相关的代码对设备驱动程序隐藏起来。
3.3 DMA映射三种情况
1. 一致性DMA映射
linux内核DMA层为一致性DMA映射提供的接口函数为dma_alloc_coherent()
-->
/*
* Allocate DMA-coherent memory space and return both the kernel remapped
* virtual and bus address for that space.
*/
void *dma_alloc_coherent(struct device *dev, size_t size, dma_addr_t *handle, gfp_t gfp)
函数分配的一致性DMA缓冲区的总线地址(也是DMA地址)由参数handle带回,
函数返回的则是映射到DMA缓冲区的虚拟地址的起始地址
接着调用->__dma_alloc(struct device *dev, size_t size, dma_addr_t *handle, gfp_t gfp,pgprot_t prot)
{
struct page *page;
void *addr;
*handle = ~0;
size = PAGE_ALIGN(size);
page = __dma_alloc_buffer(dev, size, gfp); /*分配大小为size的一段连续的物理内存页,并且对应得虚拟地址范围已经使cache失效*/
if (!page)
return NULL;
if (!arch_is_coherent())/*arch_is_coherent确定体系结构是否通过硬件来保证cache一致性(arm不是,所以函数返回0)*/
addr = __dma_alloc_remap(page, size, gfp, prot);/*在(?--0xffe0 0000)之间寻找一段虚拟地址段,将其重建新映射到page,
--------------------------------------------------------->由于关闭了cache功能所以保证了DMA操作时不会出现cache一致性问题*/-
else
addr = page_address(page);
if (addr)
*handle = pfn_to_dma(dev, page_to_pfn(page));
return addr;
}
一致性所获得的DMA缓冲区的大小都是页面的整数倍,如果驱动程序需要更小的DMA一致性的DMA缓冲区,则应该使用内核提供的DMA池(pool)机制
/*释放一致性DMA缓冲区*/
void dma_free_coherent(struct device *dev, size_t size, void *cpu_addr, dma_addr_t handle)
/*cpu_addr表示要释放的DMA缓冲区的起始虚拟地址,参数bus表示DMA缓冲区的总线地址*/
对于一致性DMA映射,在分配DMA缓冲区时各平台相关代码已经从根本上解决了cache一致性问题.
但是,一致性映射也会遇到无法克服的困难,主要是指驱动程序中使用的DMA缓冲区并非由驱动程序分配,
而是来自其他模块(如网络设备驱动程序中用于数据包传输的skb->data所指向的缓冲区),此时需要流式DMA映射。
2. 流式DMA映射
流式DMA映射的特点是DMA传输通道使用的缓冲区不是由当前驱动程序自身分配的,
而且往往对每次DMA传输都会重新建立一个流式映射的缓冲区,所以使用流式DMA映射时,
设备驱动程序必须小心负责处理可能出现的cache一致性。
linux内核DMA层为设备驱动提供的建立流式DMA映射的函数---dma_map_single
//dma_map_single(d, a, s, r) dma_map_single_attrs(d, a, s, r, NULL)
static inline dma_addr_t dma_map_single(struct device *dev, void *cpu_addr,
size_t size, enum dma_data_direction dir)
/*dev-->设备对象指针,cpu_addr-->cpu的虚拟地址,
size-->流式空间的范围,dir-->表明当前流式映射中DMA传输通道中的数据方向*/
函数返回数据类型-->dma_addr_t即表示DMA操作中的源地址和目的地址。
/*下面分析ARM的平台*/
/*将cpu_addr表示的段虚拟地址映射到DMA缓冲区中,返回该缓冲区的起始地址*/
2.1
static inline dma_addr_t dma_map_single(struct device *dev, void *cpu_addr,
size_t size, enum dma_data_direction dir)
{
dma_addr_t addr;
addr = __dma_map_single(dev, cpu_addr, size, dir);////
return addr;
}
2.2
static inline dma_addr_t __dma_map_single(struct device *dev, void *cpu_addr,
size_t size, enum dma_data_direction dir)
{
__dma_single_cpu_to_dev(cpu_addr, size, dir);
return virt_to_dma(dev, cpu_addr);
}
2.3
void ___dma_page_cpu_to_dev(struct page *page, unsigned long off,
size_t size, enum dma_data_direction dir)
{
unsigned long paddr;
dma_cache_maint_page(page, off, size, dir, dmac_map_area);
paddr = page_to_phys(page) + off;
if (dir == DMA_FROM_DEVICE) {
outer_inv_range(paddr, paddr + size); /*保证读数据使得时候,使得(paddr, paddr + size)对应的cache失效*/
} else {
outer_clean_range(paddr, paddr + size);
}
}
/*
sync_single_for_cpu方法用于数据从设备传到主存的情况:
为了避免cache的介入导致CPU读到的只是cache中旧的数据,
驱动程序需要在CPU读取之前调用该函数---->使得cache无效,这样处理器将直接从主存中获得数据。
sync_single_for_device方法用于数据从主存传到设备,
在启动DMA操作之前,CPU需要将数据放在位于主存的DMA缓冲区中,
为了防止write buffer的介入,导致数据只是临时写到write buffer中,
驱动程序需要在CPU往主存写数据之后启动DMA操作之前调用该函数。
*/
3. 分散/聚集DMA映射
分散/聚集DMA映射通过将虚拟地址上分散的DMA缓冲区通过一个struct scatterlist的数组或链表组织起来,
然后通过一次的DMA传输操作在主存RAM与设备之间传输数据。
分散/聚集DMA映射本质上是通过一次DMA操作把内存中分散的数据块在主存与设备之间进行传输,对于其中的每个数据块
内核都会建立对应的一个流式DMA映射。---》需要设备的支持。
3.4 回弹缓冲区
如果CPU侧虚拟地址对应的物理地址不适合设备的DMA操作,那么需要建立回弹缓冲区,相当于一个 中转站,把数据往设备传输时,
驱动程序需要把CPU给的数据拷贝到回弹缓冲区,然后再启动DMA操作。
3.5 DMA池
由于DMA映射所建立的缓冲区是单个页面的整数倍,
如果驱动程序需要更小的一致性映射的DMA缓冲区,
可以使用内核提供的DMA池机制(非常类似于Linux内存管理中的slab机制).
struct dma_pool就是内核用来完成该任务的数据结构
struct dma_pool { /* the pool */
struct list_head page_list; /*用来将一致性DMA映射建立的页面组织成链表*/
spinlock_t lock; /*自旋锁*/
size_t size; /*该DMA池用来分配一致性DMA映射的缓冲区的大小,也称为块大小*/
struct device *dev; /*进行DMA操作的 设备对象指针*/
size_t allocation;
size_t boundary;
char name[32]; /*dma池的名称*/
wait_queue_head_t waitq; /*等待队列*/
struct list_head pools; /*用来将当前DMA池对象加入到dev->dma_pools链表中*/
};
/*相关操作*/
1--创建dma_pool,并初始化
/* dma_pool_create - Creates a pool of consistent memory blocks, for dma.
* @name: name of pool, for diagnostics
* @dev: device that will be doing the DMA
* @size: size of the blocks in this pool.
* @align: alignment requirement for blocks; must be a power of two
* @boundary: returned blocks won't cross this power of two boundary
* Context: !in_interrupt()
*/
struct dma_pool *dma_pool_create(const char *name, struct device *dev,
size_t size, size_t align, size_t boundary)
2--释放DMA池中的DMA缓冲块
/**
* dma_pool_free - put block back into dma pool
* @pool: the dma pool holding the block
* @vaddr: virtual address of block
* @dma: dma address of block
**/
void dma_pool_free(struct dma_pool *pool, void *vaddr, dma_addr_t dma)
3--销毁dma_pool
/* dma_pool_destroy - destroys a pool of dma memory blocks.
* @pool: dma pool that will be destroyed
*/
void dma_pool_destroy(struct dma_pool *pool)
//参考文献:陈雪松-深入Linux设备驱动程序内核机制