DMA使用指南

=========================

Dynamic DMA mapping Guide

作者:
David S. Miller davem@redhat.com
Richard Henderson rth@cygnus.com
Jakub Jelinek jakub@redhat.com

这是设备驱动程序编写人员如何使用DMA API和示例伪代码的指南。有关该API的简明描述,请参见DMA-API.txt。

PCI设备有很好的可配置型和易操作性,这很大方面要归功于其地址空间的可动态分配的特性。而动态分配地址空间就是依赖于BAR(base address register)实现的。

CPU和DMA地址

DMA API中涉及到几种不同的地址,理解它们之间的区别是很重要的。

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

虚拟内存系统(TLB,页表等)将虚拟地址转换为CPU物理地址,并以phys_addr_tresource_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空间以及将它们连接到系统的host桥。
例如:
如果PCI设备具有BAR,则内核从BAR读取总线地址(A),并将其转换为CPU物理地址(B)。 地址B存储在结构资源中,通常通过/proc/iomem公开。 当驱动操作设备时,通常使用ioremap()将物理地址B映射到虚拟地址(C)。 然后,它可以使用例如ioread32©访问总线地址A处的设备寄存器。

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

在一些简单的系统中,设备可以直接对物理地址 y 进行 DMA。但在许多其他地方,IOMMU 硬件将 DMA 地址转换为物理地址,例如,它将 z 转换为 y。这就是 DMA API 的部分原因: 驱动程序可以为 DMA_map_single()之类的接口提供虚拟地址 x,该接口设置所需的 IOMMU 映射并返回 DMA 地址 z。然后驱动程序告诉设备执行 DMA 到 z,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() or kmem_cache_alloc()) ,那么您可以使用从这些例程返回的地址来DMA访问该内存。

这意味着您可以不使用从 vmalloc()返回的内存/地址进行DMA。但可以将DMA映射到 vmalloc()区域的底层内存,但这需要遍历页表来获取物理地址,然后使用 _va()之类的东西将每个页转换回内核地址。

这个规则还意味着您既不能使用内核映像地址(data/text/bss 段中的项) ,也不能使用模块映像地址,也不能使用 DMA 的堆栈地址。这些都可以被映射到与其他物理内存完全不同的地方。即使这些内存类可以在物理上使用 DMA,您也需要确保 I/O 缓冲区是 cacheline 对齐的。如果不这样做,就会在 cpu 上看到使用 DMA-incoherent 缓存的 cacheline 共享问题(数据损坏)。(CPU 可以写入一个单词,DMA 可以写入同一个缓存行中的不同单词,并且其中一个可以被覆盖。)

同样,这意味着您无法返回kmap()调用和DMA往返。 这类似于vmalloc()

那么块 I/O 和网络缓冲区呢?块 I/O 和联网子系统确保它们使用的缓冲区对于 DMA from/to 来说是有效的。

DMA寻址限制

您的设备有DMA寻址限制吗? 例如,您的设备仅能驱动低序的24位地址吗?
如果是这样,则需要将此事实通知内核。

默认情况下,内核假定您的设备可以寻址完整的32位。
对于支持64位的设备,这需要增加。 如前段所述,对于有局限性的设备,需要减少它。

关于PCI的特别说明:PCI-X规范要求PCI-X设备支持所有事务的64位寻址(DAC)。
当IO总线处于PCI-X模式时,至少有一个平台(SGI SN2)需要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是描述设备支持的地址的哪些位的位掩码。
如果您的卡在给定的地址掩码的情况下可以在机器上正确执行DMA,则返回零。
通常,设备的设备结构嵌入在设备的总线特定设备结构中。
例如:&pdev->dev是指向PCI设备的设备结构的指针(pdev是指向设备的PCI设备结构的指针)。

如果返回非零值,则您的设备将无法在此平台上正确执行DMA,
并且尝试执行DMA将导致未定义的行为。 您必须使用其他掩码,或者不使用DMA。

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

1)如有可能,请使用另一个DMA掩码(请参见下文)。
2)如果可能,请使用某些非DMA模式进行数据传输。
3)忽略此设备,不要初始化它。

建议您的驱动程序在执行 # 2或 # 3时打印一条内核 KERN_warning 消息。通过这种方式,如果您的驱动程序的用户报告性能不好或者甚至没有检测到设备,您可以要求他们提供内核消息,以找出确切的原因。
标准的32位寻址设备将执行以下操作:

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

另一个常见的场景是一个64位的设备。这里的方法是尝试64位寻址,但退回到不应该失败的32位掩码。内核可能会失败64位掩码,不是因为平台不能64位寻址。相反,在这种情况下它可能会失败,因为32位寻址比64位寻址效率更高。例如,Sparc64 PCI SAC 寻址比 DAC 寻址更有效。

这是处理具有64位功能的设备的方法,该设备在访问流DMA时可以驱动所有64位:

	int using_dac;

	if (!dma_set_mask(dev, DMA_BIT_MASK(64))) {
		using_dac = 1;
	} else if (!dma_set_mask(dev, DMA_BIT_MASK(32))) {
		using_dac = 0;
	} else {
		dev_warn(dev, "mydev: No suitable DMA available\n");
		goto ignore_this_device;
	}

如果卡也能够使用64位一致分配,则情况如下所示:

int using_dac, consistent_using_dac;

if (!dma_set_mask_and_coherent(dev, DMA_BIT_MASK(64))) {
	using_dac = 1;
	consistent_using_dac = 1;
} else if (!dma_set_mask_and_coherent(dev, DMA_BIT_MASK(32))) {
	using_dac = 0;
	consistent_using_dac = 0;
} else {
	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映射有两种类型:

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

    将“一致”视为“同步”或“连贯”。 当前的默认值是在DMA空间的低32位中返回一致的内存。 但是,为了将来的兼容性,即使此默认值适合您的驱动程序,也应设置一致的掩码。
    关于使用一致映射的示例如下:

    • 网卡DMA环描述符。
    • SCSI适配器邮箱命令数据结构。
    • 设备固件微码从主存储器中执行。

    这些示例都要求不变的是,任何存储在内存中的CPU都对设备立即可见,反之亦然。
    一致的映射可确保这一点。
    重要
    一致的DMA内存并不排除使用适当的内存屏障。CPU可能会将存储重新排序到一致的内存,就像 它可能会正常存储一样。 例如: 如果设备看到描述符的第一个字在第二个字之前更新是很重要的,你必须这样做:

		desc->word0 = address;
		wmb();
		desc->word1 = DESC_VALID;
为了在所有平台上获得正确的行为。
此外,在某些平台上,驱动程序可能需要刷新 CPU 写缓冲区,就像它需要刷新 PCI 桥中的写缓冲区一样(比如在写入后读取寄存器的值)。
  1. 流式 DMA 映射,通常映射为一个 DMA 传输,映射后立即取消映射(除非您使用下面的 DMA_sync_*) ,并且硬件可以为顺序访问优化这些映射。
    可以将“流”看作“异步”或“一致性域之外”。
    使用流映射的好例子有:
    • 设备传输/接收的网络缓冲区。
    • 由 SCSI 设备写/读的文件系统缓冲区。

使用这种类型映射的接口设计成这样一种方式,即实现可以进行硬件允许的任何性能优化。为此,在使用此类映射时,必须明确说明希望发生什么。

任何一种 DMA 映射都没有来自底层总线的对齐限制,尽管有些设备可能有这样的限制。此外,当底层缓冲区不与其他数据共享缓存线时,具有非 dma 一致性缓存的系统工作得更好。

使用一致的 DMA 映射

To allocate and map large (PAGE_SIZE or so) consistent DMA regions,
you should do::
要分配和映射大型(PAGE_size左右)一致的 DMA 区域,您应该:

	dma_addr_t dma_handle;

	cpu_addr = dma_alloc_coherent(dev, size, &dma_handle, gfp);

其中dev是一个 struct device *。这可以在中断上下文中用 GFP_ATOMIC 标志调用。
size 是要分配的区域的长度,以字节为单位。

这个例程将为该区域分配 RAM,因此它的行为类似于 __get_free_pages()(但是采用大小而不是页面顺序)。如果您的驱动程序需要比页面小的区域大小,您可能更喜欢使用 dma_pool接口,如下所述。

对于非 NULL dev(non-NULL dev),一致的 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);

其中devsize与上面的调用相同,并且返回给您的值dma_alloc_coherent()cpu_addrdma_handle。这个函数不能在中断上下文中调用。

如果您的驱动程序需要很多较小的内存区域,您可以编写自定义代码来细分dma_alloc_coherent()返回的页,或者您可以使用dma_pool API 来实现这一点。dma_pool类似于 kmem_cache缓存,但它使用dma_alloc_coherent() ,而不是 __get_free_pages()。此外,它还能理解常见的硬件约束对齐,比如队列头需要在 n 个字节边界上对齐。

创建一个 dma_pool

像这样创建一个 dma_pool:

struct dma_pool *pool;

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

name用于诊断(如kmem_cache名称) ; devsize 如上所示。对于这种类型的数据,设备的硬件对齐要求是align(以字节表示,必须是2的幂)。如果您的设备没有边界跨越限制,传递0表示边界; 传递4096表示从这个池分配的内存不能跨越4KByte 边界(但是在那个时候最好直接使用 dma_alloc_coherent())。

从 DMA 池中分配内存

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

如果允许阻塞(不是in_interrupt,也不持有 SMP 锁) ,则标志为 GFP_KERNEL,否则为 GFP_ATOMIC。与dma_alloc_coherent()一样,这返回两个值: cpu_addrdma_handle

释放dma_pool中申请的内存:

dma_pool_free(pool, cpu_addr, dma_handle);

其中 pool 是传递给 dma_pool_alloc()的值,cpu_addrdma_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方向说明符map/unmap它们。对于接收数据包,恰恰相反,使用 DMA_FROM_DEVICE 方向说明符map/unmap它们。

使用流 DMA 映射

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

映射单个区域

	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接口对。这些接口处理page/offset对,而不是 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 传输已经完成的中断。

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

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 条目都可以合并为一个条目——事实上,这对于卡片来说是一个巨大的优势,因为它既不能进行分散收集,也不能进行非常有限的分散收集条目scatter-gather entries) ,并返回它映射到的 sg 条目的实际数量。失败返回0。

然后您应该使用循环计数次数(注意: 次数可以小于 nents 次) ,并使用 sg_dma_address()sg_dma_len()宏来访问前面所示的 sg->addresssg->length

解除映射散列表

dma_unmap_sg(dev, sglist, nents, direction);

同样,确保 DMA 活动已经结束。

注意:
调用的nents参数必须与传递给dma_unmap_sg 调用的nents参数相同,它不应该是从 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)) {
		/*
		 * 减少当前的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;

		/*
		 * 检查标题以查看是否要接受数据。 
		 * 但是先将DMA传输与CPU同步,以便我们看到更新的内容。
		 */
		dma_sync_single_for_cpu(&cp->dev, cp->rx_dma,
					cp->rx_len,
					DMA_FROM_DEVICE);

		/*现在可以安全地检查缓冲区了。 */
		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不应写入DMA_FROM_DEVICE映射的区域,
			 * 因此此处不需要dma_sync_single_for_device()。 
			 * 如果修改了内存,则需要DMA_BIDIRECTIONAL映射。
			 */
			give_rx_buf_to_card(cp);
		}
	}
}

完全转换为此接口的驱动程序不应再使用 virt_to_bus(),也不应该使用bus_to_virt()。有些驱动程序需要稍微修改一下,因为在动态 DMA 映射方案中不再有类似于 bus_to_virt()的等价物——您必须总是将 dma_alloc_coherent()dma_pool_alloc()返回的 DMA 地址存储起来,而 dma_map_single()调用(dma_map_sg()存储在 scatterlist本身中,如果平台在硬件中支持动态 DMA 映射) ,则存储在驱动结构和/或寄存器中。

所有驱动程序都应该使用这些接口,没有例外。计划完全删除 virt_to_bus()bus_to_virt(),因为它们完全不推荐使用。一些端口已经不提供这些功能,因为不可能正确地支持它们。

错误处理

DMA 地址空间在某些体系结构上是有限的,分配失败可以通过以下方式来确定:

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

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

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)) {
	/*
	 * 减少当前的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);
}

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

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

优化未映射状态空间消耗

在许多平台上,dma_unmap_{single,page}()只是一个 nop。因此,跟踪映射地址和长度是对空间的浪费。与其让您的驱动程序充满 ifdefs 之类的东西来“解决work around”这个问题(这会破坏可移植 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);
};
  1. 使用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);
  1. 使用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);

这应该是不言自明的。我们将 ADDRLEN 分开处理,因为一个实现只需要地址就可以执行 unmap 操作。

平台问题

如果您只是在为 Linux 编写驱动程序,而没有维护内核的体系结构端口,那么您可以安全地跳到Closing

  1. 结构散布式要求。

    如果架构支持 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位对象的对齐约束)。

  • 8
    点赞
  • 27
    收藏
    觉得还不错? 一键收藏
  • 3
    评论
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值