
CPU Cache 通常分為三級緩存:L1 Cache、L2 Cache、L3 Cache,級別越低的離 CPU 核心越近,訪問速度也快,但是存儲(chǔ)容量相對就會(huì)越小。其中,在多核心的 CPU 里,每個(gè)核心都有各自的 L1/L2 Cache,而 L3 Cache 是所有核心共享使用的。
多核CPU同時(shí)工作的時(shí)候,每個(gè)核心都會(huì)從內(nèi)存中讀取一份數(shù)據(jù)并緩存到自己的Cache中,當(dāng)發(fā)生寫操作的時(shí)候,有兩種情況
1、寫直達(dá),只要有數(shù)據(jù)寫入,都會(huì)直接把數(shù)據(jù)寫入到內(nèi)存里面,這種方式簡單直觀,但是性能就會(huì)受限于內(nèi)存的訪問速度;
2、寫回,對于已經(jīng)緩存在 Cache 的數(shù)據(jù)的寫入,只需要更新其數(shù)據(jù)就可以,不用寫入到內(nèi)存,只有在需要把緩存里面的臟數(shù)據(jù)交換出去的時(shí)候,才把數(shù)據(jù)同步到內(nèi)存里,這種方式在緩存命中率高的情況,性能會(huì)更好;
上面的操作帶來的問題是當(dāng)一個(gè)核心的數(shù)據(jù)發(fā)生改變的時(shí)候,其他核心并不知道,仍然在用舊的數(shù)據(jù)進(jìn)行讀寫操作,導(dǎo)致出現(xiàn)數(shù)據(jù)不一致問題
要想解決數(shù)據(jù)不一致問題,只要兩個(gè)方面
第一點(diǎn)是寫傳播,也就是當(dāng)某個(gè) CPU 核心發(fā)生寫入操作時(shí),需要把該事件廣播通知給其他核心;
第二點(diǎn)是事物的串行化,這個(gè)很重要,只有保證了這個(gè),才能保障我們的數(shù)據(jù)是真正一致的,我們的程序在各個(gè)不同的核心上運(yùn)行的結(jié)果也是一致的;
MESI協(xié)議
MESI 協(xié)議其實(shí)是 4 個(gè)狀態(tài)單詞的開頭字母縮寫,分別是:
- Modified,已修改
- Exclusive,獨(dú)占
- Shared,共享
- Invalidated,已失效
這四個(gè)狀態(tài)來標(biāo)記 Cache Line 四個(gè)不同的狀態(tài)。
「已修改」?fàn)顟B(tài)就是我們前面提到的臟標(biāo)記,代表該 Cache Block 上的數(shù)據(jù)已經(jīng)被更新過,但是還沒有寫到內(nèi)存里。而「已失效」?fàn)顟B(tài),表示的是這個(gè) Cache Block 里的數(shù)據(jù)已經(jīng)失效了,不可以讀取該狀態(tài)的數(shù)據(jù)。
「獨(dú)占」和「共享」?fàn)顟B(tài)都代表 Cache Block 里的數(shù)據(jù)是干凈的,也就是說,這個(gè)時(shí)候 Cache Block 里的數(shù)據(jù)和內(nèi)存里面的數(shù)據(jù)是一致性的。
「獨(dú)占」和「共享」的差別在于,獨(dú)占狀態(tài)的時(shí)候,數(shù)據(jù)只存儲(chǔ)在一個(gè) CPU 核心的 Cache 里,而其他 CPU 核心的 Cache 沒有該數(shù)據(jù)。這個(gè)時(shí)候,如果要向獨(dú)占的 Cache 寫數(shù)據(jù),就可以直接自由地寫入,而不需要通知其他 CPU 核心,因?yàn)橹挥心氵@有這個(gè)數(shù)據(jù),就不存在緩存一致性的問題了,于是就可以隨便操作該數(shù)據(jù)。
另外,在「獨(dú)占」?fàn)顟B(tài)下的數(shù)據(jù),如果有其他核心從內(nèi)存讀取了相同的數(shù)據(jù)到各自的 Cache ,那么這個(gè)時(shí)候,獨(dú)占狀態(tài)下的數(shù)據(jù)就會(huì)變成共享狀態(tài)。
那么,「共享」?fàn)顟B(tài)代表著相同的數(shù)據(jù)在多個(gè) CPU 核心的 Cache 里都有,所以當(dāng)我們要更新 Cache 里面的數(shù)據(jù)的時(shí)候,不能直接修改,而是要先向所有的其他 CPU 核心廣播一個(gè)請求,要求先把其他核心的 Cache 中對應(yīng)的 Cache Line 標(biāo)記為「無效」?fàn)顟B(tài),然后再更新當(dāng)前 Cache 里面的數(shù)據(jù)。
我們舉個(gè)具體的例子來看看這四個(gè)狀態(tài)的轉(zhuǎn)換:
- 當(dāng) A 號 CPU 核心從內(nèi)存讀取變量 i 的值,數(shù)據(jù)被緩存在 A 號 CPU 核心自己的 Cache 里面,此時(shí)其他 CPU 核心的 Cache 沒有緩存該數(shù)據(jù),于是標(biāo)記 Cache Line 狀態(tài)為「獨(dú)占」,此時(shí)其 Cache 中的數(shù)據(jù)與內(nèi)存是一致的;
- 然后 B 號 CPU 核心也從內(nèi)存讀取了變量 i 的值,此時(shí)會(huì)發(fā)送消息給其他 CPU 核心,由于 A 號 CPU 核心已經(jīng)緩存了該數(shù)據(jù),所以會(huì)把數(shù)據(jù)返回給 B 號 CPU 核心。在這個(gè)時(shí)候, A 和 B 核心緩存了相同的數(shù)據(jù),Cache Line 的狀態(tài)就會(huì)變成「共享」,并且其 Cache 中的數(shù)據(jù)與內(nèi)存也是一致的;
- 當(dāng) A 號 CPU 核心要修改 Cache 中 i 變量的值,發(fā)現(xiàn)數(shù)據(jù)對應(yīng)的 Cache Line 的狀態(tài)是共享狀態(tài),則要向所有的其他 CPU 核心廣播一個(gè)請求,要求先把其他核心的 Cache 中對應(yīng)的 Cache Line 標(biāo)記為「無效」?fàn)顟B(tài),然后 A 號 CPU 核心才更新 Cache 里面的數(shù)據(jù),同時(shí)標(biāo)記 Cache Line 為「已修改」?fàn)顟B(tài),此時(shí) Cache 中的數(shù)據(jù)就與內(nèi)存不一致了。
- 如果 A 號 CPU 核心「繼續(xù)」修改 Cache 中 i 變量的值,由于此時(shí)的 Cache Line 是「已修改」?fàn)顟B(tài),因此不需要給其他 CPU 核心發(fā)送消息,直接更新數(shù)據(jù)即可。
- 如果 A 號 CPU 核心的 Cache 里的 i 變量對應(yīng)的 Cache Line 要被「替換」,發(fā)現(xiàn) Cache Line 狀態(tài)是「已修改」?fàn)顟B(tài),就會(huì)在替換前先把數(shù)據(jù)同步到內(nèi)存。
所以,可以發(fā)現(xiàn)當(dāng) Cache Line 狀態(tài)是「已修改」或者「獨(dú)占」?fàn)顟B(tài)時(shí),修改更新其數(shù)據(jù)不需要發(fā)送廣播給其他 CPU 核心,這在一定程度上減少了總線帶寬壓力。
事實(shí)上,整個(gè) MESI 的狀態(tài)可以用一個(gè)有限狀態(tài)機(jī)來表示它的狀態(tài)流轉(zhuǎn)。還有一點(diǎn),對于不同狀態(tài)觸發(fā)的事件操作,可能是來自本地 CPU 核心發(fā)出的廣播事件,也可以是來自其他 CPU 核心通過總線發(fā)出的廣播事件。下圖即是 MESI 協(xié)議的狀態(tài)圖:

總結(jié)
CPU 在讀寫數(shù)據(jù)的時(shí)候,都是在 CPU Cache 讀寫數(shù)據(jù)的,原因是 Cache 離 CPU 很近,讀寫性能相比內(nèi)存高出很多。對于 Cache 里沒有緩存 CPU 所需要讀取的數(shù)據(jù)的這種情況,CPU 則會(huì)從內(nèi)存讀取數(shù)據(jù),并將數(shù)據(jù)緩存到 Cache 里面,最后 CPU 再從 Cache 讀取數(shù)據(jù)。
而對于數(shù)據(jù)的寫入,CPU 都會(huì)先寫入到 Cache 里面,然后再在找個(gè)合適的時(shí)機(jī)寫入到內(nèi)存,那就有「寫直達(dá)」和「寫回」這兩種策略來保證 Cache 與內(nèi)存的數(shù)據(jù)一致性:
- 寫直達(dá),只要有數(shù)據(jù)寫入,都會(huì)直接把數(shù)據(jù)寫入到內(nèi)存里面,這種方式簡單直觀,但是性能就會(huì)受限于內(nèi)存的訪問速度;
- 寫回,對于已經(jīng)緩存在 Cache 的數(shù)據(jù)的寫入,只需要更新其數(shù)據(jù)就可以,不用寫入到內(nèi)存,只有在需要把緩存里面的臟數(shù)據(jù)交換出去的時(shí)候,才把數(shù)據(jù)同步到內(nèi)存里,這種方式在緩存命中率高的情況,性能會(huì)更好;
當(dāng)今 CPU 都是多核的,每個(gè)核心都有各自獨(dú)立的 L1/L2 Cache,只有 L3 Cache 是多個(gè)核心之間共享的。所以,我們要確保多核緩存是一致性的,否則會(huì)出現(xiàn)錯(cuò)誤的結(jié)果。
要想實(shí)現(xiàn)緩存一致性,關(guān)鍵是要滿足 2 點(diǎn):
- 第一點(diǎn)是寫傳播,也就是當(dāng)某個(gè) CPU 核心發(fā)生寫入操作時(shí),需要把該事件廣播通知給其他核心;
- 第二點(diǎn)是事物的串行化,這個(gè)很重要,只有保證了這個(gè),才能保障我們的數(shù)據(jù)是真正一致的,我們的程序在各個(gè)不同的核心上運(yùn)行的結(jié)果也是一致的;
基于總線嗅探機(jī)制的 MESI 協(xié)議,就滿足上面了這兩點(diǎn),因此它是保障緩存一致性的協(xié)議。
MESI 協(xié)議,是已修改、獨(dú)占、共享、已失效這四個(gè)狀態(tài)的英文縮寫的組合。整個(gè) MSI 狀態(tài)的變更,則是根據(jù)來自本地 CPU 核心的請求,或者來自其他 CPU 核心通過總線傳輸過來的請求,從而構(gòu)成一個(gè)流動(dòng)的狀態(tài)機(jī)。另外,對于在「已修改」或者「獨(dú)占」?fàn)顟B(tài)的 Cache Line,修改更新其數(shù)據(jù)不需要發(fā)送廣播給其他 CPU 核心。