动态 DMA 映射指南

作者:

David S. Miller <davem@redhat.com>

作者:

Richard Henderson <rth@cygnus.com>

作者:

Jakub Jelinek <jakub@redhat.com>

本文档为设备驱动程序编写者提供关于如何使用 DMA API 的指南,并附有伪代码示例。有关 API 的简明描述,请参阅使用通用设备的动态 DMA 映射

CPU 和 DMA 地址

DMA API 中涉及几种地址,理解它们之间的差异非常重要。

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

虚拟内存系统(TLB、页表等)将虚拟地址转换为 CPU 物理地址,这些地址存储为 “phys_addr_t” 或 “resource_size_t”。内核将设备资源(如寄存器)作为物理地址管理。这些地址位于 /proc/iomem 中。物理地址对于驱动程序没有直接用处;它必须使用 ioremap() 来映射空间并生成虚拟地址。

I/O 设备使用第三种地址:“总线地址”。如果设备的寄存器位于 MMIO 地址,或者如果它执行 DMA 来读取或写入系统内存,则设备使用的地址是总线地址。在某些系统中,总线地址与 CPU 物理地址相同,但通常情况下并非如此。IOMMU 和主机桥可以产生物理地址和总线地址之间的任意映射。

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

这是一个图片和一些示例

             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)。地址 B 存储在 struct resource 中,通常通过 /proc/iomem 公开。当驱动程序声明设备时,它通常使用 ioremap() 将物理地址 B 映射到虚拟地址 (C)。然后,它可以例如使用 ioread32(C) 来访问总线地址 A 处的设备寄存器。

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

在一些简单的系统中,设备可以直接对物理地址 Y 进行 DMA。但在许多其他系统中,存在 IOMMU 硬件将 DMA 地址转换为物理地址,例如,它将 Z 转换为 Y。这是 DMA API 的一部分原因:驱动程序可以将虚拟地址 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_*() 接口。

首先,您应该确保

#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 的通用代码集成时,更新此内容。]

此规则还意味着您不能使用内核映像地址(数据/文本/bss 段中的项)、模块映像地址或堆栈地址进行 DMA。这些都可能映射到与其余物理内存完全不同的位置。即使这些类型的内存可以在物理上与 DMA 一起使用,您也需要确保 I/O 缓冲区是缓存行对齐的。如果没有对齐,您会在具有 DMA 不一致缓存的 CPU 上看到缓存行共享问题(数据损坏)。(CPU 可以写入一个字,DMA 会写入同一缓存行中的另一个字,其中一个可能会被覆盖。)

此外,这意味着您不能获取 kmap() 调用的返回值并对其进行 DMA 操作。这类似于 vmalloc()。

块 I/O 和网络缓冲区怎么样?块 I/O 和网络子系统会确保它们使用的缓冲区对您进行 DMA 操作是有效的。

DMA 寻址能力

默认情况下,内核假设您的设备可以寻址 32 位的 DMA 寻址。对于具有 64 位功能的设备,这需要增加,而对于具有限制的设备,这需要减少。

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

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

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

int dma_set_mask_and_coherent(struct device *dev, u64 mask);

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

流式映射的设置是通过调用 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 消息。这样,如果您的驱动程序的用户报告性能不佳或甚至未检测到设备,您可以要求他们提供内核消息,以确切了解原因。

24 位寻址设备的操作方式如下:

if (dma_set_mask_and_coherent(dev, DMA_BIT_MASK(24))) {
        dev_warn(dev, "mydev: No suitable DMA available\n");
        goto ignore_this_device;
}

标准的 64 位寻址设备的操作方式如下:

dma_set_mask_and_coherent(dev, DMA_BIT_MASK(64))

当使用 DMA_BIT_MASK(64) 时,dma_set_mask_and_coherent() 永远不会返回失败。典型的错误代码如

/* Wrong code */
if (dma_set_mask_and_coherent(dev, DMA_BIT_MASK(64)))
        dma_set_mask_and_coherent(dev, DMA_BIT_MASK(32))

当大于 32 时,dma_set_mask_and_coherent() 永远不会返回失败。所以典型的代码如下

/* Recommended code */
if (support_64bit)
        dma_set_mask_and_coherent(dev, DMA_BIT_MASK(64));
else
        dma_set_mask_and_coherent(dev, DMA_BIT_MASK(32));

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

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 位,您可能会执行类似以下的操作

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() 必须是针对最具体的掩码。

以下是显示如何执行此操作的伪代码

#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 映射有两种类型

  • 一致性 DMA 映射通常在驱动程序初始化时进行映射,在结束时取消映射,硬件应保证设备和 CPU 可以并行访问数据,并且无需任何显式软件刷新即可看到彼此进行的更新。

    将“一致性”视为“同步”或“相干”。

    当前的默认值是在 DMA 空间的低 32 位中返回一致性内存。但是,为了将来的兼容性,即使此默认值对于您的驱动程序来说足够好,您也应设置一致性掩码。

    以下是使用一致性映射的一些很好的示例

    • 网卡 DMA 环描述符。

    • SCSI 适配器邮箱命令数据结构。

    • 在主内存中执行的设备固件微码。

    这些示例都需要的恒定条件是,任何 CPU 对内存的存储都应立即对设备可见,反之亦然。一致性映射保证了这一点。

    重要提示

    一致性 DMA 内存并不排除使用适当的内存屏障。CPU 可以像对普通内存一样对一致性内存的存储进行重新排序。示例:如果设备必须在第二个字更新之前看到描述符的第一个字更新,则必须执行类似以下的操作

    desc->word0 = address;
    wmb();
    desc->word1 = DESC_VALID;
    

    以便在所有平台上获得正确的行为。

    此外,在某些平台上,您的驱动程序可能需要刷新 CPU 写入缓冲区,就像它需要刷新 PCI 桥中的写入缓冲区一样(例如,在写入寄存器的值之后读取该值)。

  • 流式 DMA 映射通常针对一个 DMA 传输进行映射,在其之后立即取消映射(除非您使用下面的 dma_sync_*),并且硬件可以针对顺序访问进行优化。

    将“流式”视为“异步”或“在相干域之外”。

    以下是使用流式映射的一些很好的示例

    • 设备传输/接收的网络缓冲区。

    • SCSI 设备写入/读取的文件系统缓冲区。

    使用此类映射的接口的设计方式使得实现可以进行硬件允许的任何性能优化。为此,当使用此类映射时,您必须明确说明您希望发生的情况。

这两种类型的 DMA 映射都没有来自底层总线的对齐限制,尽管某些设备可能具有此类限制。此外,当底层缓冲区不与其他数据共享缓存行时,具有非 DMA 相干缓存的系统将更好地工作。

使用一致性 DMA 映射

要分配和映射大的(PAGE_SIZE 左右)一致性 DMA 区域,您应该执行以下操作

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 千字节的块,则您收到的缓冲区的范围不会跨越 64K 边界。

要取消映射并释放此类 DMA 区域,您需要调用

dma_free_coherent(dev, size, cpu_addr, dma_handle);

其中 dev、size 与上述调用中的相同,cpu_addr 和 dma_handle 是 dma_alloc_coherent() 返回给您的值。此函数不得在中断上下文中调用。

如果您的驱动程序需要大量较小的内存区域,您可以编写自定义代码来细分 dma_alloc_coherent() 返回的页面,或者可以使用 dma_pool API 来执行此操作。dma_pool 类似于 kmem_cache,但它使用 dma_alloc_coherent(),而不是 __get_free_pages()。此外,它还了解用于对齐的常见硬件约束,例如队列头需要对齐到 N 字节边界。

像这样创建一个 dma_pool

struct dma_pool *pool;

pool = dma_pool_create(name, dev, size, align, boundary);

“name”用于诊断(如 kmem_cache 名称);dev 和 size 如上所述。此类型数据的设备硬件对齐要求是“align”(以字节表示,必须是 2 的幂)。如果您的设备没有边界交叉限制,则为边界传递 0;传递 4096 表示从此池分配的内存不得跨越 4KByte 边界(但此时最好直接使用 dma_alloc_coherent())。

像这样从 DMA 池分配内存

cpu_addr = dma_pool_alloc(pool, flags, &dma_handle);

如果允许阻塞(不在中断中,也不持有 SMP 锁),则标志为 GFP_KERNEL;否则为 GFP_ATOMIC。与 dma_alloc_coherent() 一样,此函数返回两个值,cpu_addr 和 dma_handle。

像这样释放从 dma_pool 分配的内存

dma_pool_free(pool, cpu_addr, dma_handle);

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

通过调用以下函数销毁 dma_pool

dma_pool_destroy(pool);

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

DMA 方向

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

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 映射

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

要映射单个区域,请执行以下操作

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);

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

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

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

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_mapping_error(),因为 dma_map_page() 可能失败并返回错误,如 dma_map_single() 讨论中所述。

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

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

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);
}

其中 nentssglist 中的条目数。

实现可以自由地将几个连续的 sglist 条目合并为一个(例如,如果 DMA 映射是以 PAGE_SIZE 粒度完成的,只要第一个条目结束并且第二个条目在页面边界上开始,则可以将任何连续的 sglist 条目合并为一个 - 事实上,对于无法进行分散/聚集或具有非常有限的分散/聚集条目数量的卡来说,这是一个巨大的优势),并返回它映射到的实际 sg 条目数。如果失败,则返回 0。

然后,您应该循环 count 次(注意:这可以少于 nents 次),并使用 sg_dma_address()sg_dma_len() 宏,您之前在其中访问了 sg->addresssg->length,如上所示。

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

dma_unmap_sg(dev, sglist, nents, direction);

再次,请确保 DMA 活动已完成。

注意

传递给 dma_unmap_sg 调用的 nents 参数必须与传递给 dma_map_sg 调用的参数_相同_,它_不应_是 dma_map_sg 调用_返回_的 count 值。

每个 dma_map_{single,sg}() 调用都应该有其对应的 dma_unmap_{single,sg}() 调用,因为 DMA 地址空间是一个共享资源,您可能会因消耗所有 DMA 地址而使机器无法使用。

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

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

dma_sync_single_for_cpu(dev, dma_handle, size, direction);

或者

dma_sync_sg_for_cpu(dev, sglist, nents, direction);

根据需要。

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

dma_sync_single_for_device(dev, dma_handle, size, direction);

或者

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() 返回的 count

在最后一次 DMA 传输后,调用其中一个 DMA 取消映射例程 dma_unmap_{single,sg}()。如果您从第一个 dma_map_*() 调用到 dma_unmap_*() 都不接触数据,则根本不必调用 dma_sync_*() 例程。

以下是伪代码,显示了需要使用 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 地址空间在某些体系结构上是有限的,可以通过以下方式确定分配失败:

  • 检查 dma_alloc_coherent() 是否返回 NULL 或 dma_map_sg 是否返回 0

  • 使用 dma_mapping_error() 检查从 dma_map_single()dma_map_page() 返回的 dma_addr_t

    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()

示例 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:

示例 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);
}

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

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

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

在许多平台上,dma_unmap_{single,page}() 仅仅是一个空操作。因此,跟踪映射地址和长度会浪费空间。为了避免用 ifdef 之类的东西来填充您的驱动程序来“解决”这个问题(这将破坏可移植 API 的整个目的),提供了以下工具。

实际上,与其逐个描述宏,不如转换一些示例代码。

  1. 在状态保存结构中使用 DEFINE_DMA_UNMAP_{ADDR,LEN}。示例,之前:

    struct ring_state {
            struct sk_buff *skb;
            dma_addr_t mapping;
            __u32 len;
    };
    

    之后:

    struct ring_state {
            struct sk_buff *skb;
            DEFINE_DMA_UNMAP_ADDR(mapping);
            DEFINE_DMA_UNMAP_LEN(len);
    };
    
  2. 使用 dma_unmap_{addr,len}_set() 来设置这些值。示例,之前:

    ringp->mapping = FOO;
    ringp->len = BAR;
    

    之后:

    dma_unmap_addr_set(ringp, mapping, FOO);
    dma_unmap_len_set(ringp, len, BAR);
    
  3. 使用 dma_unmap_{addr,len}() 来访问这些值。示例,之前:

    dma_unmap_single(dev, ringp->mapping, ringp->len,
                     DMA_FROM_DEVICE);
    

    之后:

    dma_unmap_single(dev,
                     dma_unmap_addr(ringp, mapping),
                     dma_unmap_len(ringp, len),
                     DMA_FROM_DEVICE);
    

它应该是不言自明的。我们分别处理 ADDR 和 LEN,因为实现可能只需要地址即可执行取消映射操作。

平台问题

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

  1. 结构 scatterlist 的要求。

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

  2. ARCH_DMA_MINALIGN

    体系结构必须确保 kmalloc'ed 缓冲区是 DMA 安全的。驱动程序和子系统依赖于此。如果体系结构不是完全 DMA 一致的(即,硬件不确保 CPU 缓存中的数据与主内存中的数据相同),则必须设置 ARCH_DMA_MINALIGN,以便内存分配器确保 kmalloc'ed 缓冲区不与其他缓冲区共享缓存行。请参阅 arch/arm/include/asm/cache.h 作为示例。

    请注意,ARCH_DMA_MINALIGN 是关于 DMA 内存对齐约束的。您无需担心体系结构数据对齐约束(例如,关于 64 位对象的对齐约束)。

结束

如果没有众多个人的反馈和建议,本文档和 API 本身就不会是现在的形式。我们想特别提及以下人员,排名不分先后:

Russell King <[email protected]>
Leo Dagum <[email protected]>
Ralf Baechle <[email protected]>
Grant Grundler <[email protected]>
Jay Estabrook <[email protected]>
Thomas Sailer <[email protected]>
Andrea Arcangeli <[email protected]>
Jens Axboe <[email protected]>
David Mosberger-Tang <[email protected]>