前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >动态 DMA 映射指南-地址类型差异-DMA寻址能力-内核驱动-一致内存DMA-流式DMA-错误处理-平台兼容等

动态 DMA 映射指南-地址类型差异-DMA寻址能力-内核驱动-一致内存DMA-流式DMA-错误处理-平台兼容等

作者头像
ssbandjl
修改2024-01-29 10:05:26
2610
修改2024-01-29 10:05:26
举报
文章被收录于专栏:DPUDPU

简介

DPU卸载/加速, 或AI云中, 大量使用的RDMA技术中, 比较重要的操作当属于DMA, 不管是e810, e1000, mlx5等网卡驱动, 或是刚玉项目(Corundum: https://github.com/corundum/corundum)中, 都大量使用DMA, 今天咱们跟随大佬一起深入分析动态DMA映射原理及API

作者

代码语言:bash
复制
David S. Miller davem@redhat.com 
Richard Henderson rth@cygnus.com 
Jakub Jelinek jakub@redhat.com

这是设备驱动程序编写者如何使用 DMA API 的指南, 带有示例伪代码。有关 API 的简要说明,请参阅DMA-API.txt。

CPU 和 DMA 地址

DMA API 涉及到的地址有好几种,了解差异很重要。

内核通常使用虚拟地址。 kmalloc()、vmalloc() 和类似接口返回的任何地址都是虚拟地址,可以存储在“void *”中。

虚拟内存系统(TLB、页表等)将虚拟内存转换为CPU物理地址,存储为“phys_addr_t”或“resource_size_t”。内核管理设备资源,如寄存器, 将其视为物理地址, 存储在/proc/iomem 中。驱动程序不能直接使用该物理地址, 它必须使用 ioremap() 来映射它们的物理地址空间并生成虚拟地址。

I/O 设备使用第三种地址:“总线地址”。如果一个设备注册了MMIO地址,或者执行DMA读/写系统内存, 设备使用的就是总线地址。在一些特殊的系统中,总线地址与 CPU 物理地址才是相同的,但一般都是不同的。IOMMU 和 host bridges可以在物理地址和总线地址之间生成任意映射。

从设备的角度来看,DMA 使用总线地址空间,但它可能仅限于该空间的子集。 例如,即使系统支持主内存和 PCI BAR 的 64 位地址,设备也可能使用 IOMMU,因此设备只需要使用 32 位 DMA 地址。

这是一张图片和一些例子

代码语言:javascript
复制
​
               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
            |       |             |      |
            |       |             |      |
            +-------+             +------+
​

在枚举过程中,内核了解 I/O 设备及其 MMIO 空间以及将它们连接到系统的主机桥。 例如,如果 PCI 设备有 BAR,则内核从 BAR 中读取总线地址(A)并将其转换为 CPU 物理地址(B)(虚拟内存系统TLB,page等)。 地址 B 存储在结构体资源中,通常通过 /proc/iomem 公开。 当驱动程序声明一个设备时,它通常使用 ioremap() 将物理地址 B 映射到虚拟地址 (C)。 然后它可以使用 ioread32(C) 等来访问总线地址 A 处的设备寄存器。

如果设备支持 DMA,驱动程序会使用 kmalloc() 或类似接口设置缓冲区,该接口返回虚拟地址 (X)。 虚拟内存系统将 X 映射到系统 RAM 中的物理地址 (Y)。 驱动程序可以使用虚拟地址 X 来访问缓冲区,但设备本身不能,因为 DMA 不经过 CPU 虚拟内存系统。

在一些简单的系统中,设备可以直接对物理地址 Y 进行 DMA。但在许多其他系统中,有 IOMMU 硬件将 DMA 地址Z转换为物理地址Y,例如,它将 Z 转换为 Y。这也就是DMA API存在的原因之一:驱动程序可以向 dma_map_single() 等接口提供虚拟地址 X,然后dma_map_single()设置任何所需的 IOMMU 映射并返回 DMA 地址 Z, 最后驱动程序告诉设备对 Z 执行 DMA,IOMMU 将其映射到 系统 RAM 中地址 Y 处的缓冲区。

为了使 Linux 能够使用动态 DMA 映射,它需要驱动程序的一些帮助,即它必须考虑到 DMA 地址应该仅在实际使用时进行映射,并在 DMA 传输后取消映射。

当然,即使在不存在此类硬件的平台上,以下 API 也可以工作。

请注意,DMA API 可与独立于底层微处理器架构的任何总线配合使用。 您应该使用 DMA API 而不是特定于总线的 DMA API,即使用 dma_map*() 接口而不是 pci_map*() 接口。

首先,确保引入dma-mapping.h头文件

代码语言:javascript
复制
#include <linux/dma-mapping.h>

在您的驱动程序中,以上头文件提供了 dma_addr_t 的定义。 此类型可以保存平台的任何有效 DMA 地址,并且应该在保存从 DMA 映射函数返回的 DMA 地址的任何地方使用。

什么内存支持 DMA?

您必须了解的第一条信息是 DMA 映射工具可以使用哪些内核内存。 对此有一套不成文的规则,本文试图最终将它们写下来。

如果您通过页面分配器(即 __get_free_page*())或通用内存分配器(即 kmalloc() 或 kmem_cache_alloc())获取内存,那么您可以使用从这些例程返回的地址与该内存进行 DMA 传输。

这具体意味着您不能将 vmalloc() 返回的内存/地址用于 DMA。 可以对映射到 vmalloc() 区域的底层内存进行 DMA,但这需要遍历页表来获取物理地址,然后使用 __va() 之类的方法将每个页面转换回内核地址。 [编辑:当我们集成执行此操作的 Gerd Knorr 通用代码时更新此内容。 ]

此规则还意味着您既不能使用内核映像地址(data/text/bss segments 中的项目),也不能使用模块映像地址,也不能使用 DMA 的堆栈地址。 这些都可以映射到与物理内存的其余部分完全不同的地方。 即使这些类别的内存在物理上可以与 DMA 配合使用,您也需要确保 I/O 缓冲区是缓存行对齐的。 否则,您会在具有 DMA 不相干缓存的 CPU 上看到缓存行共享问题(数据损坏),(CPU 可以写入一个字(word),DMA 可以写入同一缓存行中的另一个字,并且其中一个字可以被覆盖。)

此外,这意味着您无法从中获取 kmap() 调用和 DMA 的返回值。 这与 vmalloc() 类似。

块 I/O 和网络缓冲区怎么样? 块 I/O 和网络子系统确保它们使用的缓冲区对于您的 DMA 传输有效。

DMA 寻址能力

默认情况下,内核假定您的设备可以寻址 32 位 DMA 寻址。 对于支持 64 位的设备,需要增加该值,对于有限制的设备,需要减少该值。

关于 PCI 的特别说明:PCI-X 规范要求 PCI-X 设备支持所有事务的 64 位寻址 (DAC)。 并且至少有一个平台(SGI SN2)需要 64 位一致分配才能在 IO 总线处于 PCI-X 模式时正确运行。

为了正确操作,您必须设置 DMA 掩码以告知内核您的设备 DMA 寻址功能。

这是通过调用 dma_set_mask_and_coherent() 来执行的::

代码语言:javascript
复制
int dma_set_mask_and_coherent(struct device *dev, u64 mask);

这将为流式 API(streaming )和一致性 API(coherent) 一起设置掩码。 如果您有一些特殊要求,则可以使用以下两个单独的调用来代替:

代码语言:javascript
复制
流映射的设置是通过调用 dma_set_mask() 来执行的
int dma_set_mask(struct device *dev, u64 mask);
​
一致分配的设置是通过调用 dma_set_coherent_mask() 来执行的
int dma_set_coherent_mask(struct device *dev, u64 mask);

这里,dev 是指向设备的设备结构的指针,mask 是描述设备支持的地址的哪些位的位掩码。 通常,设备的设备结构嵌入在设备的总线特定设备结构中。 例如,&pdev->dev 是指向 PCI 设备的设备结构的指针(pdev 是指向您设备的 PCI 设备结构的指针)。

这些调用通常返回零,表示您的设备可以在给定您提供的地址掩码的机器上正确执行 DMA,但如果掩码太小而无法在给定系统上支持,则它们可能会返回错误。 如果它返回非零,则您的设备无法在此平台上正确执行 DMA,并且尝试这样做将导致未定义的行为。 除非 dma_set_mask 函数系列返回成功,否则不得在此设备上使用 DMA。

这意味着在失败的情况下,您有两种选择:

  1. 如果可能,使用某种非 DMA 模式进行数据传输。
  2. 忽略该设备,不对其进行初始化。

建议您的驱动程序在设置 DMA 掩码失败时打印内核 KERN_WARNING 消息。 通过这种方式,如果您的驱动程序的用户报告性能很差或者甚至没有检测到设备,您可以向他们询问内核消息以找出确切的原因。

标准 64 位寻址设备会执行以下操作:

代码语言:javascript
复制
if (dma_set_mask_and_coherent(dev, DMA_BIT_MASK(64))) {
  dev_warn(dev, "mydev: No suitable DMA available\n");
  goto ignore_this_device;
}

如果设备仅支持一致分配中描述符的 32 位寻址,但支持流映射的完整 64 位,则它将如下所示:

代码语言:javascript
复制
if (dma_set_mask(dev, DMA_BIT_MASK(64))) {
  dev_warn(dev, "mydev: No suitable DMA available\n");
  goto ignore_this_device;
}

相干掩码将始终能够设置与流掩模相同或更小的掩码。 然而,对于设备驱动程序仅使用一致分配, 十分罕见情况,所以必须检查 dma_set_coherent_mask() 的返回值。

最后,如果您的设备只能驱动低 24 位地址, 你可能会做类似的事情::

代码语言:javascript
复制
if (dma_set_mask(dev, DMA_BIT_MASK(24))) {
  dev_warn(dev, "mydev: 24-bit DMA addressing not available\n");
  goto ignore_this_device;
}

当 dma_set_mask() 或 dma_set_mask_and_coherent() 成功并返回零时,内核会保存您提供的此掩码。 稍后当您进行 DMA 映射时,内核将使用此信息。 目前我们知道一个案例,值得在本文档中提及。 如果您的设备支持多种功能(例如声卡提供播放和录音功能),并且各种不同的功能具有不同的 DMA 寻址限制,您可能希望探测每个掩码并仅提供机器可以处理的功能。 重要的是,最后一次调用 dma_set_mask() 是针对最具体的掩码。

以下是如何完成此操作的伪代码::

代码语言:javascript
复制
#define PLAYBACK_ADDRESS_BITS DMA_BIT_MASK(32)
#define RECORD_ADDRESS_BITS DMA_BIT_MASK(24)
​
struct my_sound_card *card;
struct device *dev;
​
...
if (!dma_set_mask(dev, PLAYBACK_ADDRESS_BITS)) {
  card->playback_enabled = 1;
} else {
  card->playback_enabled = 0;
  dev_warn(dev, "%s: Playback disabled due to DMA limitations\n",
         card->name);
}
if (!dma_set_mask(dev, RECORD_ADDRESS_BITS)) {
  card->record_enabled = 1;
} else {
  card->record_enabled = 0;
  dev_warn(dev, "%s: Record disabled due to DMA limitations\n",
         card->name);
}

这里以声卡为例,因为这种 PCI 设备在 PCI 前端的情况下可能有 ISA 芯片,因此保留了 ISA 的 16MB DMA 寻址限制。

DMA 映射的类型

DMA 映射有两种类型:

  • (1) 一致的 DMA 映射(Consistent)通常在驱动程序初始化时映射,在结束时取消映射,为此硬件应保证设备和 CPU 可以并行访问数据,并且会看到彼此进行的更新,而无需任何显式软件刷新(flushing)。 将“一致”视为“同步”或“一致”(Think of "consistent" as "synchronous" or "coherent".) 当前默认是在 DMA 空间的低 32 位中返回一致的内存。 但是,为了将来的兼容性,即使此默认值适合您的驱动程序,您也应该设置一致的掩码。 使用一致映射的好例子是:
    • 网卡 DMA 环描述符(ring descriptors)
    • SCSI 适配器邮箱命令数据结构(mailbox)
    • 在主存储器外执行的设备固件微代码

    这些示例都要求不变的是,任何 CPU 存储到内存的操作都对设备立即可见,反之亦然。 一致的映射保证了这一点 .. 重要的::

一致的 DMA 内存并不排除使用适当的内存屏障。 CPU 可以将存储重新排序到一致内存,就像它可以正常内存一样。 示例:如果设备在第二个单词之前看到描述符的第一个单词更新很重要,则必须执行以下操作:

代码语言:javascript
复制
  desc->word0 = address;
  wmb();
  desc->word1 = DESC_VALID;

为了在所有平台上获得正确的行为。此外,在某些平台上,您的驱动程序可能需要以与刷新 PCI 桥中的写入缓冲区大致相同的方式刷新 CPU 写入缓冲区(例如,在写入寄存器的值后读取该值)。

  • (2) 流 DMA 映射(Streaming),通常针对一次 DMA 传输进行映射,在其之后立即取消映射(除非您使用下面的 dma_sync_*),并且硬件可以针对顺序访问进行优化 将“流”视为“异步”或“在一致性域之外” (Think of "streaming" as "asynchronous" or "outside the coherency domain") 使用流映射的好例子是:
    • 设备发送/接收的网络缓冲区。
    • 文件系统缓冲区由 SCSI 设备写入/读取。

使用此类映射的接口的设计方式使得实现可以进行硬件允许的任何性能优化。 为此,在使用此类映射时,您必须明确您想要发生的情况。 两种类型的 DMA 映射都没有来自底层总线的对齐限制,尽管某些设备可能有此类限制。 此外,当底层缓冲区不与其他数据共享缓存行时,具有非 DMA 一致性缓存的系统将工作得更好。

使用一致的 DMA 映射

要分配和映射大的(如, PAGE_SIZE 左右)一致的 DMA 区域,您应该这样做

代码语言:javascript
复制
  dma_addr_t dma_handle;
  cpu_addr = dma_alloc_coherent(dev, size, &dma_handle, gfp);

其中 device 是一个“struct device *”。 这可以在中断上下文中使用 GFP_ATOMIC 标志来调用。 Size 是要分配的区域的长度(以字节为单位)。 该例程将为该区域分配 RAM,因此它的作用与 __get_free_pages() 类似(但采用大小而不是页面顺序)。 如果您的驱动程序需要的大小, 小于页面的区域,您可能更喜欢使用 dma_pool 接口,如下所述。 一致的 DMA 映射接口默认返回 32 位可寻址的 DMA 地址。 即使设备指示(通过 DMA 掩码)它可以寻址高 32 位,如果已通过 dma_set_coherent_mask() 显式更改了一致的 DMA 掩码,一致分配也只会返回 > 32 位的 DMA 地址。 dma_pool 接口也是如此。 dma_alloc_coherent() 返回两个值:可用于从 CPU 访问它的虚拟地址和传递给设备的 dma_handle。 CPU 虚拟地址和 DMA 地址都保证, 大于或等于请求大小的最小 PAGE_SIZE 顺序对齐。 这个不变量的存在(例如)是为了保证如果您分配小于或等于 64 KB 的块,则您接收的缓冲区范围不会跨越 64K 边界。

代码语言:javascript
复制
创建一个dma_pool:
​
  struct dma_pool *pool;
​
  pool = dma_pool_create(name, dev, size, align, boundary);

“那么”用于诊断(如 kmem_cache 名称); dev 和 size 如之前的一样。 设备对此类数据的硬件对齐要求是“align”(以字节表示,并且必须是2的幂)。 如果您的设备没有边界穿越限制,则为边界传递 0; 传递 4096 表示从该池分配的内存不得跨越 4KByte 边界(但此时直接使用 dma_alloc_coherent() 可能更好)。

从 DMA 池中分配内存,如下所示

代码语言:javascript
复制
cpu_addr = dma_pool_alloc(pool, flags, &dma_handle)

如果允许阻塞(不允许 in_interrupt 或持有 SMP 锁),则标志为 GFP_KERNEL,否则为 GFP_ATOMIC。 与 dma_alloc_coherent() 类似,它返回两个值:cpu_addr 和 dma_handle。 从 dma_pool 释放空闲内存如下:

代码语言:javascript
复制
dma_pool_free(pool, cpu_addr, dma_handle);

其中 pool 是传递给 dma_pool_alloc() 的内容,cpu_addr 和 dma_handle 是 dma_pool_alloc() 返回的值。 该函数可以在中断上下文中调用

销毁dma_pool:

代码语言:javascript
复制
dma_pool_destroy(pool);

确保在销毁池之前已为从池分配的所有内存调用 dma_pool_free()。 该函数不能在中断上下文中调用

DMA方向

本文档后续部分中描述的接口采用 DMA 方向参数,该参数是一个整数,并采用以下值之一

代码语言:javascript
复制
 DMA_BIDIRECTIONAL
 DMA_TO_DEVICE
 DMA_FROM_DEVICE
 DMA_NONE

如果您知道的话,您应该提供准确的 DMA 方向

DMA_TO_DEVICE 表示从"主存到设备",

DMA_FROM_DEVICE 表示 “从设备到主存”,

它们是 DMA 传输过程中数据移动的方向。

强烈建议您尽可能准确地指定这一点

如果您绝对无法知道 DMA 传输的方向,请指定 DMA_BIDIRECTIONAL。 这意味着 DMA 可以朝任一方向进行。 该平台保证您可以合法地指定这一点,并且它会起作用,但这可能会以性能为代价。 值 DMA_NONE 用于调试。 在您知道精确方向之前,可以将其保存在数据结构中,这将有助于捕获方向跟踪逻辑未能正确设置的情况。 精确指定该值的另一个优点(除了潜在的特定于平台的优化之外)是为了调试。 有些平台实际上有一个写权限布尔值,可以用它来标记 DMA 映射,就像用户程序地址空间中的页面保护一样。 当 DMA 控制器硬件检测到违反权限设置时,此类平台可以并且确实会在内核日志中报告错误。 仅流映射指定方向,一致映射隐式具有 DMA_BIDIRECTIONAL 方向属性设置。 SCSI 子系统告诉您驱动程序正在处理的 SCSI 命令的“sc_data_direction”成员中使用的方向。 对于网络驱动程序来说,这是一件相当简单的事情。 对于传输数据包,使用 DMA_TO_DEVICE 方向说明符映射/取消映射它们。 对于接收数据包,恰恰相反,使用 DMA_FROM_DEVICE 方向说明符映射/取消映射它们。

使用流 DMA 映射(Streaming DMA mappings)

流 DMA 映射例程可以从中断上下文中调用。 每个映射/取消映射都有两个版本,一种将映射/取消映射单个内存区域,另一种将映射/取消映射分散列表(scatterlist)。

映射单个区域:

代码语言:javascript
复制
  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;
  }

取消映射

代码语言:javascript
复制
dma_unmap_single(dev, dma_handle, size, direction);

您应该调用 dma_mapping_error(),因为 dma_map_single() 可能会失败并返回错误。 这样做将确保映射代码在所有 DMA 实现上正确工作,而不依赖于底层实现的细节。 在不检查错误的情况下使用返回的地址可能会导致各种失败,从恐慌到静默数据损坏。 这同样适用于 dma_map_page()

当 DMA 活动完成时,您应该调用 dma_unmap_single(),例如,从通知您 DMA 传输已完成的中断中调用 dma_unmap_single()

像这样对单个映射使用 CPU 指针有一个缺点:您无法以这种方式引用 HIGHMEM 内存。 因此,存在类似于 dma{map,unmap}single() 的映射/取消映射接口对。 这些接口处理页/偏移量对而不是 CPU 指针。 具体来说

代码语言:javascript
复制
  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);

这里,“offset”是指给定页面内的字节偏移量

您应该调用 dma_mapping_error(),因为 dma_map_page() 可能会失败并返回错误,如 dma_map_single() 讨论中所述

当 DMA 活动完成时,您应该调用 dma_unmap_page(),例如,从通知您 DMA 传输已完成的中断中调用 dma_unmap_page()

使用散列表,您可以通过以下方式从多个区域映射散列表区域集

代码语言:javascript
复制
  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);
  }

其中 nents 是 sglist 中的条目数

该实现可以自由地将多个连续的 sglist 条目合并为一个(例如,如果 DMA 映射以 PAGE_SIZE 粒度完成,则任何连续的 sglist 条目都可以合并为一个,前提是第一个在页面边界上结束,第二个在页面边界上开始,事实上, 对于无法进行聚散或聚散条目数量非常有限的设备来说,这是一个巨大的优势)最后, 返回将其映射到的 sg 条目的实际数量。 失败时返回 0

然后,您应该循环 count 次(注意:这可能小于 nents 次),并在您之前访问 sg->address 和 sg->length 的位置使用 sg_dma_address() 和 sg_dma_len() 宏,如上所示

要取消映射聚散列表,只需调用

代码语言:javascript
复制
dma_unmap_sg(dev, sglist, nents, direction);

再次确保 DMA 活动已经完成

注意: dma_unmap_sg 调用的“nents”参数必须与您传递给 dma_map_sg 调用的参数相同,它不应该是从 dma_map_sg 调用返回的“计数”值

每个 dma_map{single,sg}() 调用都应该有其 dma_unmap{single,sg}() 对应项,因为 DMA 地址空间是共享资源,您可能会通过消耗所有 DMA 地址而导致机器不可用

如果您需要多次使用相同的流 DMA 区域并在 DMA 传输之间接触数据,则需要正确同步缓冲区,以便 CPU 和设备能够看到最新且正确的数据副本的DMA缓冲区

因此,首先,只需使用 dma_map_{single,sg}() 进行映射,然后在每次 DMA 传输调用之后

代码语言:javascript
复制
dma_sync_single_for_cpu(dev, dma_handle, size, direction);

代码语言:javascript
复制
dma_sync_sg_for_cpu(dev, sglist, nents, direction);

然后,如果您希望让设备再次访问DMA 区域,在实际将缓冲区提供给硬件调用之前, 完成 CPU 对数据的访问

代码语言:javascript
复制
dma_sync_single_for_device(dev, dma_handle, size, direction);

代码语言:javascript
复制
dma_sync_sg_for_device(dev, sglist, nents, direction);

注意: dma_sync_sg_for_cpu() 和 dma_sync_sg_for_device() 的“nents”参数必须与传递给 dma_map_sg() 的参数相同。 它不是 dma_map_sg() 返回的计数

最后一次 DMA 传输后,调用 DMA 取消映射例程之一 dma_unmap{single,sg}()。 如果您不接触第一个 dma_map() 调用直到 dma_unmap_() 之间的数据,那么您根本不必调用 dma_sync*() 例程。 这是伪代码,显示了您需要使用 dma_sync*() 接口的情况

代码语言:javascript
复制
  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);
      }
    }
  }

完全转换为该接口的驱动程序不应再使用 virt_to_bus(),也不应使用bus_to_virt()。 一些驱动程序必须稍作更改,因为动态 DMA 映射方案中不再有与 bus_to_virt() 等效的函数 - 您必须始终存储 dma_alloc_coherent()、dma_pool_alloc() 和 dma_map_single( ) 返回的DMA地址,即在驱动程序结构和/或设备寄存器中调用 dma_map_sg() (如果平台支持硬件中的动态 DMA 映射,则将它们存储在聚散表本身中)。 所有驱动程序都应该无一例外地使用这些接口。 计划完全删除 virt_to_bus() 和 bus_to_virt(),因为它们已完全弃用。 有些端口已经不提供这些,因为不可能正确支持它们

处理错误

DMA 地址空间在某些架构上受到限制,分配失败可以通过以下方式确定

  • 检查 dma_alloc_coherent() 是否返回 NULL 或 dma_map_sg 返回 0
  • 使用 dma_mapping_error() 检查从 dma_map_single() 和 dma_map_page() 返回的 dma_addr_t, 如:
代码语言:javascript
复制
dma_addr_t dma_handle;
​
  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_page()
代码语言:javascript
复制
Example 1::
​
  dma_addr_t dma_handle1;
  dma_addr_t dma_handle2;
​
  dma_handle1 = dma_map_single(dev, addr, size, direction);
  if (dma_mapping_error(dev, dma_handle1)) {
    /*
     * reduce current DMA mapping usage,
     * delay and try again later or
     * reset driver.
     */
    goto map_error_handling1;
  }
  dma_handle2 = dma_map_single(dev, addr, size, direction);
  if (dma_mapping_error(dev, dma_handle2)) {
    /*
     * reduce current DMA mapping usage,
     * delay and try again later or
     * reset driver.
     */
    goto map_error_handling2;
  }
​
  ...
​
  map_error_handling2:
    dma_unmap_single(dma_handle1);
  map_error_handling1:
​
Example 2::
​
  /*
   * if buffers are allocated in a loop, unmap all mapped buffers when
   * mapping error is detected in the middle
   */
​
  dma_addr_t dma_addr;
  dma_addr_t array[DMA_BUFFERS];
  int save_index = 0;
​
  for (i = 0; i < DMA_BUFFERS; i++) {
​
    ...
​
    dma_addr = dma_map_single(dev, addr, size, direction);
    if (dma_mapping_error(dev, dma_addr)) {
      /*
       * reduce current DMA mapping usage,
       * delay and try again later or
       * reset driver.
       */
      goto map_error_handling;
    }
    array[i].dma_addr = dma_addr;
    save_index++;
  }
​
  ...
​
  map_error_handling:
​
  for (i = 0; i < save_index; i++) {
​
    ...
​
    dma_unmap_single(array[i].dma_addr);
  }
​

如果传输钩子函数(hook) (ndo_start_xmit) 上的 DMA 映射失败,网络驱动程序必须调用 dev_kfree_skb() 来释放套接字缓冲区并返回 NETDEV_TX_OK。 这意味着套接字缓冲区在失败情况下被丢弃

如果队列命令挂钩中的 DMA 映射失败,SCSI 驱动程序必须返回 SCSI_MLQUEUE_HOST_BUSY。 这意味着SCSI子系统稍后再次将命令传递给驱动程序

优化取消映射状态空间消耗

在许多平台上,dma_unmap_{single,page}() 只是一个 nop。 因此,跟踪映射地址和长度是浪费空间。 不是用 ifdef 等填充驱动程序来“解决(绕过)”这个问题(这会破坏可移植 API 的全部目的),而是提供以下功能。 实际上,我们不会一一描述宏,而是转换一些示例代码

  1. 在状态保存结构中使用DEFINE_DMA_UNMAP_{ADDR,LEN}。 例如,之前
代码语言:javascript
复制
before::
  struct ring_state {
    struct sk_buff *skb;
    dma_addr_t mapping;
    __u32 len;
  };
​
   after::
​
  struct ring_state {
    struct sk_buff *skb;
    DEFINE_DMA_UNMAP_ADDR(mapping);
    DEFINE_DMA_UNMAP_LEN(len);
  };
  1. 使用 dma_unmap{addr,len}set() 设置这些值
代码语言:javascript
复制
before::
ringp->mapping = FOO;
  ringp->len = BAR;
​
   after::
​
  dma_unmap_addr_set(ringp, mapping, FOO);
  dma_unmap_len_set(ringp, len, BAR);
  1. 使用 dma_unmap_{addr,len}() 访问这些值
代码语言:javascript
复制
Example, before::
​
  dma_unmap_single(dev, ringp->mapping, ringp->len,
       DMA_FROM_DEVICE);
​
   after::
​
  dma_unmap_single(dev,
       dma_unmap_addr(ringp, mapping),
       dma_unmap_len(ringp, len),
       DMA_FROM_DEVICE);

这确实应该是不言自明的。 我们单独对待 ADDR 和 LEN,因为实现可能只需要地址即可执行取消映射操作

平台问题

如果您只是为 Linux 编写驱动程序并且不维护内核的体系结构端口,您可以安全地跳到“结束”

1)构造聚散列表(SGL)要求

如果架构支持 IOMMU(包括软件 IOMMU),则需要启用 CONFIG_NEED_SG_DMA_LENGTH

2)ARCH_DMA_MINALIGN, 基于架构DMA的最小对齐

架构必须确保 kmalloc 的缓冲区是 DMA 安全的。 驱动程序和子系统依赖于它。 如果架构不完全 DMA 一致(即硬件不能确保 CPU 缓存中的数据与主内存中的数据相同),则必须设置 ARCH_DMA_MINALIGN,以便内存分配器确保 kmalloc 缓冲区不共享 与其他缓存行。 请参阅 arch/arm/include/asm/cache.h 作为示例。 请注意,ARCH_DMA_MINALIGN 与 DMA 内存对齐约束有关。 您无需担心架构数据对齐约束(例如关于 64 位对象的对齐约束)

结束

如果没有大家的反馈和建议,也就不会有本文档及API说明。 特别感谢以下人员的贡献(排名不分先后)

代码语言:javascript
复制
Russell King <rmk@arm.linux.org.uk>
  Leo Dagum <dagum@barrel.engr.sgi.com>
  Ralf Baechle <ralf@oss.sgi.com>
  Grant Grundler <grundler@cup.hp.com>
  Jay Estabrook <Jay.Estabrook@compaq.com>
  Thomas Sailer <sailer@ife.ee.ethz.ch>
  Andrea Arcangeli <andrea@suse.de>
  Jens Axboe <jens.axboe@oracle.com>
  David Mosberger-Tang <davidm@hpl.hp.com>

参考

https://www.kernel.org/doc/Documentation/DMA-API-HOWTO.txt

https://zhuanlan.zhihu.com/p/680203187

晓兵(ssbandjl)

博客: https://cloud.tencent.com/developer/user/5060293/articles | https://logread.cn | https://blog.csdn.net/ssbandjl | https://www.zhihu.com/people/ssbandjl/posts

DPU专栏

https://cloud.tencent.com/developer/column/101987

晓兵技术杂谈(系列)

https://cloud.tencent.com/developer/user/5060293/video

本文系外文翻译,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文系外文翻译前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 简介
  • CPU 和 DMA 地址
  • 什么内存支持 DMA?
  • DMA 寻址能力
  • DMA 映射的类型
  • 使用一致的 DMA 映射
  • DMA方向
  • 使用流 DMA 映射(Streaming DMA mappings)
  • 处理错误
  • 优化取消映射状态空间消耗
  • 平台问题
  • 结束
    • 参考
      • 晓兵(ssbandjl)
        • DPU专栏
        • 晓兵技术杂谈(系列)
    领券
    问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档