DMA总结

1、DMA是什么

不通过CPU的内存访问。

DMA降低了CPU的负担,性能也比传统的非DMA(PIO)访存方式提高数倍(具体提升大小与与硬件相关)。

内存访问:

计算机内部主要涉及两种内存访问。一种是 物理内存DRAM,另一种是 设备内存(如网卡,显卡等PCIe设备)。

内存访问方向

以上两种类型内存的访问,主要包括上图中三个方向:

  1. CPU -> DRAM: 从CPU 读写物理内存中的数据或程序指令;

  2. Device DMA -> DRAM: 设备的DMA 控制器发出针对DRAM的 DMA 读写,例如网卡从物理内存中DMA读取数据包进行发包。

  3. CPU -> Device Memory: 主机CPU 发出针对设备的读写,例如主机需要初始化设置显卡寄存器,让其开始工作。

内存访问过程

A. CPU访问物理内存(CPU -> DRAM)

  1. CPU使用虚拟地址发出内存读写请求

  2. MMU将虚拟地址转为DRAM的物理地址

  3. 如果访问数据的物理地址在Cache 有缓存数据,则直接从Cache 读取即可

  4. 如果Cache 中没有缓存,则通过内存控制器,访问DRMA(当然这涉及到页表的管理,不在本文的讨论范围),如果有兴趣,强烈建议深入阅读《深入理解计算机系统》

B. 设备访问物理内存(Device DMA -> DRAM)

  1. 外设使用总线地址,发起针对DRAM的DMA读写访问

  2. IOMMU负责将总线地址转为物理地址

  3. 通过内存控制器,访问DRMA

C. 设备内存访问(CPU -> Device Memory)

  1. CPU使用虚拟地址发出内存读写请求

  2. MMU将虚拟地址转为设备内存的物理地址(通过 cat /proc/iomem 可以查看到)

  3. Host Bridge 将物理地址转为总线地址

  4. 通过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判断是否失败

在映射失败后,记得释放已经成功的地址区域。

未完待续,持续更新

参考博客:

一文读懂 内存DMA 及 设备内存控制

周谈(34)-DMA地址映射api使用

DMA问题定位小结

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值