声明:本文非原创,只是翻译!
原文:https://lwn/Articles/822521/
作者:John Stultz ( Linaro 成员,kernel timekeeping maintainer)
备注:本文需要有 DMA-BUF 的背景知识,如果你还不了解 DMA-BUF,建议先阅读译者本人的《dma-buf 由浅入深》系列 第三章 “map attachment” 和 第六章 “begin / end cpu_access”。

在上一篇文章中,我绍了一些关于 ION、DMA-BUF Heap、DMA API 的背景知识,以及 CPU Cache “所有权”的基本概念,最后站在传统 DMA API 的角度来描述 DMA-BUF 是如何处理 cache 同步问题的。文章最后还讨论了为什么传统的 DMA API 在现代移动平台上性能会这么差。本文将和大家一起讨论如何让 DMA-BUF exporter 避免不必要的 cache 操作,并就如何改进这些方法提出了一些大致的建议。

站在 DMA API 的角度来看:通过调用 dma_buf_map_attachment(),DMA-BUF 的所有权就转移到了 DMA 设备,通过调用 dma_buf_unmap_attachment() 又将所有权交还给 CPU,而每次调用这两个函数都会执行 cache 相关的操作。虽然这样的一个顺序操作能够确保 CPU Cache 处理的正确性,但是对于涉及多个 DMA 设备的 buffer pipeline 操作,CPU 实际上根本没有参与过对这些 buffer 的访问,而每次的 cache map 和 unmap 操作加起来则可能会导致明显的性能问题。

谁拥有 buffer ?

为了避免这些多余的 cache 操作,DMA-BUF 接口允许将 DMA API 的某些规则颠倒过来。需要注意的是,DMA API 假设 CPU 是所有 memory 的天然拥有者,且只有在 DMA 传输过程中(buffer 的所有权已明确转移给了 DMA 设备),才需要考虑这一反向规则。DMA-BUF 接口要求 CPU 在访问 DMA-BUF 之前需要先调用 dma_buf_begin_cpu_access(),在访问结束后调用 dma_buf_end_cpu_access()。如果 CPU 想从用户空间访问该 buffer,则可以使用 DMA_BUF_IOCTL_SYNC ioctl() 命令来发起 begin/end cpu_access 的调用。

特殊接口:

  • dma_buf_begin_cpu_access()
    通过该接口,exporter 驱动可以确保当前 buffer 只允许被 CPU 访问,这个过程中可能需要 allocate 或 swap-in 以及 pin(固定)后端存储。另外,exporter 驱动还需要确保 CPU 访问的方向与它所请求的方向是一致的。
  • dma_buf_end_cpu_access()
    该接口在 importer 完成 CPU Access 时调用,exporter 可以在该接口中实现 cache flush 操作,并 unpin 之前在 dma_buf_begin_cpu_access() 中 pin 过的内存资源。

以上接口在使用时,我们可以认为 DMA-BUF memory 默认情况下是属于 DMA 设备的,而不是 CPU。因此,需要在这些接口中完成 CPU Cache 的同步操作,以确保 CPU 拿到的数据是和 DMA-BUF 中一致的。同时,该方法还能避免那些仅在多个设备之间传递、映射和访问 DMA-BUF 时,所造成的高昂的 cache 同步操作。

然而,这种与 DMA API 不一致的调用规则可能会带来一下困扰,而且并不是所有 DMA-BUF exporter 驱动都采用相同的实现策略。某些 exporter 驱动打算仍然遵循 DMA API 调用规则,在每次执行 map 和 unmap 操作时都对 CPU cache 做 flush 和 invalidate 动作;另一些 exporter 驱动则可能只会在它们的 begin 和 end 回调接口中才进行 cache 同步操作,而还有的 exporter 驱动则可能这两种方案都实现。

虽然 DMA BUF 设计出来的目的是为了在用户空间和多个 DMA 设备之间共享内存,但是最先导出 DMA-BUF 的 exporter 往往是一个比较特殊的驱动程序,它拥有厂商定制化的、驱动强相关的 buffer allocation 代码。例如,一个 GPU 驱动程序,它分配了一个 buffer,然后对它进行渲染操作,并返回一个 handle 给用户空间。随后,用户空间应用程序可以将该 buffer 和其他 buffer 一起再送回给 GPU,以便将 web 浏览器窗口与桌面的其他窗口进行合成。DMA-BUF 提供了一种更通用的 handle 类型,因此即使该 buffer 不用于多设备共享,它的 handle 一样可以使用。

但是,要知道 buffer 只有在 CPU 和 DMA 设备之间共享时才需要考虑 cache 同步操作,因此 DMA-BUF exporter 可以针对仅在多设备之间共享 buffer 的情况做一些 cache 的优化处理。例如,一些 DMA-BUF exporter 驱动在第一次执行 DMA mapping 操作时,先将 scatter-gather table 保存下来,只要后续的 dma_buf_map_attachment() 调用都是在同一 DMA 方向上执行的,那么就继续使用这些 table。这样,我们就能避免每次在调用 dma_buf_map_attachment()dma_buf_unmap_attachment() 时所伴随的高昂的 cache 操作,并最终在 dma_buf_detach() 中释放之前的 DMA mapping 资源。这些优化之所以能起作用,是因为 exporter 被绑定到了 DMA 设备上,所以 buffer 其实并没有真正被共享,或者说共享 buffer 的 DMA 设备之间都是 Cache 一致的,所以也就没必要维护 cache 的操作了。

译者注:上面这段实在不好翻译,尤其是下面黑体字部分,实在不好理解,故将原文贴出来,希望能有高人纠正错误,以免误人子弟。


But, knowing that the buffer was shared between just the CPU and the device, the DMA-BUF exporter could optimize some of the cache operations. For instance, some DMA-BUF exporters cache the scatter/gather table resulting from the first DMA mapping operation and, as long as the dma_buf_map_attachment() calls are done in the same direction, reuse that table. In this way, they can avoid expensive cache operations on each dma_buf_map_attachment() and dma_buf_unmap_attachment() call, finally releasing the mapping in dma_buf_detach(). These optimizations work because the exporters are tied to the device, so the buffers aren’t really being shared, or the devices the buffers are shared with are cache coherent, so the cache maintenance is unnecessary.

  1. 第一句话明明说的是 buffer 在 CPU 和 device 之间共享,那这种情况肯定是每次都要进行 cache 操作的,怎么优化?
  2. 第二句话为什么说 exporter 是被绑定到了 device 上的?为什么说 buffer 并不是真正被共享的?多个 DMA 设备之间共享 buffer 就不叫共享了?

这种方法虽然有效,但是它导致了 upstream 版本中,十几个 DMA-BUF exporter 驱动却有着各自不同的 cache 处理方式和调用规则。因此,当我们开始研究如何实现一个通用的 DMA-BUF exporter 框架,以便从某种性能的角度来支持 multi-device pipeline 时,却始终没能找到一个明确的实现方案。

处理具有多种映射关系的 buffer 所有权问题

虽然 DMA API 为如何使用 map 和 unmap 调用(来指定 buffer 所有权)提供了良好的说明文档,但要想在移动平台上获得优异的性能,通常需要多个 DMA 设备和 CPU 同时建立起对 buffer 的有效映射,这使得 buffer 所有权的概念变得更加微妙。例如,在图形系统中,通常由 GPU 和 Display 同时映射到同一块 buffer 上。为此,系统必须在这一帧绘制完成之前就建立起多个设备之间的 framebuffer 共享映射关系。这样 GPU 可以直接往该 buffer 写入数据,然后在写入完成后向 display 驱动发送信号,接着 display 驱动就可以立即显示该 buffer 了。

针对于这种特定的应用场景,DMA-BUF 添加了基于 explicit fence 架构的 dma-fence,为驱动程序(或用户空间)提供了一套等待 buffer fence 的机制。最终会有另外一个驱动对该 fence 进行 signal,从而启动 buffer 所有权的切换工作。但是,要支持这种并行的映射关系就需要小心翼翼的处理 cache 同步问题,通常这由驱动程序调用 DMA API 同步接口来实现。当某个开发人员在一个集成设备上使用厂商定制的 kernel 进行开发时,他可能知道某个 buffer 具体来自哪个驱动,又将被传递给谁,从而能够添加最合适的、正确的 cache 处理代码。但是一旦超出他的可控范围,情况就会变得相当复杂。

所以我们这里看到有两种不同的 所有权跟踪 处理方式。隐式处理(Implicit handling)意味着 DMA-BUF 的所有权是在 dma map 或 unmap 的时候发生切换的,而 显式处理(Explicit handling)是指 buffer 已经映射到两个或多个设备上了,它的所有权则是通过 DMA-BUF fence 来完成有效切换的。

DMA-BUF exporter 通常在传递 buffer 所有权时处理 cache 相关操作。它们可以在调用 dma_buf_map_attachment()dma_buf_unmap_attachment() 的隐式上下文中正执行此操作,或者也可以在 dma_buf_begin_cpu_access()dma_buf_end_cpu_access() 调用中执行。然而,在显式处理情况下,DMA-BUF exporter 没有 DMA-BUF fence 信号的回调接口,因此 exporter 无法对所有权的切换进行任何 cache 管理操作,这就造成了一种困境。在这种情况下,buffer cache 管理的职责被分摊到了 DMA-BUF exporter 和使用该 buffer 的驱动程序身上。要正确执行此操作,每个驱动程序都必须了解其在 buffer pipeline 中的位置,进而了解其下游设备的 cache 一致性。

更麻烦的是,即使 DMA-BUF exporter 确实有 dma-fence signal 的回调接口,它也无法知道当前使用的是哪种所有权跟踪方式。假设显式处理模式下默认为 CPU 所有权,我们就在 map 和 unmap 函数中执行 cache 操作?或者隐式处理模式下默认为设备所有权,我们于是在 dma_buf_begin_cpu_access()dma_buf_end_cpu_access() 中进行 cache 操作?又或者驱动程序通过执行 explicit fence signal 来切换所有权的时候,我们是否避免了额外的 cache 开销?这些选择可能会给我们留下一个要么使用太慢,要么可能与某些驱动程序不兼容的实现方案,这完全违背了 DMA BUF 作为通用交换机制的初衷。

因此对于试图编写 DMA-BUF exporter 驱动的开发人员来说,这一切开始让人觉得像是在 Rusty Russell 经典 API 等级 中的10级(“阅读文档,你会弄错”)或11级(“遵循常规,你会弄错”),尤其是在你关心性能的情况下。这给在 vendor 厂商之间共享通用 DMA-BUF Heap 的目标带来了巨大障碍。

可行的解决方案

我觉得我们可以改善这种情况,并且我有一些想法可以拿出来和大家一起讨论。由于 DMA-BUF 接口已经偏离了 DMA API,我认为我们应该为 DMA-BUF 的使用方法建立一些明确的规范,并形成良好的开发文档,这样 DMA-BUF exporter 作者和 DMA-BUF 使用者都能对该模型有一个统一的认识。我们应该重点朝着下面这几个方向努力:

  • 在 DMA API 隐式的 map/unmap 函数之外,为 DMA-BUF 对象创建一个正式的所有权。
  • 提供一套跟踪所有权的调用机制,这些接口可以添加到 dma buf_ops 结构体中,这样 exporter 驱动就可以知道这些所有权的状态变化。
  • 不赞成使用隐式处理模式,应该让驱动程序使用以上新的机制来标记显式处理模式下的所有权切换。
  • 为 DMA-BUF 添加一些状态跟踪的接口,这样我们就可以知道它们的 cache 状态,并且只有在所有权发生切换时才执行相应的 cache 操作,因此那些状态跟踪接口就变得尤为重要了。

以上大部分内容可以通过形成文档以及加强当前的 DMA-BUF exporter 调用机制来实现。dma_buf_begin_cpu_access()dma_buf_end_cpu_access() 调用足以处理 device-to-CPU 和 CPU-to-device 的转换。但我们需要明确定义这些函数的正确使用规范,并始终应该由 DMA-BUF exporter 驱动来实现,从而让 buffer 默认为 device-owned 的概念正规化。这样就可以放心地实现 pre-flushed buffer 并跳过不必要的 cache 操作。

但是,这种方法有一个缺点,对于 CPU 需要多次访问 buffer 的情况(中间不涉及到 device 的参与),每次调用都会有不必要的 cache flush 操作。此外,还有一个问题是,对于既有 CPU-coherent 设备、又有 non-coherent 设备参与的混合系统而言,在这些设备之间切换所有权时,我们可能需要进行 CPU-cache 同步操作。对于这两种情况,使用 device-usage 函数调用和状态跟踪接口也许会有所帮助,这样就可以决定是否要做所有权的切换了(而不仅仅只是使用)。

这种 所有权 的概念还需要考虑将来的 partial cache flush 操作,以便允许 CPU 和 DMA 设备同时对同一块 buffer 进行访问。这样的话,buffer 所有权(以及相关的 cache 操作)将在单个 cache line 的粒度上进行管理,而不是在整个 buffer 的层面上管理,这看起来更像是文件操作上的协同锁(advisory range locks)。

不可否认,DMA-BUF Heap(以及之前的 ION)在某些情况下,用户空间会比内核空间更了解 buffer 的用途。因此,让用户空间来为某个 pipeline 选择 buffer 分配类型是最为合适的。DMA-BUF 设计理念为我们提供了非常实用的灵活性,它允许将 buffer 的规则和策略留给 exporter 驱动去实现,因此我并不想消除这些灵活性。但是我又确实认为,随着 vendor 厂商开始他们的 ION 迁移工作,能有一个明确的、既定的规范,不至于让大家掉到坑里才是更加重要的事情,这样可以避免出现一批不必要的、不兼容的 heap 和使用者。希望本文能抛砖引玉,引起大家进一步的讨论。

鸣谢

非常感谢 Rob Clark、Robert Foss、Sumit Semwal、Azam Sadiq Pasha kapatral Syed、Daniel Vetter 和 Linus Walleij 对于这两篇文章的前期评审和反馈意见!



上一篇:《LWN 翻译:DMA-BUF cache handling: Off the DMA API map (part 1)》

DMA-BUF 文章汇总: 《我的 DMA-BUF 专栏》

更多推荐

LWN 翻译:DMA-BUF cache handling: Off the DMA API map (part 2)