关注了就能看到更多这么棒的文章哦~
Dancing the DMA two-step
By Jonathan Corbet
November 14, 2024
Gemini-1.5-flash translation
https://lwn.net/Articles/997563/
Direct memory access (DMA) 直接内存访问 I/O , 简单的说,就是外围设备在 CPU 忙于其他任务时直接与内存进行数据传输。然而,实际情况往往比理论复杂得多,内核为此开发了一套复杂的内部 API 来支持 DMA。目前的 DMA API 可能会影响一些高带宽设备的性能。为了解决这个问题,Leon Romanovsky 正在通过 这个补丁系列 添加一个新的两步映射 API,使 API 更加复杂。
DMA 的挑战
早期,设备驱动程序通过将内存缓冲区的物理地址传递给设备并告知其执行操作来启动 DMA 操作。然而,在当前系统中,事情不可能如此简单,原因如下:
设备可能无法访问该缓冲区。例如,ISA 设备的 DMA 地址限制为 24 位,因此任何超出该范围(above that range)的内存都无法被这些设备访问。近来,许多设备仍然限于 32 位地址,但希望这种情况随着时间的推移有所改善。如果缓冲区超出设备的访问范围,则必须将其复制到可访问的内存中(也就是称为“弹性缓冲”(bounce buffering)的技术),然后再设置 I/O 操作。
CPU 的内存缓存(cache)和 DMA 的结合可能会导致对内存中数据的视图不一致——例如,设备无法看到仅存在于缓存中的数据。如果管理不当,缓存一致性(或不一致性)会导致数据损坏,这通常被认为是坏事。
参与传输的缓冲区可能分散在物理内存中;对于较大的传输,几乎可以肯定如此。内核的 DMA 层管理描述这些操作所需的散射/收集列表(“散射列表”,scatterlists)。
现代系统通常不会让设备直接访问物理内存空间;相反,这种访问是通过 I/O 内存管理单元(IOMMU)进行管理的,IOMMU 为外围设备创建了一个独立的地址空间。任何 DMA 操作都需要在 IOMMU 中设置映射,以允许设备访问缓冲区。IOMMU 可以使物理分散的缓冲区对设备看起来是连续的。它还可以防止设备访问缓冲区以外的内存;此功能对于安全地允许虚拟机直接访问 I/O 设备是必要的。
两个外围设备之间的 DMA 操作(完全不涉及主内存)——P2PDMA——增加了全新的复杂性。
设备驱动程序通常无法了解其运行的每个系统的组织方式,因此它必须能够适应其遇到的 DMA 映射要求。
所有这些都需要一个内核层来抽象 DMA 映射任务并向设备驱动程序提供统一的接口。内核 具有这样的抽象层,并且其当前方案已经存在多年了。该层核心是散射列表 API。然而,正如 Romanovsky 在补丁说明信中提到的那样,这个 API 一直以来都显现出性能不足的迹象。
散射列表在 DMA API 中被大量使用,但它们从根本上是基于内核的 page
结构(描述单个内存页)的。这使得散射列表无法处理更大的页面分组(folios),而无需将其拆分为单个页面。基于 struct page
也使 P2PDMA 变得复杂;由于这些操作只涉及设备内存,因此没有 page
结构可以使用。越来越多的 I/O 操作已经在内核中以不同的形式表示(例如,块操作的 bio
结构数组),将这些信息重新格式化为散射列表大多是不必要的开销。因此,人们长期以来一直有兴趣改进或替换散射列表;例如,参见 2023 年的 phyr 讨论。但是到目前为止,这些努力对散射列表来说都难有效果。
拆分操作
Romanovsky 致力于创建一个 DMA API,以解决许多关于散射列表的抱怨,同时提高性能。他说,核心思想是“改为拆分 DMA API,允许调用者使用他们自己的数据结构”。在这种情况下,拆分是在为操作分配 I/O 虚拟地址 (IOVA) 空间以及将内存映射到该空间之间进行的。这个新的 API 旨在成为在具有 IOMMU 的高端系统上的补充选项;它不会替换现有的 DMA API。
使用这个新 API 的第一步是分配一个 IOVA 空间范围,用于即将到来的传输:
booldma_iova_try_alloc(structdevice*dev,structdma_iova_state*state,phys_addr_tphys,size_tsize);
此函数将尝试为给定设备 ( dev = ) 分配一个大小为 =size
字节的 IOVA 范围。 phys
参数仅指示此范围所需的对齐方式;对于只需要页面对齐的设备,传递零即可。调用者必须提供 state
结构,但此调用将对其进行完全初始化。
如果分配动作成功,则此函数将返回 true
,并且该范围的物理地址(如设备所见)将存储在 state.addr
中。否则,返回值将为 false
,并且必须改用旧的 DMA API。因此,新的 API 无法从任何驱动程序中删除散射列表支持;它只是在支持它的系统上提供了一个更高性能的替代方案。
如果分配成功,结果就是一个已分配的 IOVA 空间范围,但尚未映射到任何内容。驱动程序可以使用以下方法将内存范围映射到此 IOVA 区域:
intdma_iova_link(structdevice*dev,structdma_iova_state*state,phys_addr_tphys,size_toffset,size_tsize,enumdma_data_directiondir,unsigned longattrs);
这里 dev
是将执行 I/O 的设备(与用于分配 IOVA 空间的设备相同),state
是用于分配地址范围的状态结构,phys
是要映射的内存范围的物理地址,offset
是此内存应映射到的 IOVA 范围内的偏移量,size
是要映射的范围的大小,dir
描述 I/O 方向(数据是移动到设备还是从设备移动),attrs
包含可选的属性,这些属性可以修改映射。返回值将为零(成功)或负错误代码。
映射所有内存后,驱动程序应调用:
intdma_iova_sync(structdevice*dev,structdma_iova_state*state,size_toffset,size_tsize);
此调用将同步 I/O 转换查找侧缓冲区(一个昂贵的操作,应仅在映射完成后执行一次)的指示 IOVA 区域范围。然后可以启动 I/O 操作。
之后,可以使用以下方法取消映射 IOVA 范围的部分内容:
voiddma_iova_unlink(structdevice*dev,structdma_iova_state*state,size_toffset,size_tsize,enumdma_data_directiondir,unsigned longattrs);
取消映射所有映射后,可以使用以下方法释放 IOVA:
voiddma_iova_free(structdevice*dev,structdma_iova_state*state);
或者,调用:
voiddma_iova_destroy(structdevice*dev,structdma_iova_state*state,size_tmapped_len,enumdma_data_directiondir,unsigned longattrs);
将取消映射整个范围(最多 mapped_len
),然后释放 IOVA 分配。
总而言之,Romanovsky 提出了一个 API,可用于将分散的一组缓冲区映射到单个连续的 IOVA 范围。无需创建单独的散射列表数据结构来表示此操作,也无需使用 page
结构来引用内存。
当前状态
此 API 此时已经过几次修订,至少有一些开发人员对此感到满意。虽然新的 API 为某些用例提供了改进的性能,但 Jens Axboe 观察到了块层中尚不清楚的性能下降。目前,Romanovsky 删除了他认为最可能是问题根源的一些块层更改。
Robin Murphy 相反,质疑了此 API 的核心假设之一:将散射/收集操作映射到连续的 IOVA 范围是有价值的:
老实说,我怀疑现在还有很多真正支持散射/收集的设备,其限制足够显著,以至于可以从 DMA 段合并中获得有意义的益处——我一直认为,现在可能最好是默认关闭此行为,并为调用者添加一个属性来显式请求它。
Christoph Hellwig 回应道,即使设备能够处理严重碎片化的 IOVA 范围,它们在连续的 IOVA 范围下也往往表现更好。Jason Gunthorpe 同意了这一点,他说 RDMA 操作在 IOVA 范围连续时“会有很大的优势”。因此,这种能力似乎是必要的。
补丁集似乎得到了相当广泛的支持,并且变化的速度似乎正在减慢。当然,可以考虑改进 API;例如,Gunthorpe 在上面链接的消息中提到了对对齐方式的更好控制,但这可以留待以后。Romanovsky 请求将其合并到 6.13 中,以便驱动程序可以轻松地开始使用它。虽然目前还没有保证(并且有一些 对该想法的抵制),但下一个内核似乎有可能包含一个新的、高性能的 DMA API。
全文完
LWN 文章遵循 CC BY-SA 4.0 许可协议。
欢迎分享、转载及基于现有协议再创作~
长按下面二维码关注,关注 LWN 深度文章以及开源社区的各种新近言论~