Linux內存管理 —— DMA和一致性緩存cache

一. 出現內存不一致的原因
CPU寫內存的時候有兩種方式:
1. write through: CPU直接寫內存,不經過cache。
2. write back: CPU只寫到cache中。cache的硬件使用LRU算法將cache裏面的內容替換到內存。通常是這種方式。

DMA可以完成從內存到外設直接進行數據搬移。但DMA不能訪問CPU的cache,CPU在讀內存的時候,如果cache命中則只是在cache去讀,而不是從內存讀,寫內存的時候,也可能實際上沒有寫到內存,而只是直接寫到了cache。

這裏寫圖片描述
這樣一來,如果DMA從將數據從外設寫到內存,CPU中cache中的數據(如果有的話)就是舊數據了,這時CPU在讀內存的時候命中cache了,就是讀到了舊數據;CPU寫數據到內存時,如果只是先寫到了cache,則內存裏的數據就是舊數據了。這兩種情況(兩個方向)都存在cache一致性問題。例如,網卡發包的時候,CPU將數據寫到cache,而網卡的DMA從內存裏去讀數據,就發送了錯誤的數據。

這裏寫圖片描述
二. 如何解決一致性問題
主要靠兩類APIs:
1.一致性DMA緩存(Coherent DMA buffers),
DMA需要的內存由內核去申請,內核可能需要對這段內存重新做一遍映射,特點是映射的時候標記這些頁是不帶cache的,這個特性也是存放在頁表裏面的。
上面說“可能”需要重新做映射,如果內核在highmem映射區申請內存並將這個地址通過vmap映射到vmalloc區域,則需要修改相應頁表項並將頁面設置爲非cache的,而如果內核從lowmem申請內存,我們知道這部分是已經線性映射好了,因此不需要修改頁表,只需修改相應頁表項爲非cache即可。

相關的接口就是dma_alloc_coherent()和dma_free_coherent()。dma_alloc_coherent()會傳一個device結構體指明給哪個設備申請一致性DMA內存,它會產生兩個地址,一個是給CPU看的,一個是給DMA看的。CPU需要通過返回的虛擬地址來訪問這段內存,纔是非cache的。至於dma_alloc_coherent()的內部實現可以不關注,它是和體系結構如何實現非cache(如mips的kseg1)相關,也可能與硬件特性(如是否支持CMA)相關。

還有一個接口dma_cache_sync(),可以手動去做cache同步,上面說dma_alloc_coherent()分配的是uncached內存,但有時給DMA用的內存是其他模塊已經分配好的,例如協議棧發包時,最終要把skb的地址和長度交給DMA,除了將skb地址轉換爲物理地址外,還要將CPU cache寫回(因爲cache裏可能是新的,內存裏是舊的)。
貼出一種實現:

void dma_cache_sync(struct device *dev, void *vaddr, size_t size,
            enum dma_data_direction direction)
{
    void *addr;

    addr = __in_29bit_mode() ?
           (void *)CAC_ADDR((unsigned long)vaddr) : vaddr;

    switch (direction) {
    case DMA_FROM_DEVICE:       /* invalidate only */
        __flush_invalidate_region(addr, size);
        break;
    case DMA_TO_DEVICE:     /* writeback only */
        __flush_wback_region(addr, size);
        break;
    case DMA_BIDIRECTIONAL:     /* writeback and invalidate */
        __flush_purge_region(addr, size);
        break;
    default:
        BUG();
    }
}



調用這個函數的時刻就是上面描述的情況:因爲內存是可cache的,因此在DMA讀內存(內存到設備方向)時,由於cache中可能有新的數據,因此要先將cache中的數據寫回到內存;在DMA寫內存(設備到內存方向)時,cache中可能還有數據沒有寫回,爲了防止cache數據覆蓋DMA要寫的內容,要先將cache無效。注意這個函數的vaddr參數接收的是虛擬地址

例如在發包時將協議棧的skb放進ring buffer之前,要做一次DMA_TO_DEVICE的flush。對應的,在收包後爲ring buffer中已被使用的skb數據buffer重新分配內存後,要做一次DMA_FROM_DEVICE的flush(invalidate的時候要注意cache align)。

還有一種針對可cache的內存做一致性的方式,就是流式DMA映射。

2.流式DMA映射(DMA Streaming Mapping),
相關接口爲 dma_map_sg(), dma_unmap_sg(),dma_map_single(),dma_unmap_single()。
一致性緩存的方式是內核專門申請好一塊內存給DMA用。而有時驅動並沒這樣做,而是讓DMA引擎直接在上層傳下來的內存裏做事情。例如從協議棧裏發下來的一個包,想通過網卡發送出去。
但是協議棧並不知道這個包要往哪裏走,因此分配內存的時候並沒有特殊對待,這個包所在的內存通常都是可以cache的。
這時,內存在給DMA使用之前,就要調用一次dma_map_sg()或dma_map_single(),取決於你的DMA引擎是否支持聚集散列(DMA scatter-gather),支持就用dma_map_sg(),不支持就用dma_map_single()。DMA用完之後要調用對應的unmap接口。

由於協議棧下來的包的數據有可能還在cache裏面,調用dma_map_single()後,CPU就會做一次cache的flush,將cache的數據刷到內存,這樣DMA去讀內存就讀到新的數據了。

注意,在map的時候要指定一個參數,來指明數據的方向是從外設到內存還是從內存到外設:
從內存到外設:CPU會做cache的flush操作,將cache中新的數據刷到內存。
從外設到內存:CPU將cache置無效,這樣CPU讀的時候不命中,就會從內存去讀新的數據。

還要注意,這幾個接口都是一次性的,每次操作數據都要調用一次map和unmap。並且在map期間,CPU不能去操作這段內存,因此如果CPU去寫,就又不一致了。
同樣的,dma_map_sg()和dma_map_single()的後端實現也都是和硬件特性相關。


說白了,如果採用第一種方式的話,就是由kernel來保證一致性,驅動程序是不用考慮的,這種方法的缺點是在某些體系結構上,效率很低;如果採用第二種方式的話,那麼是有驅動程序來保證一致性的,所以當驅動要使用dma來進行數據傳輸時,必須首先檢測內存和硬件cache的一致性,linux提供了這類方法。

3. cache coherent
上面說的是常規DMA,有些SoC可以用硬件做CPU和外設的cache coherence,例如在SoC中集成了叫做“Cache Coherent interconnect”的硬件,它可以做到讓DMA踏到CPU的cache或者幫忙做cache的刷新。這樣的話,dma_alloc_coherent()申請的內存就沒必要是非cache的了。

來自:https://www.spinics.net/lists/arm-kernel/msg322447.html
Arnd Bergmann:
dma_alloc_coherent() is a wrapper around a device-specific allocator,
based on the dma_map_ops implementation. The default allocator
from arm_dma_ops gives you uncached, buffered memory. It is expected
that the driver uses a barrier (which is implied by readl/writel
but not __raw_readl/__raw_writel or readl_relaxed/writel_relaxed)
to ensure the write buffers are flushed.
If the machine sets arm_coherent_dma_ops rather than arm_dma_ops,
the memory will be cacheable, as it's assumed that the hardware
is set up for cache-coherent DMAs.

 

cache和uncache 在使用上的區別主要體現在dev->dma_ops(提供了dma內存申請、釋放、映射等一系列方法,dma_alloc_coherent申請內存時就是用的alloc方法)上,默認使用的 arm_dma_opsuncache的,cache coherent使用arm_coherent_dma_ops。可以通過 arch_setup_dma_ops函數設置 dev->dma_ops 爲arm_coherent_dma_ops或arm_dma_ops,不設置默認使用uncache的arm_dma_ops。

 

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