11.6 DMA(直接存储器访问)
DMA是一种无须CPU参与就可以让外设和系统内存之间进行双向数据传输的硬件机制。使用DMA可以使系统CPU从实际的I/O数据传输过程中摆脱出来,从而大大提高系统的吞吐率(bit/s)。DMA通常与硬件体系结构,特别是外设的总线技术密切相关。
DMA方式的数据传输由DMA控制器(DMAC)控制,在传输期间,CPU可以并发地执行其他任务。当DMA结束后,DMAC通过中断通知CPU数据传输已经结束,然后由CPU执行相应的中断服务程序进行后处理。
11.6.1 DMA与Cache一致性
Cache被用作CPU针对内存的缓存,利用程序的空间局部性和时间局部性原理,达到较高的命中率,从而避免CPU每次都必须要与相对慢速的内存交互数据来提高数据的访问速率。DMA可以作为内存与外设之间传输数据的方式,在这种传输方式下,数据并不需要经过CPU中转。
假设DMA针对内存的目的地址与Cache缓存的对象没有重叠区域(如图11.12所示),DMA和Cache之间将相安无事。如果DMA的目的地址与Cache所缓存的内存地址访问有重叠(如图11.13所示),经过DMA操作,与Cache缓存对应的内存中的数据已经被修改,而CPU本身并不知道,它仍认为Cache中的数据就是内存中的数据,在以后访问Cache映射的内存时,它仍使用陈旧的Cache数据。就会发生Cache与内存之间数据“不一致性”的错误。
图11.12 DMA目的地址与Cache对象没有重叠
图11.13 DMA目的地址与Cache对象有重叠
Cache数据与内存数据的不一致性,指在采用Cache的系统中,同样一个数据可能既存在于Cache中,也存在于主存中,Cache与主存中的数据一样则具有一致性,数据若不一样则具有不一致性。
备注: 在发生Cache与内存不一致性错误后,驱动将无法正常运行。Cache的不一致性问题并不是只发生在DMA的情况下,还存在于Cache使能和关闭的时刻。例如,对于带MMU功能的ARM处理器,在开启MMU之前,需先置Cache无效,对于TLB(转换旁路缓存),也是如此。
11.6.2 Linux下的DMA编程
DMA本身不属于一种等同于字符设备、块设备和网络设备的外设,DMA只是一种外设与内存交互数据的式。
内存中用于与外设交互数据的一块区域称为DMA缓冲区,在设备不支持scatter/gather(分散/聚集)操作的情况下,DMA缓冲区在物理上必须是连续的。
1.DMA区域
对于x86系统的ISA设备来说,DMA操作只能在16MB以下的内存中进行,在使用kmalloc()、__get_free_pages()及其类似函数申请DMA缓冲区时应使用GFP_DMA标志,这样能保证获得的内存位DMA区域中,并具备DMA能力。
在内核中定义__get_free_pages()针对DMA的“快捷方式”__get_dma_pages(),它在申请标志中添加了GFP_DMA,如下所示:
<linux/gfp.h>
#define _ _get_dma_pages(gfp_mask, order) \__get_free_pages((gfp_mask) | GFP_DMA,(order))
如果不想使用order为参数申请DMA内存,可以使用函数dma_mem_alloc(),其源代码如代码清单11.13所示。
arch/x86/include/asm/floppy.h
static unsigned long dma_mem_alloc(unsigned long size)
{
int order = get_order(size); /* 大小 -> 指数 */
return __get_dma_pages(GFP_KERNEL|__GFP_NORETRY, order);}
对于大多数现代嵌入式处理器来说,DMA操作可以在整个常规内存区域进行,因此DMA区域就直接覆盖了常规内存。
2.虚拟地址、物理地址和总线地址
基于DMA的硬件使用的是总线地址,总线地址是从设备角度看到的内存地址,物理地址是从CPU、 MMU控制器外围角度看到的内存地址,虚拟地址是从CPU核角度看到的内存地址。虽然在PC上,对于ISA和PCI(外围设备互联总线)来说,总线地址即为物理地址,但并不是每个平台都是如此。因为有时接口总线通过桥接电路连接,桥接电路会将I/O地址映射为不同的物理地址。还有一些系统提供了页面映射机制,它能将任意的页面映射为连续的外设总线地址。
内核提供如下函数以进行简单的虚拟地址/总线地址转换:
unsigned long virt_to_bus(volatile void *address);void *bus_to_virt(unsigned long address);
在使用IOMMU或反弹缓冲区的情况下,上述函数一般不会正常工作。这两个函数并不建议使用。如图11.14所示,IOMMU的工作原理与CPU内的MMU非常类似,不过它针对的是外设总线地址和内存地址之间的转化。由于IOMMU可以使得外设DMA引擎看到“虚拟地址”,因此在使用IOMMU的情况下,在修改映射寄存器后,可以使得SG(分散/聚集)中分段的缓冲区地址对外设变得连续。
图11.14 MMU与IOMMU
3.DMA地址掩码
设备并不一定能在所有的内存地址上执行DMA操作,这种情况下应该通过下列函数执行DMA地址掩码:
arch/arm64/include/asm/dma-mapping.h
int dma_set_mask(struct device *dev, u64 mask);
例如,对于只能在24位地址上执行DMA操作的设备,就应该调用dma_set_mask(dev,0xffffff)。该API本质上就是修改device结构体中的dma_mask成员,如ARM64平台的定义为:
static inline int dma_set_mask(struct device *dev, u64 mask)
{
if (!dev->dma_mask || !dma_supported(dev, mask))
return -EIO;
*dev->dma_mask = mask;
return 0;
}
#include <linux/device.h>
struct device {
struct device *parent;
struct device_private *p;
struct kobject kobj;
const char *init_name; /* initial name of the device */
const struct device_type *type;
struct mutex mutex; /* mutex to synchronize calls to
* its driver.
*/
struct bus_type *bus; /* type of bus device is on */
struct device_driver *driver; /* which driver has allocated this
device */
void *platform_data; /* Platform specific data, device
core doesn't touch it */
void *driver_data; /* Driver data, set and get with
dev_set/get_drvdata */
struct dev_pm_info power;
struct dev_pm_domain *pm_domain;
#ifdef CONFIG_GENERIC_MSI_IRQ_DOMAIN
struct irq_domain *msi_domain;
#endif
#ifdef CONFIG_PINCTRL
struct dev_pin_info *pins;
#endif
#ifdef CONFIG_NUMA
int numa_node; /* NUMA node this device is close to */
#endif
u64 *dma_mask; /* dma mask (if dma'able device) */
u64 coherent_dma_mask;/* Like dma_mask, but for
alloc_coherent mappings as
not all hardware supports
64 bit addresses for consistent
allocations such descriptors. */
unsigned long dma_pfn_offset;
struct device_dma_parameters *dma_parms;
struct list_head dma_pools; /* dma pools (if dma'ble) */
struct dma_coherent_mem *dma_mem; /* internal for coherent mem
override */
#ifdef CONFIG_DMA_CMA
struct cma *cma_area; /* contiguous memory area for dma
allocations */
#endif
struct removed_region *removed_mem;
/* arch specific additions */
struct dev_archdata archdata;
struct device_node *of_node; /* associated device tree node */
struct acpi_dev_node acpi_node; /* associated ACPI device node */
dev_t devt; /* dev_t, creates the sysfs "dev" */
u32 id; /* device instance */
spinlock_t devres_lock;
struct list_head devres_head;
struct klist_node knode_class;
struct class *class;
const struct attribute_group **groups; /* optional groups */
void (*release)(struct device *dev);
struct iommu_group *iommu_group;
bool offline_disabled:1;
bool offline:1;
};
在device结构体中,dma_mask是设备DMA可以寻址的范围,coherent_dma_mask作用于申请一致性的DMA缓冲区。
4.一致性DMA缓冲区
DMA映射包括两个方面的工作:分配一片DMA缓冲区;为这片缓冲区产生设备可访问的地址。同时,DMA映射也必须考虑Cache一致性问题。内核中提供如下函数以分配一个DMA一致性的内存区域:
void * dma_alloc_coherent(struct device *dev, size_t size, dma_addr_t *handle,gfp_t gfp);
该函数的返回值为申请到的DMA缓冲区的虚拟地址,此外,该函数还通过参数handle返回DMA缓冲区的总线地址。handle的类型为dma_addr_t,代表的是总线地址。
dma_alloc_coherent()申请一片DMA缓冲区,以进行地址映射并保证该缓冲区的Cache一致性。与
dma_alloc_coherent()对应的释放函数为:
void dma_free_coherent(struct device *dev, size_t size, void *cpu_addr,dma_addr_t handle);
用于分配一个写合并(Writecombining)的DMA缓冲区的函数:
void * dma_alloc_writecombine(struct device *dev, size_t size, dma_addr_t *handle, gfp_t gfp);
与dma_alloc_writecombine()对应的释放函数dma_free_writecombine()实际上就是
dma_free_coherent(),它定义为:
#define dma_free_writecombine(dev,size,cpu_addr,handle) \
dma_free_coherent(dev,size,cpu_addr,handle)
Linux内核还提供了PCI设备申请DMA缓冲区的函数pci_alloc_consistent(),
include/asm-generic/pci-dma-compat.h
其原型为:
void * pci_alloc_consistent(struct pci_dev *pdev, size_t size, dma_addr_t *dma_addrp);
对应的释放函数为pci_free_consistent(),其原型为:
void pci_free_consistent(struct pci_dev *hwdev, size_t size, void *vaddr, dma_addr_t dma_handle);
备注:
dma_alloc_xxx()函数虽然是以dma_alloc_开头的,但是其申请的区域不一定在DMA区域里面。
在使用ARM等嵌入式Linux系统的时候,GPU、Camera、HDMI等都需要预留大量连续内存,这部分内存平时不用,但是一般的做法又必须先预留着。
更多推荐
第11章 内存与IO访问之DMA
发布评论