围城
分类: LINUX
2016-08-22 16:21:41
根据LDD3说法,DMA是一种硬件机制,是说硬件具有这种仲裁能力,在cpu不干预的情况下设备可以作为主设备来对内存的直接读写访问,这样可以大大提高大数据流的传输速度。我所调试的PCI网卡和视频采集卡就是支持DMA机制的设备。
在讲解内核提供的通用DMA层之前,LDD3介绍了底层直接来分配DMA缓冲的方法,当然这种方法在编写驱动的过程中是不提倡的,原因在LDD3的15.4.4通用DMA层有解释,主要是缓存一致性和可移植性的问题,但是学习底层直接分配DMA的方法对于我们理解DMA的工作原理还是有好处的,在通用DMA层之前有2个知识点需要记录:
1
LDD3说道“随 DMA 缓冲带来的主要问题是, 当它们大于一页, 它们必须占据物理内存的连续页因为设备使用 ISA 或者 PCI 系统总线传输数据, 它们都使用物理地址”。
这段话说明分配DMA缓冲区时必须是物理上连续的一段空间,原因我的理解是因为DMA是设备的一种机制,真正使用DMA的是设备,也就是说cpu分配好缓冲区给设备,设备来进行DMA操作,完成对缓冲区的访问,这个过程对cpu是不透明的,设备对缓冲区的寻址是物理地址,如果这个设备是挂载在PCI总线上,则设备对DMA缓冲区的寻址就是pci总线地址。这里还有2点需要说明,一是对于普通的具有DMA的设备,他们只能对物理上连续的地址进行访问,所以一般设备的DMA缓冲区必须物理连续,还有一类设备具有sgDMA的能力,这类设备的DMA缓冲区可以在物理上分散的。
2
在15.4.3一节中说明了总线地址,说明如下:
一个使用 DMA 的设备驱动必须和连接到接口总线的硬件通讯, 总线使用物理地址, 而程序代码使用虚拟地址.事实上, 情况比这个稍微有些复杂. 基于 DMA 的硬件使用总线地址, 而不是物理地址. 尽管 ISA 和 PCI 总线地址在 PC 上完全是物理地址, 这对每个平台却不总是真的. 有时接口总线被通过桥接电路连接, 它映射 I/O 地址到不同的物理地址. 一些系统甚至有一个页映射机制, 使任意的页连续出现在外设总线.
这一段话我刚开始看不是很理解,现在结合调试PCI总线设备的DMA,才算是想通,这段话主要涉及到3个地址:虚拟地址 物理地址 总线地址,只要把这3个地址区分开,这段话就可以理解了。
理解物理地址和虚拟地址还是比较简单的,虚拟地址是通过了操作系统内存管理单元映射之后的地址。物理地址是站在cpu角度看到的外设资源的地址。起初我把物理地址和总线地址混淆在一起,感觉物理地址就是总线地址,但是经过调试PCI网卡和视频卡的DMA才理解了这个问题,说清楚这个问题,首先要说清楚pci控制器的地址映射。
对于cpu来说,不管是设备直接挂接在cpu本地总线还是通过pci总线挂接,cpu去访问这个设备都是通过物理地址,这个物理地址就是本地总线的地址。pci总线上的设备需要将自己的资源映射到本地总线,这样cpu直接去访问这个地址就可以访问到pci设备的资源。pci控制器就实现了这个地址的转换,pci控制器有mem地址窗口,对于pci设备,配置空间的BAR0~5就说明了设备资源的偏移和大小,pci设备资源基于pci地址窗口的偏移就是cpu访问设备资源的本地总线地址了。
对于pci设备来说,设备做DMA时需要去访问内存,这段内存就需要映射到PCI总线上,这个映射也是由PCI控制器来完成的,这时包括PCI控制器在内的CPU资源就是从设备,而做DMA的设备是主设备,对于从设备来说,它资源在总线上的映射是由配置空间的BAR0~5决定的,就是说pci控制器也会有自己的配置空间,pci控制器配置空间的BAR0~5决定了CPU资源在pci总线上的映射地址,举个简单的例子,决定DDR映射的BAR4 5的值是0x80000000,那么物理地址0x10000000的内存单元在pci总线设备看来就是0x90000000.
一句话,物理地址和总线地址需要经由PCI控制器的转换 ,PCI控制器也就是LDD3上所说的桥接电路!
如果不是挂接在外部总线上,设备做DMA时使用的物理地址和总线地址是一致的,因为这是本地总线。
这个问题对于内核驱动在分配DMA时非常的关键,这段时间调试PCI网卡以及视频卡就遇到了这个问题,因为没有设置好PCI控制器的配置空间BAR0~5,分配的DMA总线地址没有做相应的转换。
通用DMA层
通用DMA层介绍了DMA映射操作的函数,DMA映射分为3种,一致性映射 DMA池 流式映射
首先我遇到的第一个需要想明白的地方,什么是DMA映射?
LDD3的解释是 一个 DMA 映射是分配一个 DMA 缓冲和产生一个设备可以存取的地址的结合。光看这句话不是很能理解,但是学习了后面的几种映射之后就大体明白了,其实DMA映射主要工作就是给做DMA的设备准备它可以正常访问的总线地址,如果是一致性映射,需要内核来分配一段内存,然后给出这段内存设备访问的总线地址,如果是流式映射,因为是给定内存和大小,所以只需要给出这段内存设备访问的总线地址就可以。
遇到的第二个问题就是做DMA映射必须考虑的问题,缓存一致性的问题,根本的原因还是因为设备做DMA对于CPU来说是不可见的,从2个数据流向来说,DMA FROM DEVICE,设备做DMA将数据写入内存中,这时CPU必须将这段内存地址对应的缓存无效掉,不然cpu从缓存中取出的数据跟内存中的数据不一致,DMA TO DEVICE,设备从内存中取数据,这时CPU必须将这段内存对应的缓存写回,不然内存中的数据太旧,没有意义。
特别是对于软件管理缓存的处理器(MIPS)这一点非常重要。
通用DMA层有一个变量类型dma_addr_t来表示DMA的总线地址。
1 一致性映射
一致性映射必须要建立在一致性缓存中,对于mips处理器一致性映射建立在无缓存的区域,也就是0xa0000000之上了。使用缓存一致区域开销比较大。一致性映射CPU和设备可以同时访问,因为这样的操作不存在数据的差异,所见即所得。
dma分配函数:
static inline void *pci_alloc_consistent(struct pci_dev *hwdev, size_t size,dma_addr_t *dma_handle);
void *dma_alloc_coherent(struct device *dev, size_t size,dma_addr_t * dma_handle, gfp_t gfp);
pci总线设备可以调用pci_alloc_consistent来分配DMA,其实就是封装了dma_alloc_coherent。下面就来分析一下dma_alloc_coherent函数。
void *dma_alloc_coherent(struct device *dev, size_t size,
dma_addr_t * dma_handle, gfp_t gfp)
{
void *ret;
if (dma_alloc_from_coherent(dev, size, dma_handle, &ret))
return ret;
gfp = massage_gfp_flags(dev, gfp);
ret = (void *) __get_free_pages(gfp, get_order(size));
if (ret) {
memset(ret, 0, size);
*dma_handle = plat_map_dma_mem(dev, ret, size);
if (!plat_device_is_coherent(dev)) {
dma_cache_wback_inv((unsigned long) ret, size);
ret = UNCAC_ADDR(ret);
}
}
return ret;
}
主要工作是获取一段连续的物理页,然后调用plat_map_dma_mem来获取DMA的总线地址,函数返回值就是这段DMA内存的虚拟地址,由驱动使用。
调用dma_alloc_coherent可以获取一段指定大小的DMA内存,但是地址不能指定,因为对于驱动来说它不需要关心缓存一致的区域在哪。
不使用调用void dma_free_coherent(struct device *dev, size_t size,void *vaddr, dma_addr_t dma_handle);
2 DMA池
一致性映射分配的是连续的物理页,最小size也是一页,如果要分配小于一页的一致性DMA需要使用DMA池。
dma池的使用我在代码中没有看到过,这里不详细说明,具体看LDD3.
3 流式映射
流失映射接口比一致性映射复杂,因为需要接受指定的内核缓冲区来建立DMA映射,处理它没得选择的内存地址。
流式映射在LDD3上给出了2条必须要遵守的规则:
缓冲必须用在只匹配它被映射时给定的方向的传输.
一旦一个缓冲已被映射, 它属于这个设备, 不是处理器. 直到这个缓冲 已被去映射, 驱动不应当以任何方式触动它的内容. 只在调用
dma_unmap_single 后驱动才可安全存取缓冲的内容(有一个例外, 我们马上见到). 其他的事情, 这个规则隐含一个在被写入设备的缓冲不能被
映射, 直到它包含所有的要写的数据.这个缓冲必须不被映射, 当 DMA 仍然激活, 否则肯定会有严重的系统不稳定.
这2点规则LDD3上的解释也让人不是很理解,我的理解还是缓存一致的原因。因为CPU和设备操作之间是不透明的,而流式映射并不一定建立在缓存一致的区域,所以必须要强制规定操作顺序来保证缓存和内存中数据的一致!
设备和CPU同一时间只能有一个拥有DMA缓冲区,也就是只能有一个对DMA缓冲区有操作权。
void dma_sync_single_for_cpu(struct device *dev, dma_handle_t bus_addr,size_t size, enum dma_data_direction direction);
void dma_sync_single_for_device(struct device *dev, dma_handle_t bus_addr, size_t size, enum dma_data_direction direction);
这2个函数来决定DMA缓冲区的使用权,查看这2个函数的源码,主要的工作就是调用函数__dma_sync来对相应的缓冲区进行写回和无效操作来实现缓存的一致性。
这样当设备做完DMA后调用dma_sync_single_for_cpu来将缓存更新,这样内存和缓存一致,CPU操作数据才能正确。对于设备也是如此。
流式映射有3种接口, 单缓冲区映射 单页映射 分离聚合映射
单缓冲映射
dma_addr_t dma_map_single(struct device *dev, void *ptr, size_t size,enum dma_data_direction direction);
void dma_unmap_single(struct device *dev, dma_addr_t dma_addr, size_t size,enum dma_data_direction direction);
这种映射针对于驱动指定的小缓冲区,CPU可以找到一段连续的物理页来实现DMA映射。
单页映射
dma_addr_t dma_map_page(struct device *dev, struct page *page,unsigned long offset, size_t size,enum dma_data_direction direction);
void dma_unmap_page(struct device *dev, dma_addr_t dma_address,size_t size, enum dma_data_direction direction);
这种映射实现了已知单物理页缓冲区来建立DMA映射
sg映射
实现sg映射必须做DMA的设备有sg映射的机制,就是说设备可以接受一个分散表数组,这个数组成员描述了每一个缓冲区的总线地址 长度等信息,设备可以实现在一个DMA操作中完成对这些缓冲区的传送。
比如最近调试的视频采集卡就是使用的sgDMA,因为对于视频来说每一帧数据的容量都会比较大,想通过分配一个连续的缓冲区来实现不太可能,因为内存中充满了碎片,get_free_pages理论上最多了一分配2^11个页,也就是8M内存,但是实际上可能连128K都分配失败。所以每一帧数据都需要很多个dma缓冲区,一般情况下sgDMA的每个缓冲区大小为一页。根据视频采集卡的代码,驱动只需要建立分散表数组scaterrlist,然后再建立一个页表缓冲区,页表缓冲区上每一个单元的值都是每一个数据缓冲区的地址,将这个页表以及分散表传给设备,设备就会自动完成DMA传输。
操作函数:
int dma_map_sg(struct device *dev, struct scatterlist *sg, int nents,enum dma_data_direction direction);
void dma_unmap_sg(struct device *dev, struct scatterlist *list, int nents,enum dma_data_direction direction);
具体的函数说明LDD3上有,这里不说了,这里说明一下scatterlist。对于misp处理器,定义如下:
struct scatterlist {
#ifdef CONFIG_DEBUG_SG
unsigned long sg_magic;
#endif
unsigned long page_link;
unsigned int offset;
dma_addr_t dma_address;
unsigned int length;
};
这个结构体就表示了sgDMA中的一个缓冲区。对于sgDMA的驱动会分配一个scatterlist数组,然后驱动会填充这个结构体中的每个成员。
对于DMA映射的通用层函数,在其实现中都可以看到dma_addr_t类型的变量也就是dma总线地址是由plat_map_dma_mem来获取的
static inline dma_addr_t plat_map_dma_mem(struct device *dev, void *addr,size_t size)
{
return virt_to_phys(addr);
}
mips处理器的实现如上,可以看出返回值就是物理地址,从前面对于总线地址 物理地址的分析,如果这个设备不是本地总线上的设备,而是PCI设备,这个返回地址必须要加上一个pci控制器配置空间BAR寄存器的地址窗口才可以!