linux dma机制

目录

1 物理地址、虚拟地址和总线地址

2 DMA能搬运哪些内存?

3 DMA的寻址能力

4 两种类型的DMA mapping:一致性DMA映射&流式DMA映射

4.1 一致性DMA

4.2 流式DMA

5 一致性DMA映射API的使用

5.1 申请一致性DMA

5.2 释放一致性DMA

5.3 一致性小块DMA的管理:DMA pool

6 流式DMA映射API的使用

6.1 DMA的方向

6.2 流式DMA相关接口

6.2.1 映射一整块内存区域

6.2.2 映射scatterlist

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接口呢?

  1. 通过伙伴系统分配器(buddy allocater)的接口(__get_free_page*()/kmalloc()/kmem_cache_alloc())分配的DMA buffer;
  2. 不建议vmalloc()返回的虚拟地址用于DMA buffer,因为大小超过一页(one page)时其物理地址不连续,一般来说DMA硬件要求物理地址连续,即使DMA硬件支持scatter-gether,vmalloc分配的虚拟地址与对应的物理地址没有固定的偏移,我们仍需要遍历页表才能找到其对应关系。综上所述,不建议使用vmalloc创建DMA buffer。
  3. 不建议使用内核全局变量(存储在data/text/bss段)、内核模块中的全局变量、栈地址作为DMA buffer。需要保证这些虚拟内存cacheline对齐,否则会在CPU和非一致性DMA中有cacheline共享问题(CPU写一个word,同时DMA可能在同一个cache line中写一个不同的word,导致其中的一个被覆盖)
  4. 不建议kmap()接口返回的虚拟地址用于DMA buffer,原因与vmalloc同。
  5. 块设备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的接口都可以在中断上下文中使用