Linux内核中DMA分析



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设备驱动程序内核机制

你可能感兴趣的:(linux内核)