一文帶你搞定ARMv8架構中的cache知識點
cache的技術背景
在最初開發ARM架構時,處理器的時鐘速度和內存的訪問速度大致相同。今天的處理器內核要復雜得多,其時鐘速度可以快上幾個數量級。但是,外部總線和內存設備的頻率并沒有擴大到同樣的程度。有可能實現小塊的片上SRAM,它可以以與內核相同的速度運行,但是與標準的DRAM塊相比,這種RAM非常昂貴,因為后者的容量可以達到數千倍。在許多基于ARM處理器的系統中,訪問外部存儲器需要幾十甚至幾百個內核周期。
緩存是位于核心和主內存之間的一個小型快速內存塊。它存儲了主存儲器中資料的副本。對高速緩沖存儲器的訪問比對主存儲器的訪問快得多。每當內核讀取或寫入一個特定的地址時,它首先在高速緩存中尋找。如果它在高速緩存中找到該地址,它就會使用高速緩存中的數據,而不是對主內存進行訪問。這大大增加了系統的性能,因為它減少了緩慢的外部存儲器訪問時間的影響。
實現ARMv8-A架構的處理器通常有兩級或更多的高速緩存。這通常意味著處理器的每個內核都有小的L1指令緩存和數據緩存。Cortex-A53和Cortex-A57處理器通常采用兩級或多級緩存,即一個小的L1指令和數據緩存和一個較大的、統一的L2緩存,該緩存在集群的多個內核之間共享。此外,還可以有一個外部L3高速緩存作為外部硬件塊,在集群之間共享。
向高速緩存提供數據的初始訪問并不比正常速度快。對緩存值的任何后續訪問才會更快,而性能的提高正是來自于此。核心硬件會檢查緩存中所有的指令獲取和數據讀取或寫入,盡管你必須將內存的某些部分,例如包含外圍設備的部分,標記為不可緩存的。因為高速緩存只容納了主內存的一個子集,所以你需要一種方法來快速確定你要找的地址是否在高速緩存中。
少數情況下,高速緩存中的數據和指令與外部存儲器中的數據可能不一樣;這是因為處理器可以更新高速緩存的內容,而這些內容還沒有被寫回主存儲器。另外,一個代理可能會在一個核心拿了自己的拷貝后更新主內存。這是一個連貫性的問題,在第14章中有描述。當你有多個內核或內存代理(如外部DMA控制器)時,這可能是一個特別的問題。(埋點+1)
在馮-諾依曼架構中,指令和數據使用一個緩存(一個統一的緩存)。修改后的哈佛架構有獨立的指令和數據總線,因此有兩個高速緩存,一個指令高速緩存(I-Cache)和一個數據高速緩存(D-Cache)。在ARMv8處理器中,有不同的指令和數據L1高速緩存,由統一的L2高速緩存支持。緩存需要保存地址、一些數據和一些狀態信息。
cache長什么樣?
下面我們一個個解釋上圖中的cache名詞:
Tag
Tag是cache內存地址的一部分,用于關聯與某一行數據的主內存地址。
64位地址的最高位(Tag)告訴cache信息來自于主內存。cache的總大小衡量了其所能容納的數據量。盡管用于容納Tag值的RAM不包括在計算中,但是Tag確實占用了cache中的物理空間。
Cache Line 一個Tag關聯一組Cache數據
如果為每個Tag地址保存一個字的數據,效率會很低,所以通常在同一個Tag下將幾個位置組合在一起。這種邏輯塊通常被稱為Cache Line,指的是緩存中最小的可加載單元,即主內存中的一個連續字塊。當一個Cache Line包含已緩存的數據或指令時,它被認為是有效的,而當它不包含已緩存的數據或指令時,則是無效的。
與每一行數據相關的是一個或多個狀態位。通常情況下,你有一個有效位,將該行標記為包含可使用的數據。這意味著該地址標簽代表了一些真實的值。在數據緩存中,你可能還有一個或多個臟位,標記緩存行(或其一部分)是否包含與主內存內容不相同(比其新)的數據。
Index
Index是內存地址的一部分,它決定了在cache的第幾行可以找到這個地址。
64位地址的中間位,即Index,標識了此地址是在Cache的第幾行。Index被用作查找Cache RAM的地址,不需要作為Tag的一部分進行存儲。
Way
一條Way是一個緩存的細分,每條Way的大小相等,并以相同的方式進行索引(Index)。一個Set由共享一個特定索引的所有方式的Cache Line組成。
這意味著地址的低幾位(稱為Offset)不需要存儲在標簽中。你需要的是整行的地址,而不是行內每個字節的地址。因此,64位地址中其余的五個或六個最不重要的位總是0。
總結一下:Cache由一片片Set和對應的Tag表組成。對于每片Set,它又由Cache Line一條條組成。Tag表標記了各行Tag所關聯的Cache Line。
注意:只有Set和Way是架構上的定義,其余名詞只是為了查找cache所提出的抽象化概念,并不是物理上的定義。
包容性和排他性cache
對于一個簡單的內存讀取,例如,單核處理器中的LDR X0, [X1]。
如果X1指向內存中的一個位置,該位置被標記為可緩存的,那么就會在L1數據緩存中進行緩存查找。如果在L1高速緩存中找到了地址,那么數據就會從L1高速緩存中讀取并返回給內核。
如果地址在L1緩存中沒有找到,但在L2緩存中找到了,那么該緩存行就會從L2緩存中加載到L1緩存中,并將數據返回給內核。這可能會導致一行被驅逐出L1以騰出空間,但它可能仍然存在于較大的L2緩存中。
如果地址不在L1或L2緩存中,數據會從外部內存加載到L1和L2緩存中,并提供給內核。這可能導致線路被驅逐。
這是個相當簡單的觀點。對于多核和多集群系統來說,在執行從外部內存加載之前,可能還要檢查集群內或其他集群的內核的L2或L1緩存。此外,在這一點上沒有考慮L3或系統緩存。
這是一個包容性的高速緩存模型,同樣的數據可以同時出現在L1和L2緩存中。在排他性緩存中,數據只能出現在一個緩存中,一個地址不能同時出現在L1和L2緩存中。
直接映射型
對于直接映射型的cache來說,每組只有一個Cache Line。
如下圖所示的cache演示,這個cache只有4個Cache Line,每行有4個字,1個字是4字節。Cache控制器根據地址中的Bit[5:2]來選擇Cache中的字,使用Bit[13:6]作為索引,來選擇4個Cache Line中的1個,Bit[43:14]存儲標記值。
查詢這個Cache時,當Index和Tag值與查詢地址相等并且此Cache包含有效數據時,則發生Cache Hit,然后使用offset值來尋找Cache中的數據。如果這個Cache包含有效數據但是Tag中表示其他地址的值時,那么就需要替換這個Cache Line。由于這個時候出現了頻繁的Cache換入換出,就會產生嚴重的Cache thrashing(顛簸),嚴重降低系統性能。
組相聯型(Set)
為了解決直接映射型cache中的顛簸問題,現代處理中廣泛使用組相聯型cache。
如下圖的2路組相聯cache為例,每一路包括4個Cache Line,因此每個Set有兩個Cache Line用于替換,他們分別來自Way0和Way1。地址0x00、0x40、0x80的數據可以映射到同一Set的不同Cache Line。因此它們有一半的幾率可以不被替換。
以A57中的一個32KB大小,4路組相聯的L1 Cache為例,我們來看看這個Cache的結構。
由于Cache大小為32KB且為4路(4個Way),因此每路Cache的大小為8KB,一共有256個Cache Line,每行大小32字節。
在Cache編碼的地址中,Bit[5:0]用于選擇Cache Line中的數據,Bit[5:2]可以尋址16個字;Bit[1:0]用于尋址每個字中的字節;Bit[13:6]用于在索引域中選擇哪一行的Cache Line;Bit[43:14]用與標記域。V表示有效位,D表示臟位。
CPU在訪問存儲器時,訪問的地址是虛擬地址(VA, virtual address),在經過TLB和MMU的映射后變成了物理地址(Physical address,PA)。
**TLB只能用于加速虛擬地址到物理地址的轉換。**在得到物理地址后,如果每次都從內存中取數據,效率會很低。因此現代處理器設計了多級cache來加速數據訪問,然而在查詢cache時使用虛擬地址還是物理地址呢?首先我們要搞清楚物理高速緩存和虛擬高速緩存。
物理高速緩存
什么是物理高速緩存?得到物理地址后,使用物理地址查詢高速緩存。缺點是CPU只有在查詢TLB和MMU后才能訪問cache,流水線時間延遲時間相對來說增加了。
虛擬高速緩存
什么是虛擬高速緩存?使用虛擬地址進行尋址cache,無需訪問TLB和MMU。
但是虛擬高速緩存會導致重名和同名問題。
重名問題
重名問題是多個虛擬地址映射到同一個物理地址引發的。
同名問題
同名問題是一個虛擬地址可能由于進程切換等原因映射到不同物理地址而引發的問題。
Cache類型
在CPU設計的時候就已經確定了是通過虛擬地址尋址cache,還是物理地址尋址cache。Cache有以下四種,VIVT、AIVIVT、VIPT、PIPT和VPIPT。
VIVT
VIVT使用虛擬地址的索引域和虛擬地址的標記域,相當于虛擬高速緩存。
早期的ARM9采用VIVT尋址方式,無須經過MMU的翻譯,就可以直接使用虛擬地址的Index和Tag來查找Cache Line,這種方式會導致嚴重的重名問題。
AIVIVT
AIVIVT是具有ASID標記的VIVT。
VIPT
VIPT使用虛擬地址的索引域和物理地址的標記域。
ARM11系列處理器采用VIPT方式,虛擬地址會同時被送到MMU/TLB中翻譯,以及cache中尋址。在MMU/TLB中,VPN被翻譯成了PFN,與此同時,使用虛擬地址的Index和offset來查詢cache組。當MMU完成地址翻譯后,再使用物理地址的Tag來匹配Cache Line。
缺點是可能會導致重名問題,當使用虛擬地址的Index和offset來查詢cache組時,可能會導致多個Cache Set被映射到同一個物理地址。
IVIPT是VIPT在指令cache中應用
PIPT
PIPT使用物理地址的索引域和物理地址的標記域,相當于物理高速緩存。
Cortex-A系列開始采用PIPT的cache尋址方式,不會導致重名問題,cache中只有一個cache set與之對應,但是加大了芯片設計復雜度。
VPIPT
VPIPT是能感知VMID的PIPT。
CTR, Cache Type Register寄存器的L1Ip表示實現的cache類型:
對于一個VPIPT指令緩存。
如果VMID被用于當前的安全狀態,那么從EL1和EL0獲取的指令只有在使用指令緩存中的條目被獲取時使用的VMID時才允許在緩存中命中。
如果VMID被用于當前安全狀態,在EL0或EL1執行的指令緩存維護指令,只有當這些條目是使用緩存維護指令執行時的VMID獲取的,才要求對指令緩存中的條目產生影響。
MESI協議
對于單核CPU來說,不存在數據一致性問題;然而對于多核系統來說,不同CPU上的cache和ram可能具有同一個數據的多個副本。這就會導致數據觀察者(CPU/GPU/DMA)能看到的數據不一致。
因此,維護cache一致性就非常有必要。維護cache一致性的關鍵是需要跟蹤每個Cache Line的狀態,并且根據讀寫操作和總線上相應的傳輸內容來更新Cache Line在不同CPU核心上的Cache Hit狀態。
維護cache一致性有軟件和硬件兩種方式?,F在大多數處理器都采用硬件來維護。在處理器中通過cache一致性協議來實現,這些協議維護了一個有限狀態機,根據存儲器讀寫指令/總線上的傳輸內容,進行狀態遷移/相應的cache操作來維護cache一致性。
cache一致性協議分為主要有兩大類:
監聽協議,每個cache被監聽/監聽其他cache的總線活動;
目錄協議,全局統一管理cache狀態。
在這里我們介紹MESI協議(Write-Once總線監聽協議),MESI這四個字母分別代表Modify、Exclusive、Shared和Invalid。Cache Line的狀態必須是這四個中的一種。前三種狀態均是數據有效下的狀態。Cache Line有兩個標志-臟(dirty)和有效(valid)。**臟代表該數據和內存不一致。**只有干凈的數據才能被多個Cache Line共享。
MESI在總線上的操作分為本地讀寫和總線操作。當操作類型是本地讀寫時,Cache Line的狀態指的是本地CPU;而當操作類型是總線讀寫時,Cache Line的狀態指的是遠端CPU。
下圖中實線表示處理器請求響應,虛線表示總線監聽響應。
解讀下圖得分兩種情況:
如果CPU發現本地副本,并且這個Cache Line的狀態為S,下圖中的I->S后,然后在總線上回復FlushOpt信號(S->I),Cache Line被發到總線上,其狀態還是S。
如果CPU發現本地副本,并且這個Cache Line的狀態為E,下圖中的I->E后,則在總線上回復FlushOpt(E->I),Cache Line被發到總線上,其狀態變成了S(E->S)。
MOESI增加了一個Owned狀態并重新定義了Shared狀態。
O狀態:表示當前Cache Line數據是當前CPU系統最新的數據副本,其他CPU可能擁有該Cache Line的副本,狀態為S。
S狀態:Cache Line的數據不一定于內存一致。如果其他CPU的Cache Line中不存在狀態O的副本,則該Cache Line于內存中的數據一致;如果其他CPU的Cache Line中存在狀態O的副本,則該Cache Line于內存中的數據不一致。
MESI狀態圖拆分
初始狀態為I
假設CPU0發起了本地讀請求,CPU0發出讀PrRd請求,因為是本地cache line是無效狀態,所以會在總線上產生一個BusRd信號,然后廣播到其他CPU。其他CPU會監聽到該請求(BusRd信號的請求)并且檢查它們的緩存來判斷是否擁有了該副本。
對于初始狀態為I的cache來說,有四種可能的狀態圖。
全部為I
假設CPU1,CPU2, CPU3上的cache line都沒有緩存數據,狀態都是I,那么CPU0會從內存中讀取數據到L1 cache,把cache line狀態設置為E。
I->S
如果CPU1發現本地副本,并且這個cache line的狀態為S,那么在總線上回復一個FlushOpt信號,即把當前的cache line的內容發送到總線上。剛才發出PrRd請求的CPU0,就能得到這個cache line的數據,然后CPU0狀態變成S。這個時候的cache line的變化情況是:CPU0上的cache line從I->S,CPU1上的cache line保存S不變。
I->E
假設CPU2發現本地副本并且cache line的狀態為E,則在總線上回應FlushOpt信號,把當前的cache line的內容發送到總線上,CPU2上的高速緩存行的狀態變成S。這個時候 cache line的變化情況:CPU0的cache line變化是I->S,而CPU2上的cache line從E變成了S。
I->M
假設CPU3發現本地副本并且cache line的狀態為M,將數據更新到內存,這時候兩個cache line的狀態都為S。cache line的變化情況:CPU0上cache line變化是I->S,CPU3上的cache line從M變成了S。
假設CPU0發起了本地寫請求,即CPU0發出讀PrWr請求:
由于CPU0的本地cache line是無效的,所以,CPU0發送BusRdX信號到總線上。這里的本地寫操作,就變成了總線寫操作,于是我們要看其他CPU的情況。
其他CPU(例如CPU1等)收到BusRdX信號,先檢查自己的高速緩存中是否有緩存副本,廣播應答信號。
假設CPU1上有這份數據的副本,且狀態為S,CPU1收到一個BusRdX信號指揮,會回復一個flushopt信號,把數據發送到總線上,把自己的cache line設置為無效,狀態變成I,然后廣播應答信號。
假設CPU2上有這份數據的副本,且狀態為E,CPU2收到這個BusRdx信號之后,會回復一個flushopt信號,把數據發送到總線上,把自己的cache line設置為無效,然后廣播應答信號。
若其他CPU上也沒有這份數據的副本,也要廣播一個應答信號。
CPU0會接收其他CPU的所有的應答信號,確認其他CPU上沒有這個數據的緩存副本后。CPU0會從總線上或者從內存中讀取這個數據:
a)如果其他CPU的狀態是S或者E的時候,會把最新的數據通過flushopt信號發送到總線上。
b)如果總線上沒有數據,那么直接從內存中讀取數據。
最后才修改數據,并且CPU0本地cache line的狀態變成M。
初始狀態為M
對于M的本地讀寫操作均無效,因為M表示此cache line的數據是最新且dirty的。
假設是CPU0的cache line的狀態為M,而在其他CPU上沒有這個數據的副本。當其他CPU(如CPU1)想讀這份數據時,CPU1會發起一次總線讀操作,所以,流程是這樣的:
若CPU0上有這個數據的副本,那么CPU0收到信號后把cache line的內容發送到總線上,然后CPU1就獲取這個cache line的內容。另外,CPU0會把相關內容發送到主內存中,把cache line的內容寫入主內存中。這時候CPU0的狀態從M->S
更改CPU1上的cache line狀態為S。
假設數據在本地CPU0上有副本并且狀態為M,而其他CPU上沒有這個數據的副本。若某個CPU(假設CPU1)想更新(寫)這份數據,CPU1就會發起一個總線寫操作。
若CPU0上有這個數據的副本,CPU0收到總線寫信號后,把自己的cache line的內容發送到內存控制器,并把該cache line的內容寫入主內存中。CPU0上的cache line狀態變成I。
CPU1從總線或者內存中取回數據到本地cache line,然后修改自己本地cache line的內容。CPU1的狀態變成M。
初始狀態為S
當本地CPU的cache line狀態為S時,
如果CPU發出本地讀操作,S狀態不變。
如果CPU收到總線讀(BusRd),狀態不變,回應一個FlushOpt信號,把數據發到總線上。
如果CPU發出本地寫操作(PrWr)
發送BusRdX信號到總線上。
本地CPU修改本地高速緩存行的內容,狀態變成M。
發送BusUpgr信號到總線上。
其他CPU收到BusUpgr信號后,檢查自己的高速緩存中是否有副本,若有,將其狀態改成I。
初始狀態為E
當本地CPU的cache line狀態為E的時候。
本地讀,從該cache line中取數據,狀態不變。
本地寫,修改該cache line的數據,狀態變成M。
收到一個總線讀信號,獨占狀態的cache line是干凈的,因此狀態變成S。
cache line的狀態先變成S。
發送FlushOpt信號,把cache line的內容發送到總線上。
發出總線讀信號的CPU,從總線上獲取了數據,狀態變成S。
收到一個總線寫,數據被修改,該cache line不再使用,狀態變成I。
cache line的狀態先變成I。
發送FlushOpt信號,把cache line的內容發送到總線上。
發出總線寫信號的CPU,從總線上獲取了數據,然后修改,狀態變成M。
四核CPU的MESI狀態轉換
現在系統中有4個CPU,每個CPU都有各自的一級cache,它們都想訪問相同地址的數據A,大小為64字節。假設:
T0時刻:4個CPU的L1 cache都沒有緩存數據A,cache line的狀態為I (無效的)
T1時刻:CPU0率先發起訪問數據A的操作
T2時刻:CPU1也發起讀數據操作
T3時刻:CPU2的程序想修改數據A中的數據
那么在這四個時間點,MESI狀態圖是如何變化的呢?下面我會畫出這四張MESI狀態圖。
T0
首先對于T0時刻,所有的cache都是I。
T1
CPU0率先發起訪問數據A的操作。
對于CPU0來說,這是一次本地讀。由于CPU0本地的cache并沒有緩存數據A,因此CPU0首先發送一個BusRd信號到總線上去查詢它的其他幾個兄弟有沒有數據A。如果其他CPU有數據A,就會通過總線發出應答。如果現在CPU1有數據A,那么它就會回應CPU0,告訴CPU0有數據。這里的情況是四個CPU都沒有數據A,此時對于CPU0來說,需要去內存中讀取數據A存到本地cache line中,然后設置此cache為獨占狀態E。
T2
CPU1也發起讀數據操作。
此時整個系統里只有CPU0中有緩存副本,CPU0會把緩存的數據發送到總線上并且應答CPU1,最后CPU0和CPU1都有緩存副本,狀態都設置為S。
T3
CPU2的程序企圖修改數據A中的數據。
此時CPU2的本地cache line并沒有緩存數據A,高速緩存行的狀態為I,因此,這是一次本地寫操作。首先CPU2會發送BusRdX信號到總線上,其他CPU收到BusRdX信號后,檢查自己的cache中是否有該數據。若CPU0和CPU1發現自己都緩存了數據A,那么會使這些cache line無效,然后發送應答信號。雖然CPU3沒有緩存數據A,但是它回復了一條應答信號,表明自己沒有緩存數據A。CPU2收集完所有的應答信號之后,把CPU2本地的cache line狀態改成M,M狀態表明這個cache line已經被自己修改了,而且已經使其他CPU上相應的cache line無效。
cache偽共享分析
假設,CPU0上的線程0想訪問和更新x,CPU1上的線程1想訪問和更新y,x和y都被加載到了一個cache line中。
現在我們根據MESI協議來分析cache line的使用情況:
CPU0第一次訪問x,由于此時cache還沒有數據x,所以此時cache line0為I。在CPU0將data放進cache line0后,此時cache line0的狀態為E。
CPU1第一次訪問y時,由于此時y已經緩存在了CPU0的cache line0中,并且此cache line0狀態為E。CPU1向總線發起讀請求,CPU0收到請求后,將這個cache line0的數據發到總線上。CPU1獲取到數據后,經本地cache line1和遠程的cache line0設置為S。此時所有的cache line都是S狀態。
CPU0想更新x時,CPU0和CPU1的cache line都是S。CPU0發送BusUpgr到總線上,然后修改本地的cache line0數據,將其改成M狀態。在CPU1收到BusUpgr信號后,必須將本地的cache line1副本設置為I。(更新數據必須關閉其他的副本)(這里只更新了x,y沒有更新)
CPU1想更新y時,此時CPU1的cache line1為I。CPU0上的cache line0緩存了舊數據y,且cache line0狀態為M。CPU1發起本地寫請求,根據MESI協議,CPU1發送BusRdX到總線上(廣播信號,人人都能聽到)。CPU0在收到信號后,發出應答信號**。由于此時CPU0上的cache line0緩存了舊數據y,且cache line0狀態為M。**CPU0會先將數據放到內存,將cache line0設置為I,然后CPU1才可以修改cache line1的數據y,cache line1改成M。
CPU0想修改x的過程和4類似。
兩個CPU像4和5一樣不斷爭奪cache line控制權,不斷使對方的cache line失效,寫數據回內存的行為導致性能下降。這種行為就叫做cache偽共享。
cache偽共享的解決
解決cache偽共享的思想是對于多線程操作的數據,使得數據處在不同的cache line。
例如采用cache line填充/cache line對齊,讓數據結構按照cache line行的大小(例如64字節對齊)對齊。
DMA和cache一致性的解決
DMA(Direct Memory Access)直接內存訪問,它在傳輸過程中是不需要CPU干預的,可以直接從內存中讀寫數據。CPU要搬移數據的話,假設是從內存A搬移到內存B,它首先要從內存A中把數據搬移到通用寄存器里,然后從通用寄存器里把數據搬移到內存B,此外,CPU搬移的過程中有可能被別的事情打斷。而DMA就是專職搬移內存的,它可以操作總線,直接從內存A搬移數據到內存B,只要DMA開始干活了,就沒有人來打擾它了,所以DMA效率上比CPU搬移要快。要使用DMA,在DMA開始干活之前,需要CPU配置DMA怎么搬移數據,從哪里搬到哪里。
但是有的時候我們會發現使用DMA獲得的數據和cache中的數據不一致。出現這個問題的原因主要有兩個:
DMA直接操作系統總線來讀寫內存地址,而CPU并不感知。
DMA修改的內存地址,在CPU的cache中有緩存,但是CPU并不知道內存數據被修改了,CPU依然去訪問cache的舊數據,導致Cache一致性問題。
解決方案:
第一種方案是使用硬件cache一致性的方案,需要SOC中CCI這種IP的支持。
第二種方案就是使用non-cacheable的內存來進行DMA傳輸,這種方案最簡單但效率最低,嚴重降低性能,還增加功耗。
第三種使用軟件主動干預的方法來幫助cache一致性。這個是比較常規的方法,特別是在類似CCI這種緩存一致性控制器沒有出來之前,都用這種方式。
對于DMA的操作,我們需要考慮以下兩種情況。
從內存到設備FIFO
傳輸路徑:內存->設備FIFO (設備例如網卡,通過DMA讀取內存數據到設備FIFO)
這種場景下,通常都是CPU的軟件來產生了新的數據,然后通過DMA數據搬到設備的FIFO里。這里類似的網卡設備的發包過程。
在DMA傳輸之前,CPU的cache可能緩存了內存數據,需要調用cache clean/flush操作,把cache內容寫入到內存中。因為CPU cache里可能緩存了最新的數據,然后再啟動DMA傳輸數據,把DMA buffer的數據傳輸到設備的FIFO。
在DMA傳數據之前,先做cache的clean或者flush操作是一個非常關鍵的點。
例如上面的圖中,最新的數據其實是在cache里。因為CPU創建一個新的數據后必定是先到cache,然后再傳遞給DMA buffer。因此在啟動DMA傳輸之前,必須要先clean/flush cache,把cache的數據回寫到DMA buffer里。
從設備FIFO到內存
傳輸路徑:設備FIFO -> 內存 (設備把數據寫入到內存中)
設備的FIFO產生了新數據,需要把數據寫入到DMA buffer里,然后CPU就可以讀到設備的數據,類似網卡的收包的過程。
在DMA傳輸之前,最新的數據是在設備的FIFO里,此時cache里的數據就是舊的無效數據,我們要先將其invalid,然后再啟動DMA傳輸。
顯然,CPU側產生新數據時需要Flush cache,CPU側獲取新數據時需要Invalid cache。
常用數據結構對齊
在proc_caches_init中,mm_struct,fs_cache,files_cache和signal_cache等結構體都通過標志位SLAB_HWCACHE_ALIGN創建了slab描述符并且與L1 cache進行了對齊。
如何對齊呢?
我們先以mm_struct進行舉例分析,kmem_cache_create_usercopy創建的cache對象是可以拷貝到用戶層的。通過是否傳入usersize來進行區分調用流程,顯然上面傳入了usersize。
如果傳入了usersize,那么就需要計算對齊的大小,calculate_alignment主要針對硬件緩存的對齊方式不能覆蓋指定的對齊方式。
cache_line_size通過讀取SYS_CTR_EL0這個寄存器來獲取CPU的L1 cache大小,獲取失敗會使用ARM64中DMA最小的對齊大小128字節。
#define ARCH_DMA_MINALIGN (128) static inline int cache_line_size_of_cpu(void) { u32 cwg = cache_type_cwg(); return cwg ? 4 << cwg : ARCH_DMA_MINALIGN; } static inline u32 cache_type_cwg(void) { return (read_cpuid_cachetype() >> CTR_CWG_SHIFT) & CTR_CWG_MASK; } static inline u32 __attribute_const__ read_cpuid_cachetype(void) { return read_cpuid(CTR_EL0); } #define read_cpuid(reg) read_sysreg_s(SYS_ ## reg)
獲得正確的對齊大小后使用create_cache->__kmem_cache_create()創建一個對齊的cache對象。
kmem_cache_create調用kmem_cache_create_usercopy并傳入空的usersize。因此只會調用到__kmem_cache_alias->find_mergeable。find_mergeable核心思想也是使用calculate_alignment先計算對齊的大小,然后去系統的slab caches鏈表中尋找一個可合并的slab緩存。
埋點:具體slab系統是如何創建的后面再講。
cache和內存交換的最小單位就是cache line,如果結構體沒有與cache line對齊,那么一個結構體很有可能占用了多個cache line,導致性能下降!
SMP系統中的對齊
針對SMP系統,一些常用的數據結構(zone,irqaction,irq_stat,worker_pool)在定義時就使用了____cacheline_aligned_in_smp和____cacheline_internodealigned_in_smp等宏來定義數據結構。之前提到的cache偽共享問題在SMP系統中會有很大的影響,解決它的辦法是讓結構體按照cache line進行對齊,例如Linux中按照L1_CACHE_BYTES對齊。
#ifndef L1_CACHE_ALIGN #define L1_CACHE_ALIGN(x) __ALIGN_KERNEL(x, L1_CACHE_BYTES) #endif #ifndef SMP_CACHE_BYTES #define SMP_CACHE_BYTES L1_CACHE_BYTES #endif #ifndef ____cacheline_aligned #define ____cacheline_aligned __attribute__((__aligned__(SMP_CACHE_BYTES))) #endif #ifndef ____cacheline_aligned_in_smp #ifdef CONFIG_SMP #define ____cacheline_aligned_in_smp ____cacheline_aligned #else #define ____cacheline_aligned_in_smp #endif /* CONFIG_SMP */ #endif #ifndef __cacheline_aligned #define __cacheline_aligned \ __attribute__((__aligned__(SMP_CACHE_BYTES), \ __section__(".data..cacheline_aligned"))) #endif /* __cacheline_aligned */ #ifndef __cacheline_aligned_in_smp #ifdef CONFIG_SMP #define __cacheline_aligned_in_smp __cacheline_aligned #else #define __cacheline_aligned_in_smp #endif /* CONFIG_SMP */ #endif #if !defined(____cacheline_internodealigned_in_smp) #if defined(CONFIG_SMP) #define ____cacheline_internodealigned_in_smp \ __attribute__((__aligned__(1 << (INTERNODE_CACHE_SHIFT)))) #else #define ____cacheline_internodealigned_in_smp #endif #endif #ifndef CONFIG_ARCH_HAS_CACHE_LINE_SIZE #define cache_line_size() L1_CACHE_BYTES #endif
獨占cache line
對于數據結構中頻繁訪問的成員我們可以設置它獨占cache line。為啥要讓它獨占呢,還是cache偽緩存問題,這個成員可能導致互相干架,頻繁導入導出cache line。例如zone->lock和zone->lru_lock這兩個頻繁的鎖,有助于提高獲取鎖的效率。在SMP系統中,自旋鎖的爭用會導致嚴重的cache line顛簸現象。
ARM HarmonyOS 架構設計
版權聲明:本文內容由網絡用戶投稿,版權歸原作者所有,本站不擁有其著作權,亦不承擔相應法律責任。如果您發現本站中有涉嫌抄襲或描述失實的內容,請聯系我們jiasou666@gmail.com 處理,核實后本網站將在24小時內刪除侵權內容。