現代計算機都是多核cpu,cpu需要和內存交互,但內存相對cpu的速度實在太慢,於是cpu和內存之間還有cache層,每個cpu都有屬於自己的cache,cache由cache line組成,每個cache line 64位(根據不同架構,也可能是32位或128位),每個cache line知道自己對應什麼範圍的物理內存地址,當cpu需要讀取某一個內存地址的值時,它會把內存地址傳遞給一級cache,一級cache會檢查它是否有這個內存地址對應的cache line。如果沒有,它會以cache line爲單位從內存加載數據,是的,一次加載整個cache line,這是基於這樣一個假設:內存訪問傾向於本地化(localized),如果我們當前需要某個地址的數據,那麼很可能我們馬上要訪問它的鄰近地址。
這是最原始的cpu架構
由於每個cpu獨立工作,那就會有一個顯著的問題:多個cache與內存之間的數據同步該怎麼做?緩存一致性協議就是要解決這個問題,協議有多種,可以分爲兩類:“窺探(snooping)”協議和“基於目錄的(directory-based)”協議,本文所講述的MESI協議屬於一種“窺探協議“。
窺探協議的基本思想
所有cache與內存,cache與cache(是的,cache之間也會有數據傳輸)之間的傳輸都發生在一條共享的總線上,而所有的cpu都能看到這條總線,同一個指令週期中,只有一個cache可以讀寫內存,所有的內存訪問都要經過仲裁(arbitrate)。
窺探協議的思想是,cahce不但與內存通信時和總線打交道,而且它會不停地窺探總線上發生的數據交換,跟蹤其他cache在做什麼。所以當一個cache代表它所屬的cpu去讀寫內存時,其它cpu都會得到通知,它們以此來使自己的cache保持同步。
MESI協議的工作方式
”MESI“該名稱來自4個狀態的首字母的縮寫,協議中最重要的內容有兩部分:cache line的狀態以及消息通知機制。
cache line的狀態有4個:
- Invalid,表明該cache line已失效,它要麼已經不在cache中,要麼它的內容已經過時。處於該狀態下的cache line等同於它從來沒被加載到cache中。
- Shared,表明該cache line是內存中某一段數據的拷貝,處於該狀態下的cache line只能被cpu讀取,不能寫入,因爲此時還沒有獨佔。不同cpu的cache line都可以擁有這段內存數據的拷貝。
- Exclusive,和 Shared 狀態一樣,表明該cache line是內存中某一段數據的拷貝。區別在於,該cache line獨佔該內存地址,其他處理器的cache line不能同時持有它,如果其他處理器原本也持有同一cache line,那麼它會馬上變成“Invalid”狀態。
- Modified,表明該cache line已經被修改,cache line只有處於Exclusive狀態才能被修改。此外,已修改cache line如果被丟棄或標記爲Invalid,那麼先要把它的內容回寫到內存中。
我們發現,cpu有讀取數據的動作,有獨佔的動作,有獨佔後更新數據的動作,有更新數據之後回寫內存的動作,根據”窺探協議“的規範,每個動作都需要通知到其他cpu,於是有以下的消息機制:
- Read,cpu發起讀取數據請求,請求中包含需要讀取的數據地址。
- Read Response,作爲Read消息的響應,該消息可能是內存響應的,也可能是某cpu響應的(比如該地址在某cpu cache Line中爲Modified狀態,則該cpu必須返回該地址的最新數據)。
- Invalidate,cpu發起”我要獨佔一個cache line,其他cpu請失效對應的cache line“的消息,消息中包含了內存地址,所有的其它cpu需要將對應cache line置爲Invalid狀態。
- Invalidate ACK,收到Invalidate消息的cpu在將對應cache line置爲Invalid後,返回Invalid ACK。
- Read Invalidate,相當於Read消息+Invalidate消息,即取得數據並且獨佔它,將收到一個Read Response和所有其它cpu的Invalidate ACK。
- Write back,寫回消息,即將狀態爲Modified的cache line寫回到內存,通常在該行將被替換時使用。現代cpu cache基本都採用”寫回(Write Back)”而非”直寫(Write Through)”的方式。
結合cache line狀態以及消息機制,我們來看看cpu之間是如何協作的。爲了簡化,假設我們有個四核cpu系統,每個cpu只有一個cache line,每個cache line大小爲1個字節,內存地址空間一共兩個字節的數據,地址分別爲0x0和0x8,有如下操作序列:
cache line時序變化圖
- 初始狀態,4個cpu的cache line都爲Invalid狀態(黑色表示Invalid)。
- cpu0發送Read消息,加載0x0的數據,數據從內存返回,cache line狀態變爲Shared。
- cpu3發送Read消息,加載0x0的數據,數據從內存返回,cache line狀態變爲Shared。
- cpu0發送Read消息,加載0x8的數據,導致cache line被替換,由於之前狀態爲Shared,即與內存中數據一致,可直接覆蓋,而無需回寫。
- cpu2發送Read Invalidate消息,從內存返回最新數據,cpu3返回Invalidate ACK,並將狀態變爲Invalid,cpu2獲得獨佔權,狀態變爲Exclusive。
- cpu2修改cache line中的數據,cache line狀態爲Modified,同時內存中0x0的數據過期。
- cpu1 對地址0x0的數據執行原子(atomic)遞增操作,發出Read Invalidate消息,cpu2將返回Read Response(而不是內存),包含最新數據,並返回Invalidate ACK,同時cache line狀態變爲Invalid。最後cpu1獲得獨佔權,cache line狀態變爲Modified,數據爲遞增後的數據,而內存中的數據仍然爲過期狀態。
- cpu1 加載0x8的數據,此時cache line將被替換,由於之前狀態爲Modified,因此需要先執行寫回操作,此時內存中0x0的數據得以更新。
總結
這就是緩存一致性協議,一個狀態機,僅此而已。因爲該協議的存在,每個cpu就可以放心操作屬於自己的cache,而不需要擔心本地cache中的數據會不會已經被其他cpu修改了之類的煩心事。
但到目前爲止,cpu並不滿足,覺得在緩存一致性協議的框架下工作性能不夠高,但這並不是協議的問題,協議本身邏輯很嚴謹,沒毛病(同類協議之間的優劣那是另外一回事)。性能差在哪裏?假如某數據存在於其他cpu的cache中,那自己每次需要修改數據時,都需要發送Read Invalidate消息,除了等待最新數據的返回,還需要等待其他cpu的Invalidate ACK才能繼續執行其他指令,這是一種同步行爲,cpu可忍不了,我們看看cpu如何優化自己?