分散/聚集映射通过将虚拟地址上分散的DMA缓冲区通过一个类型为struct scatterlist的数组或者链表组织起来,然后通过一次的DMA传输操作在主存RAM与设备之间传输数据,如图所示:
图中显示了主存中三个分散的物理页面与设备之间进行的一次DMA传输时分散/聚集映射示意,其中单个物理页面与设备之间可以看做是一个单一的流式映射,每个这样的单一映射在内核中有数据结构struct scatterlist来表示:
<include/asm-generic/scatterlist.h>
struct scatterlist {
unsigned long page_link;
unsigned int offset;
unsigned int length;
dma_addr_t dma_address;
#ifdef CONFIG_NEED_SG_DMA_LENGTH
unsigned int dma_length;
#endif
};
如果从CPU的角度看这种分散/聚集映射,它对应的需求是有三块数据(分别存放在三段分散的虚拟地址空间中)需要和设备进行交互(发送或者接收),通过建立struct scatterlist类型的数组/链表在一次DMA传输中完成所有的数据传递。在struct scatterlist结构体中,page_link指明了虚拟地址所对应的物理页面struct page对象的地址(因为地址的最低两位总是0,所以内核在page_link的后两位中安插了一些其他信息。例如:如果最低两位是01,表示当前对象的page_link将会是指向下一个scatterlist数组的首地址,此时形成scatterlist的链式结构;如果最低两位是10,表明当前对象是scatterlist数组中的最后一个),比如下面的代码:
//sg是struct scatterlist类型指针
struct page*spage=(struct page*)(sg->page_link&~0x03);
offset是数据在DMA缓冲区中的偏移地址,length是要传输的数据块的大小,dma_address则是设备DMA操作要使用的DMA地址(物理地址)。内核中的DMA层为分散/聚集映射所提供的接口函数为dma_map_sg,其原型为:
int dma_map_sg(struct device *dev, struct scatterlist *sg,
int nents, enum dma_data_direction dir);
参数dev是设备对象的指针,sg是struct scatterlist类型数组的首地址,nents表示当前的分散/聚集映射中单一流式映射的个数,也是struct scatterlist数组/链表中的元素个数,dir用于指明DMA传输中数据流的方向。接下来分别以x86和ARM平台来讨论dma_map_sg建立分散/聚集映射的内核机制。首先是x86平台,该平台的dma_map_sg通过struct dma_map_ops对象来调用其map_sg方法,此处依然选择以nommu_dma_ops对象为例,其map_sg方法的函数实现为:
<arch/x86/kernel/pci-nommu.c>
static int nommu_map_sg(struct device *hwdev, struct scatterlist *sg,
int nents, enum dma_data_direction dir,
struct dma_attrs *attrs)
{
struct scatterlist *s;
int i;
WARN_ON(nents == 0 || sg[0].length == 0);
for_each_sg(sg, s, nents, i) {
BUG_ON(!sg_page(s));
s->dma_address = sg_phys(s);
if (!check_addr("map_sg", hwdev, s->dma_address, s->length))
return 0;
s->dma_length = s->length;
}
flush_write_buffers();
return nents;
}
函数的主体通过for_each_sg遍历sg数组/链表的所有元素,对于每个元素,都执行一个流式映射,对于x86而言,如果没有IOMMU的介入,设备的DMA操作时使用的DMA地址就是物理地址,因此只需通过sg_phys获得当前元素所对应的物理地址即可,其代码如下:
<include/linux/scatterlist.h>
static inline dma_addr_t sg_phys(struct scatterlist *sg)
{
return page_to_phys(sg_page(sg)) + sg->offset;
}
sg_page通过scatterlist对象sg的page_link成员取得所对应的物理页面的struct page对象地址:
static inline struct page *sg_page(struct scatterlist *sg)
{
…
return (struct page *)((sg)->page_link & ~0x3);
}
sg_phys再将返回的struct page指针通过page_to_phys获得页面的起始物理地址,加上实际数据块在页面中的偏移值sg->offset,就获得了本次DMA操作的DMA地址。ARM平台的dma_map_sg的实现为:
<arch/arm/mm/dma-mapping.c>
int dma_map_sg(struct device *dev, struct scatterlist *sg, int nents,
enum dma_data_direction dir)
{
…
for_each_sg(sg, s, nents, i) {
s->dma_address = dma_map_page(dev, sg_page(s), s->offset,
s->length, dir);
if (dma_mapping_error(dev, s->dma_address))
goto bad_mapping;
}
return nents;
bad_mapping:
for_each_sg(sg, s, i, j)
dma_unmap_page(dev, sg_dma_address(s), sg_dma_len(s), dir);
return 0;
}
函数通过dma_map_page来映射scatterlist上的page_link,跟x86平台不一样的是,ARM架构需要通过软件来保证cache一致性问题,所以做完这种虚拟物理地址的转换之后,ARM需要做的是使映射区对应的cache无效,以保证设备通过DMA将数据放到主存之后,CPU读到的不是cache中的数据,或者是保证CPU写到RAM中的数据立刻反映到RAM中,而不是暂时缓存到cache中,这样后续DMA在把主存中的数据传到设备中时,才能确保数据的有效性。通过上面的讨论可以看到,分散/聚集DMA映射本质上是通过一次DMA操作把主存中分散的数据块在主存与设备之间进行传输,对于其中的每个数据块内核都会建立对应的一个流式DMA映射。另外,分散/聚集DMA映射需要设备的支持,而不完全由内核或者驱动程序决定。
如果CPU侧虚拟地址对应的物理地址不适合设备的DMA操作,那么需要建立所谓的回弹缓冲区,它相当于一个中转站的作用,在把数据往设备方向传输时,驱动程序需要把CPU给的数据拷贝到回弹缓冲区,然后再启动DMA操作,反之亦然。所以回弹缓冲区必然是可以直接与设备进行DMA传输的,当传输结束时,再通过CPU的介入把回弹缓冲区中的数据搬移到最终的目标,所以除非外部传入的地址不能进行DMA传输,否则不应当使用它。下图展示了一个回弹缓冲区的使用:
前面在讨论一致性DMA映射时,知道这种DMA映射所建立的缓冲区大小是单个页面的整数倍,如果驱动程序需要更小的一致性映射的DMA缓冲区,可以使用内核提供的DMA池机制。DMA池机制非常类似于Linux内存管理中的slab机制,它的实现建立在一致性DMA映射所获得的连续物理页面的基础之上,通过DMA池的接口函数在物理页面之上分配所谓块大小的DMA缓冲区,为方便叙述,我们称这样的块为DMA缓冲块,以区别于一致性DMA映射中页面级大小的缓冲区。显然为了管理跟踪物理页面中DMA缓冲块的分配和余下空闲空间的多少,内核需要引入对应的管理数据结构,struct dma_pool就是内核用来完成该任务的数据结构,其定义如下:
<mm/dmapool.c>
struct dma_pool {
struct list_head page_list;
spinlock_t lock;
size_t size;
struct device *dev;
size_t allocation;
size_t boundary;
char name[32];
wait_queue_head_t waitq;
struct list_head pools;
};
一些成员的作用如下:struct list_head page_list用来将一致性DMA映射建立的页面组织成链表。size_t size该DMA池用来分配一致性DMA映射的缓冲区的大小,也称块大小。struct device *dev进行DMA操作的设备对象指针。char name[32]DMA池的名称,主要在调试或者诊断时使用。struct list_head pools用来将当前DMA池对象加到dev->dma_pools链表中。
在利用DMA池进行缓冲区分配之前,首先需要创建一个DMA池,这是通过函数dma_pool_create来完成的,该函数的核心实现为:
<mm/dmapool.c>
struct dma_pool *dma_pool_create(const char *name, struct device *dev,
size_t size, size_t align, size_t boundary)
{
struct dma_pool *retval;
size_t allocation;
…
allocation = max_t(size_t, size, PAGE_SIZE);
if (!boundary) {
boundary = allocation;
} else if ((boundary < size) || (boundary & (boundary - 1))) {
return NULL;
}
retval = kmalloc_node(sizeof(*retval), GFP_KERNEL, dev_to_node(dev));
if (!retval)
return retval;
strlcpy(retval->name, name, sizeof(retval->name));
retval->dev = dev;
INIT_LIST_HEAD(&retval->page_list);
spin_lock_init(&retval->lock);
retval->size = size;
retval->boundary = boundary;
retval->allocation = allocation;
init_waitqueue_head(&retval->waitq);
…
list_add(&retval->pools, &dev->dma_pools);
…
}
函数的核心工作就是分配一个struct dma_pool对象并初始化。不过有些细节还是值得探讨一下,先看函数的参数,name用于指定即将创建的DMA池的名称,size用于指定在该DMA池中分配缓冲块的大小,align用于指定当前DMA池分配操作所遵守的对齐方式。函数首先确定当前DMA池分配的对齐指标。接下来allocation用来保存参数size与PAGE_SIZE之间的最大值,所以如果size小于一个页面大小的话,allocation将等于PAGE_SIZE,在后续的DMA池的页面分配部分,该值用来决定需要分配的连续物理页的数量。如果函数调用时没有指定boundary(boundary为空),那么boundary就是allocation的大小。kmalloc_node函数用来分配一个struct dma_pool对象,紧接着就是对该对象进行初始化。以上是dma_pool_create函数总体框架,现在我们有了一个已经被初始化过的DMA池对象,如果要在该对象中分配一个一致性映射的DMA缓冲区块,应该使用dma_pool_alloc函数:
<mm/dmapool.c>
void *dma_pool_alloc(struct dma_pool *pool, gfp_t mem_flags,
dma_addr_t *handle)
{
unsigned long flags;
struct dma_page *page;
size_t offset;
void *retval;
spin_lock_irqsave(&pool->lock, flags);
restart:
list_for_each_entry(page, &pool->page_list, page_list) {
if (page->offset < pool->allocation)
goto ready;
}
page = pool_alloc_page(pool, GFP_ATOMIC);
if (!page) {
if (mem_flags & __GFP_WAIT) {
DECLARE_WAITQUEUE(wait, current);
__set_current_state(TASK_INTERRUPTIBLE);
__add_wait_queue(&pool->waitq, &wait);
spin_unlock_irqrestore(&pool->lock, flags);
schedule_timeout(POOL_TIMEOUT_JIFFIES);
spin_lock_irqsave(&pool->lock, flags);
__remove_wait_queue(&pool->waitq, &wait);
goto restart;
}
retval = NULL;
goto done;
}
ready:
page->in_use++;
offset = page->offset;
page->offset = *(int *)(page->vaddr + offset);
retval = offset + page->vaddr;
*handle = offset + page->dma;
done:
spin_unlock_irqrestore(&pool->lock, flags);
return retval;
}
这个函数的主线框架是,如果当前DMA池中有页面满足接下来的缓冲块分配需求,那么就在该页面上分配,否则通过调用pool_alloc_page来重新分配一段连续物理页。DMA池中每段这样的页面都用一个struct dma_page类型的对象来表示:
<mm/dmapool.c>
struct dma_page {
struct list_head page_list;
void *vaddr;
dma_addr_t dma;
unsigned int in_use;
unsigned int offset;
};
dma_pool_alloc函数返回DMA池中某一段物理页面中空闲块的虚拟地址,其对应的DMA地址由参数handle返回。如果调用dma_pool_alloc函数时在mem_flags中指定了__GFP_WAIT标志,那么在系统中暂时没有一段连续的物理页面满足分配需求时,函数会进入睡眠等待状态,等POOL_TIMEOUT_JIFFIES指定的时间到期,或者前面的分配请求可以被满足(比如有模块调用了dma_pool_free来释放当前DMA池中的某一DMA缓冲块),该函数才会从睡眠等待状态中醒来。与dma_pool_alloc相对应,如果要从一个DMA池中释放某一DMA缓冲块,则应该调用dma_pool_free函数,该函数原型为:
<include/linux/dmapool.h>
void dma_pool_free(struct dma_pool *pool, void *vaddr, dma_addr_t addr);
参数pool用于指明要释放的DMA缓冲块隶属的DMA池,vaddr是要释放的DMA缓冲块的虚拟地址,addr则是其DMA地址。如果一个DMA池不再使用,应该调用函数dma_pool_destroy销毁之,该函数的原型为:
<include/linux/dmapool.h>
void dma_pool_destroy(struct dma_pool *pool);
参数pool是指向要销毁的DMA池对象的指针。函数调用者需要保证调用该函数时DMA池中已经没有DMA缓冲块还在使用,而且一旦DMA池对象被销毁,后续将没有模块试图再去使用它。