为什么会有DMA(直接内存访问)?我们知道通常情况下,内存数据跟外设之间的通信是通过cpu来传递的。cpu运行io指令将数据从内存拷贝到外设的io端口,或者从外设的io端口拷贝到内存。由于外设的访问速度跟内存的访问速度相比非常慢,因此这种访问操作会严重浪费cpu的指令周期,DMA的出现就是为了解决这个问题。支持DMA机制的系统通常会包含一个DMA控制器,下挂支持DMA的外设,外设和内存之间的数据交换通过DMA控制器完成。
那么,如何使用DMA?不同平台提供不同的DMA控制器,其控制器驱动也不尽相同,因此DMA控制器驱动不在夲篇分析。支持DMA的设备一般都会提供至少两个io端口(用于设置DMA地址和大小)和中断请求线,io端口用于指定缓存的物理地址(无iommu)或者总线地址(有iommu)和大小,中断请求线用于当设备完成dma操作后,通知cpu开始下一步操作。DMA控制器的通道请求,源地址和目的地址的设置等操作一般由外设完成,无需用户介入。一个DMA的读操作如下:
(1)cpu分配一块连续的物理内存缓冲区。
(2)cpu将分配好的内存缓冲区的物理地址或者总线地址和大小写入外设的DMA地址和大小io端口。
(3)cpu设置外设的控制端口,启动外设进行DMA操作。然后,cpu清除读取完成标志,并将当前读取进程挂起,调度其他任务运行。
(4)当外设完成了DMA操作之后,通过中断请求线,通知cpu操作完成。
(5)cpu进入中断处理函数,设置读取完成标志,唤醒挂起在等待队列上的进程。
(6)读取进程被唤醒后,检查读取完成标志。如果完成,对缓冲区数据进行下一步处理,否则,继续挂起等待。
DMA的写操作类似这个过程。仔细分析可以发现,用户只需要提供缓存的物理地址或总线地址和大小,注册中断处理函数,并启动设备进行DMA操作即可。那么缓存的物理地址或总线地址是如何得到的呢?DMA缓存的分配是内核DMA编程的核心,下面我们主要分析两种DMA缓存的分配方法:DMA一致性缓存和DMA映射缓存
(1)DMA一致性缓存分配接口 dma_alloc_coherent():
我们知道,cpu访问内存使用的是虚拟地址,而外设访问内存使用的是io地址。当外设所在的总线不支持iommu时,io地址就是内存的物理地址,如果外设所在的总线支持iommu,那么io地址经过iommu映射之后才是内存的物理地址。因此,一个DMA缓存对应两种地址,cpu虚拟地址和外设io地址,而这些地址在不同平台配置也不同。以x86为例,x86没有iommu,其io地址其实就是内存的物理地址。而x86外设总线宽度为24位,这又限制了DMA内存的分配只能在低16M地址空间进行。dma_alloc_coherent() 一次性分配DMA缓存(DMA ZONE的连续物理内存),并返回缓存的cpu虚拟地址和外设的io地址。适用于在驱动模块内部分配内存,通常在驱动模块加载或驱动open操作时分配,并在整个驱动生命周期内生效。这种方式的缺点是,持有dma缓存时间太长,在某些系统上(如x86)dma资源紧缺,浪费资源。
static inline void *dma_alloc_coherent(struct device *dev, size_t size,
dma_addr_t *dma_handle, gfp_t flag)
{
return dma_alloc_attrs(dev, size, dma_handle, flag, 0);
}
static inline void *dma_alloc_attrs(struct device *dev, size_t size,
dma_addr_t *dma_handle, gfp_t flag,
unsigned long attrs)
{
const struct dma_map_ops *ops = get_dma_ops(dev); // 获取dma分配接口
void *cpu_addr;
BUG_ON(!ops);
WARN_ON_ONCE(dev && !dev->coherent_dma_mask);
if (dma_alloc_from_dev_coherent(dev, size, dma_handle, &cpu_addr))
return cpu_addr;
/* let the implementation decide on the zone to allocate from: */
flag &= ~(__GFP_DMA | __GFP_DMA32 | __GFP_HIGHMEM);
if (!arch_dma_alloc_attrs(&dev))
return NULL;
if (!ops->alloc)
return NULL;
cpu_addr = ops->alloc(dev, size, dma_handle, flag, attrs); // 调用分配接口
debug_dma_alloc_coherent(dev, size, *dma_handle, cpu_addr);
return cpu_addr;
}
// 下面以通用的分配接口为例分析
const struct dma_map_ops dma_direct_ops = {
.alloc = dma_direct_alloc,
.free = dma_direct_free,
.map_page = dma_direct_map_page,
.map_sg = dma_direct_map_sg,
.dma_supported = dma_direct_supported,
.mapping_error = dma_direct_mapping_error,
};
void *dma_direct_alloc(struct device *dev, size_t size, dma_addr_t *dma_handle,
gfp_t gfp, unsigned long attrs) // 接口返回值是cpu虚拟地址,dma_handle是io地址
{
unsigned int count = PAGE_ALIGN(size) >> PAGE_SHIFT; // 获取缓存占用的page数
int page_order = get_order(size);
struct page *page = NULL;
void *ret;
/* we always manually zero the memory once we are done: */
gfp &= ~__GFP_ZERO;
/* GFP_DMA32 and GFP_DMA are no ops without the corresponding zones: */
if (dev->coherent_dma_mask <= DMA_BIT_MASK(ARCH_ZONE_DMA_BITS))
gfp |= GFP_DMA; // 可以在 DMA zone内分配
if (dev->coherent_dma_mask <= DMA_BIT_MASK(32) && !(gfp & GFP_DMA))
gfp |= GFP_DMA32; // 可以在 DMA32 zone内分配
again:
/* CMA can be used only in the context which permits sleeping */
if (gfpflags_allow_blocking(gfp)) {
page = dma_alloc_from_contiguous(dev, count, page_order,
gfp & __GFP_NOWARN); // 优先从contiguous分配page
if (page && !dma_coherent_ok(dev, page_to_phys(page), size)) {
dma_release_from_contiguous(dev, page, count);
page = NULL;
}
}
if (!page)
page = alloc_pages_node(dev_to_node(dev), gfp, page_order); // 其次从伙伴系统分配pages
if (page && !dma_coherent_ok(dev, page_to_phys(page), size)) {
__free_pages(page, page_order);
page = NULL;
if (IS_ENABLED(CONFIG_ZONE_DMA32) &&
dev->coherent_dma_mask < DMA_BIT_MASK(64) &&
!(gfp & (GFP_DMA32 | GFP_DMA))) {
gfp |= GFP_DMA32;
goto again;
}
if (IS_ENABLED(CONFIG_ZONE_DMA) &&
dev->coherent_dma_mask < DMA_BIT_MASK(32) &&
!(gfp & GFP_DMA)) {
gfp = (gfp & ~GFP_DMA32) | GFP_DMA;
goto again;
}
}
if (!page)
return NULL;
ret = page_address(page); // page转cpu虚拟地址
if (force_dma_unencrypted()) {
set_memory_decrypted((unsigned long)ret, 1 << page_order);
*dma_handle = __phys_to_dma(dev, page_to_phys(page)); // page转io地址
} else {
*dma_handle = phys_to_dma(dev, page_to_phys(page)); // page转io地址
}
memset(ret, 0, size);
return ret;
}
简而言之,上述接口本质上是从DMA/DMA32内存区分配连续物理内存,返回内存的cpu虚拟地址和映射的io地址。
(2)DMA映射缓存 dma_map_single():
DMA一致性缓存不仅有占用dma资源周期长的缺点,还有一种场景无法满足:如果缓存已经被其他内核模块或用户进程分配完成,使用DMA一致性缓存就必须进行一次cpu内存拷贝(因为一致性缓存是重新分配内存)。那么有没有一种接口可以避免DMA一致性缓存的这些缺点呢?DMA映射缓存可以满足。dma_map_single()不去重新分配内存,而是将已经存在的缓存的cpu虚拟地址直接转换成io地址,当完成DMA操作之后可以立即使用 dma_unmap_single()。不仅可以映射已经存在的内存,而且可以只在需要DMA传输的时候才占用DMA资源,节省DMA资源。
#define dma_map_single(d, a, s, r) dma_map_single_attrs(d, a, s, r, 0)
static inline dma_addr_t dma_map_single_attrs(struct device *dev, void *ptr,
size_t size,
enum dma_data_direction dir,
unsigned long attrs)
{
const struct dma_map_ops *ops = get_dma_ops(dev); // 获取dma分配接口
dma_addr_t addr;
BUG_ON(!valid_dma_direction(dir));
addr = ops->map_page(dev, virt_to_page(ptr),
offset_in_page(ptr), size,
dir, attrs); // 调用分配接口的map_page
debug_dma_map_page(dev, virt_to_page(ptr),
offset_in_page(ptr), size,
dir, addr, true);
return addr;
}
dma_addr_t dma_direct_map_page(struct device *dev, struct page *page,
unsigned long offset, size_t size, enum dma_data_direction dir,
unsigned long attrs)
{
dma_addr_t dma_addr = phys_to_dma(dev, page_to_phys(page)) + offset; // page转io地址
if (!check_addr(dev, dma_addr, size, __func__))
return DIRECT_MAPPING_ERROR;
return dma_addr; // 返回io地址
}
(3)scatter/gather DMA映射传输dma_map_sg():
无论是dma_alloc_coherent()还是dma_map_single(),都要求内存是物理连续的。当一次需要传输多个内存缓冲区的时候,上述两个接口都无法满足要求。dma_map_sg()可以满足一次传递多个内存缓冲区,但是要求每个缓冲区内部是物理连续的。dma_map_sg()以dma_direct_map_sg()为例:
int dma_direct_map_sg(struct device *dev, struct scatterlist *sgl, int nents,
enum dma_data_direction dir, unsigned long attrs)
{
int i;
struct scatterlist *sg;
for_each_sg(sgl, sg, nents, i) { // 轮寻sgl的每个sg
BUG_ON(!sg_page(sg));
sg_dma_address(sg) = phys_to_dma(dev, sg_phys(sg)); // 映射dma地址
if (!check_addr(dev, sg_dma_address(sg), sg->length, __func__))
return 0;
sg_dma_len(sg) = sg->length; // 和长度
}
return nents;
}
至此,我们已经基本介绍完了DMA的内核大体框架。当然DMA控制器驱动没有介绍,以及反弹缓冲区也没有介绍。这些细节和拓展后续再去分析和总结。
更多推荐
java dma_Kernel DMA
发布评论