声明:本文非原创,只是翻译!
原文:https://lwn.net/Articles/822052/
作者:John Stultz ( Linaro 成员,kernel timekeeping maintainer)
备注:本文需要有 DMA-BUF 的背景知识,如果你还不了解 DMA-BUF,建议先阅读译者本人的《dma-buf 由浅入深》系列第三章和第六章。
去年,DMA-BUF Heap 被正式合入到了 linux-5.6 中,该接口的功能和 ION 类似,而 ION 已经被 Android 平台厂商使用了多年。然而,在推动 Vendor 厂商迁移到 DMA-BUF Heap 的过程中,我们渐渐发现 DMA API 并不能很好的适应现代的移动设备。不仅如此,由于在如何高效处理 Cache 这方面缺少清晰的指导文档,导致许多 Vendor 厂商使用了各自硬件强相关的优化代码,而这些代码往往因为不够通用而无法被社区所接受。这篇文章主要讲解造成以上问题的根本原因,而下一篇文章我将会和大家一起来探讨针对该问题的解决方案。
kernel 中的 DMA API 都是用来在 CPU 和 device 之间共享 memory 的。近年来,传统的 DMA API 已经被运用到 ION、DMA-BUF 和 DMA-BUF Heap 这些接口中,但是在接下来的讲解中我们会看到,关于内存共享的效率问题,到现在都还没能被彻底解决掉。
ION 作为 linux kernel 调用接口,本身已经相当宽松了,它允许应用程序给厂商特有的、或者说是 out-of-tree 的 heap allocation 驱动,传递自定义的、私有的 flags 和参数。除此之外,由于这些接口的调用程序只会跑在自家厂商的硬件上,而这些硬件使用的都是它们自己修改的 kernel 驱动,因此它们的工程师很少会去关心如何创建一个更有价值的通用接口。这也就导致许多 Vendor 厂商可能使用了相同的 HEAP ID 却用于不同的用途,又或者他们可能实现了完全相同的 heap 功能却使用不同的 HEAP ID 或 flags 参数。更糟糕的是,许多 Vendor 厂商居然大幅修改 ION 接口及其内部实现!于是,各个厂商的 ION 驱动除了接口名字和基本功能相同以外,再也找不出半点相似之处。最终,ION 沦为了 Vendor 厂商自己 hack 的游乐场!
同时,对 upstream 接口的普遍反感,常常会混淆 Vendor 厂商使用 ION 来解决的深层次问题。不过好在 DMA-BUF Heap 接口已经 upstream 了,一些厂商也已经开始将他们的 ION heap 驱动往 DMA-BUF Heap 上进行迁移了(当然,也希望他们的代码最终能进入社区)。在这种情况下,面对具有更多规范约束的 DMA-BUF Heap 接口,工程师们开始纠结如何才能实现那些曾经在 ION 上得以实现的功能和优化方案。
诱导 Vendor 厂商将他们的 heap 代码 upstream 也是有代价的,我们不得不了解更多关于厂商如何使用 DMA-BUF 的细节和复杂性。他们花了大量的时间和精力来优化如何在设备之间移动数据,因为对于这些移动平台厂商来说,性能是极其重要的。另外,他们使用共享内存不仅仅只是为了在 CPU 和 单个设备之间搬运数据,还为了在多个不同设备之间共享数据。通常这种情况下,数据由一个设备产生,然后被其它设备进一步处理,这整个过程不会有 CPU 参与访问。
举例来说,一个 Camera Sensor 捕获了一帧 raw 数据并保存到一块 buffer 上,该 buffer 接下来会被传递给 ISP 硬件做颜色矫正和参数调优。在 ISP 处理过程中又会生成一个新的 buffer,该 buffer 会直接交给 display compositor 做合成并最终上屏显示。ISP 还有可能会再生成一块 buffer,该 buffer 会被 Encoder 编码器编码到另一块新的 buffer 上,这块新的 buffer 又会被传给神经网络加速引擎做人脸识别检测。
这种 multi-device buffer sharing 的模型在移动平台上十分常见,但是在社区 upstream 版本中却并不常见,而且它还暴露了 DMA API 的一些局限性 —— 尤其是在需要处理 cache 同步操作的时候。注意,虽然 CPU 和 DMA 设备都可以有自己的 cache,但在本文中,我只关注 CPU cache,device cache 则留给相应的设备驱动程序自己去处理。
DMA API
现有的 DMA API, 在 CPU 和单个 DMA 设备之间共享内存方面,有着一个非常清晰的模型框架。为了避免数据同步问题,DMA API 尤其关心如何处理 buffer 所有权(与 CPU cache 有关)的问题。默认情况下,memory 被认为是 CPU 虚拟内存空间的一部分,而 CPU 则是它实际的拥有者。我们假设 CPU 可以随意读写内存,只有当 DMA 设备对内存发起 DMA 传输时,该 memory 的所有权才会移交给 dma device。
DMA API 描述了2种内存体系架构,一种叫“一致性内存”(consistant),另一种叫“非一致性内存”(non-consistent),或者有时候也叫 “coherent” 和 “non-coherent”。在一致性内存中,凡是对内存数据的修改,都会直接引起 CPU cache 的 update 和 invalidate 动作。最终的结果就是,device 或 CPU 可以在它们写完这块内存后立即读取这块内存,而不用担心 cache 同步问题(尽管 DMA API 指出,在 device 读取内存数据之前,可能需要先对 CPU cache 做 flush 操作)。在 X86 平台上,大多数使用的是一致性内存(除了 GPU 的处理是个列外)。而在 ARM 平台上,我们看到大多数的设备和 CPU 的关系是不一致的,因此它们使用的是非一致性内存架构。也就是说,如果一个设备在 ARM64 平台上具有类似 PCIe 功能的时候,系统上通常会混合着一致性和非一致性设备。
对于非一致性内存,必须特别注意如何正确处理 CPU cache 的状态,以避数据被破坏。如果不遵循 DMA API 的“所有权”规则,设备可能会在 CPU 不知情的情况下往内存中写数据,那么就会导致 CPU 仍然使用 cache 中的旧数据。同样的,CPU 也可能会将 cache 里的旧数据,回写到设备刚填充完新数据的内存中。无论哪种情况,都可能导致数据被破坏。
因此,DMA API 调用规则有助于建立起更通用的 cache 处理方式,确保了 CPU cache 会在设备读取内存之前被 flush,在设备写入内存之后被 invalidate。通常,这些 cache 操作是在 CPU 和 device 之间交换 buffer 所有权时完成的,比如当一块 buffer 被 DMA 设备 map 和 unmap 的时候(通过调用 dma_map_single() 这类函数接口实现)。
如果你想了解更多信息,Laurent Pinchart 在 ELC 2014 大会上关于 DMA API 的演讲非常值得一看,他的 PPT 文档可以通过点击这里查阅。
从 DMA API 的角度来看,CPU 和多个 DMA 设备共享内存其实和单个 DMA 设备共享内存没有多大区别,只不过与多个设备共享内存是在一系列独立的操作中完成的。CPU 分配一块 buffer,然后将该 buffer 的所有权移交给第一个设备(可能会涉及到 flush cache 操作)。接下来,CPU 允许该设备发起 DMA 传输,并在传输完成后执行 dma unmap 操作(可能会涉及到 cache invalidate 操作),从而将 buffer 所有权交还给 CPU,然后再对下一个设备及其之后的设备重复执行该过程。
这里其实隐藏了一个问题,如果把这些 cache 操作全部加起来,尤其是在上面整个过程中 CPU 都没有真正接触过该 buffer 的情况下。理想情况下,如果我们与一堆非一致性 DMA 设备共享内存,只需要在最初执行一次 CPU cache flush 操作,然后该 buffer 就可以被其它 DMA 设备依次使用了,中间无需额外的 cache 操作。针对这种情况 DMA API 也确实提供了一些灵活性,因此有一些方法可以让 map 操作跳过 CPU 同步。还有一些 dma_sync_*_for_cpu/device() 调用,允许在已经执行了 map 操作的情况下对 cache 再进行单独的操作。但这些工具太专业了,又没有指导文档,而且驱动程序在使用这些优化函数时需要特别小心。
DMA-BUF
DMA-BUF 的引入为应用程序和驱动程序共享内存提供了一种通用的方法。DMA-BUF 本身由 DMA-BUF exporter 创建,DMA-BUF exporter 是一个驱动程序,它可以分配特定类型的内存,而且还为 kernel、user space、device 提供了多种回调函数来处理 buffer 的 map 和 unmap 问题。
DMA-BUF 的一般使用流程如下(有关详细信息,请参阅 dma_buf_ops 结构体):
-
dma_buf_attach()
将 dma-buf attach 到一个 device 上(后续会使用该 buffer),exporter 驱动根据需要可以试着移动该 buffer,以确保新的 DMA 设备可以访问到它,或者直接返回错误。该 buffer 可以被 attach 到多个 device 上。 -
dma_buf_map_attachment()
将 buffer map 到一个 device (已经 attach 上)的设备地址空间里,该 buffer 可以被多个设备进行 map 操作。 -
dma_buf_unmap_attachment()
从 attach 的设备地址空间 unmap buffer -
dma_buf_detach()
表示设备已完成 buffer 的使用,exporter 驱动可以在这里执行它们想要的清理工作。
如果我们以传统的 DMA API 的视角来看,我们可以认为 DMA-BUF 通常被 CPU 所拥有。只有在调用 dma_buf_map_attachment() 时,buffer 的所有权才会移交给 DMA 设备(并涉及 cache flush 操作)。然后在调用 dma_buf_unmap_attachment() 时,buffer 被 unmap,所有权又交还给 CPU(同样需要执行 cache invalidate 操作)。实际上 DMA-BUF exporter 驱动已经变相的成为遵循 DMA API 所有权规则的执行实体。
而这种 buffer 共享机制的问题就在于,由多个 DMA 设备组成的 buffer pipeline 处理流程中,CPU 实际上并没有真正操作过该 buffer。为了遵循 DMA API 的调用规则,需要在每个 dma_buf_map_attachment() 和 dma_buf_unmap_attachment() 中调用 dma_map_sg() 和 dma_unmap_sg() 接口,进而导致大量的 cache 同步操作,这明显会影响系统性能。自从 kerne 4.12 版本合入了一系列 ION 清理的 patch 后(更加规范的使用 DMA API),ION 用户对于该 patch 带来的性能问题简直可以用“感同身受”来形容。以前,ION 代码中做了许多 hack,且不符合 DMA API 调用规范,某些情况下会导致 buffer 数据被破坏。有关更多详细信息,请查看 Laura Abbott 演讲的 PPT。这些规范的清理 patch 却给 ION 用户造成了性能的急剧下降,导致一些 Vendor 厂商在其 kernel 4.14 的产品中又重新使用 kernel 4.9 的 ION 代码,而其他厂商为了提高性能则添加了他们自己的 hack 代码。
因此,在多设备之间共享 buffer 的时候,如何才能让 DMA-BUF exporter 既能很好地与 DMA API 保持一致,又能满足现代设备所需的性能要求呢?在接下来的第二部分,我们将继续讨论 DMA-BUF 中的一些独特的调用规则及其灵活性,以及该灵活性所存在的缺点,这些调用规则和灵活性能让驱动程序避免这种潜在的性能问题。最后,我还将分享一些关于如何避免这些(灵活性背后的)缺点的想法。
下一篇:《LWN 翻译:DMA-BUF cache handling: Off the DMA API map (part 2)》
DMA-BUF 文章汇总: 《我的 DMA-BUF 专栏》