DMA(直接内存访问)

直接内存访问 (DMA) 功能允许硬件访问 内存不足,而无需 CPU 干预。 在最高级别,为硬件指定 要传输的内存区域(及其大小),并被告知复制数据。 一些硬件外围设备甚至支持 "散布 / 收集"多个复制操作 可以逐个执行,而无需额外的 CPU 干预。

DMA 注意事项

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

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

下面我们将依次介绍各个要点。

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

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

图:虚拟地址与实际地址之间的关系

在上图中,我们展示了具体的流程(流程 12),该流程包含 虚拟地址(蓝色)。 MMU 负责将蓝色虚拟地址映射到 CPU 物理地址 总线地址(红色)。 每个进程都有自己的映射;即使进程 12 包含虚拟地址 300,则一些其他进程也可能具有虚拟地址 300。 系统会映射其他进程的虚拟地址 300(如果存在) 与进程 12 中的实际地址不同

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

图中显示的要点如下:

  1. 虚拟地址可按组(显示三个地址,300-303420-421、 和 770-771)、
  2. 几乎连续(例如300-303)未必是连续的。
  3. 某些虚拟地址未映射(例如,没有虚拟地址) 304)
  4. 并非所有物理地址都可供每个进程(例如,进程 12 无权访问实际地址 120

设备的地址空间取决于平台上可用的硬件 可能会采用类似翻译,也可能不会采用类似的翻译。 如果没有 IOMMU,外围设备使用的地址与 CPU 使用的物理地址:

图:不使用 IOMMU 的设备

在上图中,部分设备地址空间(例如, 帧缓冲区或控制寄存器)直接显示在 CPU 的物理 指定地址范围 也就是说,设备占用物理地址 122125 (含边界值)。

要使进程访问设备内存,需要创建 从一些虚拟地址映射到实际地址的 MMU(122125。 下面我们将介绍如何操作。

但使用 IOMMU 时,外设看到的地址可能不同于 CPU 的物理地址:

图:使用 IOMMU 的设备

在这里,设备有自己的“device-physical”知道它自己知道的地址, 即地址 03(含边界值)。 设备物理地址 03 的映射由 IOMMU 决定 CPU 物理地址 109110101119

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

  • 一个集合(例如,300303)发送到 CPU 物理地址空间(分别为 109110101119), 通过 MMU 进行的,
  • 一组来自 CPU 物理地址空间(地址 109110101、 和 119)通过 IOMMU 传送到设备物理地址(03)。

虽然这看起来很复杂,但 Zircon 提供了一个抽象概念, 降低复杂性

另外,我们将在下文中看到 与使用 MMU 获得的结果相似。

内存的连续性

分配大块内存时(例如使用 calloc()): 当然,您的进程会看到一个较大、连续的虚拟地址范围。 MMU 在虚拟寻址处营造出连续内存的错觉 即使 MMU 可能会选择使用物理内存来支持该内存区域 在物理地址级别分配不连续内存。

此外,当进程分配和取消分配内存时, 从物理内存到虚拟地址空间的 以便吸引更多“瑞士奶酪”空洞(即 更多不连续性)。

因此,请务必记住连续的虚拟地址 并不一定是连续的物理地址, 会随着时间推移而变得越来越宝贵

访问权限控制

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

IOMMU

通常首选连续物理内存。 进行一次传输会更加高效(包含一个源地址和一个 目标地址)比设置和管理 传输(可能需要在每次传输 以便设置下一个)。

IOMMU(如果有的话)可通过对 CPU 的 MMU 针对进程执行的操作 - 它会为外围设备 就像是处理一个连续的地址空间 将多个不连续的数据块映射到几乎连续的空间。 通过限制映射区域,IOM 还可以确保安全性(与 而 MMU 会这样做),方法是阻止外围设备访问不在“范围内”的内存 用于当前操作。

融会贯通

因此,您似乎需要同时考虑虚拟、物理和设备-物理 地址空间。 但事实并非如此。

DMA 和您的驱动程序

Zircon 提供了一系列函数,可让您清晰地处理 。 以下工具可以配合使用:

  • Bus Transaction Initiator (BTI) 以及
  • 虚拟内存对象 (VMO)。

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

在驱动程序的初始化中,调用 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)。 它会返回一个 BTI 通过 out_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 返回的每个地址的长度为一页。 可以通过设置 ZX_BTI_COMPRESS 选项来请求更大的分块 。options 在这种情况下,返回的每个条目的长度均对应于“最小连续性”属性。 虽然您无法设置此属性,但可以通过 zx_object_get_info(). 实际上,最小连续性属性可保证 zx_bti_pin() 将始终能够返回至少在这么长的字节数中是连续的地址。

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

固定内存令牌 (PMT)

zx_bti_pin() 会返回已固定的内存令牌 (PMT) 在 pmt 参数中成功触发代码。 当设备处于以下状态时,驱动程序必须调用 zx_pmt_unpin() 取消固定和撤消设备对内存页面的访问权限。

高级主题

缓存一致性

在完全 DMA 一致的架构中,硬件确保 CPU 缓存中的数据相同 存储为主内存中的数据,而无需软件干预并非所有架构 符合 DMA。在这些系统中,驱动程序必须通过 在执行 DMA 操作之前,对内存范围调用适当的缓存操作; 这样就不会访问过时的数据

为了调用由以下参数表示的内存的缓存操作: VMO 时,请使用 zx_vmo_op_range() 系统调用。在读取外围设备之前 (驱动程序写入)操作,请使用 ZX_VMO_OP_CACHE_CLEAN 清理缓存,以便将脏数据写出 主内存在进行外围设备写入(驱动程序读取)之前,请使用 ZX_VMO_OP_CACHE_CLEAN_INVALIDATE 执行以下操作 清理缓存行并将其标记为无效,确保下次能够从主内存提取数据 访问权限。