自拍偷在线精品自拍偷,亚洲欧美中文日韩v在线观看不卡

解鎖Linux內(nèi)存屏障:讓程序運(yùn)行更有序

系統(tǒng) Linux
內(nèi)存屏障,也叫內(nèi)存柵欄(Memory Fence) ,是一種在多處理器系統(tǒng)中,用于控制內(nèi)存操作順序的同步機(jī)制。它就像是一個(gè) “關(guān)卡”,確保在它之前的內(nèi)存讀寫操作,一定在它之后的內(nèi)存讀寫操作之前完成 。

在當(dāng)今的計(jì)算機(jī)世界里,多核心處理器已成為主流,無(wú)論是日常辦公的電腦,還是數(shù)據(jù)中心的超級(jí)服務(wù)器,它們內(nèi)部的多個(gè)核心都在同時(shí)忙碌地工作著,力求高效地處理各種任務(wù)。在 Linux 系統(tǒng)中,這些核心共同協(xié)作,為無(wú)數(shù)的應(yīng)用程序提供運(yùn)行支撐。然而,這看似和諧的運(yùn)行背后,實(shí)則隱藏著一個(gè)棘手的問(wèn)題 —— 內(nèi)存訪問(wèn)的混亂。

你或許想象不到,在處理器內(nèi)部,為了提升性能,CPU 常常會(huì)對(duì)指令進(jìn)行亂序執(zhí)行。與此同時(shí),每個(gè)核心都配備了自己的高速緩存,數(shù)據(jù)在緩存與主內(nèi)存之間頻繁穿梭,這就導(dǎo)致了不同核心對(duì)內(nèi)存數(shù)據(jù)的訪問(wèn)順序和時(shí)機(jī)變得難以捉摸。在單線程環(huán)境下,這種亂序執(zhí)行或許不會(huì)引發(fā)明顯問(wèn)題,但一旦進(jìn)入多線程或多處理器協(xié)同工作的場(chǎng)景,問(wèn)題就會(huì)接踵而至,數(shù)據(jù)不一致、程序運(yùn)行結(jié)果與預(yù)期相悖等情況屢見(jiàn)不鮮,嚴(yán)重影響了程序的正確性和穩(wěn)定性。

而Linux內(nèi)存屏障,正是為解決這一系列問(wèn)題而生的關(guān)鍵技術(shù)。它宛如一位公正的秩序維護(hù)者,巧妙地介入 CPU 與內(nèi)存之間,通過(guò)特殊的指令或編譯器輔助手段,強(qiáng)制規(guī)定內(nèi)存操作的先后順序,讓各個(gè)核心在訪問(wèn)內(nèi)存時(shí)能夠 “井然有序”,從而確保程序按照開(kāi)發(fā)者預(yù)期的方式運(yùn)行。接下來(lái),就讓我們一同深入探索 Linux 內(nèi)存屏障的奧秘,揭開(kāi)它為程序運(yùn)行保駕護(hù)航的神秘面紗 。

一、內(nèi)存屏障簡(jiǎn)介

1.1內(nèi)存屏障概述

內(nèi)存屏障,也叫內(nèi)存柵欄(Memory Fence) ,是一種在多處理器系統(tǒng)中,用于控制內(nèi)存操作順序的同步機(jī)制。它就像是一個(gè) “關(guān)卡”,確保在它之前的內(nèi)存讀寫操作,一定在它之后的內(nèi)存讀寫操作之前完成 。在單核單線程的程序里,我們通常不用擔(dān)心指令執(zhí)行順序的問(wèn)題,因?yàn)?CPU 會(huì)按照代碼編寫的順序依次執(zhí)行。但在多處理器或者多線程的環(huán)境下,情況就變得復(fù)雜起來(lái)。

現(xiàn)代處理器為了提高性能,會(huì)采用諸如指令亂序執(zhí)行、緩存等技術(shù)。比如,當(dāng)處理器執(zhí)行一段代碼時(shí),可能會(huì)根據(jù)自身的優(yōu)化策略,將某些指令的執(zhí)行順序進(jìn)行調(diào)整,只要最終結(jié)果不受影響就行。在多線程場(chǎng)景中,如果多個(gè)線程同時(shí)訪問(wèn)和修改共享內(nèi)存,指令重排序就可能導(dǎo)致數(shù)據(jù)不一致的問(wèn)題。內(nèi)存屏障的出現(xiàn),就是為了解決這類問(wèn)題,它能夠阻止編譯器和處理器對(duì)特定內(nèi)存操作的重排序,保證內(nèi)存操作的順序性和數(shù)據(jù)的可見(jiàn)性。

大多數(shù)處理器提供了內(nèi)存屏障指令:

  • 完全內(nèi)存屏障(full memory barrier)保障了早于屏障的內(nèi)存讀寫操作的結(jié)果提交到內(nèi)存之后,再執(zhí)行晚于屏障的讀寫操作。
  • 內(nèi)存讀屏障(read memory barrier)僅確保了內(nèi)存讀操作;
  • 內(nèi)存寫屏障(write memory barrier)僅保證了內(nèi)存寫操作。

內(nèi)核代碼里定義了這三種內(nèi)存屏障,如x86平臺(tái):arch/x86/include/asm/barrier.h

#define mb()    asm volatile("mfence":::"memory")
#define rmb()   asm volatile("lfence":::"memory")
#define wmb()   asm volatile("sfence" ::: "memory")

個(gè)人理解:就類似于我們喝茶的時(shí)候需要先把水煮開(kāi)(限定條件),然后再切茶,而這一整套流程都是限定特定環(huán)節(jié)的先后順序(內(nèi)存屏障),保障切出來(lái)的茶可以更香。

硬件層的內(nèi)存屏障分為兩種:Load Barrier 和 Store Barrier即讀屏障和寫屏障。

內(nèi)存屏障有兩個(gè)作用:

  • 阻止屏障兩側(cè)的指令重排序;
  • 強(qiáng)制把寫緩沖區(qū)/高速緩存中的臟數(shù)據(jù)等寫回主內(nèi)存,讓緩存中相應(yīng)的數(shù)據(jù)失效。

對(duì)于Load Barrier來(lái)說(shuō),在指令前插入Load Barrier,可以讓高速緩存中的數(shù)據(jù)失效,強(qiáng)制從新從主內(nèi)存加載數(shù)據(jù);對(duì)于Store Barrier來(lái)說(shuō),在指令后插入Store Barrier,能讓寫入緩存中的最新數(shù)據(jù)更新寫入主內(nèi)存,讓其他線程可見(jiàn)。

1.2不同場(chǎng)景內(nèi)存屏障

(1)java內(nèi)存屏障

  • java的內(nèi)存屏障通常所謂的四種即LoadLoad,StoreStore,LoadStore,StoreLoad實(shí)際上也是上述兩種的組合,完成一系列的屏障和數(shù)據(jù)同步功能。
  • LoadLoad屏障:對(duì)于這樣的語(yǔ)句Load1; LoadLoad; Load2,在Load2及后續(xù)讀取操作要讀取的數(shù)據(jù)被訪問(wèn)前,保證Load1要讀取的數(shù)據(jù)被讀取完畢。
  • StoreStore屏障:對(duì)于這樣的語(yǔ)句Store1; StoreStore; Store2,在Store2及后續(xù)寫入操作執(zhí)行前,保證Store1的寫入操作對(duì)其它處理器可見(jiàn)。
  • LoadStore屏障:對(duì)于這樣的語(yǔ)句Load1; LoadStore; Store2,在Store2及后續(xù)寫入操作被刷出前,保證Load1要讀取的數(shù)據(jù)被讀取完畢。
  • StoreLoad屏障:對(duì)于這樣的語(yǔ)句Store1; StoreLoad; Load2,在Load2及后續(xù)所有讀取操作執(zhí)行前,保證Store1的寫入對(duì)所有處理器可見(jiàn)。它的開(kāi)銷是四種屏障中最大的。在大多數(shù)處理器的實(shí)現(xiàn)中,這個(gè)屏障是個(gè)萬(wàn)能屏障,兼具其它三種內(nèi)存屏障的功能

(2)volatile語(yǔ)義中的內(nèi)存屏障

  • volatile的內(nèi)存屏障策略非常嚴(yán)格保守,非常悲觀且毫無(wú)安全感的心態(tài):
  • 在每個(gè)volatile寫操作前插入StoreStore屏障,在寫操作后插入StoreLoad屏障;
  • 在每個(gè)volatile讀操作前插入LoadLoad屏障,在讀操作后插入LoadStore屏障;
  • 由于內(nèi)存屏障的作用,避免了volatile變量和其它指令重排序、線程之間實(shí)現(xiàn)了通信,使得volatile表現(xiàn)出了鎖的特性。

(3)final語(yǔ)義中的內(nèi)存屏障

對(duì)于final域,編譯器和CPU會(huì)遵循兩個(gè)排序規(guī)則:

新建對(duì)象過(guò)程中,構(gòu)造體中對(duì)final域的初始化寫入和這個(gè)對(duì)象賦值給其他引用變量,這兩個(gè)操作不能重排序;
初次讀包含final域的對(duì)象引用和讀取這個(gè)final域,這兩個(gè)操作不能重排序;(意思就是先賦值引用,再調(diào)用final值)

總之上面規(guī)則的意思可以這樣理解,必需保證一個(gè)對(duì)象的所有final域被寫入完畢后才能引用和讀取。這也是內(nèi)存屏障的起的作用:

  • 寫final域:在編譯器寫final域完畢,構(gòu)造體結(jié)束之前,會(huì)插入一個(gè)StoreStore屏障,保證前面的對(duì)final寫入對(duì)其他線程/CPU可見(jiàn),并阻止重排序。
  • 讀final域:在上述規(guī)則2中,兩步操作不能重排序的機(jī)理就是在讀final域前插入了LoadLoad屏障。
  • X86處理器中,由于CPU不會(huì)對(duì)寫-寫操作進(jìn)行重排序,所以StoreStore屏障會(huì)被省略;而X86也不會(huì)對(duì)邏輯上有先后依賴關(guān)系的操作進(jìn)行重排序,所以LoadLoad也會(huì)變省略。

二、為什么需要內(nèi)存屏障?

由于現(xiàn)在計(jì)算機(jī)存在多級(jí)緩存且多核場(chǎng)景,為了保證讀取到的數(shù)據(jù)一致性以及并行運(yùn)行時(shí)所計(jì)算出來(lái)的結(jié)果一致,在硬件層面實(shí)現(xiàn)一些指令,從而來(lái)保證指定執(zhí)行的指令的先后順序。比如上圖:雙核cpu,每個(gè)核心都擁有獨(dú)立的一二級(jí)緩存,而緩存與緩存之間需要保證數(shù)據(jù)的一致性所以這里才需要加添屏障來(lái)確保數(shù)據(jù)的一致性。三級(jí)緩存為各CPU共享,最后都是主內(nèi)存,所以這些存在交互的CPU都需要通過(guò)屏障手段來(lái)保證數(shù)據(jù)的唯一性。

內(nèi)存屏障存在的意義就是為了解決程序在運(yùn)行過(guò)程中出現(xiàn)的內(nèi)存亂序訪問(wèn)問(wèn)題,內(nèi)存亂序訪問(wèn)行為出現(xiàn)的理由是為了提高程序運(yùn)行時(shí)的性能,Memory Bariier能夠讓CPU或編譯器在內(nèi)存訪問(wèn)上有序。

2.1內(nèi)存屏障出現(xiàn)的背景(內(nèi)存亂序是怎么出現(xiàn)的?)

早期的處理器為有序處理器(In-order processors),有序處理器處理指令通常有以下幾步:

  • 指令獲取
  • 如果指令的輸入操作對(duì)象(input operands)可用(例如已經(jīng)在寄存器中了),則將此指令分發(fā)到適當(dāng)?shù)墓δ軉卧小H绻粋€(gè)或者多個(gè)操 作對(duì)象不可用(通常是由于需要從內(nèi)存中獲?。瑒t處理器會(huì)等待直到它們可用
  • 指令被適當(dāng)?shù)墓δ軉卧獔?zhí)行
  • 功能單元將結(jié)果寫回寄存器堆(Register file,一個(gè) CPU 中的一組寄存器)

相比之下,亂序處理器(Out-of-order processors),處理指令通常有以下幾步:

  • 指令獲取
  • 指令被分發(fā)到指令隊(duì)列(Invalidate Queues,后面會(huì)講到)
  • 指令在指令隊(duì)列中等待,直到輸入操作對(duì)象可用(一旦輸入操作對(duì)象可用,指令就可以離開(kāi)隊(duì)列,即便更早的指令未被執(zhí)行)
  • 指令被分配到適當(dāng)?shù)墓δ軉卧?zhí)行
  • 執(zhí)行結(jié)果被放入隊(duì)列(放入到store buffer中,而不是直接寫到cache中,后面也會(huì)講到)
  • 只有所有更早請(qǐng)求執(zhí)行的指令的執(zhí)行結(jié)果被寫入cache后,指令執(zhí)行的結(jié)果才被寫入cache(執(zhí)行結(jié)果重排序,讓執(zhí)行看起來(lái)是有序的)

已經(jīng)了解了cache的同學(xué)應(yīng)該可以知道,如果CPU需要讀取的地址中的數(shù)據(jù)已經(jīng)已經(jīng)緩存在了cache line中,即使是cpu需要對(duì)這個(gè)地址重復(fù)進(jìn)行讀寫,對(duì)CPU性能影響也不大,但是一旦發(fā)生了cache miss(對(duì)這個(gè)地址進(jìn)行第一次寫操作),如果是有序處理器,CPU在從其他CPU獲取數(shù)據(jù)或者直接與主存進(jìn)行數(shù)據(jù)交互的時(shí)候需要等待不可用的操作對(duì)象,這樣就會(huì)非常慢,非常影響性能。舉個(gè)例子:

如果CPU0發(fā)起一次對(duì)某個(gè)地址的寫操作,但是其local cache中沒(méi)有數(shù)據(jù),這個(gè)數(shù)據(jù)存放在CPU1的local cache中。為了完成這次操作,CPU0會(huì)發(fā)出一個(gè)invalidate的信號(hào),使其他CPU的cache數(shù)據(jù)無(wú)效(因?yàn)镃PU0需要重新寫這個(gè)地址中的值,說(shuō)明這個(gè)地址中的值將被改變,如果不把其他CPU中存放的該地址的值無(wú)效,那么就有可能會(huì)出現(xiàn)數(shù)據(jù)不一致的問(wèn)題)。只有當(dāng)其他之前就已經(jīng)存放了改地址數(shù)據(jù)的CPU中的值都無(wú)效了后,CPU0才能真正發(fā)起寫操作。需要等待非常長(zhǎng)的時(shí)間,這就導(dǎo)致了性能上的損耗。

但是亂序處理器山就不需要等待不可用的操作對(duì)象,直接把invalidate message放到invalidate queues中,然后繼續(xù)干其他事情,提高了CPU的性能,但也帶來(lái)了一個(gè)問(wèn)題,就是程序執(zhí)行過(guò)程中,可能會(huì)由于亂序處理器的處理方式導(dǎo)致內(nèi)存亂序,程序運(yùn)行結(jié)果不符合我們預(yù)期的問(wèn)題。

2.2理解內(nèi)存屏障

不少開(kāi)發(fā)者并不理解一個(gè)事實(shí) — 程序?qū)嶋H運(yùn)行時(shí)很可能并不完全按照開(kāi)發(fā)者編寫的順序訪問(wèn)內(nèi)存。例如:

x = r;
y = 1;

這里,y = 1很可能先于x = r執(zhí)行。這就是內(nèi)存亂序訪問(wèn)。內(nèi)存亂序訪問(wèn)行為出現(xiàn)的理由是為了提升程序運(yùn)行時(shí)的性能。編譯器和CPU都可能引起內(nèi)存亂序訪問(wèn):

  • 編譯時(shí),編譯器優(yōu)化進(jìn)行指令重排而導(dǎo)致內(nèi)存亂序訪問(wèn);
  • 運(yùn)行時(shí),多CPU間交互引入內(nèi)存亂序訪問(wèn)。

編譯器和CPU引入內(nèi)存亂序訪問(wèn)通常不會(huì)帶來(lái)什么問(wèn)題,但在一些特殊情況下(主要是多線程程序中),邏輯的正確性依賴于內(nèi)存訪問(wèn)順序,這時(shí),內(nèi)存亂序訪問(wèn)會(huì)帶來(lái)邏輯上的錯(cuò)誤,例如:

// thread 1
while(!ok);
do(x);
 
// thread 2
x = 42;
ok = 1;

Ok初始化為0, 線程1等待ok被設(shè)置為1后執(zhí)行do函數(shù)。假如,線程2對(duì)內(nèi)存的寫操作亂序執(zhí)行,也就是x賦值晚于ok賦值完成,那么do函數(shù)接受的實(shí)參很有可能出乎開(kāi)發(fā)者的意料,不為42。我們可以引入內(nèi)存屏障來(lái)避免上述問(wèn)題的出現(xiàn)。內(nèi)存屏障能讓CPU或者編譯器在內(nèi)存訪問(wèn)上有序。一個(gè)內(nèi)存屏障之前的內(nèi)存訪問(wèn)操作必定先于其之后的完成。

三、為什么要有內(nèi)存屏障?

為了解決cpu,高速緩存,主內(nèi)存帶來(lái)的的指令之間的可見(jiàn)性和重序性問(wèn)題。

我們都知道計(jì)算機(jī)運(yùn)算任務(wù)需要CPU和內(nèi)存相互配合共同完成,其中CPU負(fù)責(zé)邏輯計(jì)算,內(nèi)存負(fù)責(zé)數(shù)據(jù)存儲(chǔ)。CPU要與內(nèi)存進(jìn)行交互,如讀取運(yùn)算數(shù)據(jù)、存儲(chǔ)運(yùn)算結(jié)果等。由于內(nèi)存和CPU的計(jì)算速度有幾個(gè)數(shù)量級(jí)的差距,為了提高CPU的利用率,現(xiàn)代處理器結(jié)構(gòu)都加入了一層讀寫速度盡可能接近CPU運(yùn)算速度的高速緩存來(lái)作為內(nèi)存與CPU之間的緩沖:將運(yùn)算需要使用

的數(shù)據(jù)復(fù)制到緩存中,讓CPU運(yùn)算可以快速進(jìn)行,計(jì)算結(jié)束后再將計(jì)算結(jié)果從緩存同步到主內(nèi)存中,這樣處理器就無(wú)須等待緩慢的內(nèi)存讀寫了。就像下面這樣:

圖片圖片

每個(gè)CPU都會(huì)有自己的緩存(有的甚至L1,L2,L3),緩存的目的就是為了提高性能,避免每次都要向內(nèi)存取,但是這樣的弊端也很明顯:不能實(shí)時(shí)的和內(nèi)存發(fā)生信息交換,會(huì)使得不同CPU執(zhí)行的不同線程對(duì)同一個(gè)變量的緩存值不同。用volatile關(guān)鍵字修飾變量可以解決上述問(wèn)題,那么volatile是如何做到這一點(diǎn)的呢?那就是內(nèi)存屏障,內(nèi)存屏障是硬件層的概念,不同的硬件平臺(tái)實(shí)現(xiàn)內(nèi)存屏障的手段并不是一樣,java通過(guò)屏蔽這些差異,統(tǒng)一由jvm來(lái)生成內(nèi)存屏障的指令。

volatile的有序性和可見(jiàn)性
volatile的內(nèi)存屏障策略非常嚴(yán)格保守,非常悲觀且毫無(wú)安全感的心態(tài):在每個(gè)volatile寫操作前插入StoreStore屏障,在寫操作后插入StoreLoad屏障;在每個(gè)volatile讀操作前插入LoadLoad屏障,在讀操作后插入LoadStore屏障;由于內(nèi)存屏障的作用,避免了volatile變量和其它指令重排序、實(shí)現(xiàn)了線程之間通信,使得volatile表現(xiàn)出了鎖的特性。
重排序:代碼的執(zhí)行順序不按照書寫的順序,為了提升運(yùn)行效率,在不影響結(jié)果的前提下,打亂代碼運(yùn)行
int a=1;
int b=2;
int c=a+b;
int c=5;
這里的int c=5這個(gè)賦值操作可能發(fā)生在int a=1這個(gè)操作之前

內(nèi)存屏障的引入,本質(zhì)上是由于CPU重排序指令引起的。重排序問(wèn)題無(wú)時(shí)無(wú)刻不在發(fā)生,主要源自以下幾種場(chǎng)景:

  1. 編譯器編譯時(shí)的優(yōu)化;
  2. 處理器執(zhí)行時(shí)的多發(fā)射和亂序優(yōu)化;
  3. 讀取和存儲(chǔ)指令的優(yōu)化;
  4. 緩存同步順序(導(dǎo)致可見(jiàn)性問(wèn)題)。

3.1編譯器優(yōu)化

編譯器在不改變單線程程序語(yǔ)義的前提下,也就是保證單線程程序執(zhí)行結(jié)果正確的情況下,可以重新安排語(yǔ)句的執(zhí)行順序。編譯器在優(yōu)化的時(shí)候是不知道當(dāng)前程序是在哪個(gè)線程中執(zhí)行的,因此它只能保證單線程的正確性。

例如,有如下程序:

if (a)
    b = a;
else
    b = 42;

在經(jīng)過(guò)編譯器優(yōu)化后可能變成:

b = 42;
if (a)
    b = a;

這種優(yōu)化在單線程下是沒(méi)有問(wèn)題的,但是如果有另外一個(gè)線程要讀取變量b的值時(shí),有可能會(huì)有問(wèn)題。前面的程序只有當(dāng)變量a的值為0時(shí),才會(huì)將b賦值42,后面的程序無(wú)論變量a的值是多少,都有一段時(shí)間會(huì)將b賦值為42。

造成這個(gè)問(wèn)題的原因是,編譯器優(yōu)化的時(shí)候只注重“結(jié)果”,不注重“過(guò)程”。這種優(yōu)化在單線程程序中沒(méi)有問(wèn)題,代碼一直都是自己運(yùn)行,只要結(jié)果對(duì)就可以了,但是在多線程程序下,代碼執(zhí)行過(guò)程中的某些狀態(tài)可能會(huì)對(duì)別的線程產(chǎn)生影響,這個(gè)是編譯器優(yōu)化無(wú)法考慮到的。

3.2處理器執(zhí)行時(shí)的多發(fā)射和亂序優(yōu)化

現(xiàn)代處理器基本上都是支持多發(fā)射的,也就是在一個(gè)指令周期內(nèi)可以同時(shí)執(zhí)行多條指令。但是,處理器的資源就那么多,可能不能同時(shí)滿足處理這些指令的要求。比如,處理器就只有一個(gè)加法器,如果同時(shí)有兩條指令都需要算加法,那么有一條指令必須等待。如果這時(shí)候再下一條指令是讀取指令,并且和前兩條指令無(wú)關(guān),那么這條指令將在前面某條加法指令之前完成。還有一種可能,就是前后指令之間具有相關(guān)性,比如對(duì)同一個(gè)地址先讀取再寫入,后面的寫入操作必須等待前面的讀取操作完成后才能執(zhí)行。但是如果這時(shí)候第三條指令是寫入一個(gè)無(wú)關(guān)的地址,那它可以在前面的寫入操作之前被執(zhí)行,執(zhí)行順序再次被打亂了。

所以,一般情況下指令亂序并不是CPU在執(zhí)行指令之前刻意去調(diào)整順序。CPU總是順序的去內(nèi)存里面取指令,然后將其順序的放入指令流水線。但是指令執(zhí)行時(shí)的各種條件,指令與指令之間的相互影響,可能導(dǎo)致順序放入流水線的指令,最終不是按照放入的順序執(zhí)行完成,在外邊看起來(lái)仿佛是“亂序”一樣,這就是所謂的“順序流入,亂序流出”。

3.3讀取和存儲(chǔ)指令的優(yōu)化

CPU有可能根據(jù)情況,將相臨的兩條讀取或?qū)懭氩僮骱喜⒊梢粭l。

例如,對(duì)于如下的兩條讀取操作:

X = *A; Y = *(A + 4);

可能被合并成一條讀取操作:

{X, Y} = LOAD {*A, *(A + 4) };

同樣的,對(duì)于如下兩條寫入操作:

*A = X; *(A + 4) = Y;

有可能會(huì)被合并成一條:

STORE {*A, *(A + 4) } = {X, Y};

以上這幾種情況,由于編譯器或CPU,出于“優(yōu)化”的目的,按照某種規(guī)則將指令重新排序的行為稱作“真”重排序。不同的是,編譯器重排序是在編譯程序時(shí)進(jìn)行的,一旦編譯成功后執(zhí)行次序就定下來(lái)了。而后面幾種是在CPU運(yùn)行程序時(shí)實(shí)時(shí)進(jìn)行的,CPU架構(gòu)不同可能起到的效果完全不同。

編譯器或CPU在執(zhí)行各種優(yōu)化的時(shí)候,都有一些必須的前提,就是至少在單一CPU上執(zhí)行不能出現(xiàn)問(wèn)題。有一些數(shù)據(jù)訪問(wèn)明顯是相互依賴的,就不能打亂它們的執(zhí)行順序。比如:

1)在一個(gè)給定的CPU上,有依賴的內(nèi)存訪問(wèn):

比如如下兩條指令:

A = Load B;
C = Load *A

第二條加載指令的地址是由第一條指令加載的,第二條指令要能正確執(zhí)行,必須要等到第一條指令執(zhí)行完成后才行,也就是說(shuō)第二條指令依賴于第一條指令。這種情況下,無(wú)論如何處理器是不會(huì)打亂這兩條指令的執(zhí)行次序的。不過(guò),有可能會(huì)在這兩條指令間插入別的指令,但必須保證第二條指令在第一條指令執(zhí)行完后才能執(zhí)行。

2)在一個(gè)給定的CPU上,交叉的加載和存儲(chǔ)操作,它們?cè)L問(wèn)的內(nèi)存地址有重疊:

例如,先存儲(chǔ)后加載同一個(gè)內(nèi)存地址上的內(nèi)容:

Store *X = A;
B = Load *X;

或者先加載后讀取同一個(gè)內(nèi)存地址上的內(nèi)容:

A = Load *X;
Store *X = B;

對(duì)同一個(gè)內(nèi)存地址的存取,如果兩條指令執(zhí)行次序被打亂了,那肯定會(huì)發(fā)生錯(cuò)誤。但是,如果是兩條加載或兩條存儲(chǔ)指令(中間沒(méi)有加載),哪怕是對(duì)同一個(gè)內(nèi)存地址的操作,也可能由于優(yōu)化產(chǎn)生變化。

有了上面兩條限制,很容易想到,那如果所有加載或存儲(chǔ)指令都沒(méi)有相關(guān)性呢?這時(shí)候就要看CPU的心情了,可以以任何次序被執(zhí)行,可以完全不按照它們?cè)诔绦蛑谐霈F(xiàn)的次序。

3.4緩存同步順序

上面的幾種情況都比較好理解,最詭異的是所謂的緩存同步順序的問(wèn)題。要把這個(gè)問(wèn)題說(shuō)清楚首先要說(shuō)一下緩存是什么。

現(xiàn)代CPU的運(yùn)算速度比現(xiàn)代內(nèi)存系統(tǒng)的速度快得多,它們的速度差了幾個(gè)數(shù)量級(jí),那怎么辦呢?硬件設(shè)計(jì)者想到了在內(nèi)存和CPU之間加入一個(gè)速度足夠快,但空間不是很大的存儲(chǔ)空間,這個(gè)就是所謂的緩存。緩存的速度足夠快,但是它一般是某個(gè)或某些CPU核獨(dú)享的,而不像計(jì)算機(jī)的主存,一般認(rèn)為是系統(tǒng)中所有CPU共享的。

圖片圖片

一旦引入了緩存,就會(huì)引入多個(gè)地方存放同一個(gè)數(shù)據(jù)的問(wèn)題,就有可能出現(xiàn)數(shù)據(jù)不一致的問(wèn)題。假設(shè)變量X所在內(nèi)存同時(shí)被兩個(gè)CPU都緩存了,但是這時(shí)候CPU0對(duì)變量X的值做出了修改,這之后CPU1如果試圖讀取變量X的值時(shí),其實(shí)讀到的是老的值。

這個(gè)時(shí)候就需要所謂的緩存一致性協(xié)議了,一般常用的是MESI協(xié)議。MESI代表“Modified”、“Exclusive”、“Shared”和“Invalid”四種狀態(tài)的縮寫,特定緩存行可以處在該協(xié)議采用的這四種狀態(tài)上:

  1. 處于“Modified”狀態(tài)的緩存行:當(dāng)前CPU已經(jīng)對(duì)緩存行的數(shù)據(jù)進(jìn)行了修改,但是該緩存行的內(nèi)容并沒(méi)有在其它CPU的緩存中出現(xiàn)。因此,處于該狀態(tài)的緩存行可以認(rèn)為被當(dāng)前CPU所“擁有”。這就是所謂的“臟”行,它的內(nèi)容和內(nèi)存中的內(nèi)容不一樣。由于只有當(dāng)前CPU的緩存持有最新的數(shù)據(jù),因此要么將“臟”數(shù)據(jù)寫回到內(nèi)存,要么將該數(shù)據(jù)“轉(zhuǎn)移”給其它緩存。
  2. 處于“Exclusive”狀態(tài)的緩存行:該狀態(tài)非常類似于“Modified”狀態(tài),緩存的內(nèi)容確保沒(méi)有在其它CPU的緩存中出現(xiàn)。唯一的差別是,該緩存行還沒(méi)有被當(dāng)前的CPU修改,也就是說(shuō)緩存行內(nèi)容和內(nèi)存中的是一樣,是對(duì)內(nèi)存數(shù)據(jù)的最新復(fù)制。但是,由于當(dāng)前CPU能夠在任何時(shí)刻將數(shù)據(jù)存儲(chǔ)到該緩存行而不考慮其它CPU,因此處于“Exclusive”狀態(tài)的緩存行也可以認(rèn)為被當(dāng)前CPU所“擁有”。
  3. 處于“Shared”狀態(tài)的緩存行:表示緩存行的數(shù)據(jù)和主存中的一樣,并且可能已經(jīng)被復(fù)制到至少一個(gè)其它CPU的緩存中。但是,在沒(méi)有得到其他CPU“許可”的情況下,任何CPU不能向該緩存行存儲(chǔ)數(shù)據(jù)。與“Exclusive”狀態(tài)相同,由于內(nèi)存中的值是最新的,因此當(dāng)需要丟棄該緩存行時(shí),可以不用向內(nèi)存回寫。
  4. 處于“Invalid”狀態(tài)的緩存行:表示該緩存行已經(jīng)失效了,不能再被繼續(xù)使用了。當(dāng)有新數(shù)據(jù)進(jìn)入緩存時(shí),它可以直接放置到一個(gè)處于“Invalid”狀態(tài)的緩存行上,不需要做其它的任何處理。

為了維護(hù)這個(gè)狀態(tài)機(jī),需要各個(gè)CPU之間進(jìn)行通信,會(huì)引入下面幾種類型的消息:

  1. 讀消息:該消息包含要讀取的緩存行的物理地址。
  2. 讀響應(yīng)消息:該消息包含較早前的讀消息所請(qǐng)求的數(shù)據(jù),這個(gè)讀響應(yīng)消息要么由物理內(nèi)存提供,要么由某一個(gè)其它CPU上的緩存提供。例如,如果某一個(gè)CPU上的緩存擁有處于“Modified”狀態(tài)的目標(biāo)數(shù)據(jù),那么該CPU上的緩存必須提供讀響應(yīng)消息。
  3. 使無(wú)效消息:該消息包含要使無(wú)效的緩存行的物理地址,所有其它CPU上的緩存必須移除相應(yīng)的數(shù)據(jù)并且響應(yīng)此消息。
  4. 使無(wú)效應(yīng)答消息:一個(gè)接收到使無(wú)效消息的CPU必須在移除指定數(shù)據(jù)后響應(yīng)一個(gè)使無(wú)效應(yīng)答消息。
  5. 讀使無(wú)效消息:該消息包含要被讀取的緩存行的物理地址,同時(shí)指示其它CPU上的緩存移除對(duì)應(yīng)的數(shù)據(jù)。因此,正如名字所示,它將讀消息和使無(wú)效消息合并成了一條消息。讀使無(wú)效消息同時(shí)需要一個(gè)讀響應(yīng)消息及一組使無(wú)效應(yīng)答消息進(jìn)行應(yīng)答。
  6. 寫回消息:該包含要回寫到物理內(nèi)存的地址和數(shù)據(jù)。這個(gè)消息允許緩存在必要時(shí)換出處于“Modified”狀態(tài)的數(shù)據(jù),以便為其它數(shù)據(jù)騰出空間。

通過(guò)上面的介紹可以看到,MESI緩存一致性協(xié)議可以保證系統(tǒng)中的各個(gè)CPU核上的緩存都是一致的。但是也帶來(lái)了一個(gè)很大的問(wèn)題,由于所有的操作都是“同步”的,必須要等待遠(yuǎn)端CPU完成指定操作后收到響應(yīng)消息才能真正執(zhí)行對(duì)應(yīng)的存儲(chǔ)或加載操作,這樣會(huì)極大降低系統(tǒng)的性能。比如說(shuō),如果CPU0和CPU1上同時(shí)緩存了同一段數(shù)據(jù),如果CPU0想對(duì)其進(jìn)行更改,那么必須先發(fā)送使無(wú)效消息給CPU1,等到CPU1真的將該緩存的數(shù)據(jù)段標(biāo)記成“Invalid”狀態(tài)后,會(huì)向CPU0發(fā)送使無(wú)效應(yīng)答消息,理論上只有CPU0收到這個(gè)消息后,才可以真的更改數(shù)據(jù)。但是,從要更改到真的能更改已經(jīng)經(jīng)過(guò)了好幾個(gè)階段了,這時(shí)CPU0只能等在那里。

魚和熊掌都兼得是不可能的,想提高性能,只能稍微放松一下對(duì)緩存一致性的要求。

具體的,會(huì)引入如下兩個(gè)模塊:

存儲(chǔ)緩沖:前面提到過(guò),在寫數(shù)據(jù)之前我們先要得到緩存段的獨(dú)占權(quán),如果當(dāng)前CPU沒(méi)有獨(dú)占權(quán),要先讓系統(tǒng)中別的CPU上緩存的同一段數(shù)據(jù)都變成無(wú)效狀態(tài)。為了提高性能,可以引入一個(gè)叫做存儲(chǔ)緩沖(Store Buffer)的模塊,將其放置在每個(gè)CPU和它的緩存之間。當(dāng)前CPU發(fā)起寫操作,如果發(fā)現(xiàn)沒(méi)有獨(dú)占權(quán),可以先將要寫入的數(shù)據(jù)放在存儲(chǔ)緩沖中,并繼續(xù)運(yùn)行,仿佛獨(dú)占權(quán)瞬間就得到了一樣。當(dāng)然,存儲(chǔ)緩沖中的數(shù)據(jù)最后還是會(huì)被同步到緩存中的,但就相當(dāng)于是異步執(zhí)行了,不會(huì)讓CPU等了。并且,當(dāng)前CPU在讀取數(shù)據(jù)的時(shí)候應(yīng)該首先檢查其是否存在于存儲(chǔ)緩沖中。

無(wú)效隊(duì)列:如果當(dāng)前CPU上收到一條消息,要使某個(gè)緩存段失效,但是此時(shí)緩存正在處理其它事情,那這個(gè)消息可能無(wú)法在當(dāng)前的指令周期中得到處理,而會(huì)將其放入所謂的無(wú)效隊(duì)列(Invalidation Queue)中,同時(shí)立即發(fā)送使無(wú)效應(yīng)答消息。那個(gè)待處理的使無(wú)效消息將保存在隊(duì)列中,直到緩存有空為止。

圖片圖片

加入了這兩個(gè)模塊之后,CPU的性能是提高了,但緩存一致性就遭到了一定程度的破壞。假設(shè)變量X所在內(nèi)存同時(shí)被兩個(gè)CPU都緩存了,但是這時(shí)候CPU0對(duì)變量X的值做出了修改,這之后CPU1如果試圖讀取變量X的值時(shí),有可能讀到的是老的值,當(dāng)然也有可能讀到的是新的值。但是,在經(jīng)過(guò)一段不確定的時(shí)間后,CPU1一定是可以讀到變量X新的值,可以理解為滿足所謂的最終一致性。

但這只是對(duì)單個(gè)變量來(lái)說(shuō)的,如果程序中有多個(gè)變量,那么在其它CPU看來(lái),它們之間的讀寫次序?qū)⑼耆珶o(wú)法得到保證。

假設(shè)有CPU0上要執(zhí)行對(duì)三個(gè)變量的寫操作:

Store A = 1;
Store B = 2;
Store C = 3;

但是,這三個(gè)變量在緩存中的狀態(tài)不一樣,假設(shè)A變量和B變量在CPU0和CPU1中的緩存都存在,也就是處于“Shared”狀態(tài),而C變量是CPU0獨(dú)占的,也就是處于“Exclusive”狀態(tài)。假設(shè)系統(tǒng)經(jīng)歷了如下幾個(gè)步驟:

在對(duì)變量A和B賦值時(shí),CPU0發(fā)現(xiàn)其實(shí)別的CPU也緩存了,因此會(huì)將它們臨時(shí)放到存儲(chǔ)緩沖中。

在對(duì)變量C賦值時(shí),CPU0發(fā)現(xiàn)是獨(dú)占的,那么可以直接修改緩存的值,該緩存行的狀態(tài)被切換成了“Modified”。注意,這個(gè)時(shí)候,如果在CPU1上執(zhí)行了讀取變量C的操作,其實(shí)已經(jīng)可以讀到變量C的最新值了,CPU1發(fā)送讀消息,CPU0發(fā)送讀響應(yīng)消息,包含最新的數(shù)據(jù),同時(shí)將緩存行的狀態(tài)都切換成“Shared”。但是,如果這個(gè)時(shí)候如果CPU1嘗試讀取變量A或者變量B的數(shù)據(jù),將會(huì)獲得老的數(shù)據(jù),應(yīng)為CPU1上對(duì)應(yīng)變量A和B的緩存行的狀態(tài)仍然是“Shared”。

CPU0開(kāi)始處理對(duì)應(yīng)變量A和B的存儲(chǔ)緩沖,將它們更新進(jìn)緩存,但之前必須要向CPU1發(fā)送使無(wú)效消息。這里再次假設(shè)變量A的緩存正忙,而變量B的可以立即處理。那么變量A的使無(wú)效消息將存放在CPU1的無(wú)效隊(duì)列中,而變量B的緩存行已經(jīng)失效。這時(shí),如果CPU1嘗試獲得變量B,是可以獲得最新的數(shù)據(jù)的,而變量A還是不行。

CPU1對(duì)應(yīng)變量A的緩存已經(jīng)空閑了,可以處理當(dāng)前無(wú)效隊(duì)列的請(qǐng)求,因此變量A對(duì)應(yīng)的緩存行將失效。直到這時(shí)CPU1才可以真正的讀到變量A的最新值。

通過(guò)以上的步驟可以看到,雖然在CPU0上是先對(duì)變量A賦值,接著對(duì)B賦值,最后對(duì)C賦值,但是在CPU1上“看到”的順序剛好是相反的,先“看到”C,接著“看到”B,最后看到“C”。在CPU1上會(huì)產(chǎn)生一種錯(cuò)覺(jué),方式CPU0是先對(duì)C賦值,再對(duì)B賦值,最后對(duì)A賦值一樣。這種由于緩存同步順序的問(wèn)題,讓程序看起來(lái)好像指令被重排序了的情況,稱作“偽”重排序。

四、內(nèi)存屏障的類型

在 Linux 系統(tǒng)中,根據(jù)其作用和功能,內(nèi)存屏障主要分為以下三種類型:

4.1全屏障(Full Barrier)

全屏障,也稱作強(qiáng)內(nèi)存屏障 ,它的功能最為強(qiáng)大。全屏障可以阻止屏障兩邊的讀寫操作進(jìn)行重排序,確保在屏障之前的所有讀寫操作,都在屏障之后的讀寫操作之前完成。在 x86 架構(gòu)中,全屏障的實(shí)現(xiàn)指令是mfence 。當(dāng) CPU 執(zhí)行到mfence指令時(shí),會(huì)將之前所有的存儲(chǔ)和加載操作都按順序完成,才會(huì)繼續(xù)執(zhí)行后面的指令。例如:

// 線程1
x = 1;  // 寫操作1
mfence();  // 全屏障
y = 2;  // 寫操作2

// 線程2
if (y == 2) {  // 讀操作1
    assert(x == 1);  // 讀操作2
}

在這個(gè)例子中,由于mfence全屏障的存在,線程 1 中x = 1的寫操作一定會(huì)在線程 2 讀取y的值之前完成,從而保證了線程 2 在讀取y為 2 時(shí),x的值也已經(jīng)被正確地更新為 1,避免了由于指令重排序?qū)е碌臄?shù)據(jù)不一致問(wèn)題。全屏障在需要嚴(yán)格保證內(nèi)存操作順序的場(chǎng)景中非常有用,比如在實(shí)現(xiàn)一些關(guān)鍵的同步機(jī)制或者對(duì)共享資源的復(fù)雜操作時(shí)。

4.2讀取屏障(Read/Load Barrier)

讀取屏障的作用是確保在該屏障之前的所有讀取操作,必須在該屏障之后的讀取操作之前完成 。它主要用于控制讀取操作的順序,防止讀取操作的重排序。在 x86 架構(gòu)中,讀取屏障對(duì)應(yīng)的指令是lfence 。例如:

// 線程1
int a = shared_variable1;  // 讀操作A
lfence();  // 讀取屏障
int b = shared_variable2;  // 讀操作B

在上述代碼中,lfence讀取屏障保證了讀操作 A 一定會(huì)在讀操作 B 之前完成。即使處理器可能有優(yōu)化策略,也不能將讀操作 B 提前到讀操作 A 之前執(zhí)行。讀取屏障在多線程環(huán)境中,當(dāng)讀取操作的順序?qū)Τ绦蜻壿嬘兄匾绊憰r(shí)非常關(guān)鍵。比如在一些依賴于特定讀取順序的算法實(shí)現(xiàn)中,或者在讀取共享狀態(tài)變量時(shí),為了確保獲取到正確的狀態(tài)信息,就需要使用讀取屏障來(lái)保證讀取操作的順序性 。

4.3寫入屏障(Write/Store Barrier)

一個(gè)寫內(nèi)存屏障可以提供這樣的保證,站在系統(tǒng)中的其它組件的角度來(lái)看,在屏障之前的寫操作看起來(lái)將在屏障后的寫操作之前發(fā)生。

如果映射到上面的例子來(lái)說(shuō),首先,寫內(nèi)存屏障會(huì)對(duì)處理器指令重排序做出一些限制,也就是在寫內(nèi)存屏障之前的寫入指令一定不會(huì)被重排序到寫內(nèi)存屏障之后的寫入指令之后。其次,在執(zhí)行寫內(nèi)存屏障之后的寫入指令之前,一定要保證清空當(dāng)前CPU存儲(chǔ)緩沖中的所有寫操作,將它們?nèi)俊疤峤弧钡骄彺嬷小_@樣的話系統(tǒng)中的其它組件(包括別的CPU),就可以保證在看到寫內(nèi)存屏障之后的寫入數(shù)據(jù)之前先看到寫內(nèi)存屏障之前的寫入數(shù)據(jù)。

圖片圖片

寫入屏障用于確保在該屏障之前的所有寫入操作,必須在該屏障之后的寫入操作之前完成 。它主要關(guān)注寫入操作的順序,防止寫入操作的重排序。在 x86 架構(gòu)中,寫入屏障的指令是sfence 。例如:

// 線程1
shared_variable1 = 10;  // 寫操作C
sfence();  // 寫入屏障
shared_variable2 = 20;  // 寫操作D

這里,sfence寫入屏障確保了寫操作 C 一定會(huì)在寫操作 D 之前完成。無(wú)論編譯器如何優(yōu)化或者處理器如何執(zhí)行指令,都不會(huì)改變這兩個(gè)寫操作的順序。寫入屏障在多線程同時(shí)修改共享數(shù)據(jù)時(shí)非常重要,它可以保證數(shù)據(jù)的更新按照預(yù)期的順序進(jìn)行,避免由于寫入順序混亂導(dǎo)致的數(shù)據(jù)不一致問(wèn)題。比如在更新一些關(guān)聯(lián)的共享變量時(shí),使用寫入屏障可以確保先更新的變量對(duì)其他線程可見(jiàn)后,再進(jìn)行后續(xù)變量的更新 。

五、內(nèi)存屏障的工作過(guò)程

內(nèi)存屏障在工作時(shí),就像是一個(gè)嚴(yán)格的 “柵欄”,對(duì)內(nèi)存操作進(jìn)行著有序的管控。以下通過(guò)一段簡(jiǎn)單的偽代碼示例,來(lái)詳細(xì)描述內(nèi)存屏障的工作過(guò)程:

// 定義共享變量
int shared_variable1 = 0;
int shared_variable2 = 0;

// 線程1執(zhí)行的代碼
void thread1() {
    shared_variable1 = 1;  // 操作A:對(duì)共享變量1進(jìn)行寫入
    memory_barrier();  // 插入內(nèi)存屏障
    shared_variable2 = 2;  // 操作B:對(duì)共享變量2進(jìn)行寫入
}

// 線程2執(zhí)行的代碼
void thread2() {
    if (shared_variable2 == 2) {  // 操作C:讀取共享變量2
        assert(shared_variable1 == 1);  // 操作D:讀取共享變量1并進(jìn)行斷言
    }
}

在上述示例中,當(dāng)線程 1 執(zhí)行時(shí):

  • 屏障前的操作:首先執(zhí)行shared_variable1 = 1(操作 A),這個(gè)寫入操作會(huì)按照正常的流程進(jìn)行,可能會(huì)被處理器優(yōu)化執(zhí)行,也可能會(huì)被暫時(shí)緩存在處理器的寫緩沖區(qū)或者緩存中 。此時(shí),操作 A 可以自由執(zhí)行和重排,只要最終的結(jié)果正確即可。
  • 遇到屏障:當(dāng)執(zhí)行到memory_barrier()內(nèi)存屏障指令時(shí),處理器會(huì)暫停執(zhí)行后續(xù)指令,直到操作 A 的寫入操作被完全確認(rèn)完成 。這意味著,操作 A 的數(shù)據(jù)必須被寫入到主內(nèi)存中,并且其他處理器的緩存也需要被更新(如果涉及到緩存一致性問(wèn)題),以確保數(shù)據(jù)的可見(jiàn)性。只有在操作 A 的所有相關(guān)內(nèi)存操作都完成之后,處理器才會(huì)繼續(xù)執(zhí)行內(nèi)存屏障后面的指令。
  • 屏障后的操作:接著執(zhí)行shared_variable2 = 2(操作 B),由于內(nèi)存屏障的存在,操作 B 不能提前于操作 A 完成,它必須在操作 A 完全結(jié)束之后才能開(kāi)始執(zhí)行 。這樣就保證了操作 A 和操作 B 的執(zhí)行順序是按照代碼編寫的順序進(jìn)行的。

當(dāng)線程 2 執(zhí)行時(shí):

  1. 先執(zhí)行if (shared_variable2 == 2)(操作 C),讀取共享變量 2 的值。如果此時(shí)線程 1 已經(jīng)執(zhí)行完內(nèi)存屏障以及后續(xù)的操作 B,那么線程 2 讀取到的shared_variable2的值就會(huì)是 2 。
  2. 接著執(zhí)行assert(shared_variable1 == 1)(操作 D),讀取共享變量 1 的值并進(jìn)行斷言。因?yàn)閮?nèi)存屏障保證了線程 1 中操作 A 先于操作 B 完成,并且操作 A 的結(jié)果對(duì)其他線程可見(jiàn),所以當(dāng)線程 2 讀取到shared_variable2為 2 時(shí),shared_variable1的值必然已經(jīng)被更新為 1,從而斷言不會(huì)失敗 。

通過(guò)這個(gè)例子可以看出,內(nèi)存屏障就像一個(gè)堅(jiān)固的 “柵欄”,將內(nèi)存操作有序地分隔開(kāi)來(lái),確保了內(nèi)存操作的順序性和數(shù)據(jù)的可見(jiàn)性,有效地避免了多線程環(huán)境下由于指令重排序和緩存不一致等問(wèn)題導(dǎo)致的數(shù)據(jù)錯(cuò)誤和程序邏輯混亂 。

六、內(nèi)存屏障的實(shí)現(xiàn)原理

6.1存儲(chǔ)器一致性模型

存儲(chǔ)器一致性模型是處理器設(shè)計(jì)者定義的一種規(guī)則,用于描述處理器對(duì)內(nèi)存操作的可見(jiàn)性和順序性 。它分為強(qiáng)一致性模型和弱一致性模型,不同的模型對(duì)內(nèi)存屏障的必要性和類型有著重要影響。

在強(qiáng)一致性模型下,處理器嚴(yán)格按照程序代碼的指令次序來(lái)執(zhí)行所有的存儲(chǔ)(Store)與加載(Load)指令 ,并且從其他處理器和內(nèi)存的角度來(lái)看,感知到的數(shù)據(jù)變化也完全是按照指令執(zhí)行的次序。這就好比在一個(gè)非常有序的隊(duì)列中,每個(gè)人都嚴(yán)格按照排隊(duì)順序依次進(jìn)行操作,不存在插隊(duì)或者提前操作的情況。在這種模型下,內(nèi)存操作的順序是非常明確的,程序不需要使用內(nèi)存屏障來(lái)保證內(nèi)存操作的正確性,因?yàn)樘幚砥饕呀?jīng)天然地保證了這一點(diǎn)。然而,這種模型雖然簡(jiǎn)單直觀,但由于對(duì)處理器的限制較多,會(huì)在一定程度上影響處理器的性能。

弱一致性模型則相對(duì)寬松一些,它允許處理器對(duì)某些指令組合進(jìn)行重排序 ,以提高處理器的執(zhí)行效率。例如,在弱一致性模型中,可能會(huì)出現(xiàn)存儲(chǔ) - 加載(Store - Load)指令重排序的情況,即如果第一條指令是存儲(chǔ)指令,第二條指令是加載指令,那么在程序執(zhí)行時(shí),加載指令可能會(huì)先于存儲(chǔ)指令執(zhí)行。這種重排序在單線程環(huán)境下通常不會(huì)產(chǎn)生問(wèn)題,因?yàn)閱尉€程環(huán)境下程序的執(zhí)行順序和結(jié)果是可預(yù)測(cè)的。但在多線程環(huán)境中,這種重排序可能會(huì)導(dǎo)致數(shù)據(jù)不一致的問(wèn)題。

比如,一個(gè)線程對(duì)共享變量進(jìn)行了修改(存儲(chǔ)操作),但由于重排序,另一個(gè)線程可能在這個(gè)修改還未完成時(shí)就讀取了這個(gè)變量(加載操作),從而獲取到舊的數(shù)據(jù)。為了解決弱一致性模型下多線程環(huán)境中的數(shù)據(jù)一致性問(wèn)題,就需要使用內(nèi)存屏障。不同類型的內(nèi)存屏障可以針對(duì)不同的指令重排序情況進(jìn)行約束,確保內(nèi)存操作的順序性和數(shù)據(jù)的可見(jiàn)性。例如,在 x86 架構(gòu)采用的完全存儲(chǔ)定序(TSO)模型下,允許 Store - Load 指令重排序,為了保證程序執(zhí)行的正確性,就需要在適當(dāng)?shù)奈恢貌迦雰?nèi)存屏障指令,如mfence、lfence、sfence等 ,來(lái)確保在加載操作之前,所有的存儲(chǔ)操作都已經(jīng)完成并對(duì)其他處理器可見(jiàn)。

6.2緩存一致性協(xié)議

在多核心處理器中,每個(gè)核心都有自己的高速緩存(如 L1、L2、L3 緩存) ,這些緩存可以大大提高處理器訪問(wèn)數(shù)據(jù)的速度。但也帶來(lái)了緩存一致性的問(wèn)題,即如何保證多個(gè)核心緩存中的數(shù)據(jù)與主內(nèi)存以及其他核心緩存中的數(shù)據(jù)保持一致。MESI 協(xié)議是一種廣泛應(yīng)用的緩存一致性協(xié)議,它通過(guò)對(duì)緩存行(Cache Line,通常為 64 字節(jié))的狀態(tài)標(biāo)記來(lái)實(shí)現(xiàn)緩存一致性。

MESI 協(xié)議中,緩存行有四種狀態(tài):

  1. 已修改(Modified,M):表示緩存行中的數(shù)據(jù)已經(jīng)被修改,并且與主內(nèi)存中的數(shù)據(jù)不一致 。此時(shí),該緩存行只存在于當(dāng)前核心的緩存中,其他核心的緩存中沒(méi)有該緩存行的副本。在數(shù)據(jù)被寫回主內(nèi)存之前,其他核心如果需要讀取該數(shù)據(jù),會(huì)收到一個(gè)無(wú)效信號(hào),然后從主內(nèi)存中讀取最新的數(shù)據(jù)。
  2. 獨(dú)占(Exclusive,E):表示緩存行中的數(shù)據(jù)與主內(nèi)存中的數(shù)據(jù)一致,并且只存在于當(dāng)前核心的緩存中 ,其他核心的緩存中沒(méi)有該緩存行的副本。在這種狀態(tài)下,如果當(dāng)前核心對(duì)緩存行中的數(shù)據(jù)進(jìn)行修改,緩存行狀態(tài)會(huì)變?yōu)橐研薷模∕);如果其他核心請(qǐng)求讀取該數(shù)據(jù),緩存行狀態(tài)會(huì)變?yōu)楣蚕恚⊿)。
  3. 共享(Shared,S):表示緩存行中的數(shù)據(jù)與主內(nèi)存中的數(shù)據(jù)一致,并且存在于多個(gè)核心的緩存中 。當(dāng)一個(gè)核心修改共享狀態(tài)的緩存行時(shí),會(huì)向總線上發(fā)送一個(gè)無(wú)效信號(hào),通知其他核心將該緩存行的狀態(tài)標(biāo)記為無(wú)效(Invalid,I),然后自己將緩存行狀態(tài)變?yōu)橐研薷模∕)。這樣可以保證在同一時(shí)刻,只有一個(gè)核心能夠修改共享數(shù)據(jù),從而維護(hù)數(shù)據(jù)的一致性。
  4. 無(wú)效(Invalid,I):表示緩存行中的數(shù)據(jù)已經(jīng)無(wú)效,不能再被使用 。當(dāng)一個(gè)核心收到其他核心發(fā)送的無(wú)效信號(hào)時(shí),會(huì)將自己緩存中對(duì)應(yīng)的緩存行狀態(tài)標(biāo)記為無(wú)效。

內(nèi)存屏障在 MESI 協(xié)議中起著關(guān)鍵的作用。當(dāng)處理器執(zhí)行內(nèi)存屏障指令時(shí),它會(huì)強(qiáng)制完成所有內(nèi)存寫入操作,確保在屏障前的所有內(nèi)存操作都能在屏障之后被其他執(zhí)行上下文(線程或處理器)看到 。例如,在一個(gè)多核心處理器系統(tǒng)中,當(dāng)一個(gè)核心執(zhí)行寫入屏障指令時(shí),它會(huì)確保之前的所有寫入操作都已經(jīng)完成,并且將修改后的數(shù)據(jù)寫回主內(nèi)存,同時(shí)通過(guò) MESI 協(xié)議通知其他核心更新它們的緩存,使其他核心緩存中的相應(yīng)數(shù)據(jù)也變?yōu)樽钚聽(tīng)顟B(tài)。這樣,當(dāng)其他核心執(zhí)行讀取操作時(shí),就能獲取到最新的數(shù)據(jù),從而保證了緩存一致性。

6.3指令序列

內(nèi)存屏障通常通過(guò)特殊指令序列來(lái)實(shí)現(xiàn),這些指令會(huì)強(qiáng)制CPU等待所有內(nèi)存操作完成,從而確保內(nèi)存操作的順序性。以x86架構(gòu)為例,常見(jiàn)的內(nèi)存屏障指令有mfence(全屏障)、lfence(讀取屏障)和sfence(寫入屏障) 。

mfence指令是全屏障指令,它會(huì)阻止屏障兩邊的讀寫操作進(jìn)行重排序 。當(dāng) CPU 執(zhí)行到mfence指令時(shí),會(huì)將之前所有的存儲(chǔ)和加載操作都按順序完成,才會(huì)繼續(xù)執(zhí)行后面的指令。例如:

// 線程1
x = 1;  // 寫操作1
mfence();  // 全屏障
y = 2;  // 寫操作2

在這個(gè)例子中,mfence指令保證了寫操作 1(x = 1)一定會(huì)在寫操作 2(y = 2)之前完成,即使處理器可能有優(yōu)化策略,也不能改變這兩個(gè)寫操作的執(zhí)行順序。

lfence指令是讀取屏障指令,它確保在該屏障之前的所有讀取操作,必須在該屏障之后的讀取操作之前完成 。例如:

// 線程1
int a = shared_variable1;  // 讀操作A
lfence();  // 讀取屏障
int b = shared_variable2;  // 讀操作B

這里,lfence指令保證了讀操作 A 一定會(huì)在讀操作 B 之前完成,防止了讀取操作的重排序。

sfence指令是寫入屏障指令,它確保在該屏障之前的所有寫入操作,必須在該屏障之后的寫入操作之前完成 。例如:

// 線程1
shared_variable1 = 10;  // 寫操作C
sfence();  // 寫入屏障
shared_variable2 = 20;  // 寫操作D

在這個(gè)例子中,sfence指令保證了寫操作 C 一定會(huì)在寫操作 D 之前完成,避免了寫入操作的重排序。

這些內(nèi)存屏障指令通過(guò)特殊的指令序列,利用 CPU 的硬件特性,實(shí)現(xiàn)了對(duì)內(nèi)存操作順序的控制,從而有效地解決了多線程和多處理器環(huán)境下的內(nèi)存一致性問(wèn)題 。

七、內(nèi)存屏障的應(yīng)用場(chǎng)景

7.1多線程編程

在多線程編程中,內(nèi)存屏障起著至關(guān)重要的作用,它能夠確保線程間數(shù)據(jù)的一致性和可見(jiàn)性。假設(shè)有兩個(gè)線程共享變量x和y,初始值都為 0,如下所示:

// 共享變量
int x = 0;
int y = 0;

// 線程1
void* thread1(void* arg) {
    x = 1;  // 寫操作1
    // 這里插入內(nèi)存屏障,假設(shè)為全屏障mfence()
    y = 2;  // 寫操作2
    return NULL;
}

// 線程2
void* thread2(void* arg) {
    if (y == 2) {  // 讀操作1
        // 這里可以根據(jù)需要插入內(nèi)存屏障,假設(shè)為讀取屏障lfence()
        assert(x == 1);  // 讀操作2
    }
    return NULL;
}

在這個(gè)例子中,如果沒(méi)有內(nèi)存屏障,線程 1 中的寫操作 1 和寫操作 2 可能會(huì)被重排序,導(dǎo)致線程 2 在讀取y為 2 時(shí),x的值還未被更新為 1,從而使斷言失敗 。通過(guò)插入內(nèi)存屏障,如線程 1 中的全屏障mfence(),可以確保寫操作 1 先于寫操作 2 完成,并且寫操作 1 的結(jié)果對(duì)其他線程可見(jiàn) 。線程 2 中的讀取屏障lfence()可以確保在讀取x之前,先讀取到y(tǒng)為 2 時(shí),x的值已經(jīng)被正確更新為 1,從而保證了線程間數(shù)據(jù)的一致性和可見(jiàn)性,避免了由于指令重排序?qū)е碌腻e(cuò)誤 。

7.2內(nèi)存共享

在內(nèi)存共享場(chǎng)景中,比如多個(gè)處理器同時(shí)訪問(wèn)共享內(nèi)存,內(nèi)存屏障能夠確保各個(gè)處理器按照正確的順序訪問(wèn)內(nèi)存。以一個(gè)簡(jiǎn)單的生產(chǎn)者 - 消費(fèi)者模型為例,假設(shè)有兩個(gè)處理器,一個(gè)作為生產(chǎn)者,一個(gè)作為消費(fèi)者,共享一個(gè)內(nèi)存緩沖區(qū)和一個(gè)標(biāo)志位flag :

// 共享內(nèi)存緩沖區(qū)
int buffer = 0;
// 標(biāo)志位,用于指示緩沖區(qū)是否有數(shù)據(jù)
int flag = 0;

// 生產(chǎn)者處理器執(zhí)行的代碼
void producer() {
    buffer = 10;  // 向緩沖區(qū)寫入數(shù)據(jù)
    // 插入寫入屏障sfence()
    flag = 1;  // 設(shè)置標(biāo)志位,表示緩沖區(qū)有數(shù)據(jù)
}

// 消費(fèi)者處理器執(zhí)行的代碼
void consumer() {
    if (flag == 1) {  // 檢查標(biāo)志位
        // 插入讀取屏障lfence()
        assert(buffer == 10);  // 讀取緩沖區(qū)數(shù)據(jù)并進(jìn)行斷言
    }
}

在這個(gè)例子中,生產(chǎn)者處理器在向緩沖區(qū)寫入數(shù)據(jù)后,通過(guò)插入寫入屏障sfence(),確保寫操作完成并對(duì)其他處理器可見(jiàn)后,再設(shè)置標(biāo)志位 。消費(fèi)者處理器在檢查標(biāo)志位后,通過(guò)插入讀取屏障lfence(),確保在讀取緩沖區(qū)數(shù)據(jù)之前,已經(jīng)看到生產(chǎn)者設(shè)置的標(biāo)志位,從而保證了緩沖區(qū)數(shù)據(jù)的一致性和正確訪問(wèn)順序 。如果沒(méi)有這些內(nèi)存屏障,可能會(huì)出現(xiàn)消費(fèi)者處理器在標(biāo)志位被設(shè)置之前就讀取緩沖區(qū)數(shù)據(jù),或者生產(chǎn)者處理器設(shè)置標(biāo)志位后,緩沖區(qū)數(shù)據(jù)還未被正確寫入的情況 。

7.3緩存一致性

在緩存一致性場(chǎng)景中,內(nèi)存屏障可以保證各處理器緩存數(shù)據(jù)的一致。在一個(gè)多處理器系統(tǒng)中,每個(gè)處理器都有自己的緩存,當(dāng)多個(gè)處理器同時(shí)訪問(wèn)共享數(shù)據(jù)時(shí),可能會(huì)出現(xiàn)緩存不一致的問(wèn)題 。例如,處理器 A 修改了共享變量x的值,并將其緩存起來(lái),此時(shí)處理器 B 的緩存中x的值還是舊的 。如果沒(méi)有內(nèi)存屏障的控制,處理器 B 在讀取x時(shí),可能會(huì)從自己的緩存中讀取到舊值,而不是處理器 A 修改后的新值 。

// 共享變量
int x = 0;

// 處理器A執(zhí)行的代碼
void processorA() {
    x = 1;  // 修改共享變量x的值
    // 插入全屏障mfence(),確保緩存一致性
}

// 處理器B執(zhí)行的代碼
void processorB() {
    // 插入全屏障mfence(),確保讀取到最新數(shù)據(jù)
    assert(x == 1);  // 讀取共享變量x的值并進(jìn)行斷言
}

在這個(gè)例子中,處理器 A 在修改共享變量x的值后,通過(guò)插入全屏障mfence(),將修改后的數(shù)據(jù)寫回主內(nèi)存,并通知其他處理器更新它們的緩存 。處理器 B 在讀取x的值之前,也插入全屏障mfence(),確保從主內(nèi)存中讀取到最新的數(shù)據(jù),從而保證了各處理器緩存數(shù)據(jù)的一致性 。內(nèi)存屏障通過(guò)與緩存一致性協(xié)議(如 MESI 協(xié)議)協(xié)同工作,有效地解決了緩存不一致的問(wèn)題,確保了多處理器系統(tǒng)中數(shù)據(jù)的正確性和可靠性 。

責(zé)任編輯:武曉燕 來(lái)源: 深度Linux
相關(guān)推薦

2017-09-15 11:53:48

廁所物聯(lián)網(wǎng)虹橋站

2025-04-15 00:00:00

2018-02-02 16:19:08

華為云

2017-09-04 15:15:48

Linux內(nèi)核內(nèi)存屏障

2009-03-10 17:15:07

Linux兼容內(nèi)核Win程序

2019-12-10 14:51:00

CPU緩存內(nèi)存

2025-04-15 06:00:00

2025-03-28 08:35:00

2010-09-15 21:14:48

IT管理網(wǎng)絡(luò)構(gòu)架Juniper Net

2025-02-10 04:00:00

Linux進(jìn)程Python

2020-04-03 21:36:54

數(shù)據(jù)科技

2020-04-21 22:18:20

MESI內(nèi)存CPU

2021-08-26 05:03:18

內(nèi)存機(jī)制磁盤

2009-08-10 21:23:20

發(fā)布管理IT運(yùn)維管理摩卡軟件

2021-07-12 14:50:25

Linux命令文件

2019-10-30 09:56:56

內(nèi)存屏障變量

2019-11-12 14:40:43

CPU緩存內(nèi)存

2020-02-26 09:42:15

主存程序存儲(chǔ)器

2021-09-17 14:10:27

區(qū)塊鏈購(gòu)物技術(shù)
點(diǎn)贊
收藏

51CTO技術(shù)棧公眾號(hào)