1、DMA是什么
不通过CPU的内存访问。
DMA降低了CPU的负担,性能也比传统的非DMA(PIO)访存方式提高数倍(具体提升大小与与硬件相关)。
内存访问:
计算机内部主要涉及两种内存访问。一种是 物理内存DRAM,另一种是 设备内存(如网卡,显卡等PCIe设备)。
内存访问方向
以上两种类型内存的访问,主要包括上图中三个方向:
-
CPU -> DRAM: 从CPU 读写物理内存中的数据或程序指令;
-
Device DMA -> DRAM: 设备的DMA 控制器发出针对DRAM的 DMA 读写,例如网卡从物理内存中DMA读取数据包进行发包。
-
CPU -> Device Memory: 主机CPU 发出针对设备的读写,例如主机需要初始化设置显卡寄存器,让其开始工作。
内存访问过程
A. CPU访问物理内存(CPU -> DRAM)
-
CPU使用虚拟地址发出内存读写请求
-
MMU将虚拟地址转为DRAM的物理地址
-
如果访问数据的物理地址在Cache 有缓存数据,则直接从Cache 读取即可
-
如果Cache 中没有缓存,则通过内存控制器,访问DRMA(当然这涉及到页表的管理,不在本文的讨论范围),如果有兴趣,强烈建议深入阅读《深入理解计算机系统》
B. 设备访问物理内存(Device DMA -> DRAM)
-
外设使用总线地址,发起针对DRAM的DMA读写访问
-
IOMMU负责将总线地址转为物理地址
-
通过内存控制器,访问DRMA
C. 设备内存访问(CPU -> Device Memory)
-
CPU使用虚拟地址发出内存读写请求
-
MMU将虚拟地址转为设备内存的物理地址(通过 cat /proc/iomem 可以查看到)
-
Host Bridge 将物理地址转为总线地址
-
通过PCIe总线寻址到设备,并进行设备内存读写
总结:访问物理内存时,可以认为 虚拟地址 和 总线地址 都是Virtual的地址,都需要通过一个地址转换硬件(CPU侧是MMU,device侧是IOMMU),将虚拟地址转换为物理内存DRAM的实际物理地址。而访问设备内存时,通过Host Bridge将物理地址转换为设备内存的总线地址。
2、CPU地址和DMA地址
系统内核使用的是虚拟地址,任何从kmalloc, valloc返回的地址都是虚拟地址,可以使用void *变量存储。虚拟地址可以通过内存管理系统(MMU)转换为CPU的物理地址。内核管理的设备资源一般都是物理地址,比如设备的寄存器地址之类的,这些地址的范围都存在于/proc/iomem
文件中。物理地址是不能直接使用的,需要通过ioremap映射得到一个虚拟地址,然后代码才可以访问。
root@keep-VirtualBox:~# cat /proc/iomem
00000000-00000fff : Reserved
00001000-0009fbff : System RAM
0009fc00-0009ffff : Reserved
000a0000-000bffff : PCI Bus 0000:00
000c0000-000c7fff : Video ROM
000e2000-000e2fff : Adapter ROM
000f0000-000fffff : Reserved
000f0000-000fffff : System ROM
00100000-dffeffff : System RAM
a7000000-a80020df : Kernel code
a8200000-a8ca6fff : Kernel rodata
a8e00000-a918af7f : Kernel data
a945e000-a99fffff : Kernel bss
dfff0000-dfffffff : ACPI Tables
e0000000-fdffffff : PCI Bus 0000:00
e0000000-e0ffffff : 0000:00:02.0
e0000000-e0ffffff : vmwgfx probe
f0000000-f01fffff : 0000:00:02.0
f0000000-f01fffff : vmwgfx probe
f0200000-f021ffff : 0000:00:03.0
f0200000-f021ffff : e1000
f0400000-f07fffff : 0000:00:04.0
f0400000-f07fffff : vboxguest
f0800000-f0803fff : 0000:00:04.0
f0804000-f0804fff : 0000:00:06.0
f0804000-f0804fff : ohci_hcd
f0806000-f0807fff : 0000:00:0d.0
f0806000-f0807fff : ahci
fec00000-fec00fff : Reserved
fec00000-fec003ff : IOAPIC 0
fee00000-fee00fff : Local APIC
fee00000-fee00fff : Reserved
fffc0000-ffffffff : Reserved
100000000-21fffffff : System RAM
IO设备还会用到一个概念:总线地址。设备使用总线地址读写系统的内存。有些系统总线地址等同于物理地址,但是大部分系统并不是这样的,是两套不同的地址描述。IOMMU可以管理物理地址和总线地址的映射关系。
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物理地址,存储在struct resource结构中,可以在/proc/iomem
中看到。然后驱动通过ioremap
把物理地址映射到虚拟地址,并通过专门的接口读写寄存器,如:ioread32(C),访问设备对应的总线地址。这个有点儿绕,驱动访问虚拟地址,最终落到设备的总线地址上面。
使用DMA操作时,驱动通过kmalloc申请一片空间A,A对应到物理地址B上去。设备要访问物理地址B的话,需要有个IOMMU通过A转换得到一个DMA地址C对应到B上去。驱动告诉设备DMA操作的目的地址是C,IOMMU会把C映射到物理地址B上面去,最后操作的是物理地址B。驱动通过dma_map_single接口,获取到虚拟地址A对应的DMA地址C。
虚拟地址A, DMA地址C,对应的物理地址都是B。
3、DMA限制
不是所有的内核内存地址都可以使用DMA技术的。使用__get_free_page或者kmalloc、kmem_cache_alloc返回的地址可以使用DMA,而使用vmalloc返回的地址不能使用DMA。还需要保证地址是cacheline对齐的,否则会出现一致性的问题,如果不是cacheline对齐的,CPU和DMA会打架的,CPU和DMA同时操作一片cache,会导致内容相互覆盖。
4、DMA寻址能力
内核默认支持32bit的DMA地址空间,支持64bit的设备相应的可以支持64bit,设备有限制的话也可以相应的减少位数。正确的操作是调用接口dma_set_mask_and_coherent设置DMA地址空间寻址位数。接口如果返回非0的话,则表示设备不支持设置的dma空间大小,那么后续就不要通过DMA方式操作,否则会出现不可预料的问题的。
if (dma_set_mask_and_coherent(dev, DMA_BIT_MASK(64))) {
dev_warn(dev, "mydev: No suitable DMA available\n");
goto ignore_this_device;
}
5、DMA的分类
DMA有两种类型的映射:一致性DMA和流式DMA。
一致性DMA在驱动注册的时候就映射了,最后才解除映射。硬件会自己保证设备和CPU可以并行的访问数据,并且数据都是实时更新的,不需要开发者做额外的工作。默认一致性DMA支持的空间为32位寻址空间。如果需要,可以自行设置DMA掩码位。有一点需要注意的是,一致性DMA并不是完全按序写入数据的,如果不同地址间数据写入存在互相依赖,需要使用wmb做同步。
使用一致性DMA的场景有:
-
网卡的ring描述符
-
SCSI适配的mailbox命令数据
-
设备固件的微码内存
流式DMA一般在需要做DMA转换的时候才做映射,而后动态的解除映射。使用流式DMA需要明确调用接口设置想要的操作。
使用流式DMA的场景有:
-
网络包buffers的接收发送
-
文件系统的读写buffers
dma_addr_t dma_handle;
cpu_addr = dma_alloc_coherent(dev, size, &dma_handle, gfp);
dma_free_coherent(dev, size, cpu_addr, dma_handle);
一致性DMA的使用代码如上,接口返回CPU可以访问的虚拟地址以及dma_handle地址(传给设备的地址)。返回的地址都是PAGE_SIZE对齐的大小的,即使你传入的size小于PAGE_SIZE也是一样的。
如果需要大量的小size的内存,可以自行管理使用dma_alloc_coherent分配的空间, 也可以使用dma_pool的接口来实现, dma_pool底层实际也是管理的dma_alloc_coherent分配的空间。
struct dma_pool *pool;
pool = dma_pool_create(name, dev, size, align, boundary); // align必须是2的次方, boundary表示内存的边界,不允许一次性从pool申请超过多少大小的内存。
cpu_addr = dma_pool_alloc(pool, flags, &dma_handle);
dma_pool_free(pool, cpu_addr, dma_handle);
dma_pool_destroy(pool); // free pool之前要保证所有从pool申请的内存都free了
流式DMA映射可以在中断上下文中调用,可以映射一个独立的内存区域,也可以映射一个scatterlist表示的多个内存区域。
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_unmap_single(dev, dma_handle, size, direction);
// 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);
dma_map_single就是不能映射high memory, 可以使用dma_map_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);
在流式DMA映射取消映射之前,CPU不应该访问DMA buffer,如果需要访问,则必须在DMA传输后相应地调用如下函数。
dma_sync_single_for_cpu(dev, dma_handle, size, direction);
dma_sync_sg_for_cpu(dev, sglist, nents, direction);
CPU访问结束后,将buffer还给设备DMA使用时,需要相应调用如下函数。
dma_sync_single_for_device(dev, dma_handle, size, direction);
dma_sync_sg_for_device(dev, sglist, nents, direction);
for_cpu 和 for_device的区别在于控制权是属于cpu还是device。
6、DMA 方向
DMA相关接口需要填写DMA方向,表示DMA操作时的方向,目前有如下的值:
DMA_BIDIRECTIONAL
DMA_TO_DEVICE
DMA_FROM_DEVICE
DMA_NONE
一般只有流式DMA需要设置方向,一致性DMA默认是双向的DMA_BIDIRECTIONAL。
7、错误处理
-
调用dma_alloc_coherent后判断返回值是否为NULL, 调用dma_map_sg判断返回值是否为0
-
dma_map_single和dma_map_page调用后,使用dma_mapping_error判断是否失败
在映射失败后,记得释放已经成功的地址区域。
未完待续,持续更新
参考博客: