使用scatterlist的原因就是系统在运行的时候内存会产生很多碎片,比如4k,100k的,1M的,有时候对应磁盘碎片,总之就是碎片。而在网络和磁盘操作中很多时候需要传送大块的数据,尤其是使用DMA的时候,因为DMA操作的物理地址必须是连续的。假设要1M内存,此时可以分配一个整的1M内存,
也可以把10个10K的和9个100K的组成一块1M的内存,当然这19个块可能是不连续的,也可能其中某些或全部是连续的,总之情况不定,为了描述这种情况,就引入了scatterlist,其实看成一个关于内存块构成的链表就OK了。
在SD/MMC代码中,在发起request的时候,都是通过scatterlist来发送数据的,定义在mmc_data里面,(MMC core就是这么设计的,跟具体的S3C2410还是PXA就没有关系了)
struct mmc_data {
。。。。。。。。。。。。。。
。。。。。。。。。。。。。。
。。。。。。。。。。。。。。
unsigned int sg_len; /* size of scatter list */
struct scatterlist *sg; /* I/O scatter list */
}
unsigned int sg_len; /* size of scatter list */
struct scatterlist *sg; /* I/O scatter list */
}
其中struct scatterlist *sg;就是指向scatter
list的指针,可以理解为数组的头指针,这个数组的作用就是保存各个scatterlist结构的地址,sg_len表示有几个
scatterlist结构,相当于数组元素个数。比如前面提到的那个例子,sg_len就应该是19了,sg组成的内存块就是
sg_mem0--->sg_mem1--->........->sg_mem18这样的内存链。所以通过sg就可以遍历19块中的任意一块内存的情况,比如位置和大小。以下是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;
unsigned int dma_length;
};
#ifdef CONFIG_DEBUG_SG
unsigned long sg_magic;
#endif
unsigned long page_link;
unsigned int offset;
unsigned int length;
dma_addr_t dma_address;
unsigned int dma_length;
};
下面以dw_mmc.c里面的scatter操作来分析,其实pxamci.c里面也有,不过pxamci.c里面只使用了DMA模式,相对要简单一点,dw_mmc.c里面还使用pio模式(其实就是cpu模式),要复杂一些,所以分析起来更有意义。
1.DMA模式下的使用
在使用DMA操作这些scatterlist之前,先要对scatterlist进行一下map:
int dma_map_sg(struct device *dev, struct scatterlist *sg, int nents,
enum dma_data_direction dir)
enum dma_data_direction dir)
其中的nents就是scatterlist的块数,sg是指针数组的首地址。返回值是map以后这些地址块被合并为多少个适合DMA搬运的块的数量,假设其中一块的结束地址和另一块的起始地址挨到一起了,这两块是会合二为一的,这就是为什么说返回的值可能会小于
nents的原因。比如上面的例子传进去的nents=19,
函数的返回值肯定是小于等于19的,当然肯定大于0,同时sg的值也被改写成了新的块链表。此时就可以把这些块放入DMA队列一个一个的进行搬运了。
s3cmci.c中的代码是这样的。
dma_len = dma_map_sg(mmc_dev(host->mmc), data->sg, data->sg_len,
rw ? DMA_TO_DEVICE : DMA_FROM_DEVICE);
rw ? DMA_TO_DEVICE : DMA_FROM_DEVICE);
。。。。。。。。。。。。。。。。。。
for (i = 0; i < dma_len; i++) {
。。。。。。。。。。。。。。。。。。。。。
sg_dma_address(&data->sg[i]),
sg_dma_len(&data->sg[i]));
sg_dma_address(&data->sg[i]),
sg_dma_len(&data->sg[i]));
res = dw_dma_enqueue(host->dma, host,
sg_dma_address(&data->sg[i]),
sg_dma_len(&data->sg[i]));
sg_dma_address(&data->sg[i]),
sg_dma_len(&data->sg[i]));
。。。。。。。。。。。。。。。。。。
}
for循环其实就是遍历内存块了,不过据下面这个网址上说的,这种用for的方式已经out了,现在要用for_each_sg, 其实是一样的:
for循环其实就是遍历内存块了,不过据下面这个网址上说的,这种用for的方式已经out了,现在要用for_each_sg, 其实是一样的:
/* Fill in list and pass it to dma_map_sg(). Then... */
for_each_sg(i, list, sgentry, nentries) {
program_hw(device, sg_dma_address(sgentry), sg_dma_len(sgentry));
}
for_each_sg(i, list, sgentry, nentries) {
program_hw(device, sg_dma_address(sgentry), sg_dma_len(sgentry));
}
2.CPU方式
CPU方式就不用调用map了,这些list本来就是CPU自己产生自己消化。在dw_mmc.c中是通过函数:
static inline int get_data_buffer(struct dw
_host *host,
u32 *bytes, u32 **pointer)
来实现的,它使用了一个计数host->pio_sgptr 来记录现在使用的是内存链中的第几块,这个值在每次request后prepare_pio的时候被清零,每次调用get_data_buffer就加 一,直到等于sg_len,表示所有的内存块都用过了。对于一个scatterlist指针sg,是如下这样获取它代表的内存块的大小和位置的:
_host *host,
u32 *bytes, u32 **pointer)
来实现的,它使用了一个计数host->pio_sgptr 来记录现在使用的是内存链中的第几块,这个值在每次request后prepare_pio的时候被清零,每次调用get_data_buffer就加 一,直到等于sg_len,表示所有的内存块都用过了。对于一个scatterlist指针sg,是如下这样获取它代表的内存块的大小和位置的:
*bytes = sg->length;/*内存块长度*/
*pointer = sg_virt(sg);/*内存块起始地址*/
*pointer = sg_virt(sg);/*内存块起始地址*/
在dw_mmc.c中使用了一点点小技巧来操作scatterlist和SD/MMC FIFO发送/接收,比如在do_pio_write中
while ((fifo = fifo_free(host)) > 3) {
if (!host->pio_bytes) {
res = get_data_buffer(host, &host->pio_bytes,
&host->pio_ptr);
if (res) {
dbg(host, dbg_pio,
"pio_write(): complete (no more data).\n");
host->pio_active = XFER_NONE;
if (!host->pio_bytes) {
res = get_data_buffer(host, &host->pio_bytes,
&host->pio_ptr);
if (res) {
dbg(host, dbg_pio,
"pio_write(): complete (no more data).\n");
host->pio_active = XFER_NONE;
return;
}
}
。。。。。。。。。。。。。。。。。。。。。。。。
if (fifo >= host->pio_bytes)
fifo = host->pio_bytes;
else
fifo -= fifo & 3;
fifo = host->pio_bytes;
else
fifo -= fifo & 3;
host->pio_bytes -= fifo;
host->pio_count += fifo;
host->pio_count += fifo;
fifo = (fifo + 3) >> 2;
ptr = host->pio_ptr;
while (fifo--)
writel(*ptr++, to_ptr);
host->pio_ptr = ptr;
}
ptr = host->pio_ptr;
while (fifo--)
writel(*ptr++, to_ptr);
host->pio_ptr = ptr;
}
它通过host->pio_bytes来记录当前的内存块还有多少数据没有发,如果FIFO里面的空间够用,那就直接都发了,如果不够呢,则先把
FIFO填满,然后等着下一次中断的时候再发。如果这个内存块的数据都发完了,则host->pio_bytes为0,此时调用
get_data_buffer来获取内存链中的下一块内存数据,在get_data_bu中host->pio_bytes会被置为新块的长度:
*bytes = sg->length
其中的*bytes就是指向host->pio_bytes的。
fifo-=fifo&3 是因为2410每次必须发四个字节,所以要把零头去掉,EVB也有这个问题。
其实scatterlist这个东东蛮有意思的,俺们的Nucleus上其实也可以借鉴的,由于内存太少,在解JPEG文件时不一定能分到那么大的一块连续内存,可以通过scatterlist来把文件分块读取,然后解码的时候DMA再一块一块的搬,总比分不到内存就返回失败来的强。不过对于应用来讲如果没有MMU支持,还是有点杯具的,如果有MMU支持,让应用层看到的是一整块的内存,估计要爽的多,甚至在文件系统层也是这样的,只要到DMA搬数之前把scatterlist的内存链分清楚就OK了。