CPU高速緩存機制的引入,主要是爲了解決CPU愈來愈快的運行速度與相對較慢的主存訪問速度的矛盾。CPU中的寄存器數量有限,在執行內存尋址指令時,常常須要從內存中讀取指令所需的數據或是將寄存器中的數據寫回內存。而CPU對內存的存取相對CPU自身的速度而言過於緩慢,在內存存取的過程當中CPU只能等待,機器效率過低。html
爲此,設計者在CPU與內存之間引入了高速緩存。CPU中寄存器的存儲容量小,訪問速度極快;內存存儲容量很大,但相對寄存器而言訪問速度很慢。而高速緩存的存儲大小和訪問速度都介於兩者之間,做爲一個緩衝橋樑來填補寄存器與主存間訪問速度過大的差別。java
引入高速緩存後,CPU在須要訪問主存中某一地址空間時,高速緩存會攔截全部對於內存的訪問,並判斷所需數據是否已經存在於高速緩存中。若是緩存命中,則直接將高速緩存中的數據交給CPU;若是緩存未命中,則進行常規的主存訪問,獲取數據交給CPU的同時也將數據存入高速緩存。但因爲高速緩存容量遠小於內存,所以在高速緩存已滿而又須要存入新的內存映射數據時,須要經過某種算法選出一個緩存單元調度出高速緩存,進行替換。程序員
因爲對內存中數據的訪問具備局部性,使用高速緩存可以極大的提升CPU訪問存儲器的效率。算法
高速緩存在命中時,意味着內存和高速緩存中擁有了同一份數據的兩份拷貝。CPU在執行修改內存數據的指令時若是高速緩存命中,只會修改高速緩存中的數據,此時便出現了高速緩存與內存中數據不一致的問題。緩存
這個不一致問題在早期單核CPU環境下彷佛不是什麼大問題,由於全部的內存操做都來自惟一的CPU。但即便是單核環境下,爲了減輕CPU在I/O時的負載、提升I/O效率,先進的硬件設計都引入了DMA機制。DMA芯片在工做時會直接訪問內存,若是高速緩存首先被CPU修改和內存不一致,就會出現DMA實際寫回磁盤的內容和程序所須要寫入的內容不一致的問題。架構
爲了更好的理解多核CPU環境下工做的MESI協議,這裏先簡單介紹單核環境下高速緩存被首先改寫而致使cache與主存不一致問題的解決方案,簡單來講有兩種方法:通寫法和回寫法。併發
通寫法(Write Through):異步
即CPU在對cache寫入數據時,同時也直接寫入主存,這樣就能使得主存和cache中的數據始終保存一致。性能
通寫法的優勢是簡單,硬件上容易實現,同時在調度緩存單元時不會有髒數據,調度速度快;缺點是每次cache寫入操做時都增長了寫主存的等待時間,效率較低。學習
回寫法(Write Back):
回寫法和通寫法的主要區別在於,回寫法在CPU寫cache時並不實時同步寫主存,而是在進行調度被覆蓋前總體的寫回主存。若是被調度出的cache單元並無被寫入過,則直接被覆蓋無需寫回主存。
回寫法的優勢是寫入cache時無需同步主存,整體效率比通寫法高。缺點是硬件實現較爲複雜。
隨着單核主頻速度的增加受到制約,CPU的發展由單核逐漸過分到了多核,目前主流的CPU都是多核心的。但隨着多核CPU提升計算機併發性能的同時,也帶來了一系列的問題,這其中就包括了多核CPU下的高速緩存一致性問題。
在多核CPU的架構下,一般每個核心都擁有着本身獨有的高速緩存,每一個核心能併發的讀寫本身的高速緩存。高速緩存能夠有多個,但其對應的內存數據邏輯上卻只有一份,多核併發的修改其高速緩存中同一內存的映射數據就會出現高速緩存中的數據不一致的問題。若是不對多核CPU下的高速緩存併發訪問施加必定的約束,那麼併發程序中對共享內存數據的存取就會出現問題,併發程序的正確性將沒法獲得有效保障。
在討論解決高速緩存一致性問題的方法前,咱們須要更精確的定義什麼是高速緩存一致性。
內存系統的一個本質特徵是:一個內存系統應該能提供一組保存值的存儲單元,當對一個存儲單元執行讀操做時,應該能返回「最近」一個對該存儲單元的寫操做所寫入的值。在串行程序中,程序員利用內存來將程序中某一點計算出來的值,傳遞到該值的使用點,實際上就是利用了上述基本性質。一樣,運行在單處理器上的多個進程或線程利用共享地址空間進行通訊,實際上也是利用了內存系統的這個性質。
一個讀操做應返回最近的向那個位置的寫操做所寫的值,而無論是哪一個線程寫的。當全部的線程運行在同一個物理處理器上時,它們經過相同的高速緩存層次來看內存,所以在這種狀況下,高速緩存不會引發問題。當在共享存儲的多處理器系統上運行一個具備多個線程的程序時,但願無論這些線程是運行在同一個處理器上,仍是位於不一樣的處理器上,程序的運行結果都是相同的。
上面摘抄自書上的概念描述有些晦澀,我我的的理解是:按照程序指令的運行順序,對於同一內存單元內容(變量值)可以令後面的讀操做讀取到以前最近寫操做後的結果,保證程序邏輯序的正確性。這一順序性的保證在單核環境下不是問題,由於全部的指令順序都使用同一個高速緩存,但在多核多高速緩存副本的狀況下運行某一程序的多個併發任務時就會出現問題,由於並無約定多個處理器核心對同一存儲單元併發操做時的全局順序,即「最近」這一律念是模糊、不明確的。
所以,一個高速緩存一致的存儲系統其首先要知足的一個條件即是:根據一個程序的任意一次執行結果,都可以對每一個內存單元的存取操做構造出邏輯上的全局串行序列(即便是多核體系下,對內存的存取邏輯上也要強制串行化)。這一全局邏輯串行序列還須要知足額外的兩個條件:一是同一處理器所發出的程序內存存取指令順序(程序邏輯序),與在全局邏輯串行序列中的前後順序保持一致;二是每一個讀操做的值,返回的是在全局邏輯串行序列中最近的寫操做以後的值。
上述高速緩存一致性的定義隱含了在多核環境下的兩個重要性質:一是寫傳播,二是寫串行化。
寫傳播(Write Propagation):一個處理器對一個位置的所寫入的值,最終對其它處理器是可見的。
寫串行化(Write Serialization):對同一內存單元的全部寫操做(不管是來自一個處理器仍是多個處理器)都能串行化。換句話說,全部的處理器能以相同的次序看到這些寫操做。
一般多核並行架構的CPU,每一個核雖然都獨自工做,但與外部存儲器的交互依然是共用同一總線進行的。經過總線,每一個核心都可以監聽、接收到來自其它核心的消息通知,這一機制被稱爲總線偵聽或是總線嗅探。
基於總線偵聽的寫傳播:
每一個核心在對本身獨有的高速緩存行進行修改時,須要將修改通知送至總線進行廣播。其它核心在監聽到總線上來自其它核心的遠程寫通知時,須要查詢本地高速緩存中是否存在一樣內存位置的數據。若是存在,須要選擇將其設置爲失效狀態或是更新爲最新的值。
基於總線偵聽的寫串行化:
總線上任意時間只能出現一個核的一個寫通知消息。多個核心併發的寫事件會經過總線仲裁機制將其轉換爲串行化的寫事件序列(能夠簡單理解爲邏輯上的一個FIFO事件隊列),在每一個寫事件廣播時,必須獲得每一個核心對事件的響應後,才進行下一個事件的處理,這一機制被稱做總線事務。
而本文的主角MESI協議即是基於總線偵聽機制,採用回寫法、寫傳播失效策略的高速緩存一致性協議,其另外一個更精確的名稱是四態緩存寫回無效協議。
下面介紹MESI協議是如何工做以實現多核間高速緩存一致性的。
MESI四態緩存寫回無效協議,爲高速緩存中的每一個存儲單元行cache line賦予了一個狀態屬性,狀態類型共有4種:Modified(已被修改)、Exclusive(被獨佔)、Shared(被共享)、Invalid(無效),這也是MESI這一名稱的由來。
任意時刻每一個緩存行都處於上述四種狀態的其中一種,而且可能會由於發生的緩存事件而遷移至另外一種狀態。
Modified:
緩存數據有效,在讀入緩存後曾經被當前CPU修改過卻沒有寫回,致使與內存中的對應數據不一致。
內存中對應的數據只在本地核心的高速緩存中存在,其它核的高速緩存中並無緩存這一內存數據。
有效,本地cache獨佔,與內存數據不一致(被修改 Modified)。
Exclusive:
緩存數據有效,在讀入緩存後沒有被當前CPU修改過,與內存中的對應數據保持一致。
內存中對應的數據只在本地核心的高速緩存中存在,其它核的高速緩存中並無緩存這一內存數據。
有效,本地cache獨佔,與內存數據一致(被獨佔 Exclusive)。
Shared:
緩存數據有效,在讀入緩存後沒有被當前CPU修改過,與內存中的對應數據保持一致。
內存中對應的數據除了本地核心的高速緩存中存在,其它核的高速緩存中也緩存了這一內存數據。
有效,與其它cache共享,與內存數據一致(被共享 Shared)。
Invalid:
緩存數據無效。無效的含義既表明着以前緩存行有效,卻由於某些事件變爲無效;也表明着對應緩存行不存在。
上述緩存行的共享/獨佔狀態,指的是本地高速緩存中存在有效的對應存儲單元緩存行,而其它核的高速緩存中不存在對應單元的內容或是對應的緩存行是Invalid無效狀態。
在MESI協議中不存在與Invalid無效能夠視做是等價的。
瞭解了MESI的四種內存緩存行狀態後,下面引入緩存內存行穩定態與非穩定態的概念。緩存內存行的穩定態指的是多核下高速緩存中的對應內存中在全部高速緩存中數據是一致的;非穩定態是穩定態的反面,指的是多核高速緩存中對應內存中在全部的高速緩存中數據出現了不一致,有不一樣的副本值。
當處於穩定態的緩存行因爲某些事件的發生轉向不穩定態時,MESI協議可以採起一些措施令整個多核存儲系統從新回到穩定態(能夠類比自平衡二叉樹在發生插入/刪除事件時,引發失衡後進行的重平衡操做)。
內存緩存行處於穩定態的幾種狀況:
一、對應內存行在全部核的高速緩存中都不存在。
二、對應內存行有且僅在一個核的高速緩存中存在,其狀態能夠是Exclusive也能夠是Modified。
三、對應內存行在一個以上核的高速緩存中存在,每一個緩存行中的存儲的數據都和內存一致,其狀態都爲Shared。
隨着多核CPU中併發程序的不斷運行,高速緩存被反覆的讀寫,緩存內存行的狀態也會在MESI這四種狀態間反覆變化。
在MESI協議中,抽象出了四種會致使緩存內存行狀態的變化緩存事件:本地讀、本地寫、遠程讀以及遠程寫。緩存事件針對的是某一內存緩存行的事件。
本地讀(Local Read):
本地讀事件指的是本地核心對本身的緩存行進行讀取。
本地寫(Local Write):
本地寫事件指的是本地核心對本身的緩存行進行寫入。
遠程讀(Remote Read):
遠程讀事件指的是總線上的其它核心對某一內存緩存行進行了讀取,當前核心監聽到的事件。
某一個核心的本地讀事件,對於其餘核心就是針對其對應內存緩存行的遠程讀事件。
遠程寫(Remote Write):
遠程寫事件指的是總線上的其它核心對某一內存緩存行進行了寫入,當前核心監聽到的事件。
某一個核心的本地寫事件,對於其餘核心就是針對其對應內存緩存行的遠程寫事件。
緩存事件的發生可能會使得本地/遠程的對應緩存行狀態發生變化。好比當本來處於獨佔狀態的本地緩存行監聽到遠程讀事件時,須要將其由Exclusive被獨佔轉變爲Shared被共享;當監聽到遠程寫事件時,若是發現對應的緩存行存在(此時一定是Shared被共享狀態),此時的內存緩存行便失去了穩定,MESI協議是採起的是寫無效策略,須要將本地對應的緩存行設置爲Invalid無效。
MESI協議中的四種狀態在發生上述四種緩存事件時均可能發生對應的狀態遷移,兩兩組合以後共有4*4=16種狀況,下面進行詳細討論。
本地讀/寫事件和其它核的遠程讀/寫事件是互相對應的,相互之間對照能更好的理解MESI協議的工做機制。
發生local read本地讀事件:
Invalid無效的緩存行,在發生本地讀事件時,必須從內存或是處於Exclusive狀態的遠程高速緩存中獲取對應的最新數據寫入本地緩存。
1.當其它核心中都不存在對應緩存行數據時,從內存中獲取。加載後全局有且只有本地緩存中有此緩存行記錄,本地緩存行的狀態由Invalid變成Exclusive。
2.當其它的某一核心中剛好也存在對應緩存行數據,且狀態爲Exclusive或Shared時,從內存或是存在緩存行的核心中獲取。此時因爲本地和其它核心都保存了對應緩存行數據,但不獨佔緩存行,本地緩存行的狀態由Invalid變成Shared。
3.當其它的某一核心中剛好也存在對應緩存行數據,且狀態爲Modified時,觸發其遠程讀事件,將修改過的最新值寫入內存後,由本地核心從內存中讀取最新的值。此時因爲本地和以前狀態爲modified的核心都保存了對應緩存行數據,本地緩存行的狀態由Invalid變成Shared。
發生local write本地寫事件:
Invalid無效的緩存行,在發生本地寫事件時,必須從內存或是處於Exclusive狀態的遠程高速緩存中獲取對應的最新數據寫入本地緩存,再進行修改。
1.當其它核心中都不存在對應緩存行數據時,從內存獲取並修改。加載後全局有且只有本地緩存中有此緩存行記錄,因爲修改過和內存中數據不一樣,本地緩存行的狀態由Invalid變成Modified。
2.當其它的某一核心中剛好也存在對應緩存行數據時,且狀態爲Exclusive或Shared時,從內存或是存在緩存行的核心中獲取並修改。爲了保持高速緩存一致性的穩定,遠端的其它核心中的數據不能與本地核心本地寫的最新數據產生衝突,遠端的其它核觸發遠程寫事件,狀態都設置爲Invalid(寫失效協議)。此時全局有且只有本地緩存中有此緩存行記錄,本地緩存行的狀態由Invalid變成Modified。
2.當其它的某一核心中剛好也存在對應緩存行數據時,且狀態爲Modified,觸發其遠程寫事件,將修改過最新值寫入內存後,由本地核心從內存中讀取最新的值並修改。遠端的核心(以前爲modified)中的數據不能與本地核心本地寫的最新數據產生衝突,狀態設置爲Invalid。此時全局有且只有本地緩存中有此緩存行記錄,本地緩存行的狀態由Invalid變成Modified。
發生remote read遠程讀事件:
Invalid無效狀態等價於緩存行不存在,不對遠程讀事件進行任何處理,狀態依然爲Invalid。
發生remote write遠程寫事件:
Invalid無效狀態等價於緩存行不存在,不對遠程寫事件進行任何處理,狀態依然爲Invalid。
發生local read本地讀事件:
Exclucive表明本地核獨佔當前緩存行。
本地讀時直接從本身的高速緩存中獲取數據便可,狀態不變,依然爲Exclusive。
發生local write本地寫事件:
Exclucive表明本地核獨佔當前緩存行,且數據和內存中一致。
本地寫會使得緩存中的數據與內存不一致,但依然獨佔,狀態由Exclusive變爲Modified。
發生remote read遠程讀事件:
當監聽到遠程讀事件時,意味着當前緩存行已經再也不是獨佔狀態,而是共享狀態了,狀態由Exclusive變爲Shared。
發生remote write遠程寫事件:
當監聽到遠程寫事件時,意味着當前緩存行的數據已經和本地緩存中的數據不一致了,須要廢棄本地的緩存行,狀態由Exclusive變爲Invalid。
發生local read本地讀事件:
Shared表明本地核心和遠程核心共享了緩存行,且數據是最新的。本地讀時直接從本身的高速緩存中獲取數據便可,狀態不變,依然爲Shared。
發生local write本地寫事件:
本地寫會觸發其它核的(處於Shared狀態)遠程寫事件,遠程核的狀態會被統一設置爲無效,本地核心將獨佔這一緩存行。因爲本地寫使得緩存行數據和內存不一致,狀態由Shared變爲Modified。
發生remote read遠程讀事件:
當監聽到遠程讀事件時,其並無改變全局狀態下緩存行的數據,狀態不變依然爲Shared。
發生remote write遠程寫事件:
當監聽到遠程寫事件時,意味着當前緩存行的數據已經和本地緩存中的數據不一致了,須要廢棄本地的緩存行,狀態由Shared變爲Invalid。
當前緩存行處於Modified狀態時:
發生local read本地讀事件:
Modified表明本地核心獨佔當前緩存行,在全局串行寫序列中,本地讀事件讀取的依然是最新的值。本地讀時直接從本身的高速緩存中獲取數據便可,狀態不變,依然爲Modified。
發生local write本地寫事件:
本地寫時依然獨佔緩存行,且和內存中數據不一致,狀態不變,依然爲Modified。
發生remote read遠程讀事件:
當監聽到遠程寫事件時,爲了使得其獲取到的值是全局串行序列中最近的值,須要先將Modified以後的最新值寫回內存,令最新值對其它核心可見。
遠程讀事件意味着有其它核心共享了對應的緩存行,本地再也不獨佔,但數據依然和本地緩存中的值保持一致,狀態由Modified變爲Shared。
發生remote write遠程寫事件:
當監聽到遠程寫事件時,爲了使得其獲取到的值是全局串行序列中最近的值,須要先將Modified以後的最新值寫回內存,令最新值對其它核心可見。
遠程讀事件意味着有其它核心共享了對應的緩存行,本地再也不獨佔,且本地緩存的數據再也不是最新的,須要令其失效,狀態由Modified變爲Invalid。
經過MESI協議,在強制串行化的總線事務幫助下可以始終保持一個全局高速緩存一致的穩定狀態。
MESI協議依賴總線偵聽機制,在某個核心發生本地寫事件時,爲了保證全局只能有一份緩存數據,要求其它覈對應的緩存行通通設置爲Invalid無效狀態。爲了確保總線寫事務的強一致性,發生本地寫的高速緩存須要等到遠端的全部核心都處理完對應的失效緩存行,返回Ack確認消息後才能繼續執行下面的內存尋址指令(阻塞)。
原始MESI協議實現時的性能問題:
1.對於進行本地寫事件的核心,遠端核心處理失效並進行響應確認相對處理器自身的指令執行速度來講是至關耗時的,在等待全部核心響應的過程當中令處理器空轉效率並不高。
2.對於響應遠程寫事件的核心,在其高速緩存壓力很大時,要求實時的處理失效事件也存在必定的困難,會有必定的延遲。
不進行優化的MESI協議在實際工做中效率會很是的低下,所以CPU的設計者在實現時對MESI協議進行了必定的改良。
針對上述本地寫事件須要等待遠端核心ACK確認,阻塞本地處理器的問題,引入了存儲緩存機制。
存儲緩存是屬於每一個CPU核心的。當使用了存儲緩存後,每當發生本地寫事件時,本地核心再也不阻塞的等待遠程核的確認響應,而是將寫入的新值放入存儲緩存中,繼續執行後面的指令。存儲緩存會替處理器接受遠端核心的ACK確認,當對應本地寫事件廣播獲得了所有遠程核心的確認後,再提交事務,將其新值寫入本地高速緩存中。存儲緩存的大小是十分有限的,當堆積的事務滿了以後,依然會阻塞CPU,直到有事務提交釋放出新的空間。
存儲緩存的引入將本地寫事件--->等待遠程寫通知確認消息並提交這一事務,從同步、強一致性變成了異步、最終一致性,提升了本地寫事件的處理效率。
本地處理器在進行本地讀事件時,因爲可能存儲緩存中新修改的數據還未提交到本地緩存中,這就會形成一個核心內,對於同一緩存行其後續指令的讀操做沒法讀取到以前寫操做的最新值。爲此,在進行本地讀操做時,處理器會先在存儲緩存中查詢對應記錄是否存在,若是存在則會從存儲緩存中直接獲取,這一機制被稱爲Store Fowarding。
針對上述遠端核心響應遠程寫事件,實時的將對應緩存行設置爲Invalid無效狀態延遲高的問題,引入了失效隊列機制。
失效隊列一樣是屬於每一個CPU核心的。當使用了失效隊列後,每當監聽到遠程寫事件時,對應的高速緩存再也不同步的處理失效緩存行後返回ACK確認信息,而是將失效通知存入失效隊列,當即返回ACK確認消息。對於失效隊列中的寫失效通知,會在空閒時逐步的進行處理,將對應的高速緩存中的緩存行設置爲無效。失效隊列的引入在很大程度上緩解了存儲緩存空間有限,容易阻塞的問題。
失效隊列的引入將監聽到遠程寫事件處理失效緩存行--->返回ACK確認消息這一事務,從同步、強一致性變成了異步、最終一致性,提升了遠程寫事件的處理效率。
存儲緩存和失效隊列的引入在提高MESI協議實現的性能同時,也帶來了一些問題。因爲MESI的高速緩存一致性是創建在強一致性的總線串行事務上的,而存儲緩存和失效隊列將事務的強一致性弱化爲了最終一致性,使得在一些臨界點上全局的高速緩存中的數據並非徹底一致的。
對於通常的緩存數據,基於異步最終一致的緩存間數據同步不是大問題。但對於併發程序,多核高速緩存間短暫的不一致將會影響共享數據的可見性,使得併發程序的正確性沒法獲得可靠保證,這是十分致命的。但CPU在執行指令時,缺失了太多的上下文信息,沒法識別出緩存中的內存數據是不是併發程序的共享變量,是否須要捨棄性能進行強一致性的同步。
CPU的設計者提供了內存屏障機制將對共享變量讀寫的高速緩存的強一致性控制權交給了程序的編寫者或者編譯器。
內存屏障分爲讀屏障和寫屏障兩種,內存屏障以機器指令的形式進行工做。
寫屏障用於保證高速緩存間寫事務的強一致性。當CPU執行寫屏障指令時,必須強制等待存儲緩存中的寫事務所有處理完再繼續執行後面的指令。至關於將存儲緩存中異步處理的本地寫事務作了強一致的同步。
寫屏障指令執行完後,當前核心位於寫屏障執行前的本地寫事務所有處理完畢,其它的核心都已經接收到了當前全部的遠程寫事件的寫無效通知。
讀屏障用於保證高速緩存間讀事務的強一致性。當CPU執行讀屏障指令時,必須先將當前處於失效隊列中的寫無效事務所有處理完,再繼續的執行讀屏障後面的指令。至關於將異步隊列中異步處理的遠程寫事務作了強一致的同步。
讀屏障指令執行完後,當前核心位於讀屏障執行前的遠程寫無效事務所有處理完畢,對於讀屏障以後的共享數據讀取會獲得最新的值。
在進行併發程序的開發時,針對關鍵的任務間共享變量的讀寫須要使用內存屏障保證其在多核間高速緩存的一致性。在對共享變量的寫入指令後,加入寫屏障,令新的數據當即對其它核心可見;在對共享變量的讀取指令前,加入讀屏障,令其能獲取最新的共享變量值。
經過在指令中的適當位置加入讀/寫內存屏障,雖然必定程度上下降了效率,但保證了併發程序在多核高速緩存條件下對於共享變量的可見性,是一個很好的折中解決方案。
因爲最近在學習有關硬件和操做系統相關的知識,在看到高速緩存相關的內容時,便想把一直以來都只知其一;不知其二的MESI協議弄懂。經過普遍的閱讀有關博客和書籍等資料,理解並整理後寫下了這篇博客,但願能幫到對MESI協議等相關內容感興趣的人。
MESI協議和內存屏障的知識,在其基礎之上有高級語言中c、java的volatile關鍵字的工做原理,在其下有多核並行處理器、高速緩存、總線等硬件電路工做原理等相關的內容。在學習的過程當中,一方面使我更好的理解了更上層的知識,另外一方面黑盒子下還有黑盒子,令我感嘆吾生也有涯,而知也無涯。但在學習的過程當中,知足了對底層工做機制的好奇心,有着理解通透以後的快樂,仍是挺有意思的。
本篇博客還存在不少不足之處,請多多指教。
主要參考書籍與博客:
http://www.javashuo.com/article/p-nqjwgkuj-nu.html
https://cloud.tencent.com/developer/article/1152642
http://www.javashuo.com/article/p-qgwgeljq-r.html
https://www.jianshu.com/p/0e036fa7af2a
https://blog.csdn.net/weixin_44936828/article/details/89430358
http://www.wowotech.net/kernel_synchronization/memory-barrier.html