设备缓存和设备内存

设备缓存时由驱动程序管理的位于系统内存DDR中的一段内存区域,而设备内存则是设备固有的一段存储空间(比如显卡的FrameBuffer)。在Linux中设备缓存和设备内存的典型用法是在二者建立DMA通道,这样当设备内存中接收到的数据达到一定阈值时,设备将启动DMA通道将数据从设备内存传输到位于主存的设备缓存中,发送数据则正好相反,需要发送的数据首先放到设备缓存中,然后在设备驱动的介入下启动DMA传输,将缓存中的数据传输到设备内存中。

综上:

DMA缓冲区是由驱动程序管理的,可以理解为是为I/O设备(如显卡)准备的位于host端主存DDR中的一段内存区域,也叫I/O设备缓冲区。例如:显卡是从PCIe总线控制器或IOMMU的角度看待DMA缓冲区的。所以,显卡需要的是DMA缓冲区的总线地址,而不是物理地址或内核虚拟地址。所以如果你想告诉显卡这段DMA缓冲区的地址信息,必须让显卡知道DMA缓冲区的总线地址。Linux内核DMA服务例程将DMA缓冲区的内核虚拟地址映射到总线地址上(总线地址的类型是dma_addr_t),以便设备和CPU都能访问到这段DMA缓冲区。

DMA映射主要为在设备与主存之间建立DMA数据传输通道时,在主存中为该DMA通道分配DMA缓冲区空间的行为。这个任务原本很简单,但是由于现代处理器cache的存在,使得事情变得有点复杂。单就cache一致性的问题,不同的体系架构有不同的策略,有些是在硬件层面予以保证(比如x86平台),有些则没有硬件支持而需要软件的参与(比如ARM平台)。

Linux内核中的通用DMA层尽力为设备驱动程序提供统一的接口来处理cache缓存一致性的问题,而将大量平台相关的代码对设备驱动程序隐藏起来。Linux内核DMA layer的一个大体框架如下所示,从图中可以看出Linux内核中的DMA层为设备驱动程序提供标准的DMA映射接口,例如一致性映射类型的dma_alloc_coherent和流式映射类型的dma_map_single。在DMA层的下方,不同平台(比如x86或者ARM平台)的Linux内核代码实现平台相关的DMA映射操作,比如,通过DMA层Linux系统为设备驱动程序屏蔽了平台的差异。

一致性DMA映射(单个缓冲区,大小为单个页面的整数倍):

static inline void* 
dma_alloc_coherent(struct device*dev, size_t size, dma_addr_t*dma_handle, gfp_t gfp)

//函数分配的一致性DMA缓冲区的总线地址(也就是DMA地址)由参数dma_handle带回。

//函数返回值是映射到DMA缓冲区的虚拟地址的起始地址。

当驱动模块不再需要分配的一致性DMA缓冲区时,需要用以下函数来释放缓冲区:

void dma_free_coherent(struct device*dev, size_t size, void*vaddr, dma_addr_t bus)

//参数vaddr表示要释放的DMA缓冲区的起始虚拟地址。

//参数bus表示DMA缓冲区的总线地址。

流式DMA映射(单个缓冲区):

在某种情况下,一致性映射也会遇到无法克服的困难,这主要是指驱动程序中使用到的DMA缓冲区并非由驱动程序分配,而是来自其他模块(典型的如网络设备驱动程序中用于数据包传输的skb>data所指的缓冲区),这时就需要另一种DMA映射方式(流式DMA映射)。其在内核中存在的形式是一个宏定义:

#define dma_map_single(d,a,s,r) dma_map_single_attrs(d,a,s,r,NULL)

分散/聚集映射(多个缓冲区):

本质是通过一次DMA操作,把主存中分散的数据块在主存和设备之间进行传输,对于其中的每个数据块内核都会建立对应的一个流式DMA映射。另外,分散/聚集DMA映射需要设备的支持,而不完全由内核或者驱动程序决定。

当要传输的数据分布在不连续的内存上时,分散/聚集能对这些不连续缓冲区的数据进行一次性发送,反过来,DMA也能吧数据卡正确地传送到地址不连续地缓冲区。分散/聚集通过减少重复地DMA传送请求来提高效率。

内核中地DMA层为分散/聚集映射所提供地接口函数为:

int dma_map_sg(struct device *dev, struct scaltterlist *sg, int nents, 
enum dma_data_direction dir);

//dev:设备对象地指针;
//sg:struct scatterlist类型数组的首地址;
//nents:当前地分散/聚集映射中单一流式映射的个数(struct scatterlist数组/链表中地元素个数);
//dir:用于指明DMA传输中数据流地方向。

DMA pool机制

前面所讨论的一致性DMA映射时,知道这种DMA映射所建立的缓冲区大小是单个页面的整数倍,如果驱动程序需要更小的一致性DMA缓冲区,可以使用内核提供的DMA池机制。

DMA池机制非常类似于Linux内存管理中的slab机制,它的实现建立再一致性DMA映射所获得的连续物理页面的基础之上,通过DMA池的接口函数再物理页面之上分配所谓块大小的DMA缓冲区(DMA缓冲块),以区别一致性DMA映射中页面级大小的缓冲区。

显然为了管理跟踪物理页面中DMA缓冲块的分配和余下空闲空间的大小,内核需要引入对应的管理数据结构,struct dma_pool就是内核用来完成该任务的数据结构,其定义如下:

struct dma_pool {        /* the pool */
    struct list_head page_list;
    spinlock_t lock;
    size_t size;
    struct device *dev;
    size_t allocation;
    size_t boundary;
    char name[32];
    struct list_head pools;
};

.......................................................................

1)在利用DMA池进行缓冲区分配之前,首先需要创建一个DMA池,这是通过函数
dma_pool_create来完成的;该函数的核心工作就是分配一个struct dma_pool对象,并初始化。

2)当有了一个已经被初始化过的DMA池对象,如果要在该对象中分配一个一致性映射的
DMA缓冲区块,应该使用dma_pool_alloc函数。

3)与dma_pool_alloc相对应,如果从哪一个DMA池中释放某一个DMA缓冲区,则应该调用
dma_pool_free函数。

4)如果一个DMA池不再使用,应该调用函数dma_pool_destroy销毁之。

dma_set_mask_and_coherent / dma_set_mask

该函数用来查询设备的DMA寻址范围,如果设备对象dev的DMA操作支持参数mask指定的范围,则函数返回0,否则返回一负的错误代码(设备对象dev在其成员dma_mask中来标识其DMA寻址范围)。

回弹缓冲区bounce buffer

如果CPU侧虚拟地址对应的物理地址不适合设备的DMA操作,那么需要建立所谓的回弹缓冲区,它相当于一个中转站的作用,在把数据往设备方向传输时,驱动程序需要把CPU给的数据拷贝到回弹缓冲区,然后启动DMA操作,反之亦然。所以回弹缓冲区必须是可以直接与设备进行DMA传输的,当传输结束时,再通过CPU的介入把回弹缓冲区中的数据搬移到最终目标,所以除非外部传入的地址不能进行DMA传输,否则不应当使用。

回弹缓冲区驻留在可作为DMA缓冲区的内存区域里,在DMA请求的源或目的没有DMA功能的内存区域时,它可以作为临时内存区域存放数据。例如,用DMA方式从32位PCI外围设备向地址高于4GB的目标传送数据时,如果没有IOMMU单元,就需要回弹缓冲区了。数据先暂时存放在回弹缓冲区,再复制到目标地址。

更多推荐

Linux驱动中的DMA