天天看點

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

聲明:本文非原創,隻是翻譯!

原文: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 專欄》