15.4 直接内存访问
直接内存访问(Direct Memory Access,简称 DMA)是完善我们对内存问题概述的高级主题。DMA 是一种硬件机制,它允许外围组件直接与主内存进行 I/O 数据传输,而无需系统处理器的参与。使用这种机制可以大大提高设备的数据传输吞吐量,因为消除了大量的计算开销。
15.4.1 DMA 数据传输概述
在介绍编程细节之前,让我们回顾一下 DMA 传输是如何进行的。为了简化讨论,这里仅考虑输入传输。
数据传输可以通过两种方式触发:要么是软件请求数据(例如通过 read
之类的函数),要么是硬件异步地将数据推送到系统。
在第一种情况下,涉及的步骤可以总结如下:
- 当一个进程调用
read
时,驱动程序方法会分配一个 DMA 缓冲区,并指示硬件将其数据传输到该缓冲区。此时,进程进入睡眠状态。 - 硬件将数据写入 DMA 缓冲区,并在完成后引发一个中断。
- 中断处理程序获取输入数据,确认中断,并唤醒进程,此时进程就可以读取数据了。
第二种情况是当 DMA 被异步使用时。例如,数据采集设备即使没有人读取数据,也会持续推送数据。在这种情况下,驱动程序应该维护一个缓冲区,以便后续的 read
调用能够将所有累积的数据返回给用户空间。这种传输方式涉及的步骤略有不同:
- 硬件引发一个中断,宣布新数据已到达。
- 中断处理程序分配一个缓冲区,并告知硬件将数据传输到该缓冲区的位置。
- 外围设备将数据写入缓冲区,并在完成后引发另一个中断。
- 处理程序分发新数据,唤醒任何相关进程,并进行必要的清理工作。
异步方式的一种变体常见于网卡。这些网卡通常期望在与处理器共享的内存中建立一个循环缓冲区(通常称为 DMA 环形缓冲区);每个传入的数据包都会被放置在环形缓冲区的下一个可用位置,并发出一个中断信号。然后,驱动程序将网络数据包传递给内核的其他部分,并在环形缓冲区中放置一个新的 DMA 缓冲区。
在所有这些情况下,处理步骤都强调了高效的 DMA 处理依赖于中断报告。虽然可以使用轮询驱动程序来实现 DMA,但这没有意义,因为轮询驱动程序会浪费 DMA 相对于更简单的处理器驱动 I/O 所提供的性能优势。[4]
[4] 当然,凡事都有例外;请参阅 15.2.6 节,了解如何使用轮询方式最好地实现高性能网络驱动程序。
这里引入的另一个相关概念是 DMA 缓冲区。DMA 要求设备驱动程序分配一个或多个适合 DMA 的特殊缓冲区。请注意,许多驱动程序在初始化时分配它们的缓冲区,并在关闭之前一直使用它们 —— 因此,前面列表中的“分配”一词意味着“获取一个先前分配的缓冲区”。
15.4.2 分配 DMA 缓冲区
本节将从底层介绍 DMA 缓冲区的分配;我们很快会介绍一个更高级的接口,但了解这里介绍的内容仍然是个好主意。
DMA 缓冲区出现的主要问题是,当它们的大小超过一页时,它们必须在物理内存中占用连续的页面,因为设备使用 ISA 或 PCI 系统总线传输数据,而这两种总线都使用物理地址。有趣的是,这种限制不适用于 SBus(请参阅 12.5 节),它在外围总线上使用虚拟地址。一些架构也可以在 PCI 总线上使用虚拟地址,但可移植的驱动程序不能依赖这种能力。
虽然 DMA 缓冲区可以在系统启动时或运行时分配,但模块只能在运行时分配它们的缓冲区。驱动程序编写者在为 DMA 操作分配内存时必须小心,因为并非所有的内存区域都适合。特别是,在某些系统和某些设备上,高端内存可能无法用于 DMA —— 外围设备根本无法处理这么高的地址。
现代总线上的大多数设备可以处理 32 位地址,这意味着正常的内存分配对它们来说就可以正常工作。然而,一些 PCI 设备未能实现完整的 PCI 标准,无法处理 32 位地址。当然,ISA 设备仅限于 24 位地址。
对于有这种限制的设备,应该通过在 kmalloc
或 get_free_pages
调用中添加 GFP_DMA
标志,从 DMA 区域分配内存。当存在这个标志时,只会分配可以用 24 位寻址的内存。或者,你可以使用通用 DMA 层(我们很快会讨论)来分配绕过设备限制的缓冲区。
15.4.2.1 手动分配
我们已经了解到 get_free_pages
可以分配多达几兆字节的内存(因为 order
可以达到 MAX_ORDER
,目前为 11),但高阶请求即使在请求的缓冲区远小于 128KB 时也容易失败,因为随着时间的推移,系统内存会变得碎片化。[5]
[5] “碎片化”这个词通常用于磁盘,表示文件没有连续存储在磁介质上。同样的概念也适用于内存,每个虚拟地址空间会分散在物理 RAM 中,当请求一个 DMA 缓冲区时,很难获取连续的空闲页面。
当内核无法返回请求的内存量,或者你需要超过 128KB 的内存(例如,PCI 帧抓取器通常有这样的需求)时,除了返回 -ENOMEM
之外,另一种选择是在系统启动时分配内存,或者为你的缓冲区保留物理 RAM 的顶部区域。我们在 8.6 节中描述了在系统启动时分配内存的方法,但这对模块不可用。通过在系统启动时向内核传递 mem=
参数,可以保留 RAM 的顶部区域。例如,如果你有 256MB 的内存,参数 mem=255M
会使内核不使用顶部的 1MB 内存。你的模块随后可以使用以下代码来访问这样的内存:
dmabuf = ioremap (0xFF00000 /* 255M */, 0x100000 /* 1M */);
本书附带的示例代码中的分配器提供了一个简单的 API 来探测和管理这种保留的 RAM,并且已经在几种架构上成功使用。然而,当你有一个高端内存系统(即物理内存超过 CPU 地址空间所能容纳的系统)时,这种技巧就不起作用了。
当然,另一种选择是使用 GFP_NOFAIL
分配标志来分配你的缓冲区。然而,这种方法会给内存管理子系统带来巨大压力,并且有使整个系统锁定的风险;除非真的没有其他办法,否则最好避免使用这种方法。
然而,如果你要费这么大的力气来分配一个大的 DMA 缓冲区,那么考虑一些替代方案是值得的。如果你的设备支持分散/聚集 I/O,你可以将缓冲区分成较小的块进行分配,然后让设备完成其余的工作。分散/聚集 I/O 也可以在直接对用户空间进行 I/O 操作时使用,当需要一个真正巨大的缓冲区时,这可能是最好的解决方案。
15.4.3 总线地址
使用 DMA 的设备驱动程序必须与连接到接口总线的硬件进行通信,该总线使用物理地址,而程序代码使用虚拟地址。
实际上,情况比这稍微复杂一些。基于 DMA 的硬件使用总线地址,而不是物理地址。虽然在 PC 上,ISA 和 PCI 总线地址只是物理地址,但并非所有平台都是如此。有时,接口总线通过桥接电路连接,该电路将 I/O 地址映射到不同的物理地址。有些系统甚至有页面映射方案,可以使任意页面在外围总线看来是连续的。
在底层(同样,我们很快会介绍一个更高级的解决方案),Linux 内核通过导出以下在 <asm/io.h>
中定义的函数,提供了一个可移植的解决方案。强烈不建议使用这些函数,因为它们仅在具有非常简单 I/O 架构的系统上才能正常工作;尽管如此,在处理内核代码时,你可能会遇到它们。
unsigned long virt_to_bus(volatile void *address);
void *bus_to_virt(unsigned long address);
这些函数在内核逻辑地址和总线地址之间进行简单的转换。在必须对 I/O 内存管理单元进行编程或必须使用反弹缓冲区的任何情况下,它们都无法正常工作。执行这种转换的正确方法是使用通用 DMA 层,因此我们现在转向这个主题。
15.4.4 通用 DMA 层
DMA 操作归根结底就是分配一个缓冲区,并将总线地址传递给设备。然而,编写能在所有架构上安全且正确执行 DMA 的可移植驱动程序,其难度超乎想象。不同系统对于缓存一致性的工作方式有不同的机制;如果处理不当,驱动程序可能会损坏内存。有些系统拥有复杂的总线硬件,这可能会使 DMA 任务变得更简单 —— 也可能更困难。而且并非所有系统都能对内存的所有部分执行 DMA 操作。幸运的是,内核提供了一个独立于总线和架构的 DMA 层,为驱动程序开发者屏蔽了大部分此类问题。我们强烈建议您在编写的任何驱动程序中,使用该层来进行 DMA 操作。
下面的许多函数都需要一个指向 struct device
的指针。这个结构体是 Linux 设备模型中设备的底层表示。驱动程序通常无需直接与之打交道,但在使用通用 DMA 层时会用到它。通常,您可以在描述设备的特定总线结构体中找到该结构体。例如,在 struct pci_device
或 struct usb_device
中,它以 dev
字段的形式存在。第 14 章将详细介绍设备结构体。
使用以下函数的驱动程序应包含 <linux/dma - mapping.h>
。
15.4.4.1 处理复杂硬件
在尝试进行 DMA 之前,必须回答的第一个问题是,给定的设备是否能够在当前主机上执行此类操作。由于多种原因,许多设备在可寻址的内存范围上受到限制。默认情况下,内核假设您的设备可以对任何 32 位地址执行 DMA。如果不是这种情况,您应该通过调用以下函数通知内核:
int dma_set_mask(struct device *dev, u64 mask);
mask
应该显示您的设备可以寻址的位;例如,如果它仅限于 24 位,您将传递 mask
为 0x0FFFFFF
。如果使用给定的掩码可以进行 DMA,则返回值为非零;如果 dma_set_mask
返回 0,则无法对该设备使用 DMA 操作。因此,对于仅限于 24 位 DMA 操作的设备,驱动程序中的初始化代码可能如下所示:
if (dma_set_mask (dev, 0xffffff))card->use_dma = 1;
else {card->use_dma = 0; /* 我们不得不不使用 DMA */printk (KERN_WARN, "mydev: DMA not supported\n");
}
同样,如果您的设备支持正常的 32 位 DMA 操作,则无需调用 dma_set_mask
。
15.4.4.2 DMA 映射
DMA 映射是分配 DMA 缓冲区并为该缓冲区生成设备可访问地址的组合。很容易通过简单调用 virt_to_bus
来获取该地址,但有充分的理由避免这种方法。首先,合理的硬件配备有 IOMMU,它为总线提供了一组映射寄存器。IOMMU 可以使任何物理内存出现在设备可访问的地址范围内,并且可以使物理上分散的缓冲区在设备看来是连续的。利用 IOMMU 需要使用通用 DMA 层;virt_to_bus
无法胜任这项任务。
请注意,并非所有架构都有 IOMMU;特别是,流行的 x86 平台不支持 IOMMU。然而,编写良好的驱动程序无需了解它运行的 I/O 支持硬件。
在某些情况下,为设备设置一个有用的地址可能还需要建立一个反弹缓冲区。当驱动程序尝试对设备无法访问的地址(例如高端内存地址)执行 DMA 时,会创建反弹缓冲区。然后,根据需要将数据复制到反弹缓冲区并从反弹缓冲区复制出来。不用说,使用反弹缓冲区会减慢速度,但有时别无选择。
DMA 映射还必须解决缓存一致性问题。请记住,现代处理器会将最近访问的内存区域的副本保存在快速的本地缓存中;没有这个缓存,就无法实现合理的性能。如果您的设备更改了主内存的某个区域,必须使覆盖该区域的任何处理器缓存失效;否则,处理器可能会使用主内存的不正确映像,从而导致数据损坏。同样,当您的设备使用 DMA 从主内存读取数据时,必须先刷新处理器缓存中该内存的任何更改。如果程序员不小心,这些缓存一致性问题可能会导致无数难以发现的隐蔽错误。有些架构在硬件中管理缓存一致性,但其他架构需要软件支持。通用 DMA 层竭尽全力确保在所有架构上都能正常工作,但正如我们将看到的,正确的行为需要遵守一小套规则。
DMA 映射定义了一种新类型 dma_addr_t
来表示总线地址。驱动程序应该将 dma_addr_t
类型的变量视为不透明的;唯一允许的操作是将它们传递给 DMA 支持例程和设备本身。作为总线地址,如果 CPU 直接使用 dma_addr_t
,可能会导致意外问题。
PCI 代码根据 DMA 缓冲区预期存在的时间长短,区分了两种类型的 DMA 映射:
- 一致 DMA 映射:这些映射通常在驱动程序的生命周期内存在。一致缓冲区必须同时对 CPU 和外围设备可用(正如我们稍后将看到的,其他类型的映射在任何给定时间可能只对其中一个可用)。因此,一致映射必须位于缓存一致的内存中。设置和使用一致映射可能成本较高。
- 流式 DMA 映射:流式映射通常为单个操作设置。正如我们将看到的,在某些架构上,使用流式映射时可以进行显著的优化,但这些映射在访问方式上也受到更严格的规则约束。内核开发者建议在可能的情况下使用流式映射而不是一致映射。这个建议有两个原因。首先,在支持映射寄存器的系统上,每个 DMA 映射都会在总线上使用一个或多个映射寄存器。一致映射的生命周期较长,即使在不使用时也可能长时间独占这些寄存器。另一个原因是,在某些硬件上,流式映射可以以一致映射无法实现的方式进行优化。
这两种映射类型必须以不同的方式进行操作;现在是详细了解的时候了。
15.4.4.3 设置一致 DMA 映射
驱动程序可以通过调用 dma_alloc_coherent
来设置一致映射:
void *dma_alloc_coherent(struct device *dev, size_t size,dma_addr_t *dma_handle, int flag);
这个函数处理缓冲区的分配和映射。前两个参数是设备结构体和所需缓冲区的大小。该函数在两个地方返回 DMA 映射的结果。函数的返回值是缓冲区的内核虚拟地址,驱动程序可以使用该地址;同时,关联的总线地址会返回在 dma_handle
中。这个函数会处理分配,以便将缓冲区放置在适合 DMA 的位置;通常,内存只是使用 get_free_pages
进行分配(但请注意,大小是以字节为单位,而不是 order
值)。flag
参数是通常的 GFP_
值,描述了如何分配内存;通常应该是 GFP_KERNEL
(通常情况)或 GFP_ATOMIC
(在原子上下文中运行时)。
当不再需要缓冲区时(通常在模块卸载时),应该使用 dma_free_coherent
将其返回给系统:
void dma_free_coherent(struct device *dev, size_t size,void *vaddr, dma_addr_t dma_handle);
请注意,与许多通用 DMA 函数一样,这个函数要求提供所有的大小、CPU 地址和总线地址参数。
15.4.4.4 DMA 池
DMA 池是一种用于小的、一致性 DMA 映射的分配机制。通过 dma_alloc_coherent
获取的映射最小大小可能为一个页面。如果你的设备需要比这更小的 DMA 区域,那么你可能应该使用 DMA 池。当你想要对嵌入在较大结构体中的小区域执行 DMA 操作时,DMA 池也很有用。一些非常隐蔽的驱动程序错误可追溯到与小 DMA 区域相邻的结构体字段的缓存一致性问题。为避免此问题,你应该始终为 DMA 操作显式分配区域,使其远离其他非 DMA 数据结构。
DMA 池相关函数在 <linux/dmapool.h>
中定义。
在使用之前,必须通过调用以下函数来创建一个 DMA 池:
struct dma_pool *dma_pool_create(const char *name, struct device *dev, size_t size, size_t align, size_t allocation);
这里,name
是池的名称,dev
是你的设备结构体,size
是从该池分配的缓冲区大小,align
是从该池分配时所需的硬件对齐(以字节为单位),allocation
如果不为零,则是分配不应跨越的内存边界。例如,如果 allocation
传入 4096,那么从该池分配的缓冲区不会跨越 4KB 边界。
当你使用完一个池后,可以使用以下函数释放它:
void dma_pool_destroy(struct dma_pool *pool);
在销毁池之前,你应该将所有分配的内存返还给池。
使用 dma_pool_alloc
进行分配:
void *dma_pool_alloc(struct dma_pool *pool, int mem_flags, dma_addr_t *handle);
对于此调用,mem_flags
是常见的 GFP_
分配标志。如果一切顺利,将分配并返回一个内存区域(大小为创建池时指定的大小)。与 dma_alloc_coherent
一样,生成的 DMA 缓冲区的地址将作为内核虚拟地址返回,并以总线地址的形式存储在 handle
中。
不需要的缓冲区应使用以下函数返还给池:
void dma_pool_free(struct dma_pool *pool, void *vaddr, dma_addr_t addr);
15.4.4.5 设置流式 DMA 映射
由于多种原因,流式映射的接口比一致性映射更为复杂。这些映射要处理的是驱动程序已经分配好的缓冲区,因此必须处理并非由其选择的地址。在某些架构中,流式映射还可以包含多个不连续的页面以及多部分的“分散/聚集”缓冲区。出于所有这些原因,流式映射有其自己的一套映射函数。
在设置流式映射时,你必须告知内核数据的移动方向。为此定义了一些符号(类型为 enum dma_data_direction
):
- DMA_TO_DEVICE
- DMA_FROM_DEVICE
这两个符号的含义应该很直观。如果数据是发送到设备(例如,响应写系统调用),则应使用DMA_TO_DEVICE
;如果数据是发往 CPU,则使用DMA_FROM_DEVICE
标记。 - DMA_BIDIRECTIONAL
如果数据可以双向移动,则使用DMA_BIDIRECTIONAL
。 - DMA_NONE
此符号仅作为调试辅助使用。尝试使用此“方向”的缓冲区会导致内核崩溃。
你可能总是想选择 DMA_BIDIRECTIONAL
,但驱动程序开发者应避免这种做法。在某些架构中,这样选择会有性能损失。
当你有一个要传输的单缓冲区时,使用 dma_map_single
进行映射:
dma_addr_t dma_map_single(struct device *dev, void *buffer, size_t size, enum dma_data_direction direction);
返回值是你可以传递给设备的总线地址;如果出现问题,则返回 NULL
。
传输完成后,使用 dma_unmap_single
删除映射:
void dma_unmap_single(struct device *dev, dma_addr_t dma_addr, size_t size, enum dma_data_direction direction);
这里,size
和 direction
参数必须与映射缓冲区时使用的参数匹配。
流式 DMA 映射有一些重要规则:
- 缓冲区只能用于与映射时指定的方向值匹配的传输。
- 一旦缓冲区被映射,它就归设备所有,而不是处理器。在缓冲区未被取消映射之前,驱动程序不应以任何方式触碰其内容。只有在调用
dma_unmap_single
之后,驱动程序访问缓冲区内容才是安全的(稍后我们会看到一个例外情况)。尤其要注意,要写入设备的缓冲区在包含所有要写入的数据之前不能进行映射。 - 在 DMA 仍在进行时,绝不能取消缓冲区的映射,否则肯定会导致严重的系统不稳定。
你可能想知道为什么缓冲区一旦被映射,驱动程序就不能再对其进行操作了。实际上,这条规则是有道理的,原因有两个。首先,当一个缓冲区为 DMA 操作进行映射时,内核必须确保该缓冲区中的所有数据都已实际写入内存。在调用 dma_unmap_single
时,可能有些数据还在处理器的缓存中,必须显式刷新。刷新后处理器写入缓冲区的数据可能对设备不可见。
其次,考虑一下如果要映射的缓冲区位于设备无法访问的内存区域会发生什么情况。在这种情况下,有些架构会直接失败,但其他架构会创建一个反弹缓冲区。反弹缓冲区只是设备可以访问的一个单独的内存区域。如果一个缓冲区以 DMA_TO_DEVICE
方向进行映射,并且需要反弹缓冲区,那么原始缓冲区的内容会作为映射操作的一部分被复制。显然,复制后对原始缓冲区所做的更改设备是看不到的。同样,对于 DMA_FROM_DEVICE
的反弹缓冲区,dma_unmap_single
会将其内容复制回原始缓冲区;在复制完成之前,设备的数据不会出现在原始缓冲区中。
顺便说一下,反弹缓冲区是正确指定方向很重要的一个原因。DMA_BIDIRECTIONAL
反弹缓冲区在操作前后都会进行复制,这通常是对 CPU 周期的不必要浪费。
偶尔,驱动程序需要在不取消映射的情况下访问流式 DMA 缓冲区的内容。为此提供了一个调用:
void dma_sync_single_for_cpu(struct device *dev, dma_handle_t bus_addr, size_t size, enum dma_data_direction direction);
在处理器访问流式 DMA 缓冲区之前,应该调用此函数。调用之后,CPU “拥有” 该 DMA 缓冲区,并可以根据需要对其进行操作。然而,在设备访问该缓冲区之前,应该使用以下函数将所有权转回给设备:
void dma_sync_single_for_device(struct device *dev, dma_handle_t bus_addr, size_t size, enum dma_data_direction direction);
调用此函数之后,处理器不应再访问该 DMA 缓冲区。
15.4.4.6 单页流式映射
偶尔,你可能想为一个你有 struct page
指针的缓冲区设置映射;例如,使用 get_user_pages
映射的用户空间缓冲区就可能出现这种情况。要使用 struct page
指针设置和拆除流式映射,请使用以下函数:
dma_addr_t dma_map_page(struct device *dev, struct page *page,unsigned long offset, size_t size,enum dma_data_direction direction);void dma_unmap_page(struct device *dev, dma_addr_t dma_address, size_t size, enum dma_data_direction direction);
offset
和 size
参数可用于映射页面的一部分。不过,建议除非你确实知道自己在做什么,否则应避免进行部分页面映射。如果分配只覆盖了缓存行的一部分,映射页面的一部分可能会导致缓存一致性问题;而这反过来又可能导致内存损坏和极难调试的错误。
15.4.4.7 分散/聚集映射
分散/聚集映射是一种特殊类型的流式 DMA 映射。假设你有多个缓冲区,所有这些缓冲区都需要与设备进行数据传输。这种情况可能通过多种方式出现,包括 readv
或 writev
系统调用、集群磁盘 I/O 请求,或者映射的内核 I/O 缓冲区中的页面列表。你可以依次映射每个缓冲区并执行所需操作,但一次性映射整个列表有一些优势。
许多设备可以接受一个包含数组指针和长度的分散列表,并在一次 DMA 操作中传输所有数据;例如,如果数据包可以分多个部分构建,“零拷贝” 网络就会更容易实现。将分散列表作为一个整体进行映射的另一个原因是利用总线硬件中具有映射寄存器的系统。在这样的系统中,从设备的角度来看,物理上不连续的页面可以组合成一个单一的连续数组。这种技术只有在分散列表中的条目长度(除了第一个和最后一个)等于页面大小时才有效,但当它起作用时,它可以将多个操作合并为一次 DMA,从而相应地提高速度。
最后,如果必须使用反弹缓冲区,将整个列表合并为一个单一的缓冲区是有意义的(因为反正都要进行复制)。
现在你应该相信,在某些情况下,对分散列表进行映射是值得的。映射分散列表的第一步是创建并填充一个 struct scatterlist
数组,用于描述要传输的缓冲区。这个结构体依赖于架构,在 <asm/scatterlist.h>
中有描述。不过,它总是包含三个字段:
struct page *page;
对应于分散/聚集操作中要使用的缓冲区的 struct page
指针。
unsigned int length;
unsigned int offset;
该缓冲区的长度以及它在页面内的偏移量。
要映射一个分散/聚集 DMA 操作,你的驱动程序应该为每个要传输的缓冲区设置 struct scatterlist
条目中的 page
、offset
和 length
字段。然后调用:
int dma_map_sg(struct device *dev, struct scatterlist *sg, int nents,enum dma_data_direction direction)
其中,nents
是传入的分散列表条目的数量。返回值是要传输的 DMA 缓冲区的数量,它可能小于 nents
。
对于输入分散列表中的每个缓冲区,dma_map_sg
会确定要提供给设备的正确总线地址。作为该任务的一部分,它还会合并内存中相邻的缓冲区。如果你的驱动程序运行的系统有 I/O 内存管理单元,dma_map_sg
还会对该单元的映射寄存器进行编程,这样从设备的角度来看,你就能够传输一个单一的连续缓冲区。不过,在调用之后你才会知道最终的传输情况如何。
你的驱动程序应该传输 pci_map_sg
返回的每个缓冲区。每个缓冲区的总线地址和长度存储在 struct scatterlist
条目中,但它们在结构体中的位置因架构而异。定义了两个宏来编写可移植代码:
dma_addr_t sg_dma_address(struct scatterlist *sg);
返回此分散列表条目的总线(DMA)地址。
unsigned int sg_dma_len(struct scatterlist *sg);
返回此缓冲区的长度。
同样,请记住,要传输的缓冲区的地址和长度可能与传递给 dma_map_sg
的不同。
传输完成后,使用以下调用取消分散/聚集映射:
void dma_unmap_sg(struct device *dev, struct scatterlist *list,int nents, enum dma_data_direction direction);
请注意,nents
必须是你最初传递给 dma_map_sg
的条目数量,而不是该函数返回给你的 DMA 缓冲区数量。
分散/聚集映射是流式 DMA 映射,适用于单缓冲区流式映射的访问规则同样适用于它们。如果你必须访问一个已映射的分散/聚集列表,你必须先对其进行同步:
void dma_sync_sg_for_cpu(struct device *dev, struct scatterlist *sg,int nents, enum dma_data_direction direction);
void dma_sync_sg_for_device(struct device *dev, struct scatterlist *sg,int nents, enum dma_data_direction direction);
15.4.4.8 PCI 双地址周期映射
通常,DMA 支持层处理 32 位总线地址,可能会受到特定设备的 DMA 掩码的限制。然而,PCI 总线还支持一种 64 位寻址模式,即双地址周期(DAC)。通用 DMA 层不支持这种模式,原因有几个,首先它是 PCI 特有的功能。此外,许多 DAC 实现最多只能算有缺陷,而且由于 DAC 比普通的 32 位 DMA 慢,可能会有性能损失。即便如此,在某些应用场景中使用 DAC 可能是正确的选择;如果你有一个设备可能要处理放置在高端内存中的非常大的缓冲区,你可能需要考虑实现 DAC 支持。这种支持仅适用于 PCI 总线,因此必须使用 PCI 特定的例程。
要使用 DAC,你的驱动程序必须包含 <linux/pci.h>
。你必须设置一个单独的 DMA 掩码:
int pci_dac_set_dma_mask(struct pci_dev *pdev, u64 mask);
只有当此调用返回 0 时,你才能使用 DAC 寻址。
一种特殊类型(dma64_addr_t
)用于 DAC 映射。要建立这样的映射,调用 pci_dac_page_to_dma
:
dma64_addr_t pci_dac_page_to_dma(struct pci_dev *pdev, struct page *page, unsigned long offset, int direction);
你会注意到,DAC 映射只能从 struct page
指针创建(毕竟它们应该位于高端内存中,否则使用它们就没有意义);必须一次创建一个页面的映射。direction
参数是通用 DMA 层中使用的 enum dma_data_direction
的 PCI 等效项;它应该是 PCI_DMA_TODEVICE
、PCI_DMA_FROMDEVICE
或 PCI_DMA_BIDIRECTIONAL
。
DAC 映射不需要外部资源,因此使用后无需显式释放。然而,必须像对待其他流式映射一样对待 DAC 映射,并遵守关于缓冲区所有权的规则。有一组用于同步 DMA 缓冲区的函数,类似于通用类型的函数:
void pci_dac_dma_sync_single_for_cpu(struct pci_dev *pdev,dma64_addr_t dma_addr,size_t len,int direction);void pci_dac_dma_sync_single_for_device(struct pci_dev *pdev,dma64_addr_t dma_addr,size_t len,int direction);
15.4.4.9 一个简单的 PCI DMA 示例
为了说明如何使用 DMA 映射,下面给出一个针对 PCI 设备的简单 DMA 编程示例。PCI 总线上的实际 DMA 操作形式很大程度上取决于所驱动的设备。因此,这个示例并不适用于任何实际的设备,而是一个名为 dad(DMA 采集设备)的假想驱动的一部分。该设备的驱动可能会定义如下的传输函数:
int dad_transfer(struct dad_dev *dev, int write, void *buffer, size_t count)
{dma_addr_t bus_addr;/* 为 DMA 操作映射缓冲区 */dev->dma_dir = (write ? DMA_TO_DEVICE : DMA_FROM_DEVICE);dev->dma_size = count;bus_addr = dma_map_single(&dev->pci_dev->dev, buffer, count, dev->dma_dir);dev->dma_addr = bus_addr;/* 配置设备 */writeb(dev->registers.command, DAD_CMD_DISABLEDMA);writeb(dev->registers.command, write ? DAD_CMD_WR : DAD_CMD_RD);writel(dev->registers.addr, cpu_to_le32(bus_addr));writel(dev->registers.len, cpu_to_le32(count));/* 启动操作 */writeb(dev->registers.command, DAD_CMD_ENABLEDMA);return 0;
}
这个函数对要传输的缓冲区进行映射,并启动设备操作。另一部分工作则要在中断服务例程中完成,如下所示:
void dad_interrupt(int irq, void *dev_id, struct pt_regs *regs)
{struct dad_dev *dev = (struct dad_dev *) dev_id;/* 确保是我们的设备触发的中断 *//* 取消 DMA 缓冲区的映射 */dma_unmap_single(dev->pci_dev->dev, dev->dma_addr, dev->dma_size, dev->dma_dir);/* 只有此时访问缓冲区、将数据复制给用户等操作才是安全的 */...
}
显然,这个示例省略了很多细节,包括为防止同时启动多个 DMA 操作可能需要采取的步骤。
15.4.5 ISA 设备的 DMA
ISA 总线支持两种类型的 DMA 传输:原生 DMA 和 ISA 总线主控 DMA。原生 DMA 使用主板上的标准 DMA 控制器电路来驱动 ISA 总线上的信号线。而 ISA 总线主控 DMA 则完全由外围设备处理。后一种类型的 DMA 很少使用,这里无需讨论,因为至少从驱动程序的角度来看,它与 PCI 设备的 DMA 类似。一个 ISA 总线主控设备的例子是 1542 SCSI 控制器,其驱动程序位于内核源码的 drivers/scsi/aha1542.c
中。
就原生 DMA 而言,ISA 总线上的 DMA 数据传输涉及三个实体:
- 8237 DMA 控制器(DMAC):该控制器保存着 DMA 传输的相关信息,如传输方向、内存地址和传输大小。它还包含一个计数器,用于跟踪正在进行的传输状态。当控制器接收到 DMA 请求信号时,它会获得总线控制权,并驱动信号线,以便设备能够读写数据。
- 外围设备:设备准备好传输数据时,必须激活 DMA 请求信号。实际的传输由 DMAC 管理;当控制器对设备进行选通操作时,硬件设备会将数据顺序地读写到总线上。传输结束后,设备通常会触发一个中断。
- 设备驱动程序:驱动程序的工作相对较少;它为 DMA 控制器提供传输方向、总线地址和传输大小。它还与外围设备进行交互,为数据传输做好准备,并在 DMA 传输结束时响应中断。
最初在 PC 中使用的 DMA 控制器可以管理四个“通道”,每个通道与一组 DMA 寄存器相关联。四个设备可以同时将它们的 DMA 信息存储在控制器中。较新的 PC 包含相当于两个 DMAC 设备的电路 [6]:第二个控制器(主控制器)连接到系统处理器,第一个(从控制器)连接到第二个控制器的通道 0 [7]。
[6] 这些电路现在是主板芯片组的一部分,但几年前它们是两个独立的 8237 芯片。
[7] 最初的 PC 只有一个控制器;第二个控制器是在基于 286 的平台中添加的。然而,第二个控制器作为主控制器连接,因为它处理 16 位传输;第一个控制器一次只传输 8 位,用于向后兼容。
通道编号从 0 到 7:通道 4 对 ISA 外围设备不可用,因为它用于将从控制器级联到主控制器。因此,可用的通道是从控制器上的 0 - 3(8 位通道)和主控制器上的 5 - 7(16 位通道)。存储在控制器中的任何 DMA 传输大小是一个 16 位数字,表示总线周期的数量。因此,从控制器的最大传输大小为 64 KB(因为它在一个周期内传输 8 位),主控制器的最大传输大小为 128 KB(它进行 16 位传输)。
由于 DMA 控制器是一个系统级资源,内核会协助管理它。内核使用一个 DMA 注册表为 DMA 通道提供请求和释放机制,并提供一组函数来配置 DMA 控制器中的通道信息。
15.4.5.1 注册 DMA 使用
你应该已经熟悉内核注册表了——我们已经了解过 I/O 端口和中断线的注册表。DMA 通道注册表与其他注册表类似。包含 <asm/dma.h>
头文件后,可以使用以下函数来获取和释放 DMA 通道的所有权:
int request_dma(unsigned int channel, const char *name);
void free_dma(unsigned int channel);
channel
参数是一个介于 0 到 7 之间的数字,更准确地说,是一个小于 MAX_DMA_CHANNELS
的正整数。在 PC 上,MAX_DMA_CHANNELS
被定义为 8 以匹配硬件。name
参数是一个标识设备的字符串。指定的名称会出现在 /proc/dma
文件中,用户程序可以读取该文件。
request_dma
的返回值为 0 表示成功,如果出现错误则返回 -EINVAL
或 -EBUSY
。前者表示请求的通道超出范围,后者表示另一个设备正在占用该通道。
我们建议你像对待 I/O 端口和中断线一样谨慎对待 DMA 通道;在设备打开时请求通道比在模块初始化函数中请求要好得多。延迟请求可以实现驱动程序之间的一些共享;例如,只要声卡和模拟 I/O 接口不同时使用,它们就可以共享 DMA 通道。
我们还建议你在请求中断线之后再请求 DMA 通道,并在释放中断线之前释放 DMA 通道。这是请求这两种资源的常规顺序;遵循这个惯例可以避免可能的死锁。请注意,每个使用 DMA 的设备也需要一条 IRQ 线;否则,它无法发出数据传输完成的信号。
在典型情况下,打开设备的代码如下,这里以我们假想的 dad 模块为例。所示的 dad 设备使用一个不支持共享 IRQ 线的快速中断处理程序。
int dad_open (struct inode *inode, struct file *filp)
{struct dad_device *my_device; /* ... */if ( (error = request_irq(my_device.irq, dad_interrupt,SA_INTERRUPT, "dad", NULL)) )return error; /* 或者实现阻塞式打开 */if ( (error = request_dma(my_device.dma, "dad")) ) {free_irq(my_device.irq, NULL);return error; /* 或者实现阻塞式打开 */}/* ... */return 0;
}
与上述打开操作对应的关闭操作代码如下:
void dad_close (struct inode *inode, struct file *filp)
{struct dad_device *my_device;/* ... */free_dma(my_device.dma);free_irq(my_device.irq, NULL);/* ... */
}
在安装了声卡的系统上,/proc/dma
文件的内容如下:
merlino% cat /proc/dma1: Sound Blaster84: cascade
有趣的是,默认的声卡驱动在系统启动时获取 DMA 通道,并且从不释放它。cascade
条目是一个占位符,表示如前所述,通道 4 对驱动程序不可用。
15.4.5.2 与 DMA 控制器通信
注册之后,驱动程序的主要工作是正确配置 DMA 控制器以进行操作。这项任务并不简单,但幸运的是,内核导出了典型驱动程序所需的所有函数。
驱动程序需要在调用 read
或 write
时,或者在为异步传输做准备时配置 DMA 控制器。后一项任务根据驱动程序及其实现的策略,要么在设备打开时执行,要么响应 ioctl
命令执行。这里展示的代码是 read
或 write
设备方法通常会调用的代码。
本小节简要概述了 DMA 控制器的内部原理,以便你理解这里介绍的代码。如果你想了解更多,我们建议你阅读 <asm/dma.h>
和一些描述 PC 架构的硬件手册。特别是,我们不讨论 8 位与 16 位数据传输的问题。如果你正在为 ISA 设备板编写设备驱动程序,你应该在设备的硬件手册中找到相关信息。
DMA 控制器是一个共享资源,如果多个处理器试图同时对其进行编程,可能会导致混乱。因此,控制器由一个名为 dma_spin_lock
的自旋锁保护。驱动程序不应直接操作该锁;不过,提供了两个函数来帮助你完成此操作:
unsigned long claim_dma_lock( );
获取 DMA 自旋锁。此函数还会屏蔽本地处理器上的中断;因此,返回值是一组描述先前中断状态的标志;当你使用完锁后,必须将其传递给以下函数以恢复中断状态。
void release_dma_lock(unsigned long flags);
释放 DMA 自旋锁并恢复先前的中断状态。
在使用接下来描述的函数时,应该持有自旋锁。不过,在实际的 I/O 操作期间不应持有该锁。驱动程序在持有自旋锁时绝不能进入睡眠状态。
必须加载到控制器中的信息包括三项:RAM 地址、必须传输的原子项数量(以字节或字为单位)以及传输方向。为此,<asm/dma.h>
导出了以下函数:
void set_dma_mode(unsigned int channel, char mode);
指示通道是必须从设备读取数据(DMA_MODE_READ
)还是向设备写入数据(DMA_MODE_WRITE
)。还有第三种模式 DMA_MODE_CASCADE
,用于释放总线控制权。级联是第一个控制器连接到第二个控制器的方式,但真正的 ISA 总线主控设备也可以使用它。这里我们不讨论总线主控。
void set_dma_addr(unsigned int channel, unsigned int addr);
指定 DMA 缓冲区的地址。该函数将 addr
的最低 24 位存储在控制器中。addr
参数必须是总线地址(请参阅本章前面的 15.4.3 节)。
void set_dma_count(unsigned int channel, unsigned int count);
指定要传输的字节数。对于 16 位通道,count
参数同样表示字节数;在这种情况下,该数字必须为偶数。
除了这些函数之外,在处理 DMA 设备时还必须使用一些辅助功能:
void disable_dma(unsigned int channel);
可以在控制器中禁用一个 DMA 通道。在配置控制器之前,应该禁用该通道以防止操作不当。(否则,可能会发生数据损坏,因为控制器是通过 8 位数据传输进行编程的,因此前面的任何函数都不是原子执行的。)
void enable_dma(unsigned int channel);
该函数告知控制器 DMA 通道包含有效数据。
int get_dma_residue(unsigned int channel);
驱动程序有时需要知道 DMA 传输是否已完成。该函数返回仍需传输的字节数。传输成功完成后返回值为 0,而在控制器工作时返回值是不可预测的(但不为 0)。这种不可预测性源于需要通过两次 8 位输入操作来获取 16 位的剩余字节数。
void clear_dma_ff(unsigned int channel)
该函数清除 DMA 触发器。触发器用于控制对 16 位寄存器的访问。寄存器通过两次连续的 8 位操作进行访问,触发器用于选择最低有效字节(当它为清除状态时)或最高有效字节(当它为设置状态时)。传输 8 位数据后,触发器会自动翻转;程序员在访问 DMA 寄存器之前必须清除触发器(将其设置为已知状态)。
使用这些函数,驱动程序可以实现如下函数来为 DMA 传输做准备:
int dad_dma_prepare(int channel, int mode, unsigned int buf,unsigned int count)
{unsigned long flags;flags = claim_dma_lock( );disable_dma(channel);clear_dma_ff(channel);set_dma_mode(channel, mode);set_dma_addr(channel, virt_to_bus(buf));set_dma_count(channel, count);enable_dma(channel);release_dma_lock(flags);return 0;
}
然后,可以使用如下函数检查 DMA 是否成功完成:
int dad_dma_isdone(int channel)
{int residue;unsigned long flags = claim_dma_lock ( );residue = get_dma_residue(channel);release_dma_lock(flags);return (residue == 0);
}
剩下要做的唯一事情就是配置设备板。这个特定于设备的任务通常包括读写几个 I/O 端口。不同的设备有很大差异。例如,一些设备要求程序员告知硬件 DMA 缓冲区的大小,有时驱动程序必须读取设备中硬编码的值。要配置设备板,硬件手册是你唯一的参考。