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

聲明:本文非原創,只是翻譯!
原文:https://lwn.net/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 專欄》

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章