特定媒体市场区域

直接内存访问 (DMA) 功能可让硬件无需 CPU 干预即可访问内存。 在最高级别,系统会为硬件提供要传输的内存区域的来源和目标位置(及其大小),并告知硬件复制数据。某些硬件外围设备甚至支持执行多个“分散 / 收集”式操作,可以逐个执行多个复制操作,而无需额外的 CPU 干预。

DMA 注意事项

为了充分了解所涉及的问题,请务必注意以下几点:

  • 每个进程都在虚拟地址空间中运行,
  • MMU 可以将一个连续的虚拟地址范围映射到多个不连续的物理地址范围(反之亦然),
  • 每个进程的物理地址空间都有限,
  • 某些外围设备通过输入 / 输出内存管理单元 (IOMMU) 支持自己的虚拟地址。

下面我们来依次讨论每个要点。

虚拟地址、物理地址和设备物理地址

进程有权访问的地址是虚拟地址;也就是说,它们是由 CPU 的内存管理单元 (MMU) 创建的幻觉。 虚拟地址由 MMU 映射到实际地址。映射粒度基于名为“页面大小”的参数,该参数至少为 4k 字节,但新型处理器上支持更大的大小。

图:虚拟地址与物理地址之间的关系

在上图中,我们展示了一个特定进程(进程 12)和多个虚拟地址(以蓝色显示)。MMU 负责将蓝色虚拟地址映射到 CPU 物理总线地址(红色)。每个进程都有自己的映射;因此,即使进程 12 具有虚拟地址 300,其他一些进程也可能具有虚拟地址 300。该其他进程的虚拟地址 300(如果存在)将映射到与进程 12 中的地址不同的物理地址。

请注意,为便于讨论,我们将小十进制数字用作“地址”。 实际上,上面显示的每个方格都代表一个内存页(4k 或更多),并由 32 位或 64 位值(具体取决于平台)标识。

图中所示的要点如下:

  1. 虚拟地址可以按组分配(显示了三个,300-303420-421770-771),
  2. 几乎连续的(例如,300-303)不一定物理连续。
  3. 某些虚拟地址未映射(例如,没有虚拟地址 304
  4. 并非所有物理地址都可供每个进程使用(例如,进程 12 无权访问物理地址 120)。

根据平台上可用的硬件,设备的地址空间不一定遵循类似的转换。如果没有 IOMMU,外围设备使用的地址与 CPU 使用的物理地址相同:

图:不使用 IOMMU 的设备

在上图中,部分设备地址空间(例如,帧缓冲区或控制寄存器)直接出现在 CPU 的物理地址范围内。也就是说,设备会占用 122125(含)之间的物理地址。

为了让该进程访问设备的内存,需要创建一个从某些虚拟地址 122125 到物理地址的 MMU 映射。具体方法如下所述。

但对于 IOMMU,外围设备看到的地址可能与 CPU 的物理地址不同:

图:使用 IOMMU 的设备

在这里,设备有自己的“设备物理”地址,即地址 03(含边界值)。IOMMU 会将设备物理地址 03 分别映射到 CPU 物理地址 109110101119

在这种情况下,为了使进程使用设备的内存,需要安排两个映射:

  • (例如,300303)通过 MMU 传输到 CPU 物理地址空间(分别为 109110101119),以及
  • 通过 IOMMU 从 CPU 物理地址空间(地址 109110101119)到设备物理地址(03)的一个集合。

虽然这看起来可能很复杂,但 Zircon 提供了一个可以消除复杂性的抽象。

此外,下面我们将看到,使用 IOMMU 的原因及其提供的优势,也与使用 MMU 获得的好处类似。

内存连续性

当您分配一大块内存(例如使用 calloc())时,您的进程当然会看到一个大而连续的虚拟地址范围。MMU 会在虚拟寻址级别制造连续内存的假象,即使 MMU 可能会选择在物理地址级别使用物理不连续的内存为该内存区域提供支持。

此外,随着进程分配和取消分配内存,物理内存到虚拟地址空间的映射往往会变得更加复杂,从而促使出现更多的“瑞士奶酪”洞(即映射中出现更多不连续的情况)。

因此,请务必注意,连续的虚拟地址不一定是连续的物理地址,连续的物理内存的确会随着时间的推移变得更加珍贵。

访问权限控制

MMU 的另一个好处是进程对其物理内存的视图有限(出于安全性和可靠性的考虑)。不过,对驱动程序的影响在于,进程必须明确请求从虚拟地址空间到物理地址空间的映射,并且拥有执行此操作所需的特权。

IOM

通常首选连续物理内存。进行一次传输(具有一个源地址和一个目的地地址)比设置和管理多项传输(可能需要 CPU 干预才能设置下一次传输)更为高效。

IOMMU(如果有)可对外围设备执行与 CPU 的 MMU 为进程执行的操作相同,从而缓解此问题;它通过将多个不连续块映射到一个几乎连续的空间,给外设营造一个在处理连续地址空间的假象。通过限制映射区域,IOMMU 还会阻止外设访问不在当前操作“范围内”的内存,从而提供安全性(与 MMU 的方式相同)。

综合应用

因此,在编写驱动程序时,您可能需要考虑虚拟、物理和设备-物理地址空间。但事实并非如此。

DMA 和您的驱动程序

Zircon 提供了一组函数,可让您彻底地处理上述所有操作。以下各项可以搭配使用:

  • 总线事务发起者 (BTI);以及
  • 虚拟内存对象 (VMO)。

BTI 内核对象提供了模型的抽象,以及用于处理与 VMO 关联的物理(或设备-物理)地址的 API。

在驱动程序的初始化中,调用 Pci::GetBti() 以获取 BTI 句柄:

#include <lib/device-protocol/pci.h>

zx_status_t Pci::GetBti(uint32_t index,
                        zx::bti* out_bti);

GetBti() 函数位于 ddk::Pci 类(就像前面讨论的所有其他 PCI 函数一样)并采用 index 参数(预留供将来使用,使用 0)。该函数通过 out_bti 指针参数返回 BTI 对象。

接下来,您需要一个 VMO。简单来说,您可以将 VMO 视为指向内存块的指针,但它不仅限于此:它是一个内核对象,代表一组虚拟页面(可能没有提交物理页面),虚拟页面可以映射到驱动程序进程的虚拟地址空间。(甚至更多,但那是针对其他章节的讨论。)

最终,这些网页将充当 DMA 传输的来源或目标位置。

有两个函数(zx_vmo_create()zx_vmo_create_contiguous()),可分配内存并将其绑定到 VMO

zx_status_t zx_vmo_create(uint64_t size,
                          uint32_t options,
                          zx_handle_t* out);

zx_status_t zx_vmo_create_contiguous(zx_handle_t bti,
                                     size_t size,
                                     uint32_t alignment_log2,
                                     zx_handle_t* out);

如您所见,它们都接受指示所需字节数的 size 参数,并且都返回一个 VMO(通过 out)。它们都针对给定大小分配几乎连续的页面。

请注意,这与标准 C 库的内存分配函数(例如 malloc())不同,后者分配几乎连续的内存,而不考虑页面边界。例如,一行中的两个小 malloc() 调用可能会分配同一页面中的两个内存区域,而 VMO 创建函数将始终分配从新页面开始分配的内存。

zx_vmo_create_contiguous() 函数的作用与 zx_vmo_create() 的作用相同,还能确保页面经过适当整理,适合与指定的 BTI 结合使用(正因如此,它需要 BTI 句柄)。此外,它还具有一个 alignment_log2 参数,可用于指定最低对齐要求。顾名思义,它必须是 2 的整数次方(值 0 表示页面对齐)。

此时,您对已分配内存有两个“视图”:

  • 一个连续的虚拟地址空间,从驱动程序的角度看表示内存,以及
  • 一组(可能是连续、可能是已提交的)物理页面,供外围设备使用。

在使用这些页面之前,您需要确保它们位于内存中(即“已提交”,您的进程可以访问物理页面),并且外围设备可以访问这些页面(通过 IOMMU(如果存在)。 此外,您还需要获取页面地址(从设备的角度),以便对设备上的 DMA 控制器进行编程以访问这些页面。

zx_bti_pin() 函数用于执行所有这些操作:

#include <zircon/syscalls.h>

zx_status_t zx_bti_pin(zx_handle_t bti, uint32_t options,
                       zx_handle_t vmo, uint64_t offset, uint64_t size,
                       zx_paddr_t* addrs, size_t addrs_count,
                       zx_handle_t* pmt);

此函数有 8 个参数:

参数 目的
bti 此外围设备的 BTI
options 选项(见下文)
vmo 此内存区域的 VMO
offset 相对于 VMO 开头的偏移量
size VMO 中的总字节数
addrs 退货地址列表
addrs_count addrs 中的元素数量
pmt 返回了 PMT(见下文)

addrs 参数是指向您提供的 zx_paddr_t 数组的指针。这是每个页面的外围设备地址返回到的位置。 该数组的长度为 addrs_count 个元素,且必须与 zx_bti_pin() 中预期的元素数量相匹配。

写入 addrs 的值适用于对外设的 DMA 控制器进行编程 - 也就是说,这些值会考虑 IOMMU 可执行的任何转换(如果存在)。

从技术角度来说,zx_bti_pin() 的另一个影响是内核将确保这些页面在固定时不会被停用(即移动或重复使用)。

options 参数实际上是选项位图:

选项 目的
ZX_BTI_PERM_READ 外围设备可读取页面(由驱动程序写入)
ZX_BTI_PERM_WRITE 外设可以写入页面(由驱动程序读取)
ZX_BTI_COMPRESS (请参阅下面的“最低连续性属性”)

例如,请参阅上面显示“设备 3”的图表。如果存在 IOMMU,则 addrs 将包含 0123(即设备物理地址)。 如果不存在 IOMMU,则 addrs 将包含 109110101119(即物理地址)。

权限

请注意,这些权限是针对外围设备(而非驱动程序)的。 例如,在块设备写入操作中,设备从内存页面读取,因此驱动程序指定 ZX_BTI_PERM_READ,反之亦然。

最低连续性属性

默认情况下,通过 addrs 返回的每个地址均为一页。通过在 options 参数中设置 ZX_BTI_COMPRESS 选项,可以请求更大的数据块。在这种情况下,返回的每个条目的长度均与“最小连续性”属性相对应。虽然您无法设置此属性,但可以使用 zx_object_get_info() 进行读取。实际上,最小连续性属性可以保证 zx_bti_pin() 始终能够返回至少连续的字节数。

例如,如果属性的值为 1MB,则请求大小为 2MB 的 zx_bti_pin() 调用最多会返回两次物理连续的运行。如果请求的大小为 2.5 MB,它最多会返回三次物理连续的运行,依此类推。

固定的内存令牌 (PMT)

zx_bti_pin()pmt 参数中成功完成后会返回固定的内存令牌 (PMT)。设备完成内存事务以取消固定和撤消设备对内存页面的访问后,驱动程序必须调用 zx_pmt_unpin()

高级主题

缓存一致性

在完全符合 DMA 的架构上,硬件可确保 CPU 缓存中的数据与主内存中的数据相同,无需软件干预。并非所有架构都符合 DMA。在这些系统上,驱动程序必须在执行 DMA 操作之前,通过在内存范围内调用适当的缓存操作来确保 CPU 缓存保持一致,以免访问过时数据。

如需在 VMO 表示的内存上调用缓存操作,请使用 zx_vmo_op_range() 系统调用。在外设读取(驱动程序写入)操作之前,使用 ZX_VMO_OP_CACHE_CLEAN 清除缓存,以将脏数据写入主内存。在外设写入 (driver-read) 之前,请使用 ZX_VMO_OP_CACHE_CLEAN_INVALIDATE 清理缓存行并将其标记为无效,以确保在下次访问时从主内存提取数据。