直接内存访问 (DMA) 功能允许硬件访问 内存不足,而无需 CPU 干预。 在最高级别,为硬件指定 要传输的内存区域(及其大小),并被告知复制数据。 一些硬件外围设备甚至支持 "散布 / 收集"多个复制操作 可以逐个执行,而无需额外的 CPU 干预。
DMA 注意事项
为了充分了解所涉及的问题,请务必 请注意以下几点:
- 每个进程都在虚拟地址空间中运行,
- MMU 可以将一个连续的虚拟地址范围映射到多个、 不连续的物理地址范围(反之亦然),
- 每个进程的物理地址空间窗口有限,
- 一些外围设备支持自己的虚拟地址 并配有输入 / 输出内存管理单元 (IOMMU)。
下面我们将依次介绍各个要点。
虚拟、物理和设备物理地址
进程有权访问的地址是虚拟地址;也就是说, CPU 的内存管理单元 (MMU) 创建的错觉。 MMU 会将虚拟地址映射到实际地址。 映射粒度基于名为“页面大小”的参数哪个 至少为 4k 字节,但现代处理器支持更大大小。
在上图中,我们展示了具体的流程(流程 12),该流程包含
虚拟地址(蓝色)。
MMU 负责将蓝色虚拟地址映射到 CPU 物理地址
总线地址(红色)。
每个进程都有自己的映射;即使进程 12 包含虚拟地址
300
,则一些其他进程也可能具有虚拟地址 300
。
系统会映射其他进程的虚拟地址 300
(如果存在)
与进程 12 中的实际地址不同
请注意,我们使用较小的十进制数字作为“地址”让讨论简单易懂 实际上,上面显示的每个方块都代表一页内存(4k 或更多), 由 32 位或 64 位值标识(具体取决于平台)。
图中显示的要点如下:
- 虚拟地址可按组(显示三个地址,
300
-303
、420
-421
、 和770
-771
)、 - 几乎连续(例如
300
-303
)未必是连续的。 - 某些虚拟地址未映射(例如,没有虚拟地址)
304
) - 并非所有物理地址都可供每个进程(例如,进程
12
无权访问实际地址120
。
设备的地址空间取决于平台上可用的硬件 可能会采用类似翻译,也可能不会采用类似的翻译。 如果没有 IOMMU,外围设备使用的地址与 CPU 使用的物理地址:
在上图中,部分设备地址空间(例如,
帧缓冲区或控制寄存器)直接显示在 CPU 的物理
指定地址范围
也就是说,设备占用物理地址 122
到 125
(含边界值)。
要使进程访问设备内存,需要创建
从一些虚拟地址映射到实际地址的 MMU(122
至
125
。
下面我们将介绍如何操作。
但使用 IOMMU 时,外设看到的地址可能不同于 CPU 的物理地址:
在这里,设备有自己的“device-physical”知道它自己知道的地址,
即地址 0
到 3
(含边界值)。
设备物理地址 0
到 3
的映射由 IOMMU 决定
CPU 物理地址 109
、110
、101
和 119
。
在这种情况下,为了让进程使用设备内存,它需要 来安排两个映射:
- 一个集合(例如,
300
至303
)发送到 CPU 物理地址空间(分别为109
、110
、101
和119
), 通过 MMU 进行的, - 一组来自 CPU 物理地址空间(地址
109
、110
、101
、 和119
)通过 IOMMU 传送到设备物理地址(0
到3
)。
虽然这看起来很复杂,但 Zircon 提供了一个抽象概念, 降低复杂性
另外,我们将在下文中看到 与使用 MMU 获得的结果相似。
内存的连续性
分配大块内存时(例如使用 calloc()): 当然,您的进程会看到一个较大、连续的虚拟地址范围。 MMU 在虚拟寻址处营造出连续内存的错觉 即使 MMU 可能会选择使用物理内存来支持该内存区域 在物理地址级别分配不连续内存。
此外,当进程分配和取消分配内存时, 从物理内存到虚拟地址空间的 以便吸引更多“瑞士奶酪”空洞(即 更多不连续性)。
因此,请务必记住连续的虚拟地址 并不一定是连续的物理地址, 会随着时间推移而变得越来越宝贵
访问权限控制
MMU 的另一个好处是,流程在 物理内存(出于安全性和可靠性方面的考虑)。 不过,对驱动程序的影响在于,进程必须明确请求 从虚拟地址空间到物理地址空间的映射,以及 拥有执行此操作所需的必要特权。
IOMMU
通常首选连续物理内存。 进行一次传输会更加高效(包含一个源地址和一个 目标地址)比设置和管理 传输(可能需要在每次传输 以便设置下一个)。
IOMMU(如果有的话)可通过对 CPU 的 MMU 针对进程执行的操作 - 它会为外围设备 就像是处理一个连续的地址空间 将多个不连续的数据块映射到几乎连续的空间。 通过限制映射区域,IOM 还可以确保安全性(与 而 MMU 会这样做),方法是阻止外围设备访问不在“范围内”的内存 用于当前操作。
融会贯通
因此,您似乎需要同时考虑虚拟、物理和设备-物理 地址空间。 但事实并非如此。
DMA 和您的驱动程序
Zircon 提供了一系列函数,可让您清晰地处理 。 以下工具可以配合使用:
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
将包含 0
、1
、2
和 3
(即
设备物理地址)。
如果不存在 IOMMU,addrs
将包含 109
、110
、101
和 119
(即
实际地址)。
权限
请注意,这些权限是
而不是驱动程序。
例如,在块设备写入操作中,设备会从内存页面读取内容,并且
因此驱动程序在块设备读取中指定 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
执行以下操作
清理缓存行并将其标记为无效,确保下次能够从主内存提取数据
访问权限。