在内核驱动程序的很多代码中,都能够看到类似sgdma的内容,sgdma全称为Scatter/Gather DMA(散列/收集 DMA),内核中抽象了scatterlist和sg table用来描述和管理这种需要做散列和收集的DMA缓冲区。在内核中设计scatterlist,主要出于两方面的原因,从硬件层面来说,目前很多DMA控制器都支持硬件的SG DMA模式,由开发人员在内存中构建一个链式的DMA缓冲区描述符(Buffer Descriptor, BD),DMA控制器根据这个链式BD进行一对多、多对一或者多对多的拷贝操作。例如Xilinx的AXI DMA IPCore,参考PG021文档中的介绍,该IP核的Block Diagram中就集成了Scatter/Gather逻辑电路
使能了SG模式后的DMA BD描述符,其内部有一个Next字段,用于指向下一个描述符的地址,控制器通过这个Next字段即可遍历整个BD链,而遍历过程完全是由硬件完成,软件只需要构造这个BD链
Gather是将多个离散的buffers拷贝到一个连续的buffer中
Scatter是将一个连续的buffer拷贝到多个离散的buffers中
从软件层面来说,对于存在虚拟内存管理的系统而言,由应用程序创建的大内存块在虚拟地址空间中是连续的,但是在物理内存的层面,大概率是离散的,而如果想要在一个单独的I/O操作中搬运这些由应用层创建的物理非连续buffer,比较笨重的办法是申请与其大小相当的连续物理页面,并将数据先拷贝到这些物理页面上,然后再进行DMA操作。但是这样做的缺陷也很明显,一方面是增加了数据拷贝的过程,性能较差,另一方面是增加了对连续物理页面资源的要求,在长时间使用过后碎片化严重的物理内存中,申请一大块连续物理内存会非常困难。使用scatterlist,结合DMA的SG功能,能够非常有效的节省对大块连续物理内存的要求,也避免了不必要的额外拷贝过程
数据结构
刚接触scatterlist的朋友可能容易被这个结构体的名字误导,scatterlist本身并不是一种链表结构,而只是用于描述一个单独的内存块
struct scatterlist {
#ifdef CONFIG_DEBUG_SG
unsigned long sg_magic;
#endif
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
};
page_link
用于记录该内存块所在的页面号,page_link
的bit[0]和bit[1]有特殊用途,因此页面必须是4字节对齐的。bit[0]为1表示该节点是一个铰链,为0表示一个普通内存节点,铰链是用于将多个链串成一个更大的链。bit[1]为1表示该节点是尾节点,为0表示一个普通节点。offset
表示内存块在页面内的偏移,length
表示内存块长度,dma_address
表示物理地址
要使用scatterlist,开发者可以自己定义一个scatterlist的数组
struct scatterlist sgt[8];
也可以使用内核提供好的封装后的sg table结构
struct sg_table {
struct scatterlist *sgl; /* the list */
unsigned int nents; /* number of mapped entries */
unsigned int orig_nents; /* original size of list */
};
其中sgl
是一个指向scatterlist数组的指针,nents
是scatterlist数组的长度,orig_nents
是scatterlist的原始长度
创建
创建一个scatterlist链有两种方式,一种是静态声明,并调用API进行初始化
struct scatterlist sgt[8];
sg_init_table(sgt, 8);
sg_init_table()函数定义如下
/**
* sg_init_table - Initialize SG table
* @sgl: The SG table
* @nents: Number of entries in table
*
* Notes:
* If this is part of a chained sg table, sg_mark_end() should be
* used only on the last table part.
*
**/
void sg_init_table(struct scatterlist *sgl, unsigned int nents)
{
memset(sgl, 0, sizeof(*sgl) * nents);
#ifdef CONFIG_DEBUG_SG
{
unsigned int i;
for (i = 0; i < nents; i++)
sgl[i].sg_magic = SG_MAGIC;
}
#endif
sg_mark_end(&sgl[nents - 1]);
}
EXPORT_SYMBOL(sg_init_table);
传入的参数sgl是scatterlist的数组地址,nents是数组长度,函数内部将数组内存初始化,然后调用了sg_mark_end
将数组的最后一个元素的page_link
标记为尾节点
另一种方法是动态创建
struct sg_table sgt;
sg_alloc_table(&sgt, 8, GFP_KERNEL);
sg_alloc_table()函数定义如下
/**
* sg_alloc_table - Allocate and initialize an sg table
* @table: The sg table header to use
* @nents: Number of entries in sg list
* @gfp_mask: GFP allocation mask
*
* Description:
* Allocate and initialize an sg table. If @nents@ is larger than
* SG_MAX_SINGLE_ALLOC a chained sg table will be setup.
*
**/
int sg_alloc_table(struct sg_table *table, unsigned int nents, gfp_t gfp_mask)
{
int ret;
ret = __sg_alloc_table(table, nents, SG_MAX_SINGLE_ALLOC,
NULL, gfp_mask, sg_kmalloc);
if (unlikely(ret))
__sg_free_table(table, SG_MAX_SINGLE_ALLOC, false, sg_kfree);
return ret;
}
EXPORT_SYMBOL(sg_alloc_table);
其内部通过__sg_alloc_table()
来动添申请内存创建。这里的SG_MAX_SINGLE_ALLOC
是一个由于各种原因的限制,即动态申请的连续内存的sg table必须在一个页面中,默认一个页面4096的话,scatterlist结构体大小20字节,那么一个页面最大能够创建大小为204的scatterlist链,而如果要创建更大数目的话,需要通过铰链的方式将多个分散在不同页面的scatterlist链串联起来,这在__sg_alloc_table()
内部已经实现,用户不需要关心
使用
通常的应用场景是将应用程序分配的buffer映射到sg table上
void map_user_buf_to_sgl(struct sg_table *sgt, void __user *buf, int len)
{
int page_nr = (((unsigned long)buf + len + PAGE_SIZE - 1) -
((unsigned long)buf & PAGE_MASK))
>> PAGE_SHIFT;
/* alloc sg table */
sg_alloc_table(sgt, pages_nr, GFP_KERNEL);
/* alloc page ptr */
struct page **pages = kcalloc(pages_nr, sizeof(struct page *), GFP_KERNEL);
/* buf to pages */
get_user_pages_fast((unsigned long)buf, pages_nr, 1/* write */,
pages);
struct scatterlist *sg = sgt->sgl;
for (i = 0; i < pages_nr; i++, sg = sg_next(sg)) {
unsigned int offset = offset_in_page(buf);
sg_set_page(sg, pages[i], len, offset);
}
}
首先根据buf和len得到页面数量,然后根据页面数量创建对应长度的sg table,然后获取buf对应的页面,最后遍历sg table,通过sg_set_page()
将页面设置到scatterlist上
其他的一些API
-
sg_alloc_table_from_pages:从一组页面创建sg table
-
sg_nents:获取sg的entity数量
-
sg_copy_from_buffer:从一个连续的buffer拷贝到sg list
-
sg_chain:将两个单独的sg list串联