Linux內(nèi)核內(nèi)存管理之處理器緩存(從這五個點入手~)
1 處理器緩存
現(xiàn)代處理器一納秒可以執(zhí)行幾十條指令,但是需要幾十納秒才能從物理內(nèi)存取出一個數(shù)據(jù),速度差距超過兩個數(shù)量級別,導(dǎo)致處理器花費(fèi)很長時間等待從內(nèi)存讀取數(shù)據(jù)。為了解決處理器執(zhí)行速度和內(nèi)存訪問速度不匹配的問題,在處理器和內(nèi)存之間增加了緩存。緩存和內(nèi)存的區(qū)別如下:
緩存是靜態(tài)隨機(jī)訪問存儲器(Static Random Access Memory,SRAM),訪問速度接近于處理器的速度,但是集成度低,和內(nèi)存相比,在容量相同的情況下體積大,并且價格昂貴。
內(nèi)存是動態(tài)隨機(jī)訪問存儲器(Dynamic Random Access Memory,DRAM),訪問速度慢,但是集成度高,和緩存相比,在容量相同的情況下體積小。
通常使用多級緩存,一級緩存集成在處理器內(nèi)部,離處理器最近,容量小,訪問時間是1個時鐘周期。二級緩存可能在處理器內(nèi)部或外部,容量更大,訪問時間是大約10個時鐘周期。有些高端處理器有三級甚至四級緩存。在SMP系統(tǒng)中,處理器的每個核有獨(dú)立的一級緩存,所有核共享二級緩存。
為了支持同時取指令和取數(shù)據(jù),一級緩存分為一級指令緩存(i-cache,instruction cache)和一級數(shù)據(jù)數(shù)據(jù)(d-cache,data cache)。二級緩存是指令和數(shù)據(jù)共享的統(tǒng)一緩存(unified cache)。
1.1 緩存結(jié)構(gòu)
如圖32KB四路組相連緩存(32KB 4-way set associative cache),首先了解緩存的結(jié)構(gòu)。
緩存的結(jié)構(gòu)如下所示:

緩存由多個容量相同的子緩存并聯(lián)組成,每個子緩存稱為路(Way),四路表示4個子緩存并聯(lián)。
緩存是通過硬件散列表實現(xiàn)的,散列表有固定長度的散列桶,硬件工程師把散列桶稱為組(Set)。四路組相連緩存的散列桶的長度是4。
緩存被劃分為容量相同的緩存行,每個緩存行有兩個狀態(tài)位:有效位表示緩存行包含數(shù)據(jù)或指令,臟位表示緩存行里面的數(shù)據(jù)比內(nèi)存里面的數(shù)據(jù)新。
每個緩存行對應(yīng)一個索引。查找一個內(nèi)存地址對應(yīng)的緩存行,需要根據(jù)內(nèi)存地址算出緩存行索引。因為可能存在多個內(nèi)存地址的緩存行索引相同,所以緩存行需要一個標(biāo)簽(tag)來區(qū)分不同的內(nèi)存地址。
內(nèi)存地址被分解為標(biāo)簽(tag)、緩存行索引(index)和緩存行內(nèi)部的字節(jié)偏移(offset)。
我們再來分析“32KB四路組相連緩存”的意思:緩存由4個子緩存并聯(lián)組成,即四路并聯(lián),四路的容量總和是32KB,每路的容量是8KB。
緩存行的標(biāo)簽通常是從物理地址生成的,索引可能從物理地址或虛擬地址生成,我們根據(jù)索引的生成方式把緩存分為兩類:
把從物理地址生成索引和標(biāo)簽的緩存稱為物理索引物理標(biāo)簽(Physically Indexed Physically Tagged,PIPT)緩存。
把從虛擬地址生成索引、從物理地址生成標(biāo)簽的緩存稱為虛擬索引物理標(biāo)簽(Virtually Indexed Physically Tagged,VIPT)緩存。
從虛擬地址生成索引的好處是:不需要等到內(nèi)存管理單元把虛擬地址轉(zhuǎn)換成物理地址以后才能開始查詢緩存,查詢緩存和地址轉(zhuǎn)換可以并行執(zhí)行,提高處理器的執(zhí)行速度,但是可能導(dǎo)致緩存別名(cache alias)問題。
如下所示,假設(shè)兩個進(jìn)程共享一個物理頁,一個物理地址被映射到兩個虛擬地址。假設(shè)頁長度是4KB,緩存中每路的容量是8KB,緩存行的長度是32字節(jié):

因為進(jìn)程1的虛擬地址的第0~11位和物理地址的第0~11位相同,進(jìn)程2的虛擬地址的第0~11位和物理地址的第0~11位相同,所以兩個虛擬地址的第0~11位相同,但是兩個虛擬地址的第12位不一定相同。
緩存行的字節(jié)偏移是虛擬地址的第0~4位,索引是虛擬地址的第5~12位。如果兩個虛擬地址的第12位不同,那么生成的緩存行索引不同,導(dǎo)致同一內(nèi)存位置的數(shù)據(jù)被復(fù)制到緩存中的兩個緩存行,這就是緩存別名問題。
當(dāng)緩存中每路的容量大于頁長度的時候,會出現(xiàn)緩存別名問題。如果緩存中每路的容量小于或等于頁長度,那么不會出現(xiàn)緩存別名問題。假設(shè)緩存中每路的容量是4KB,索引是虛擬地址的第5~11位,因為兩個虛擬地址的第0~11位相同,所以兩個虛擬地址生成的緩存行索引相同。
對于可寫的數(shù)據(jù),緩存別名問題的危害是:如果修改了一個緩存行中的數(shù)據(jù),但另一個緩存行中的數(shù)據(jù)仍然是舊的,將導(dǎo)致從兩個虛擬地址讀到的數(shù)據(jù)不同。對于指令和只讀的數(shù)據(jù),緩存別名問題沒有危害。
軟件可以規(guī)避緩存別名問題:把共享內(nèi)存映射到進(jìn)程的虛擬地址空間的時候,如果分配的虛擬內(nèi)存區(qū)域的起始地址是緩存中每路的容量的整數(shù)倍,就可以規(guī)避緩存別名問題。
ARM64處理器的數(shù)據(jù)緩存和統(tǒng)一緩存通常是PIPT緩存,也可以是沒有別名問題的VIPT緩存(緩存中每路的容量小于或等于頁長度)。
ARM64處理器的指令緩存有3種類型:
PIPT緩存。
VPIPT(VMID-aware PIPT)緩存,即感知虛擬機(jī)標(biāo)識符的PIPT緩存。
VIPT緩存。
VPIPT緩存和VIPT緩存都實現(xiàn)了ARM IVIPT(Instruction cache VIPT)擴(kuò)展:只需要在向存放指令的物理地址寫入新的數(shù)據(jù)以后維護(hù)指令緩存。
【文章福利】小編推薦自己的Linux內(nèi)核技術(shù)交流群:【891587639】整理了一些個人覺得比較好的學(xué)習(xí)書籍、視頻資料共享在群文件里面,有需要的可以自行添加哦?。。。ê曨l教程、電子書、實戰(zhàn)項目及代碼)? ? ??


1.2 緩存策略
緩存分配有兩種策略:
1.寫分配(write allocation)。
如果處理器寫數(shù)據(jù)的時候沒有命中緩存行,那么分配一個緩存行,然后讀取數(shù)據(jù)并填充緩存行,接著把數(shù)據(jù)寫到緩存行。
2.讀分配(read allocation)。
如果處理器讀數(shù)據(jù)的時候沒有命中緩存行,那么分配一個緩存行。
緩存更新有兩種策略:
1.寫回(write-back)。
處理器寫數(shù)據(jù)的時候,只更新緩存,把緩存行標(biāo)記為臟。只在緩存行被替換或被程序清理的時候更新內(nèi)存。
2.寫透(write-through)。
處理器寫數(shù)據(jù)的時候,同時更新緩存和內(nèi)存,但不會把緩存行標(biāo)記為臟。
指定內(nèi)存的可緩存屬性時,需要分別指定內(nèi)部屬性和外部屬性。對內(nèi)部和外部的劃分是由具體實現(xiàn)定義的,如下所示,典型的劃分是集成在處理器內(nèi)部的緩存使用內(nèi)部屬性,外部緩存使用外部屬性:

處理器可能投機(jī)性地訪問內(nèi)存,即把程序沒有請求過的內(nèi)存位置的數(shù)據(jù)自動加載到緩存中。程序可以指示處理器哪個數(shù)據(jù)將來被使用,讓處理器預(yù)先把數(shù)據(jù)讀到緩存中。ARM64處理器提供了預(yù)加載提示指令(PRFM,它是Prefetch from Memory的縮寫):
PRFM , | label
PLD表示加載預(yù)取(prefetch for load),PST表示存儲預(yù)取(prefetch for store)。
L1表示1級緩存,L2表示2級緩存,L3表示3級緩存。
KEEP表示把預(yù)取的數(shù)據(jù)保存在緩存中(因為預(yù)取的數(shù)據(jù)會被使用多次),稱為保存預(yù)?。╮etain prefetch)或暫存預(yù)取(temporal prefetch);STRM(stream)表示直接把預(yù)取的數(shù)據(jù)傳給處理器的核(因為預(yù)取的數(shù)據(jù)只會使用一次,不需要保存在緩存中),稱為流動預(yù)取(streaming prefetch)或非暫存預(yù)?。╪on-temporal prefetch)。
例如:PRFM PLDL1KEEP, [Xm, #imm]表示為一個從虛擬地址(Xm + 偏移)加載數(shù)據(jù)的操作預(yù)取數(shù)據(jù)到1級緩存中,并且暫存在緩存中。
1.3 緩存維護(hù)
內(nèi)核在以下情況需要維護(hù)緩存:
內(nèi)核修改或刪除頁表項的時候,需要沖刷緩存。
內(nèi)核使用內(nèi)核虛擬地址修改進(jìn)程的物理頁,為了避免產(chǎn)生內(nèi)核虛擬地址和用戶虛擬地址之間的緩存別名問題,需要沖刷緩存。
和外圍設(shè)備交互時,處理器寫數(shù)據(jù)到DMA區(qū)域的內(nèi)存塊,然后通過設(shè)置外圍設(shè)備的控制器上的控制寄存器發(fā)送命令,外圍設(shè)備通過DMA控制器從物理內(nèi)存讀取數(shù)據(jù)。DMA控制器讀取數(shù)據(jù)時不經(jīng)過處理器的緩存,所以處理器寫完數(shù)據(jù)以后必須沖刷緩存,把緩存中的數(shù)據(jù)寫回到物理內(nèi)存。
沖刷(flush)緩存的意思解釋如下:
如果緩存行的有效位被設(shè)置,且臟位被設(shè)置,即緩存行包含數(shù)據(jù)并且被修改過,那么把數(shù)據(jù)寫回到物理內(nèi)存,然后清除有效位。
如果緩存行的有效位被設(shè)置,但臟位沒有被設(shè)置,即緩存行包含數(shù)據(jù)并且沒有被修改過,那么只需要清除有效位。
1.內(nèi)核修改頁表
進(jìn)程退出時刪除進(jìn)程的所有頁表項,或者進(jìn)程在執(zhí)行execve()以裝載新程序的時候刪除所有舊的頁表項,必須按照下面的順序執(zhí)行:
內(nèi)核修改某個虛擬地址范圍內(nèi)的頁表項,必須按照下面的順序執(zhí)行:
內(nèi)核修改一條頁表項,必須按照下面的順序執(zhí)行:
在修改或刪除頁表項以前必須沖刷緩存,因為從虛擬地址生成索引的緩存要求:從緩存沖刷虛擬地址的時候,虛擬地址到物理地址的映射必須存在。
內(nèi)核提供的在修改頁表前沖刷緩存的函數(shù)如下所示。所有處理器架構(gòu)需要實現(xiàn)這些函數(shù):如果處理器使用從虛擬地址生成索引的緩存,必須實現(xiàn)這些函數(shù);如果處理器使用從物理地址生成索引的緩存,只需要把這些函數(shù)定義成空函數(shù):

2.內(nèi)核修改進(jìn)程的物理頁
內(nèi)核在修改進(jìn)程的物理頁時,使用內(nèi)核虛擬地址(線性映射區(qū)域的虛擬地址,或者使用kmap/kmap_atomic臨時把進(jìn)程的物理頁映射到內(nèi)核地址空間)修改進(jìn)程的物理頁。為了避免可能存在的內(nèi)核虛擬地址和用戶虛擬地址之間的緩存別名問題,內(nèi)核修改完以后需要從緩存沖刷內(nèi)核虛擬地址,讓用戶空間看見修改后的數(shù)據(jù)。例如:
1.void copy_user_page(void *to, void *from, unsigned long addr, struct page *page)
父進(jìn)程分叉生成子進(jìn)程,子進(jìn)程和父進(jìn)程共享物理頁。當(dāng)其中一個進(jìn)程試圖寫私有的匿名頁時,觸發(fā)頁錯誤異常,執(zhí)行寫時復(fù)制,分配新的物理頁,使用函數(shù)copy_user_page把舊的物理頁里面的數(shù)據(jù)復(fù)制到新的物理頁,內(nèi)核使用函數(shù)kmap_atomic臨時把進(jìn)程的物理頁映射到內(nèi)核虛擬地址空間。
2.void clear_user_page(void *to, unsigned long addr, struct page *page)
把進(jìn)程的物理頁清零。內(nèi)核使用函數(shù)kmap_atomic臨時把進(jìn)程的物理頁映射到內(nèi)核虛擬地址空間。
3.void copy_to_user_page(struct vm_area_struct *vma, struct page *page, unsigned long user_vaddr, void *dst, void *src, int len)
內(nèi)核復(fù)制數(shù)據(jù)到用戶頁,內(nèi)核使用kmap臨時把用戶頁映射到內(nèi)核虛擬地址空間。
內(nèi)核提供的在修改進(jìn)程的物理頁以后沖刷緩存的函數(shù)如下所示。所有處理器架構(gòu)需要實現(xiàn)這些函數(shù):如果處理器使用從虛擬地址生成索引的緩存,必須實現(xiàn)這些函數(shù);如果處理器使用從物理地址生成索引的緩存,只需要把這些函數(shù)定義成空函數(shù):

3.ARM64處理器的緩存維護(hù)
ARM64處理器支持3種緩存操作:
使緩存行失效(invalidate):清除緩存行的有效位。
清理(clean)緩存行:首先把標(biāo)記為臟的緩存行里面的數(shù)據(jù)寫到下一級緩存或內(nèi)存,然后清除緩存行的有效位。只適用于使用寫回策略的數(shù)據(jù)緩存。
清零(zero):把緩存里面的一個內(nèi)存塊清零,不需要先從內(nèi)存讀數(shù)據(jù)到緩存中。只適用于數(shù)據(jù)緩存。
ARM64處理器提供的緩存維護(hù)指令的形式如下:
<Xt>是X0~X30中的一個寄存器,<cache>和<operation>的取值如下所示:
編輯切換為居中
下面解釋ARM64架構(gòu)的兩個術(shù)語:
一致點(Point of Coherency,PoC):保證所有可能訪問內(nèi)存的觀察者(例如處理器核和DMA控制器)看到某一內(nèi)存位置的相同副本的點,通常是物理內(nèi)存。
統(tǒng)一點(Point of Unification,PoU):一個核的統(tǒng)一點是保證這個核的指令緩存、數(shù)據(jù)緩存和TLB看到某內(nèi)存位置的相同副本的點。例如,如果一個處理器核有一級指令緩存、一級數(shù)據(jù)緩存和TLB,那么這個處理器核的統(tǒng)一點是統(tǒng)一二級緩存。
1.4 SMP緩存一致性
在SMP系統(tǒng)中,處理器的每個核有獨(dú)立的一級緩存,同一內(nèi)存位置的數(shù)據(jù),可能在多個核的一級緩存中存在多個副本,所以存在數(shù)據(jù)一致性問題。目前主流的緩存一致性協(xié)議是MESI協(xié)議及其衍生協(xié)議。
原生的MESI協(xié)議有4種狀態(tài)。MESI是4種狀態(tài)的首字母縮寫,緩存行的4種狀態(tài)分別如下:
修改(Modified):表示數(shù)據(jù)只在本處理器的緩存中存在副本,數(shù)據(jù)是臟的,即數(shù)據(jù)被修改過,沒有寫回到內(nèi)存。
獨(dú)占(Exclusive):表示數(shù)據(jù)只在本處理器的緩存中存在副本,數(shù)據(jù)是干凈的,即副本和內(nèi)存中的數(shù)據(jù)相同。
共享(Shared):表示數(shù)據(jù)可能在多個處理器的緩存中存在副本,數(shù)據(jù)是干凈的,即所有副本和內(nèi)存中的數(shù)據(jù)相同。
無效(Invalid):表示緩存行中沒有存放數(shù)據(jù)。
為了維護(hù)緩存一致性,處理器之間需要通信,MESI協(xié)議提供了以下消息:
讀(Read):包含想要讀取的緩存行的物理地址。
讀響應(yīng)(Read Response):包含讀消息請求的數(shù)據(jù)。讀響應(yīng)消息可能是由內(nèi)存控制器發(fā)送的,也可能是由其他處理器的緩存發(fā)送的。如果一個處理器的緩存有想要的數(shù)據(jù),并且處于修改狀態(tài),那么必須發(fā)送讀響應(yīng)消息。
使無效(Invalidate):包含想要刪除的緩存行的物理地址。所有其他處理器必須從緩存中刪除對應(yīng)的數(shù)據(jù),并且發(fā)送使無效確認(rèn)消息來應(yīng)答。
使無效確認(rèn)(Invalidate Acknowledge):處理器收到使無效消息,必須從緩存中刪除對應(yīng)的數(shù)據(jù),并且發(fā)送使無效確認(rèn)消息來應(yīng)答。
讀并且使無效(Read Invalidate):包含想要讀取的緩存行的物理地址,同時要求從其他緩存中刪除數(shù)據(jù)。它是讀消息和使無效消息的組合,需要接收者發(fā)送讀響應(yīng)消息和使無效確認(rèn)消息。
寫回(Writeback):包含想要寫回到內(nèi)存的地址和數(shù)據(jù)。
緩存行狀態(tài)的轉(zhuǎn)換如下所示:

轉(zhuǎn)換a,修改到獨(dú)占:處理器收到寫回消息,把緩存行寫回內(nèi)存,但是緩存行保留數(shù)據(jù)。
轉(zhuǎn)換b,獨(dú)占到修改:處理器寫數(shù)據(jù)到緩存行。
轉(zhuǎn)換c,修改到無效:處理器收到“讀并且使無效”消息,發(fā)送讀響應(yīng)消息和使無效確認(rèn)消息,刪除本地副本(不需要寫回內(nèi)存,因為發(fā)送“讀并且使無效”消息的處理器需要寫數(shù)據(jù))。
轉(zhuǎn)換d,無效到修改:處理器存儲不在本地緩存中的數(shù)據(jù),發(fā)送“讀并且使無效”消息,通過讀響應(yīng)消息收到數(shù)據(jù)。處理器可以在收到所有其他處理器的使無效確認(rèn)消息以后轉(zhuǎn)換到修改狀態(tài)。
轉(zhuǎn)換e,共享到修改:處理器存儲數(shù)據(jù),發(fā)送使無效消息,收到所有其他處理器的使無效確認(rèn)消息以后轉(zhuǎn)換到修改狀態(tài)。
轉(zhuǎn)換f,修改到共享:其他處理器讀取緩存行,發(fā)送讀消息,本處理器收到讀消息后,寫回內(nèi)存,保留一個只讀副本,發(fā)送讀響應(yīng)消息。
轉(zhuǎn)換g,獨(dú)占到共享:其他處理器讀取緩存行,發(fā)送讀消息,本處理器收到后發(fā)送讀響應(yīng)消息,保留一個只讀副本。
轉(zhuǎn)換h,共享到獨(dú)占:本處理器意識到很快需要寫數(shù)據(jù),發(fā)送使無效消息,收到所有其他處理器的使無效確認(rèn)消息以后轉(zhuǎn)換到獨(dú)占狀態(tài)。
轉(zhuǎn)換i,獨(dú)占到無效:其他處理器存儲數(shù)據(jù),發(fā)送“讀并且使無效”消息,本處理器收到消息后,發(fā)送讀響應(yīng)消息和使無效確認(rèn)消息。
轉(zhuǎn)換j,無效到獨(dú)占:處理器存儲不在本地緩存中的數(shù)據(jù),發(fā)送“讀并且使無效”消息,收到讀響應(yīng)消息和所有其他處理器的使無效確認(rèn)消息后轉(zhuǎn)換到獨(dú)占狀態(tài),完成存儲操作后轉(zhuǎn)換到修改狀態(tài)。
轉(zhuǎn)換k,無效到共享:處理器加載不在本地緩存中的數(shù)據(jù),發(fā)送讀消息,收到讀響應(yīng)消息后轉(zhuǎn)換到共享狀態(tài)。
轉(zhuǎn)換l,共享到無效:其他處理器存儲本地緩存中的數(shù)據(jù),發(fā)送使無效消息,本處理器收到后,把緩存行的狀態(tài)轉(zhuǎn)換為無效,發(fā)送使無效確認(rèn)消息。
舉例說明,假設(shè)有兩個處理器:處理器0和處理器1。剛開始兩個處理器的緩存行處于無效狀態(tài),緩存行的狀態(tài)轉(zhuǎn)換過程如下:
處理器0加載地址n的數(shù)據(jù),因為本地緩存沒有副本,所以發(fā)送讀消息。內(nèi)存控制器從內(nèi)存讀取數(shù)據(jù)后發(fā)送讀響應(yīng)消息。處理器0收到讀響應(yīng)消息后,緩存行從無效狀態(tài)轉(zhuǎn)換到共享狀態(tài)。
處理器1加載地址n的數(shù)據(jù),因為本地緩存沒有副本,所以發(fā)送讀消息。處理器0收到讀消息后,緩存行保持共享狀態(tài)不變,發(fā)送讀響應(yīng)消息。處理器1收到讀響應(yīng)消息后,把緩存行從無效狀態(tài)轉(zhuǎn)換到共享狀態(tài)。
處理器0存儲地址n的數(shù)據(jù),因為緩存行處于共享狀態(tài),所以發(fā)送使無效消息。處理器1收到使無效消息后,把緩存行從共享狀態(tài)轉(zhuǎn)換到無效狀態(tài),發(fā)送使無效確認(rèn)消息。處理器0收到使無效確認(rèn)消息后,把緩存行從共享狀態(tài)轉(zhuǎn)換到修改狀態(tài)。
下面分兩種情況:
1)如果處理器1加載地址n的數(shù)據(jù),因為本地緩存沒有副本,所以發(fā)送讀消息。處理器0收到讀消息后,寫回內(nèi)存,從修改狀態(tài)轉(zhuǎn)換到共享狀態(tài),發(fā)送讀響應(yīng)消息。處理器1收到讀響應(yīng)消息后,從無效狀態(tài)轉(zhuǎn)換到共享狀態(tài)。 2)如果處理器1存儲地址n的數(shù)據(jù),因為本地緩存沒有副本,所以發(fā)送“讀并且使無效”消息。處理器0收到“讀并且使無效”消息后,發(fā)送讀響應(yīng)消息和使無效確認(rèn)消息,從修改狀態(tài)轉(zhuǎn)換到無效狀態(tài)。處理器1收到讀響應(yīng)消息和使無效確認(rèn)消息后,修改數(shù)據(jù),從無效狀態(tài)轉(zhuǎn)換到修改狀態(tài)。
1.存儲緩沖區(qū)
假設(shè)處理器0的緩存中沒有地址n的數(shù)據(jù),處理器1的緩存中包含地址n的數(shù)據(jù)。處理器0要寫地址n的數(shù)據(jù),需要從處理器1讀取緩存行,發(fā)送“讀并且使無效”消息,等收到處理器1的讀響應(yīng)消息和使無效確認(rèn)消息后才能寫,延遲時間很長。
實際上,處理器0沒有理由延遲這么長時間,因為不管處理器1發(fā)送的是什么數(shù)據(jù),處理器0都將無條件地覆蓋數(shù)據(jù)。
為了避免延遲,在處理器和緩存之間增加 1個存儲緩沖區(qū)(store buffer),如下所示。處理器0發(fā)送“讀并且使無效”消息以后,只須把數(shù)據(jù)寫到存儲緩沖區(qū),然后繼續(xù)執(zhí)行。等處理器0收到處理器1的讀響應(yīng)消息以后,把數(shù)據(jù)從存儲緩沖區(qū)沖刷到緩存行:

把數(shù)據(jù)寫到存儲緩沖區(qū),帶來兩個問題:
第一個問題是:假設(shè)變量a的值是0,處理器0的緩存中沒有變量a的副本,處理器1的緩存中有變量a的副本。處理器0把變量a設(shè)置為1,在自己的緩存中沒有找到變量a,發(fā)送“讀并且使無效”消息,把變量a的值寫到存儲緩沖區(qū)中,然后繼續(xù)執(zhí)行指令,后面有一條加載指令加載變量a。處理器0收到處理器1的讀響應(yīng)消息,變量a的值是0。處理器0在緩存中找到變量a,值為0,但是變量a的最新值是1,在存儲緩沖區(qū)中。 解決辦法是:處理器加載數(shù)據(jù)時,必須首先在自己的存儲緩沖區(qū)中查找數(shù)據(jù),即把存儲直接轉(zhuǎn)發(fā)給隨后的加載操作,稱為“存儲轉(zhuǎn)發(fā)(store forwarding)”,如下所示:

第二個問題是:如果數(shù)據(jù)在存儲緩沖區(qū)中,其他處理器看不見數(shù)據(jù)的新值,看到的是數(shù)據(jù)的舊值。
假設(shè)變量a和b的初始值都是0,處理器0和1執(zhí)行下面的程序:
當(dāng)處理器1看到變量b的值是1時,如果變量a的最新值1在處理器0的存儲緩沖區(qū)中,處理器1看到的是變量a的舊值0。
解決辦法是:使用寫內(nèi)存屏障(write memory barrier)使處理器執(zhí)行后面的存儲指令時,首先沖刷存儲緩沖區(qū),然后才允許把數(shù)據(jù)寫到緩存中。處理器在執(zhí)行后面的存儲指令時有兩種可選的處理方式:
把存儲緩沖區(qū)的所有數(shù)據(jù)沖刷到緩存中。
寫內(nèi)存屏障為當(dāng)前存儲緩沖區(qū)中的所有數(shù)據(jù)做標(biāo)記,如果存儲緩沖區(qū)中有標(biāo)記過的數(shù)據(jù),那么后面的存儲指令只能把數(shù)據(jù)寫到存儲緩沖區(qū)中,不能寫到緩存中。
處理器提供了寫內(nèi)存屏障指令,內(nèi)核封裝了宏smp_wmb()。
將程序修改為:
2.使無效隊列
假設(shè)處理器0和1的緩存中都包含地址n的數(shù)據(jù),緩存行處于共享狀態(tài)。處理器0寫地址n的數(shù)據(jù),發(fā)送使無效消息,把數(shù)據(jù)寫到存儲緩沖區(qū)中。存儲緩沖區(qū)比較小,處理器執(zhí)行多條存儲指令后很快就能填滿存儲緩沖區(qū)。如果存儲緩沖區(qū)已經(jīng)滿了,必須先把存儲緩沖區(qū)里面的所有數(shù)據(jù)沖刷到緩存中,但是把存儲緩沖區(qū)里面的數(shù)據(jù)沖刷到緩存中之前,必須等待處理器1的使無效確認(rèn)消息,確認(rèn)處理器1已經(jīng)使緩存行無效。如果處理器1的緩存很忙,處理器1正在密集地加載和存儲緩存里面的數(shù)據(jù),使無效消息的處理被延遲,就會導(dǎo)致處理器0花費(fèi)很長時間等待使無效確認(rèn)消息。
解決辦法是:為每個處理器增加 1個使無效隊列(invalidate queue),存放使無效消息,如下所示。處理器把使無效消息存放到使無效隊列中,立即發(fā)送使無效確認(rèn)消息,不需要執(zhí)行使緩存行無效的操作:

處理器寫數(shù)據(jù)時,如果找到包含數(shù)據(jù)的緩存行,并且緩存行處于共享狀態(tài),那么需要發(fā)送使無效消息,在準(zhǔn)備發(fā)送使無效消息時必須查詢自己的使無效隊列。如果使無效隊列有這個緩存行的使無效消息,那么處理器不能立即發(fā)送使無效消息,必須等到使無效隊列中這個緩存行的使無效消息被處理。
使無效隊列引入了一個問題。假設(shè)變量a和b的初始值都是0,處理器0和1執(zhí)行下面的程序:
假設(shè)處理器0和1的緩存都有變量a的副本,處于共享狀態(tài),可能存在下面的執(zhí)行順序:
處理器0執(zhí)行“a=1”,因為緩存行處于共享狀態(tài),所以處理器0把a(bǔ)的新值放在存儲緩沖區(qū)中,發(fā)送使無效消息。
處理器1收到使無效消息,存放到使無效隊列中,立即發(fā)送使無效確認(rèn)消息。
當(dāng)處理器1執(zhí)行“assert(a == 1)”的時候,因為a的舊值還在處理器1的緩存中,所以斷言失敗。
處理器1處理使無效隊列中的消息,使包含a的緩存行無效。
解決辦法是:使用讀內(nèi)存屏障(read memory barrier)為當(dāng)前使無效隊列中的所有消息做標(biāo)記,后面的加載指令必須等待所有標(biāo)記過的使無效消息被處理完。處理器提供了讀內(nèi)存屏障指令,內(nèi)核封裝了宏smp_rmb()。
將程序修改為:
寫內(nèi)存屏障和讀內(nèi)存屏障必須配對使用,寫者執(zhí)行寫內(nèi)存屏障,讀者執(zhí)行讀內(nèi)存屏障。因為處理器1讀變量a的時候,兩種情況都可能出現(xiàn):
變量a的最新值在處理器0的存儲緩沖區(qū)中,處理器0需要執(zhí)行寫內(nèi)存屏障。
處理器1的使無效隊列包含使包含變量a的緩存行無效的消息,處理器1需要執(zhí)行讀內(nèi)存屏障。
還有一種通用內(nèi)存屏障,它是寫內(nèi)存屏障和讀內(nèi)存屏障的組合,同時處理存儲緩沖區(qū)和使無效隊列,內(nèi)核封裝了宏smp_mb()。
1.5 利用緩存提高性能的編程技巧
在編程的時候,可以利用GCC編譯器的對齊屬性“__attribute__((__aligned__(n)))”,從而利用處理器的緩存提高程序的執(zhí)行速度:
使變量的起始地址對齊到一級緩存行長度的整數(shù)倍。
使結(jié)構(gòu)體對齊到一級緩存行長度的整數(shù)倍,實現(xiàn)的效果是:結(jié)構(gòu)體的所有變量的起始地址是一級緩存行長度的整數(shù)倍,并且結(jié)構(gòu)體的長度是一級緩存行長度的整數(shù)倍。
使結(jié)構(gòu)體中一個字段的偏移對齊到一級緩存行長度的整數(shù)倍。
內(nèi)核的頭文件“include/linux/cache.h”定義了以下宏:
1.以4個下劃線開頭的宏____cacheline_aligned:對齊到一級緩存行的長度。
2.以4個下劃線開頭的宏____cacheline_aligned_in_smp:在對稱多處理器系統(tǒng)中等價于宏____cacheline_aligned,在單處理器系統(tǒng)中是空的宏。
3.以兩個下劃線開頭的宏__cacheline_aligned:對齊到一級緩存行的長度,并且把變量放在“.data…cacheline_aligned”節(jié)中。
4.以兩個下劃線開頭的宏__cacheline_aligned_in_smp:在對稱多處理器系統(tǒng)中等價于宏__cacheline_aligned,在單處理器系統(tǒng)中是空的宏。
以結(jié)構(gòu)體netdev_queue為例說明:
結(jié)構(gòu)體netdev_queue對齊到一級緩存行長度的整數(shù)倍,該結(jié)構(gòu)體的所有變量的起始地址是一級緩存行長度的整數(shù)倍,并且該結(jié)構(gòu)體的長度是一級緩存行長度的整數(shù)倍。
把只讀字段集中放在一起,把可寫字段集中放在一起,把第一個可寫字段的偏移對齊到一級緩存行長度的整數(shù)倍,讓只讀字段和可寫字段處于不同的緩存行中,避免可寫字段影響只讀字段:
