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 中的一些獨特的語義及其靈活性,以及該靈活性所存在的缺點,這些語義和靈活性能讓驅動程序避免這種潛在的性能問題。最後,我還將分享一些關於如何避免這些(靈活性背後的)缺點的想法。




DMA-BUF 文章彙總: 《我的 DMA-BUF 專欄》

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