动态DMA映射指南 【ChatGPT】

发布时间 2023-12-09 16:48:56作者: 摩斯电码

动态DMA映射指南

作者

本指南旨在向设备驱动程序编写者介绍如何使用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和主机桥可以在物理地址和总线地址之间产生任意映射。

从设备的角度来看,DMA使用总线地址空间,但可能被限制在该空间的子集上。例如,即使系统支持64位主存储器和PCI BAR的地址,它也可能使用IOMMU,以便设备只需使用32位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设置掩码。如果您有一些特殊要求,那么可以使用以下两个单独的调用:

  • 通过调用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。

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

  • 如果可能的话,使用某种非DMA模式进行数据传输。
  • 忽略此设备并不对其进行初始化。

建议您的驱动程序在设置DMA掩码失败时打印内核KERN_WARNING消息。通过这种方式,如果您的驱动程序的用户报告性能不佳或设备甚至未被检测到,您可以要求他们提供内核消息,以便准确了解失败的原因。

标准的64位寻址设备可能会这样做:

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位,可能会是这样:

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 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的通用代码后,我们将更新这一点。]

这一规则还意味着您不能使用内核映像地址(数据/文本/未初始化数据段中的项)、模块映像地址或堆栈地址进行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的掩码。如果您有一些特殊要求,那么可以使用以下两个单独的调用:

  • 用于流式映射的设置通过调用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。

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

  • 如果可能,为数据传输使用某种非DMA模式。
  • 忽略此设备并不对其进行初始化。

建议您的驱动程序在设置DMA掩码失败时打印内核KERN_WARNING消息。通过这种方式,如果您的驱动程序的用户报告性能不佳或设备甚至未被检测到,您可以要求他们提供内核消息以准确了解失败的原因。

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

if (dma_set_mask_and_coherent(dev, DMA_BIT_MASK(64))) {
        dev_warn(dev, "mydev: 没有合适的DMA可用\n");
        goto ignore_this_device;
}

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

if (dma_set_mask(dev, DMA_BIT_MASK(64))) {
        dev_warn(dev, "mydev: 没有合适的DMA可用\n");
        goto ignore_this_device;
}

一致掩码总是能够设置与流式掩码相同或更小的掩码。然而,对于仅使用一致分配的罕见情况,需要检查dma_set_coherent_mask()的返回值。

最后,如果您的设备只能驱动低24位地址,可能会执行以下操作:

if (dma_set_mask(dev, DMA_BIT_MASK(24))) {
        dev_warn(dev, "mydev: 24位DMA寻址不可用\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: 由于DMA限制,播放已禁用\n",
               card->name);
}
if (!dma_set_mask(dev, RECORD_ADDRESS_BITS)) {
        card->record_enabled = 1;
} else {
        card->record_enabled = 0;
        dev_warn(dev, "%s: 由于DMA限制,录音已禁用\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;
    
    • 以便在所有平台上获得正确的行为。

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

  • 流式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掩码已经通过dma_set_coherent_mask()显式更改后才会返回> 32位地址。dma_pool接口也是如此。

dma_alloc_coherent()返回两个值:您可以用于从CPU访问它的虚拟地址和传递给卡的dma_handle。

CPU虚拟地址和DMA地址都保证对最小的PAGE_SIZE顺序进行了对齐,该顺序大于或等于请求的大小。这个不变性存在(例如)是为了保证如果您分配了一个小于或等于64KB的块,您收到的缓冲区的范围不会跨越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的幂)。如果您的设备没有边界交叉限制,请将boundary传递为0;传递4096表示从此池分配的内存不得跨越4K字节边界(但在那时直接使用dma_alloc_coherent()可能更好)。

从DMA池中分配内存如下:

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

如果允许阻塞(不在中断中,也不持有SMP锁),则flags为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方向。

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映射例程可以从中断上下文中调用。每个map/unmap都有两个版本,一个用于映射/取消映射单个内存区域,另一个用于映射/取消映射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)) {
        /*
         * 减少当前DMA映射的使用,
         * 延迟并稍后重试或
         * 重置驱动程序。
         */
        goto map_error_handling;
}

取消映射则可以这样做:

dma_unmap_single(dev, dma_handle, size, direction);

在调用dma_map_single()时应该调用dma_mapping_error(),因为它可能失败并返回错误。这样做将确保映射代码在所有DMA实现上都能正确工作,而不依赖于底层实现的具体细节。在不检查错误的情况下使用返回的地址可能导致从紧急情况到静默数据损坏的失败。dma_map_page()也是如此。

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

像这样使用CPU指针进行单个映射有一个缺点:您无法以这种方式引用HIGHMEM内存。因此,有一对类似于dma_{map,unmap}_single()的接口,这些接口处理的是页面/偏移对,而不是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)) {
        /*
         * 减少当前DMA映射的使用,
         * 延迟并稍后重试或
         * 重置驱动程序。
         */
        goto map_error_handling;
}

取消映射则可以这样做:

dma_unmap_page(dev, dma_handle, size, direction);

这里,“offset”表示给定页面内的字节偏移量。

在调用dma_map_page()时应该调用dma_mapping_error(),就像在dma_map_single()讨论中所概述的那样。

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

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

这里,nents是sglist中的条目数。

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

然后,您应该循环count次(注意:这可能少于nents次),并在先前访问sg->address和sg->length的地方使用sg_dma_address()和sg_dma_len()宏。

要取消映射scatterlist,只需调用:

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传输调用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)) {
                /*
                 * 减少当前DMA映射的使用,
                 * 延迟并稍后重试或
                 * 重置驱动程序。
                 */
                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)) {
        /*
         * 减少当前DMA映射使用,
         * 延迟并稍后重试,或者
         * 重置驱动程序。
         */
        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)) {
        /*
         * 减少当前DMA映射使用,
         * 延迟并稍后重试,或者
         * 重置驱动程序。
         */
        goto map_error_handling1;
}
dma_handle2 = dma_map_single(dev, addr, size, direction);
if (dma_mapping_error(dev, dma_handle2)) {
        /*
         * 减少当前DMA映射使用,
         * 延迟并稍后重试,或者
         * 重置驱动程序。
         */
        goto map_error_handling2;
}

...

map_error_handling2:
        dma_unmap_single(dma_handle1);
map_error_handling1:

示例2

/*
 * 如果缓冲区在循环中分配,当检测到映射错误时,取消所有已映射的缓冲区
 */

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)) {
                /*
                 * 减少当前DMA映射使用,
                 * 延迟并稍后重试,或者
                 * 重置驱动程序。
                 */
                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。这意味着在失败情况下,套接字缓冲区将被丢弃。

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

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

在许多平台上,dma_unmap_{single,page}()只是一个nop。因此,跟踪映射地址和长度是一种空间浪费。为了避免在驱动程序中充斥着条件编译等内容以“解决”这个问题(这将违背可移植API的初衷),提供了以下功能。

实际上,我们将转换一些示例代码,而不是逐个描述宏。

  • 在状态保存结构中使用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);
};
  • 使用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);
  • 使用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编写驱动程序,并且不维护内核的体系结构端口,可以安全地跳到“结束”。

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

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

结语

如果没有来自众多个人的反馈和建议,本文档和API本身将不会以目前的形式存在。我们特别要提到以下人员(排名不分先后):