目录
4 两种类型的DMA mapping:一致性DMA映射&流式DMA映射
6.2.3 使用流式dma接口过程中,如何在CPU和device之间手动sync dma数据?
1 物理地址、虚拟地址和总线地址
CPU发出的访存地址都是虚拟地址“virt_addr_t”,像kmalloc()、vmalloc()这种接口返回的都是虚拟地址。虚拟内存系统通过TLB、MMU、页表将虚拟地址转换为物理地址“phys_addr_t”,即normal memory上实际的layout地址
而设备看到的是第三种内存地址形式————总线地址。如果设备寄存器在MMIO地址,或者设备操作DMA读写normal memory,此时device看到的内存就是总线内存。
CPU CPU Bus
Virtual Physical Address
Address Address Space
Space Space
+-------+ +------+ +------+
| | |MMIO | Offset | |
| | Virtual |Space | applied | |
C +-------+ --------> B +------+ ----------> +------+ A
| | mapping | | by host | |
+-----+ | | | | bridge | | +--------+
| | | | +------+ | | | |
| CPU | | | | RAM | | | | Device |
| | | | | | | | | |
+-----+ +-------+ +------+ +------+ +--------+
| | Virtual |Buffer| Mapping | |
X +-------+ --------> Y +------+ <---------- +------+ Z
| | mapping | RAM | by IOMMU
| | | |
| | | |
+-------+ +------+
简单理解:CPU使用虚拟地址,MMU使用物理地址; 外设使用总线地址,IOMMU使用物理地址,虚拟地址和总线地址都是virtual的地址,都需要通过一个地址转换硬件(CPU侧是MMU,device侧是IOMMU),转换为normal memory的实际物理地址。
2 DMA能搬运哪些内存?
哪些内存可以使用DMA mapping framework提供的API接口呢?
- 通过伙伴系统分配器(buddy allocater)的接口(__get_free_page*()/kmalloc()/kmem_cache_alloc())分配的DMA buffer;
- 不建议vmalloc()返回的虚拟地址用于DMA buffer,因为大小超过一页(one page)时其物理地址不连续,一般来说DMA硬件要求物理地址连续,即使DMA硬件支持scatter-gether,vmalloc分配的虚拟地址与对应的物理地址没有固定的偏移,我们仍需要遍历页表才能找到其对应关系。综上所述,不建议使用vmalloc创建DMA buffer。
- 不建议使用内核全局变量(存储在data/text/bss段)、内核模块中的全局变量、栈地址作为DMA buffer。需要保证这些虚拟内存cacheline对齐,否则会在CPU和非一致性DMA中有cacheline共享问题(CPU写一个word,同时DMA可能在同一个cache line中写一个不同的word,导致其中的一个被覆盖)
- 不建议kmap()接口返回的虚拟地址用于DMA buffer,原因与vmalloc同。
- 块设备IO和网络buffer是可以用于DMA buffer的,这一点由块设备IO和网络子系统保证。
3 DMA的寻址能力
默认情况,内核假设外设的DMA寻址能力是32-bits,但这并不普适(比如有些外设的寻址能力是24-bits,只能访问0MB--16MB的物理地址)。正确的操作应该是在设备初始化过程中,显示的指定DMA的寻址能力,有如下三个接口可以完成该操作:
/* 流式DMA设置寻址能力 */
int dma_set_mask(struct device *dev, u64 mask);
/* 一致性DMA设置寻址能力 */
int dma_set_coherent_mask(struct device *dev, u64 mask);
/* 流式DMA与一致性DMA同时设置寻址能力(两种模式的寻址能力相同) */
int dma_set_mask_and_coherent(struct device *dev, u64 mask);
需要强调的是,一致性DMA的寻址能力(掩码值)小于等于流式DMA的寻址能力(掩码值)(下一小节会详细介绍两种不同类型的DMA)。
4 两种类型的DMA mapping:一致性DMA映射&流式DMA映射
4.1 一致性DMA
在驱动初始化时mapping,在驱动shutdown时unmapping**(意味着不是一次性的,是持续性的使用该DMA映射)**。硬件需要保证外设和CPU能并行访问同一块数据,并且保证在软件无显式flush操作的情况下,CPU和外设能同步看到对方对数据的更新。一致性(consistent)可以理解为同步(synchronous)。
典型的使用一致性DMA的例子:网卡DMA环形缓冲区(ring descriptors)。
一致性DMA不妨碍内存屏障(memory barriers)的使用,比如设备需要先看到word0的修改,再看到word1的修改,代码可以如下:
desc->word0 = address;
wmb();
desc->word1 = DESC_VALID;
4.2 流式DMA
一般是需要一次DMA transfer时map,传输结束后unmap(当然也可以有dma_sync的操作,下文会详聊),硬件可以优化存取的顺序。流式(streaming)可以理解为异步(asynchronous)。
典型用例:网卡进行数据传输使用的DMA buffer;SCSI设备写入/读取的文件系统buffer;
设计这样的接口是为了充分优化硬件的性能。
另外需要注意的是,无论是哪种类型的DMA都有对齐的限制;此外,如果系统中的cache不是DMA-coherent的,而且底层的DMA buffer不和其他数据共享cache lines,这样的系统将会有更好的性能。
将DMA的方向问题写在这一部分
5 一致性DMA映射API的使用
5.1 申请一致性DMA
申请&映射大块的DAM region,使用如下接口:
dma_addr_t dma_handle;
cpu_addr = dma_alloc_coherent(dev, size, &dma_handle, gfp);
如果你需要的DMA buffer小于一个page,最好使用dma_pool接口,会在接下来描述。dma_alloc_coherent返回两个值:一个是从CPU角度访问DMA buffer的虚拟地址;另一个是从dma controller角度看到的总线地址,驱动可以将该值传递给硬件。另外,该接口可以在中断上下文中调用。
5.2 释放一致性DMA
释放一致性DMA的接口如下:
dma_free_coherent(dev, size, cpu_addr, dma_handle);
其中的cpu_addr和dma_handle是dma_alloc_coherent的两个返回值;值得注意的是该接口不能在中断上下文中调用(因为free dma的操作会引发TLB的维护操作,从而引发cpu core之间的通信,如果此时关闭了IRQ,会锁死在SMP 的IPI代码逻辑中)。
5.3 一致性小块DMA的管理:DMA pool
需要的dma buffer小于一个page时,可以使用dma_pool接口,它概念上类似于kmem_cache。创建一个dma_pool的操作如下:
struct dma_pool *pool;
pool = dma_pool_create(name, dev, size, align, boundary);
其中,align用于dev硬件的对齐需求,是2的整数倍,如果传入4096意味着从该pool中申请的内存不能跨过4KB的边界,但与此同时,最好使用dma_alloc_coherent代替dma_pool_create。之后的dma申请释放都从dma_pool_create返回的pool中进行,接口如下:
cpu_addr = dma_pool_alloc(pool, flags, &dma_handle);
dma_pool_free(pool, cpu_addr, dma_handle);
最后,如果想要销毁一个dma_pool,使用如下接口:
dma_pool_destroy(pool);
在destroy一个pool之前,需要确保使用dma_pool_free把所有从pool中申请的小块内存都释放掉了。
6 流式DMA映射API的使用
在聊流式DMA映射前,需要先明确DMA方向的概念。
6.1 DMA的方向
DMA的方向有以下4种:
DMA_BIDIRECTIONAL //双向
DMA_TO_DEVICE //从normal memory到device memory
DMA_FROM_DEVICE //从device memory到normal memory
DMA_NONE //用于debug
只有流式DMA(streaming mappings)需要指定方向,一致性DMA具有隐式(implicitly)的方向属性为双向(DMA_BIDIRECTIONAL)。在方向属性的使用中,如果不明确DMA的传输方向,可以使用DMA_BIDIRECITONAL;但如果能明确传输方向,建议指明DMA_TO_DEVICE或者DMA_FROM_DEVICE,这样能提高性能。
举例来说,在网络驱动中,对于发送逻辑中的map和unmap,都是DMA_TO_DEVICE(因为数据是从内存中发送到网络驱动,最终通过硬件网卡发送出去);对于接收逻辑中的map和unmap,则使用DMA_FROM_DEVICE。
6.2 流式DMA相关接口
有两个使用流式DMA的典型场景,一种是需要映射单独一块内存区域,另外一种是映射一个scatterlist(可以理解为一个内存块的链表)。
6.2.1 映射一整块内存区域
如果是映射一整块内存区域,可以像这样:
struct device *dev = &my_dev->dev;
dma_addr_t dma_handle;
void *addr = buffer->ptr;
size_t size = buffer->len;
dma_handle = dma_map_single(dev, addr, size, direction);
if (dma_mapping_error(dev, dma_handle)) {
/*
* reduce current DMA mapping usage,
* delay and try again later or
* reset driver.
*/
goto map_error_handling;
}
需要注意,dma_map_single的返回值需要通过dma_mapping_error的检测,如果不检测直接使用返回值dma_handle,可能会导致data corruption从而使系统panic,这种问题很难定位。解映射 dma_unmap_single 一般发生在DMA传输结束后给CPU发送的中断处理函数中。
使用dma_map_signal接口有一个问题:无法映射高端内存。因此另一个以page为参数的接口应运而生。其中,offset参数是给定page中的字节偏移量。
struct device *dev = &my_dev->dev;
dma_addr_t dma_handle;
struct page *page = buffer->page;
unsigned long offset = buffer->offset;
size_t size = buffer->len;
dma_handle = dma_map_page(dev, page, offset, size, direction);
if (dma_mapping_error(dev, dma_handle)) {
/*
* reduce current DMA mapping usage,
* delay and try again later or
* reset driver.
*/
goto map_error_handling;
}
...
dma_unmap_page(dev, dma_handle, size, direction);
6.2.2 映射scatterlist
int i, count = dma_map_sg(dev, sglist, nents, direction);
struct scatterlist *sg;
for_each_sg(sglist, sg, count, i) {
hw_address[i] = sg_dma_address(sg);
hw_len[i] = sg_dma_len(sg);
}
...
dma_unmap_sg(dev, sglist, nents, direction);
6.2.3 使用流式dma接口过程中,如何在CPU和device之间手动sync dma数据?
如果你需要在一次流式DMA map/unmap过程中多次操作DMA映射地址中的数据,需要代码自行保证CPU和device看到的数据是最新的,这需要用到以下接口:
CPU 和 DMA引擎通过以下接口获取 DMA内存 操作权限
/* 在CPU碰流式dma映射地址里的数据前,需要使用以下接口 */
dma_sync_single_for_cpu(dev, dma_handle, size, direction);
dma_sync_sg_for_cpu(dev, sglist, nents, direction);
/* 在device碰流式dma映射地址里的数据前,需要使用以下接口 */
dma_sync_single_for_device(dev, dma_handle, size, direction);
dma_sync_sg_for_device(dev, sglist, nents, direction);
如果map和unmap之间不需要碰dma映射地址中的数据,不需要使用以上dma_sync_*接口。以下是一个简单的使用示例:
my_card_setup_receive_buffer(struct my_card *cp, char *buffer, int len)
{
dma_addr_t mapping;
mapping = dma_map_single(cp->dev, buffer, len, DMA_FROM_DEVICE);
if (dma_mapping_error(cp->dev, mapping)) {
/*
* reduce current DMA mapping usage,
* delay and try again later or
* reset driver.
*/
goto map_error_handling;
}
cp->rx_buf = buffer;
cp->rx_len = len;
cp->rx_dma = mapping;
give_rx_buf_to_card(cp);
}
...
my_card_interrupt_handler(int irq, void *devid, struct pt_regs *regs)
{
struct my_card *cp = devid;
...
if (read_card_status(cp) == RX_BUF_TRANSFERRED) {
struct my_card_header *hp;
/* Examine the header to see if we wish
* to accept the data. But synchronize
* the DMA transfer with the CPU first
* so that we see updated contents.
*/
dma_sync_single_for_cpu(&cp->dev, cp->rx_dma,
cp->rx_len,
DMA_FROM_DEVICE);
/* Now it is safe to examine the buffer. */
hp = (struct my_card_header *) cp->rx_buf;
if (header_is_ok(hp)) {
dma_unmap_single(&cp->dev, cp->rx_dma, cp->rx_len,
DMA_FROM_DEVICE);
pass_to_upper_layers(cp->rx_buf);
make_and_setup_new_rx_buf(cp);
} else {
/* CPU should not write to
* DMA_FROM_DEVICE-mapped area,
* so dma_sync_single_for_device() is
* not needed here. It would be required
* for DMA_BIDIRECTIONAL mapping if
* the memory was modified.
*/
give_rx_buf_to_card(cp);
}
}
}
最后,所有流式DMA的接口都可以在中断上下文中使用。