深度剖析底層原理:CPU 緩存一致性的奧祕

對於很多深入研究計算機底層架構的朋友來說,CPU 緩存一致性無疑是一個繞不開的關鍵話題。從單核處理器邁向多核時代,計算能力呈指數級增長,可隨之而來的挑戰也接踵而至。CPU 爲了加速數據存取,引入了多層緩存結構,這本是提升性能的妙招,然而,當多個核心同時讀寫共享數據時,緩存中的數據副本如何保持同步、確保一致,就成了棘手難題。這涉及到複雜的硬件協議、緩存控制機制以及軟件層面的優化策略。

CPU 緩存一致性原理是指在多核 CPU 系統中,多個 CPU 的緩存副本應該保持一致,以保證數據的正確性和一致性。當一個 CPU 要修改內存中的數據時,它首先會把這個數據的副本從內存讀入到自己的緩存中,然後修改緩存中的副本。如果其他 CPU 也在操作同一份數據,那麼它們的緩存中的數據就是舊的,不是最新的。這樣就會出現數據不一致的問題。現在,就讓我們開啓這場探索之旅,深入瞭解 CPU 緩存一致性的奧祕。

一、CPU 緩存概述

1.1CPU 緩存的 “前世今生”

在計算機的發展歷程中,CPU 緩存的誕生是爲了解決一個關鍵問題:CPU 與內存之間日益增大的速度鴻溝。早期,CPU 的運算速度相對較慢,內存的讀寫速度尚能與之匹配。但隨着科技的飛速進步,CPU 猶如裝上了超級引擎,運算速度呈指數級增長,而內存的發展速度卻像是在悠閒漫步,遠遠跟不上 CPU 的步伐。這就導致 CPU 在執行指令時,常常要花費大量時間等待內存數據的傳輸,就像一輛高速跑車在擁堵的慢車道上,有勁使不出,計算機的整體性能也因此大打折扣。

爲了打破這個瓶頸,CPU 緩存應運而生。它就像是 CPU 身邊的一位 “得力助手”,位於 CPU 和內存之間,憑藉着超高的讀寫速度,成爲了數據的 “快速中轉站”。當 CPU 需要讀取數據時,會首先在緩存中查找,由於緩存的速度極快,通常能在短短几個時鐘週期內就將數據交付給 CPU,大大減少了 CPU 的等待時間,讓計算機的運行效率得到了質的飛躍。

起初,CPU 緩存只有一級,容量較小但速度超羣,猶如 CPU 的 “貼身保鏢”,緊緊跟隨並快速響應其需求。後來,隨着計算機處理任務的愈發複雜,對緩存容量的需求也越來越大,二級緩存、三級緩存等多級緩存架構逐漸嶄露頭角。這些不同層級的緩存,就像是一個分工明確的團隊,各自承擔着不同的職責,共同協作,爲 CPU 提供高效的數據支持。

在單核處理器時代,CPU 緩存的管理相對簡單,數據的一致性比較容易保證。但多核處理器的出現,猶如一場風暴,徹底改變了計算機的格局。多個核心如同多個並肩作戰的 “戰士”,可以同時處理不同的任務,大幅提升了計算能力。然而,這也帶來了一個棘手的問題:緩存一致性。每個核心都有自己的緩存,當多個核心同時操作同一份數據時,它們各自緩存中的數據副本可能會出現不一致的情況,就好比多個士兵對同一作戰指令有不同的理解,這必然會導致混亂,使計算機的運算結果出錯。因此,確保 CPU 緩存一致性,成爲了多核時代計算機系統穩定高效運行的關鍵挑戰。

1.2CPU 多核

現代的 CPU 比內存系統快很多,2006 年的 cpu 可以在一納秒之內執行 10 條指令,尤其是多 CPU,CPU 多核。我們先講解一些基礎概念:

多核 CPU 和多 CPU 的區別主要在於性能和成本。多核 CPU 性能最好,但成本最高;多 CPU 成本小,便宜,但性能相對較差。一個 CPU 但是多核可以實現並行,單核就是 CPU 集成了一個運算核心;雙核是兩個運算核心,相當於兩個 CPU 同時工作;四核是四個運算核心,相當於四個 CPU 同時工作;簡單的比喻:完成同樣的任務,由一條生產線來完成或由兩條稍慢的生產線來完成或由四條更慢的生產線來完成,雖然生產線的生產速度慢,但由於同時進行的生產線多,所以任務的最終完成時間可能最短。

假如一個 CPU 運行多個程序,就意味着要經常進行進程上下文切換,這裏說一句進程切換比線程切換成本要高出許多,即使單 CPU 是多核的,也只是多個處理器核心,其它設備都是公用的,所以多個線程就必然要經常進行進程上下文切換。一個現代 CPU 除了處理器核心之外還包括寄存器、L1L2L3 緩存這些存儲設備、浮點運算單元、整數運算單元等一些輔助運算設備以及內部總線等。

一個多核的 CPU 也就是一個 CPU 上有多個處理器核心,這樣有什麼好處呢?比如說現在我們要在一臺計算機上跑一個多線程的程序,因爲是一個進程裏的線程,所以需要一些共享一些存儲變量,如果這臺計算機都是單核單線程 CPU 的話,就意味着這個程序的不同線程需要經常在 CPU 之間的外部總線上通信,同時還要處理不同 CPU 之間不同緩存導致數據不一致的問題,所以在這種場景下多核單 CPU 的架構就能發揮很大的優勢,通信都在內部總線,共用同一個緩存。

二、CPU 緩存核心原理

2.1CPU 緩存

即高速緩衝存儲器,是位於 CPU 與主內存間的一種容量較小但速度很高的存儲器。由於 CPU 的速度遠高於主內存,CPU 直接從內存中存取數據要等待一定時間週期,Cache 中保存着 CPU 剛用過或循環使用的一部分數據,當 CPU 再次使用該部分數據時可從 Cache 中直接調用, 減少 CPU 的等待時間,提高了系統的效率。

現在我們來看一下每級的緩存的處理速度對比:

從上圖可知,這裏面產生了至少兩個數量級的速度差距。在這樣的問題下,cpu cache 應運而生。CPU 緩存是位於 CPU 與內存之間的臨時存儲器,它的容量比內存小的多但是交換速度卻比內存要快得多。

CPU 高速緩存的出現主要是爲了解決 CPU 運算速度與內存讀寫速度不匹配的矛盾,按照數據讀取順序和與 CPU 結合的緊密程度,CPU 緩存可以分爲一級緩存,二級緩存,如今主流 CPU 還有三級緩存,甚至有些 CPU 還有四級緩存。每一級緩存中所儲存的全部數據都是下一級緩存的一部分,這三種緩存的技術難度和製造成本是相對遞減的,所以其容量也是相對遞增的。目前流行的多級緩存結構如下截圖:

緩存行:緩存系統中是以緩存行爲單位存儲的。緩存行是 2 的整數冪個連續字節,一般爲 32-256 個字節。最常見的緩存行大小是 64 字節。當多線程修改互相獨立的變量時,如果這些變量共享一個緩存行,就會無意中影響彼此的性能,這就是僞共享。緩存行上的寫競爭是運行在 SMP 系統中並行線程實現可伸縮性最重要的限制因素。有人將僞共享描述成無聲的性能殺手,因爲從代碼中很難看清楚是否會出現僞共享。

僞共享問題:

圖中說明了僞共享的問題。在覈心 1 上運行的線程想更新變量 X,同時核心 2 上的線程想要更新變量 Y。不幸的是,這兩個變量在同一個緩存行中。每個線程都要去 競爭緩存行的所有權來更新變量。如果核心 1 獲得了所有權,緩存子系統將會使核心 2 中對應的緩存行失效。當核心 2 獲得了所有權然後執行更新操作,核心 1 就要 使自己對應的緩存行失效。這會來來回回的經過 L3 緩存,大大影響了性能。如果互相競爭的核心位於不同的插槽,就要額外橫跨插槽連接,問題可能更加嚴重。

java 中避免僞共享參數:-XX:-RestrictContended

但是現在大部分服務器都是多 CPU,數據的讀寫就會變得異常複雜。我們在進行讀寫 cache 的時候,不能簡單地讀寫,因爲如果只修改本地 cpu 的 cache,而不處理其他 cpu 上的同一個數據,那麼就會造成一份數據多個不同副本,這就是數據衝突。而解決這個數據衝突的方法就是 “緩存一致性協議 MESI”。

2.2 緩存一致性引發的 “混亂局面”

(1) 多核心緩存修改衝突

爲了更直觀地感受緩存不一致帶來的問題,讓我們來看一個具體的例子。假設有一個多核 CPU,其中兩個核心 Core A 和 Core B 同時對一個共享變量 “count” 進行累加操作,初始時 “count” 的值爲 0。Core A 從內存中讀取了 “count” 的值 0 到自己的緩存中,然後進行加 1 操作,此時 Core A 緩存中的 “count” 變爲 1,但由於寫回策略,這個新值還沒有同步到內存。與此同時,Core B 也從內存讀取 “count”,由於內存中的值尚未更新,它讀到的依然是 0,接着 Core B 也對其加 1,並將結果 0 + 1 = 1 寫回內存。現在,兩個核心都完成了一次累加操作,但最終內存中的 “count” 值卻爲 1,而不是我們預期的 2,這顯然是一個錯誤的結果,根源就在於兩個核心的緩存數據沒有及時同步,導致了不一致。

(2) 寫傳播與事務串行化難題

在多核處理器環境下,要保證緩存一致性,關鍵要滿足兩點:寫傳播和事務串行化。寫傳播確保當某個 CPU 核心裏的 Cache 數據更新時,這個更新事件必須要傳播到其他核心的 Cache。這就好比在一個團隊中,任何一個成員獲得了新的關鍵信息,都要及時通知其他成員,讓大家的信息保持同步。比如在上述多核累加的例子中,如果 Core A 更新了 “count” 的值後能立即通知 Core B,讓 Core B 知曉這個變化,就能避免 Core B 使用舊值進行計算。

而事務串行化則要求某個 CPU 核心裏對數據的操作順序,在其他核心看來必須是一樣的。想象一下,在一個多線程的項目中,不同的線程對共享數據有着不同的操作,如果這些操作的執行順序在各個線程眼中不一樣,必然會導致混亂。例如,有三個

三、CPU 緩存架構詳解

緩存與主存解讀緩存一致性(Cache Coherency),先看一下 CPU 的架構:

圖示一個 4 核 CPU,有三個級別的緩存,分爲是 L1 Cache(一級緩存)、L2 Cache(二級緩存)、L3 Cache(三級緩存)其中一級緩存有兩部分組成:L1I Cache(一級指令緩存)和 L1D Cache(一級數據緩存)。

越靠近 CPU 的緩存速度越快,單價也更昂貴。其中一級和二級如今都屬於片內緩存(在 CPU 核內,早期 L2 緩存是片外的)獨立歸屬給各個 CPU,而三級緩存是 CPU 間共享的。

查詢緩存的時候也是由近及遠,優先從一級緩存去查找,找到就結束查找,找不到則再去二級緩存查找。二級緩存找不到去三級緩存查找。三級緩存還找不到就去主存(Main Memory)查找。這裏說的主存,就是我們平常說的內存,內存是 DRAM(Dynamic RAM),緩存是 SRAM(Static RAM)。

3.1 緩存行

CPU 操作緩存的單位是” 緩存行 “(cacheline),也就是說如果 CPU 要讀一個變量 x,那麼其實是讀變量 x 所在的整個緩存行。

緩存行大小

好了,既然我們知道了 CPU 讀寫緩存的單位是緩存行,那麼緩存行的大小是多少呢?

查看機器緩存行大小的方法有很多,在 Linux 上你可以查看如下文件確認緩存行大小:

# L1D Cache
cat /sys/devices/system/cpu/cpu0/cache/index0/coherency_line_size

# L1I Cache
cat /sys/devices/system/cpu/cpu0/cache/index1/coherency_line_size

# L2 Cache
cat /sys/devices/system/cpu/cpu0/cache/index2/coherency_line_size

# L3 Cache
cat /sys/devices/system/cpu/cpu0/cache/index3/coherency_line_size

或者用 getconf 命令:

# L1D Cache
getconf LEVEL1_DCACHE_LINESIZE

# L1I Cache
getconf LEVEL1_ICACHE_LINESIZE

# L2 Cache
getconf LEVEL2_CACHE_LINESIZE

# L3 Cache
getconf LEVEL3_CACHE_LINESIZE

一般會看到:64。表示的是 64 字節。注意,單核 CPU 上,可能沒有 L3 緩存。

併發場景下(比如多線程)如果操作相同變量,如何保證每個核中緩存的變量是正確的值,這涉及到一些” 緩存一致性 “的協議。其中應用最廣的就是 MESI 協議(當然這並不是唯一的緩存一致性協議)。

3.2 總線嗅探機制

CPU 和內存通過總線(BUS)互通消息;CPU 感知其他 CPU 的行爲(比如讀、寫某個緩存行)就是是通過嗅探(Snoop)線性中其他 CPU 發出的請求消息完成的,有時 CPU 也需要針對總線中的某些請求消息進行響應。這被稱爲” 總線嗅探機制 “。

在衆多解決緩存一致性問題的方案中,總線嗅探是最爲常見的一種,它就像是計算機系統中的 “情報員”,時刻監聽着總線的一舉一動。其工作原理並不複雜,當某個 CPU 核心修改了自己緩存中的數據時,會立即向總線發出通知,這個通知就像是一聲 “警報”,廣播給所有其他的 CPU 核心。其他核心就像警覺的 “衛士”,一直在監聽總線,一旦收到這個通知,便會檢查自己的緩存中是否有相同的數據。如果發現有,就會採取相應的行動,通常是將自己緩存中的該數據標記爲無效,或者根據具體協議更新數據,以此來保證數據的一致性。

四、CPU 緩存一致性

4.1 爲什麼需要緩存一致

目前主流電腦的 CPU 都是多核心的,多核心的有點就是在不能提升 CPU 主頻後,通過增加核心來提升 CPU 吞吐量。每個核心都有自己的 L1 Cache 和 L2 Cache,只是共用 L3 Cache 和主內存。每個核心操作是獨立的,每個核心的 Cache 就不是同步更新的,這樣就會帶來緩存一致性(Cache Coherence)的問題。

有 2 個 CPU,主內存裏有個變量x=0。CPU A 中有個需要將變量x1。CPU A 就將變量x加載到自己的緩存中,然後將變量x1。因爲此時 CPU A 還未將緩存數據寫回主內存,CPU B 再讀取變量x時,變量x的值依然是0

緩存一致性是指在分佈式系統中,多個節點之間的緩存數據保持一致的狀態。它的重要性體現在以下幾個方面:

爲了維護緩存一致性,常見的策略包括使用鎖機制、發佈 / 訂閱模式、版本號比較等。這樣可以確保在讀取和更新緩存時保持一致,並提供高性能和準確的數據訪問。

4.2MESI 協議:緩存一致性的 “救星”

(1)MESI 協議的四種狀態

面對總線嗅探的諸多問題,MESI 協議應運而生,它如同一位智慧的 “指揮官”,巧妙地利用四種狀態來管理緩存行,爲緩存一致性問題帶來了高效的解決方案。這四種狀態分別是:Modified(已修改)、Exclusive(獨享、互斥)、Shared(共享)和 Invalid(無效)。

Modified 狀態,就像是被標記了 “機密” 的文件,意味着該緩存行中的數據已經被修改,與內存中的數據不一致,並且是唯一的副本,只存在於當前 CPU 核心的緩存中。此時,這個緩存行必須時刻警惕,監聽所有試圖讀取該緩存行對應主存地址的操作,一旦監聽到,就必須在該操作執行前,爭分奪秒地把緩存行中的數據寫回主內存,以保證其他核心能獲取到最新的數據。

Exclusive 狀態,如同被一個人獨佔的寶藏,該緩存行的數據未被修改,與內存中的數據一致,且只存在於當前 CPU 核心的緩存中。不過,它也不能掉以輕心,要監聽其他緩存讀取主存中對應數據的操作,一旦發現,就得大方地將自己的狀態轉變爲 Shared,允許其他核心共享這份數據。

Shared 狀態,彷彿是被公開分享的知識,該緩存行的數據未被修改,存在於多個 CPU 核心的緩存中。每個處於此狀態的緩存行都肩負着監聽的重任,一旦察覺到有其他核心試圖將該緩存行設置爲 Modified 或 Exclusive 狀態,就必須立刻將自己的狀態改爲 Invalid,避免數據衝突。

Invalid 狀態,則像是被廢棄的紙張,表明該緩存行的數據無效,不能用於讀寫操作。

這四種狀態之間的轉換並非隨意,而是遵循着嚴格的規則,如同精密的齒輪相互咬合。下面通過一個圖表來直觀展示它們之間的轉換條件:

7JLCMD

通過這樣清晰的狀態定義和轉換規則,MESI 協議爲緩存一致性的維護奠定了堅實的基礎。

(2)MESI 協議的工作流程

讓我們以一個多核 CPU 讀取、修改數據的實際場景爲例,深入剖析 MESI 協議是如何有條不紊地確保緩存一致性的。假設有一個多核 CPU,包含 Core A、Core B 和 Core C 三個核心,它們的緩存初始狀態均爲空,主內存中有一個變量 “x”,初始值爲 0。

與總線嗅探相比,MESI 協議的優勢顯而易見。總線嗅探每次核心修改數據都要向總線發出廣播,無論其他核心是否需要該數據,都得耗費總線資源去通知,就像在一個大教室裏,無論同學們是否感興趣,老師都要大聲向所有人重複每一個小通知,容易造成總線擁堵。而 MESI 協議則像是精準推送,只有當數據狀態發生關鍵變化,且可能影響其他核心時,纔會進行有針對性的通知,大大減輕了總線的負擔,提高了系統的整體性能,讓計算機的各個核心能夠更加高效地協同工作。

4.3 如何解決緩存一致性問題

(1) 通過在總線加 LOCK 鎖的方式

在鎖住總線上加一個 LOCK 標識,CPU A 進行讀寫操作時,鎖住總線,其他 CPU 此時無法進行內存讀寫操作,只有等解鎖了才能進行操作。該方式因爲鎖住了整個總線,所以效率低。

(2)MESI 協議中的狀態

CPU 中每個緩存行(caceh line)使用 4 種狀態進行標記(使用額外的兩位 (bit) 表示):

在學習 MESI 協議之前,簡單瞭解一下總線嗅探機制(Bus Snooping)。要對自己的緩存加鎖,需要通知其他 CPU,多個 CPU 核心之間的數據傳播問題。最常見的一種解決方案就是總線嗅探。

這個策略,本質上就是把所有的讀寫請求都通過總線廣播給所有的 CPU 核心,然後讓各個核心去 “嗅探” 這些請求,再根據本地的情況進行響應。MESI 就是基於總線嗅探機制的緩存一致性協議。

MESI 協議的由來是對 Cache Line 的四個不同的標記,分別是:

整個 MESI 的狀態,可以用一個有限狀態機來表示它的狀態流轉。需要注意的是,對於不同狀態觸發的事件操作,可能來自於當前 CPU 核心,也可能來自總線裏其他 CPU 核心廣播出來的信號。我把各個狀態之間的流轉用表格總結了一下:

zN30PK

注意:對於 M 和 E 的狀態而言總是精確的,他們在緩存行的真正狀態是一致的,二 S 狀態可能是非一致的,如果一個緩存將處於 S 狀態的緩存行作廢了,而另一個緩存實際上可能已經獨享了該緩存行,但是該緩存卻不會將該緩存行升遷爲 E 狀態,這是因爲其它緩存不會廣 播他們作廢掉該緩存行的通知,同樣由於緩存並沒有保存該緩存行的 copy 的數量,因此(即使有這種通知)也沒有辦法確定自己是否已經獨享了該緩存行。

從上面的意義看來 E 狀態是一種投機性的優化:如果一個 CPU 想修改一個處於 S 狀態的緩存行,總線事務需要將所有該緩存行的 copy 變成 invalid 狀態,而修改 E 狀態的緩存不需要使用總線事務。

(3)MESI 狀態轉換

無效(I)狀態轉換

獨佔(E)狀態轉換

共享(S)狀態轉換

修改(M)狀態轉換

(4)MESI 協議中的運行機制

假設有三個 CPU A、B、C,對應三個緩存分別是 cache a、b、 c。在主內存中定義了 x 的引用值爲 0。

①單核讀取,那麼執行流程是:CPU A 發出了一條指令,從主內存中讀取 x。從主內存通過 bus 讀取到緩存中(遠端讀取 Remote read), 這是該 Cache line 修改爲 E 狀態(獨享)。

②雙核讀取,那麼執行流程是:

  1. CPU A 發出了一條指令,從主內存中讀取 x。

  2. CPU A 從主內存通過 bus 讀取到 cache a 中並將該 cache line 設置爲 E 狀態。

  3. CPU B 發出了一條指令,從主內存中讀取 x。

  4. CPU B 試圖從主內存中讀取 x 時,CPU A 檢測到了地址衝突。這時 CPU A 對相關數據做出響應。此時 x 存儲於 cache a 和 cache b 中,x 在 chche a 和 cache b 中都被設置爲 S 狀態 (共享)。

③修改數據,那麼執行流程是:

  1. CPU A 計算完成後發指令需要修改 x.

  2. CPU A 將 x 設置爲 M 狀態(修改)並通知緩存了 x 的 CPU B, CPU B 將本地 cache b 中的 x 設置爲 I 狀態 (無效)

  3. CPU A 對 x 進行賦值。

④同步數據,那麼執行流程是:

  1. CPU B 發出了要讀取 x 的指令。

  2. CPU B 通知 CPU A,CPU A 將修改後的數據同步到主內存時 cache a 修改爲 E(獨享)

  3. CPU A 同步 CPU B 的 x, 將 cache a 和同步後 cache b 中的 x 設置爲 S 狀態(共享)。

五、MESI 優化和他們引入的問題

緩存的一致性消息傳遞是要時間的,這就使其切換時會產生延遲。當一個緩存被切換狀態時其他緩存收到消息完成各自的切換並且發出迴應消息這麼一長串的時間中 CPU 都會等待所有緩存響應完成。可能出現的阻塞都會導致各種各樣的性能問題和穩定性問題。

CPU 切換狀態阻塞解決 - 存儲緩存(Store Bufferes);比如你需要修改本地緩存中的一條信息,那麼你必須將 I(無效)狀態通知到其他擁有該緩存數據的 CPU 緩存中,並且等待確認。等待確認的過程會阻塞處理器,這會降低處理器的性能。應爲這個等待遠遠比一個指令的執行時間長的多。

5.1Store Bufferes

爲了避免這種 CPU 運算能力的浪費,Store Bufferes 被引入使用。處理器把它想要寫入到主存的值寫到緩存,然後繼續去處理其他事情。當所有失效確認(Invalidate Acknowledge)都接收到時,數據纔會最終被提交。
這麼做有兩個風險。

第一、就是處理器會嘗試從存儲緩存(Store buffer)中讀取值,但它還沒有進行提交。這個的解決方案稱爲 Store Forwarding,它使得加載的時候,如果存儲緩存中存在,則進行返回。
第二、保存什麼時候會完成,這個並沒有任何保證。

舉例說明:

value = 3

void exeToCPUA(){
  value = 10;
  isFinsh = true;
}
void exeToCPUB(){
  if(isFinsh){
    //value一定等於10?!
    assert value == 10;
  }
}

試想一下開始執行時,CPU A 保存着 finished 在 E(獨享) 狀態,而 value 並沒有保存在它的緩存中。(例如,Invalid)。在這種情況下,value 會比 finished 更遲地拋棄存儲緩存。完全有可能 CPU B 讀取 finished 的值爲 true,而 value 的值不等於 10;即 isFinsh 的賦值在 value 賦值之前。

這種在可識別的行爲中發生的變化稱爲重排序(reordings)。注意,這不意味着你的指令的位置被惡意(或者好意)地更改。它只是意味着其他的 CPU 會讀到跟程序中寫入的順序不一樣的結果。

5.2 硬件內存模型

執行失效也不是一個簡單的操作,它需要處理器去處理。另外,存儲緩存(Store Buffers)並不是無窮大的,所以處理器有時需要等待失效確認的返回。這兩個操作都會使得性能大幅降低。爲了應付這種情況,引入了失效隊列。它們的約定如下:

對於所有的收到的 Invalidate 請求,Invalidate Acknowlege 消息必須立刻發送。Invalidate 並不真正執行,而是被放在一個特殊的隊列中,在方便的時候纔會去執行。處理器不會發送任何消息給所處理的緩存條目,直到它處理 Invalidate。即便是這樣處理器已然不知道什麼時候優化是允許的,而什麼時候並不允許。乾脆處理器將這個任務丟給了寫代碼的人。這就是內存屏障(Memory Barriers)。

寫屏障 Store Memory Barrier(a.k.a. ST, SMB, smp_wmb) 是一條告訴處理器在執行這之後的指令之前,應用所有已經在存儲緩存(store buffer)中的保存的指令。

讀屏障 Load Memory Barrier (a.k.a. LD, RMB, smp_rmb) 是一條告訴處理器在執行任何的加載前,先應用所有已經在失效隊列中的失效操作的指令。

void executedOnCpu0() {
    value = 10;
    //在更新數據之前必須將所有存儲緩存(store buffer)中的指令執行完畢。
    storeMemoryBarrier();
    finished = true;
}
void executedOnCpu1() {
    while(!finished);
    //在讀取之前將所有失效隊列中關於該數據的指令執行完畢。
    loadMemoryBarrier();
    assert value == 10;
}

現在確實安全了。完美無暇。

六、全文總結

操作系統的 CPU 和內存並不是直接交互操作的。我們的 CPU 有一級緩存,CPU 直接操作一級緩存,由一級緩存和內存進行交互。

當然,有的 CPU 有二級緩存,甚至三級緩存等。實際上,大概二十年前,一級緩存是直接和內存交互的,現在,一般是二級緩存和內存直接通訊。每個 CPU 都有一級緩存,但是,我們卻無法保證每個 CPU 的一級緩存數據都是一樣的。所以同一個程序,CPU 進行切換的時候,切換前和切換後的數據可能會有不一致的情況。那麼這個就是一個很大的問題了。

如何保證各個 CPU 緩存中的數據是一致的。就是 CPU 的緩存一致性問題。一種處理一致性問題的辦法是使用 Bus Locking(總線鎖)。當一個 CPU 對其緩存中的數據進行操作的時候,往總線中發送一個 Lock 信號。這個時候,所有 CPU 收到這個信號之後就不操作自己緩存中的對應數據了,當操作結束,釋放鎖以後,所有的 CPU 就去內存中獲取最新數據更新。

但是用鎖的方式總是避不開性能問題。總線鎖總是會導致 CPU 的性能下降。所以出現另外一種維護 CPU 緩存一致性的方式,MESI。

MESI 是保持一致性的協議。它的方法是在 CPU 緩存中保存一個標記位,這個標記位有四種狀態:

  1. M: Modify,修改緩存,當前 CPU 的緩存已經被修改了,即與內存中數據已經不一致了

  2. E: Exclusive,獨佔緩存,當前 CPU 的緩存和內存中數據保持一致,而且其他處理器並沒有可使用的緩存數據

  3. S: Share,共享緩存,和內存保持一致的一份拷貝,多組緩存可以同時擁有針對同一內存地址的共享緩存段

  4. I: Invalid,實效緩存,這個說明 CPU 中的緩存已經不能使用了

CPU 的讀取遵循下面幾點:

  1. 如果緩存狀態是 I,那麼就從內存中讀取,否則就從緩存中直接讀取。

  2. 如果緩存處於 M 或 E 的 CPU 讀取到其他 CPU 有讀操作,就把自己的緩存寫入到內存中,並將自己的狀態設置爲 S。

  3. 只有緩存狀態是 M 或 E 的時候,CPU 纔可以修改緩存中的數據,修改後,緩存狀態變爲 M。

這樣,每個 CPU 都遵循上面的方式則 CPU 的效率就提高上來了。

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/V6XVgbOu5LhDKmo1chXslg