1. 程式人生 > >kernel如何保證cache資料一致性

kernel如何保證cache資料一致性

在嵌入式系統中,cache位於CPU與DDR之間,是一段SRAM,讀寫效能遠高於DDR,利用cache line提供了預取功能,平衡CPU與DDR之間的效能差異,提高系統的效能。

據我瞭解,ARM/PPC/MIPS三款主流嵌入式處理器都是軟體管理cache,即有專門的指令來進行cache操作,如PPC的iccci icbi,ARM的CP15協處理器也提供對cache的操作。

cache的操作有2種:寫回和無效。寫回操作是將cache中資料寫回到DDR中,無效操作是無效掉cache中原有資料,下次讀取cache中資料時,需要從DDR中重新讀取。這兩種操作其實都是為了保證cache資料一致性。
在kernel平臺彙編程式碼中也封裝了cache操作函式,這裡以ARM v7處理器
3.4.55核心為例,在arch/arm/mm/cache-v7.S中就封裝了cache的操作函式,其中v7_dma_flush_range重新整理函式是完成了寫回和無效2種操作。

其他平臺(ARM MIPS)中cache操作函式也類似。

對cache敏感是因為在cache問題上被坑過2次。。。,其中一次在移植uboot時,沒有注意cache,網絡卡dma始終不能工作,表面上dma描述符該配置的我都已經配置了,但是dma就是不能執行,在將uboot整個啟動程式碼都過了一遍後我才考慮到是cache的問題,我寫入的描述符資料並沒有完全寫入DDR中,而是cache住了,uboot對系統執行效能要求不高,索性將cache關掉,發現dma果然正常了。這個問題前後折騰了我半個多月,cache的問題實在不好查,讀取寫入DDR中的資料是完全正確的(因為讀到的其實是cache中的資料),只能靠猜測推斷這個問題原因。導致我現在一看到cache操作就緊張。

不扯題外話了,正因為嵌入式處理器軟體管理cache,就需要我們程式碼主動去操作cache,但在核心開發中卻很少會直接進行cache操作,那麼kernel是在什麼時候進行的cache操作。

首先想明白一點,為什麼要進行cache操作,只能說cache是天使也是魔鬼。

cache在提高了系統性能同時卻導致了資料的不一致性。嵌入式處理器軟體管理cache的初衷就是保證資料一致性。

那什麼地方需要保證資料一致性呢?

對於由CPU完全操作的資料,資料是完全一致的。也就是該資料完全由CPU寫讀操作,沒有對CPU不透明的操作。這種情況下CPU讀寫的資料都是來自於cache,我們程式碼(程式碼由處理器執行,我們就應該站在處理器視角來看這個問題)完全不用考慮cache一致性的問題。

想來想去,我覺得kernel中有2種情況是需要保證資料一致性的:

(1)暫存器地址空間。暫存器是CPU與外設交流的介面,有些狀態暫存器是由外設根據自身狀態進行改變,這個操作對CPU是不透明的。有可能這次CPU讀入該狀態暫存器,下次再讀時,該狀態暫存器已經變了,但是CPU還是讀取的cache中快取的值。但是暫存器操作在kernel中是必須保證一致的,這是kernel控制外設的基礎,IO空間通過ioremap進行對映到核心空間。ioremap在對映暫存器地址時頁表是配置為uncached的。資料不走cache,直接由地址空間中讀取。保證了資料一致性。

(2)DMA緩衝區的地址空間。DMA操作對於CPU來說也是不透明的,DMA導致記憶體中資料更新,對於CPU來說是完全不可見的。反之亦然,CPU寫入資料到DMA緩衝區,其實是寫到了cache,這時啟動DMA,操作DDR中的資料並不是CPU真正想要操作的。

kernel中對於DMA操作是如何保證cache一致性的呢?

在LDD3的記憶體對映和DMA一章中詳細介紹了通用DMA操作層的一些函式,當時看這一章有些說法看得我挺暈的,現在站在cache的角度再來看就明瞭了好多。

通用DMA層主要分為2種類型的DMA對映:
(1)一致性對映,代表函式:
void *dma_alloc_coherent(struct device *dev, size_t size, dma_addr_t *handle, gfp_t gfp);
void dma_free_coherent(struct device *dev, size_t size, void *cpu_addr, dma_addr_t handle);

(2)流式DMA對映,代表函式:
dma_addr_t dma_map_single(struct device *dev, void *cpu_addr, size_t size, enum dma_data_direction dir)
void dma_unmap_single(struct device *dev, dma_addr_t handle, size_t size, enum dma_data_direction dir)

void dma_sync_single_for_cpu(struct device *dev, dma_addr_t handle, size_t size, enum dma_data_direction dir)
void dma_sync_single_for_device(struct device *dev, dma_addr_t handle, size_t size, enum dma_data_direction dir)

int dma_map_sg(struct device , struct scatterlist , int, enum dma_data_direction);
void dma_unmap_sg(struct device , struct scatterlist , int, enum dma_data_direction);

首先來看一致性對映如何保證cache資料一致性的。直接看下dma_alloc_coherent實現。

[cpp] view plain copy
void *
dma_alloc_coherent(struct device *dev, size_t size, dma_addr_t *handle, gfp_t gfp)
{
void *memory;

if (dma_alloc_from_coherent(dev, size, handle, &memory))  
    return memory;  

return __dma_alloc(dev, size, handle, gfp,  
           pgprot_dmacoherent(pgprot_kernel),  
           __builtin_return_address(0));  

}
關鍵點在於pgprot_dmacoherent,實現如下:

ifdef CONFIG_ARM_DMA_MEM_BUFFERABLE

define pgprot_dmacoherent(prot) \

__pgprot_modify(prot, L_PTE_MT_MASK, L_PTE_MT_BUFFERABLE | L_PTE_XN)  

define __HAVE_PHYS_MEM_ACCESS_PROT

struct file;
extern pgprot_t phys_mem_access_prot(struct file *file, unsigned long pfn,
unsigned long size, pgprot_t vma_prot);

else

define pgprot_dmacoherent(prot) \

__pgprot_modify(prot, L_PTE_MT_MASK, L_PTE_MT_UNCACHED | L_PTE_XN)  

endif

其實就是修改page property為uncached,這裡需要了解,page property是kernel頁管理的頁面屬性,在缺頁異常填TLB時,該屬性就會寫到TLB的儲存屬性域中。保證了dma_alloc_coherent對映的地址空間是uncached的。
並且__dma_alloc中呼叫__dma_alloc_buffer,在./arch/arm/mm/dma-mapping.c的100行處,如下:

[cpp] view plain copy
/*
* Ensure that the allocated pages are zeroed, and that any data
* lurking in the kernel direct-mapped region is invalidated.
*/
ptr = page_address(page);
memset(ptr, 0, size);
dmac_flush_range(ptr, ptr + size);
outer_flush_range(__pa(ptr), __pa(ptr) + size);

在分配到緩衝區後,對緩衝區進行刷cache以及可能存在的外部cache(二級cache)的重新整理。

dma_alloc_coherent首先對分配到的緩衝區進行cache重新整理,之後將該緩衝區的頁表修改為uncached,以此來保證之後DMA與CPU操作該塊資料的一致性。
LDD3中說一致性對映緩衝區可同時被CPU與DMA訪問,就是通過uncached的TLB對映來保證的。

再來看流式DMA對映,以dma_map_single為例,因程式碼呼叫太深,這裡就只是列出呼叫關係,跟cache有關的呼叫如下:

[cpp] view plain copy
dma_map_single ==> __dma_map_page ==> __dma_page_cpu_to_dev ==> ___dma_page_cpu_to_dev
___dma_page_cpu_to_dev實現如下:
void ___dma_page_cpu_to_dev(struct page *page, unsigned long off,
size_t size, enum dma_data_direction dir)
{
unsigned long paddr;

dma_cache_maint_page(page, off, size, dir, dmac_map_area);  

paddr = page_to_phys(page) + off;  
if (dir == DMA_FROM_DEVICE) {  
    outer_inv_range(paddr, paddr + size);  
} else {  
    outer_clean_range(paddr, paddr + size);  
}  
/* FIXME: non-speculating: flush on bidirectional mappings? */  

}
dma_cache_maint_page會對對映地址空間呼叫dmac_map_area,該函式最終會呼叫到arch/arm/mm/cache-v7.S中v7處理器的cache處理函式v7_dma_map_area,如下:
/*
* dma_map_area(start, size, dir)
* - start - kernel virtual start address
* - size - size of region
* - dir - DMA direction
*/
ENTRY(v7_dma_map_area)
add r1, r1, r0
teq r2, #DMA_FROM_DEVICE
beq v7_dma_inv_range
b v7_dma_clean_range
ENDPROC(v7_dma_map_area)
指定方向為DMA_FROM_DEVICE,則v7_dma_inv_range無效掉該段地址cache。方向為DMA_TO_DEVICE,則v7_dma_clean_range寫回該段地址cache。保證了cache資料一致性。
之後在___dma_page_cpu_to_dev中還對外部cache進行了重新整理。

dma_map_single中並沒有對cpu_addr指定緩衝區對映的儲存屬性進行修改,還是cached的,但是對該緩衝區根據資料流向進行了cache寫回或者無效,這樣也保證了cache資料一致性。

再來看dma_unmap_single,呼叫關係如下:
[cpp] view plain copy
dma_unmap_single ==> __dma_unmap_page ==> __dma_page_dev_to_cpu ==>
___dma_page_dev_to_cpu ==> dmac_unmap_area ==》v7_dmac_unmap_area
dmac_unmap_area實現如下:
/*
* dma_unmap_area(start, size, dir)
* - start - kernel virtual start address
* - size - size of region
* - dir - DMA direction
*/
ENTRY(v7_dma_unmap_area)
add r1, r1, r0
teq r2, #DMA_TO_DEVICE
bne v7_dma_inv_range
mov pc, lr
ENDPROC(v7_dma_unmap_area)

指定方向為DMA_TO_DEVICE,不做任何操作。方向為DMA_FROM_DEVICE,則v7_dma_unmap_area無效掉該段地址cache。
我的理解因為對於指定為CPU需要讀取的資料,在釋放該緩衝區後必須保證cache資料一致性,接下來CPU就要讀取資料進行處理了。而對於指定為CPU寫的資料,緩衝區釋放後CPU不會再去操作該緩衝區,所以不做任何操作。

從這裡就可以看出來,LDD3講到,流式DMA對映對於CPU何時可以操作DMA緩衝區有嚴格的要求,只能等到dma_unmap_single後CPU才可以操作該緩衝區。
究其原因,是因為流式DMA緩衝區是cached,在map時刷了下cache,在裝置DMA完成unmap時再刷cache(根據資料流向寫回或者無效),來保證了cache資料一致性,在unmap之前CPU操作緩衝區是不能保證資料一致的。因此kernel需要嚴格保證操作時序。
當然kernel也提供函式dma_sync_single_for_cpu與dma_sync_single_for_device,可以在未釋放時操作緩衝區,很明顯這2個函式實現中肯定是再次進行重新整理cache的操作保證資料一致性。

到這裡DMA的2種類型對映都分析完了,很清晰的看出一致性對映與流式DMA對映核心區別就是在於緩衝區頁表對映是否為cached,一致性對映採用uncached頁表保證了CPU與外設都可以同時訪問。
不過這些都是核心為驅動開發者已經封裝好的介面函式,驅動開發者並不需要關心cache問題,只需要按照LDD3的規定呼叫這些介面即可。
這也就是為什麼在驅動中很少見到cache操作,核心程式碼將cache操作做到對驅動不透明瞭。
這也讓我想起了在開發網絡卡驅動時,DMA描述符的分配是一致性對映,是因為DMA描述符需要CPU與裝置同時操作。而資料收發緩衝區分配是流式的,隨用隨分配,用完釋放後CPU才可以操作資料!

到目前為止,我所接觸到的核心TLB對映,做了uncached對映的只有2個:暫存器空間(ioremap)和一致性DMA緩衝區(dma_alloc_coherent),其他地址空間都是cached,來保證系統性能。

核心操作cache的時機是在操作DMA緩衝區時,其他時候核心程式碼不需要關心cache問題。這篇文章就分析到這!