动态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使用总线地址空间,但它可能仅限于该空间的一个子集。例如,即使系统为主内存和PCI BAR支持64位地址,它也可能使用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的通用代码时更新此内容。]

此规则还意味着您不能将内核映像地址(data/text/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的掩码。如果您有特殊要求,则可以改为使用以下两个单独的调用:

流式映射的设置通过调用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是指向您设备的device结构体的指针,mask是一个位掩码,描述您的设备支持地址的哪些位。通常,您设备的device结构体嵌入在您设备的特定总线device结构体中。例如,&pdev->dev是指向PCI设备的device结构体指针(pdev是指向您设备的PCI device结构体的指针)。

这些调用通常返回零,表示在给定您提供的地址掩码的情况下,您的设备可以在机器上正确执行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掩码通过dma_set_coherent_mask()明确更改后,才会返回大于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的幂)。如果您的设备没有跨越边界的限制,则为boundary传递0;传递4096表示从该池分配的内存不得跨越4KB边界(但那时最好直接使用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之前,请确保您已对从该池分配的所有内存调用了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()的映射/取消映射接口对。这些接口处理页面/偏移量对,而不是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);

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

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

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

对于scatterlists,您通过以下方式映射从多个区域收集的区域:

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_dma_address()和sg_dma_len()宏,而不是像上面那样直接访问sg->address和sg->length。

要取消映射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_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()返回的计数。

在最后一次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);
}

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

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

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

在许多平台上,dma_unmap_{single,page}()仅仅是一个空操作(nop)。因此,跟踪映射地址和长度是空间浪费。为了避免在您的驱动程序中充斥着ifdefs等来“解决”这个问题(这将违背可移植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. Struct scatterlist要求。

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

  2. ARCH_DMA_MINALIGN

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

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

结束

本文档以及API本身,若无众多人士的反馈和建议,将不会以目前的形式存在。我们想特别提及以下人员(排名不分先后):

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>