Cache和DMA一致性

yooooooo發表於2024-03-12

DMA應該多多少少知道點吧。DMA(Direct Memory Access)是指在外接可以不用CPU干預,直接把資料傳輸到記憶體的技術。這個過程中可以把CPU解放出來,可以很好的提升系統效能。那麼DMA和Cache有什麼關係呢?這也需要我們關注?

需要解決什麼問題

我們知道DMA可以幫我們在I/O和主存之間搬運資料,且不需要CPU參與。快取記憶體是CPU和主存之間的資料互動的橋樑。而DMA如果和cache之間沒有任何關係的話,可能會出現資料不一致。

例如,CPU修改了部分資料依然躺在cache中(採用寫回機制)。DMA需要將資料從記憶體搬運到裝置I/O上,如果DMA獲取的資料是從主存那裡,那麼就會得到舊的資料。導致程式的不正常執行。這裡告訴我們,DMA透過匯流排獲取資料時,應該先檢查cache是否命中,如果命中的話,資料應該來自cache而不是主存。但是是否先需要檢查cache呢?這取決於硬體設計。

匯流排監視技術

還記得《Cache組織方式》文章提到的PIPT Cache嗎?它是作業系統最容易管理的Cache。PIPT Cache也很容易實現匯流排監視技術。什麼是匯流排監視技術呢?其實就是為了解決以上問題提出的技術,cache控制器會監視匯流排上的每一條記憶體訪問,然後檢查是否命中。根據命中情況做出下一步操作。我們知道DMA操作的地址是實體地址,既然cache控制器可以監視匯流排操作,說明系統使用的cache必須是支援實體地址查詢的。而PIPT完全符合條件。

VIVT是根據虛擬地址查詢cache,所以不能實現匯流排監視技術。VIPT可以嗎?沒有別名的VIPT也可以實現匯流排監視,但是有別名的情況的VIPT是不行的(當然硬體如果強行檢查所有可能產生別名的cache line,或許也可以)。匯流排監視對於軟體來說是透明的,軟體不需要任何干涉即可避免不一致問題。但是,並不是所有的硬體都支援匯流排監視,同時作業系統應該相容不同的硬體。因此在不支援匯流排監視的情況下,我們在軟體上如何避免問題呢?

最簡單的方法(nocahe)

當我們使用DMA時,首先是配置。我們需要在記憶體中申請一段記憶體當做buffer,這段記憶體用作需要使用DMA讀取I/O裝置的快取,或者寫入I/O裝置的資料。為了避免cache的影響,我們可以將這段記憶體對映nocache,即不使用cache。對映的最小單位是4KB,因此在記憶體對映上至少4KB是nocahe的。這種方法簡單實用,但是缺點也很明顯。如果只是偶爾使用DMA,大部分都是使用資料的話,會由於nocache導致效能損失。這也是Linux系統中dma_alloc_coherent()介面的實現方法。

軟體維護cache一致性

為了充分使用cache帶來的好處。我們對映依然採用cache的方式。但是我們需要格外小心。根據DMA傳輸方向的不同,採取不同的措施。

  1. 如果DMA負責從I/O讀取資料到記憶體(DMA Buffer)中,那麼在DMA傳輸之前,可以invalid DMA Buffer地址範圍的快取記憶體。在DMA傳輸完成後,程式讀取資料不會由於cache hit導致讀取過時的資料。
  2. 如果DMA負責把記憶體(DMA Buffer)資料傳送到I/O裝置,那麼在DMA傳輸之前,可以clean DMA Buffer地址範圍的快取記憶體,clean的作用是寫回cache中修改的資料。在DMA傳輸時,不會把主存中的過時資料傳送到I/O裝置。

注意,在DMA傳輸沒有完成期間CPU不要訪問DMA Buffer。例如以上的第一種情況中,如果DMA傳輸期間CPU訪問DMA Buffer,當DMA傳輸完成時。CPU讀取的DMA Buffer由於cache hit導致取法獲取最終的資料。同樣,第二情況下,在DMA傳輸期間,如果CPU試圖修改DMA Buffer,如果cache採用的是寫回機制,那麼最終寫到I/O裝置的資料依然是之前的舊資料。所以,這種使用方法程式設計開發人員應該格外小心。這也是Linux系統中流式DMA對映dma_map_single()介面的實現方法。

DMA Buffer對齊要求

假設我們有2個全域性變數temp和buffer,buffer用作DMA快取。初始值temp為5。temp和buffer變數毫不相關。可能buffer是當前DMA操作程序使用的變數,temp是另外一個無關程序使用的全域性變數。

int temp = 5;
char buffer[64] = { 0 };

假設,cacheline大小是64位元組。那麼temp變數和buffer位於同一個cacheline,buffer橫跨兩個cacheline。

假設現在想要啟動DMA從外設讀取資料到buffer中。我們進行如下操作:

  1. 按照上一節的理論,我們先invalid buffer對應的2行cacheline。
  2. 啟動DMA傳輸。
  3. 當DMA傳輸到buff[3]時,程式改寫temp的值為6。temp的值和buffer[0]-buffer[60]的值會被快取到cache中,並且標記dirty bit。
  4. DMA傳輸還在繼續,當傳輸到buff[50]的時候,其他程式可能讀取資料導致temp變數所在的cacheline需要替換,由於cacheline是dirty的。所以cacheline的資料需要寫回。此時,將temp資料寫回,順便也會將buffer[0]-buffer[60]的值寫回。

在第4步中,就出現了問題。由於寫回導致DMA傳輸的部分資料(buff[3]-buffer[49])被改寫(改寫成了沒有DMA傳輸前的值)。這不是我們想要的結果。因此,為了避免出現這種情況。我們應該保證DMA Buffer不會跟其他資料共享cacheline。所以我們要求DMA Buffer首地址必須cacheline對齊,並且buffer的大小也cacheline對齊。這樣就不會跟其他資料共享cacheline。也就不會出現這樣的問題。

Linux對DMA Buffer分配的要求

Linux中,我們要求DMA Buffer不能是從棧和全域性變數分配。這個主要原因是沒辦法保證buffer是cacheline對齊。我們可以透過kmalloc分配DMA Buffer。這就要求某些不支援匯流排監視的架構必須保證kmalloc分配的記憶體必須是cacheline對齊。所以linux提供了一個宏,保證kmalloc分配的object最小的size。例如ARM64平臺的定義如下:

#define ARCH_DMA_MINALIGN	(128)

ARM64使用的cacheline大小一般是64或者128位元組。為了保證分配的記憶體是cacheline對齊,取了最大值128。而x86_64平臺則沒有定義,因為x86_64硬體保證了DMA一致性。所以我們看到x86_64平臺,slub管理的kmem cache最小的是kmalloc-8。而ARM64平臺,slub管理的kmem cache最小的是kmalloc-128。其實ARM64平臺分配小記憶體的代價挺高的。即使申請8位元組記憶體,也給你分配128位元組的object,確實有點浪費。

相關文章