天天看點

Linux核心的記憶體屏障

考慮下面這個系統的抽象模型:

每個cpu執行一個有記憶體通路操作的程式。在這個抽象的cpu中,記憶體操作的順序是非常寬松的。假若能讓程式的因果關系看起來是保持着的,cpu就可以以任意它喜歡的順序執行記憶體操作。同樣,隻要不影響程式的結果,編譯器可以以它喜歡的任何順序安排指令。

是以,上圖中,一個cpu執行記憶體操作的結果能被系統的其它部分感覺到,因為這些操作穿過了cpu與系統其它部分之間的接口(虛線)。

例如,請考慮以下的事件序列:

記憶體系統能看見的通路順序可能有24種不同的組合:

是以,可能産生四種不同的值組合:

此外,一個cpu 送出store指令到存儲系統,另一個cpu執行load指令時感覺到的這些store的順序可能并不是第一個cpu送出的順序。

另一個例子,考慮下面的事件序列:

這裡有一個明顯的資料依賴,d的值取決于cpu 2從p取得的位址。執行結束時,下面任一結果都是有可能的;

注意:cpu 2永遠不會将c的值賦給d,因為cpu在對*q發出load指令之前會先将p賦給q。

一些硬體的控制接口,是一組存儲單元,但這些控制寄存器的通路順序是非常重要的。例如,考慮擁有一系列内部寄存器的以太網卡,它通過一個位址端口寄存器(a)和一個資料端口寄存器(d)通路。現在要讀取編号為5的内部寄存器,可能要使用下列代碼:

但上面代碼可能表現出下列兩種順序:

其中第二個幾乎肯定會導緻故障,因為它在讀取寄存器之後才設定位址值。

下面是cpu必須要保證的最小集合:

任意cpu,有依賴的記憶體通路指令必須按順序發出。這意味着對于

cpu會發出下列記憶體操作:

并且總是以這種順序。

在一個特定的cpu中,重疊的load和store指令在該cpu中将會看起來是有序的。這意味着對于:

cpu發出的記憶體操隻會是下面的順序:

對于:

cpu隻會發出:

(如果load和store指令的目标記憶體塊有重疊,則稱load和store重疊了。)。

還有一些必須要和一定不能假設的東西:

一定不能假設無關聯的load和store指令會按給定的順序發出,這意味着對于:

我們可能得到下面的序列之一:

必須要假定重疊的記憶體通路可能會被合并或丢棄。這意味着對于

如上所述,沒有依賴關系的記憶體操作實際會以随機的順序執行,但對cpu-cpu的互動和i / o來說卻是個問題。我們需要某種方式來指導編譯器和cpu以限制執行順序。

記憶體屏障就是這樣一種幹預手段。它們會給屏障兩側的記憶體操作強加一個偏序關系。

這種強制措施是很重要的,因為一個系統中,cpu和其它硬體可以使用各種技巧來提高性能,包括記憶體操作的重排、延遲和合并;預取;推測執行分支以及各種類型的緩存。記憶體屏障是用來禁用或抑制這些技巧的,使代碼穩健地控制多個cpu和(或)裝置的互動。

記憶體屏障有四種基本類型:

write(或store)記憶體屏障。

write記憶體屏障保證:所有該屏障之前的store操作,看起來一定在所有該屏障之後的store操作之前執行。

write屏障僅保證store指令上的偏序關系,不要求對load指令有什麼影響。

随着時間推移,可以視cpu送出了一系列store操作到記憶體系統。在該一系列store操作中,write屏障之前的所有store操作将在該屏障後面的store操作之前執行。

[!]注意,write屏障一般與read屏障或資料依賴障礙成對出現;請參閱“smp屏障配對”小節。

資料依賴屏障。

資料依賴屏障是read屏障的一種較弱形式。在執行兩個load指令,第二個依賴于第一個的執行結果(例如:第一個load執行擷取某個位址,第二個load指令取該位址的值)時,可能就需要一個資料依賴屏障,來確定第二個load指令在擷取目标位址值的時候,第一個load指令已經更新過該位址。

資料依賴屏障僅保證互相依賴的load指令上的偏序關系,不要求對store指令,無關聯的load指令以及重疊的load指令有什麼影響。

參考”記憶體屏障順序執行個體”小節圖中的順序限制。

[!]注意:第一個load指令确實必須有一個資料依賴,而不是控制依賴。如果第二個load指令的目标位址依賴于第一個load,但是這個依賴是通過一個條件語句,而不是實際加載的位址本身,那麼它是一個控制依賴,需要一個完整的read屏障或更強的屏障。檢視”控制依賴”小節,了解更多資訊。

[!]注意:資料依賴屏障一般與寫障礙成對出現;看到“smp屏障配對”章節。

read(或load)記憶體屏障。

read屏障是資料依賴屏障外加一個保證,保證所有該屏障之前的load操作,看起來一定在所有該屏障之後的load操作之前執行。

read屏障僅保證load指令上的偏序關系,不要求對store指令有什麼影響。

read屏障包含了資料依賴屏障的功能,是以可以替代資料依賴屏障。

[!]注意:read屏障通常與write屏障成對出現;請參閱“smp屏障配對”小節。

通用記憶體屏障。

通用屏障確定所有該屏障之前的load和store操作,看起來一定在所有屏障之後的load和store操作之前執行。

通用屏障能保證load和store指令上的偏序關系。

通用屏障包含了read屏障和write屏障,是以可以替代它們兩者。

一對隐式的屏障變種:

lock操作。

lock操作可以看作是一個單向滲透的屏障。它保證所有在lock之後的記憶體操作看起來一定在lock操作後才發生。

lock操作之前的記憶體操作可能會在lock完成之後發生。

lock操作幾乎總是與unlock操作成對出現。

unlock操作。

這也是一個單向滲透屏障。它保證所有unlock操作之前的記憶體操作看起來一定在unlock操作之前發生。

unlock操作之後的記憶體操作可能會在unlock完成之前發生。

lock和unlock操作嚴格保證自己對指令的順序。

使用了lock和unlock操作,一般就不需要其它類型的記憶體屏障了(但要注意在”mmio write屏障”一節中提到的例外情況)。

僅當兩個cpu之間或者cpu與其它裝置之間有互動時才需要屏障。如果可以確定某段代碼中不會有任何這種互動,那麼這段代碼就不需要記憶體屏障。

注意,這些是最低限度的保證。不同的架構可能會提供更多的保證,但是它們不是必須的,不應該依賴其寫代碼(they may not be relied upon outside of arch specific code)。

有一些事情,linux核心的記憶體屏障并不保證:

不能保證,任何在記憶體屏障之前的記憶體通路操作能在記憶體屏障指令執行完成時也執行完成;記憶體屏障相當于在cpu的通路隊列中劃了一條界線,相應類型的指令不能跨過該界線。

不能保證,一個cpu發出的記憶體屏障能對另一個cpu或該系統中的其它硬體有任何直接影響。隻會間接影響到第二個cpu看第一個cpu的存取操作發生的順序,但請看下一條:

不能保證,一個cpu看到第二個cpu存取操作的結果的順序,即使第二個cpu使用了記憶體屏障,除非第一個cpu也使用與第二個cpu相比對的記憶體屏障(見”smp屏障配對”小節)。

不能保證,一些cpu相關的硬體[*]不會對記憶體通路重排序。 cpu緩存的一緻性機制會在多個cpu之間傳播記憶體屏障的間接影響,但可能不是有序的。

[*]總線主要dma和一緻性相關資訊,請參閱:

documentation/pci/pci.txt

documentation/pci/pci-dma-mapping.txt

documentation/dma-api.txt

資料依賴屏障的使用條件有點微妙,且并不總是很明顯。為了闡明問題,考慮下面的事件序列:

這裡很明顯存在資料依賴,看起來在執行結束後,q不是&a就是&b,并且:

但是,從cpu 2可能先感覺到p更新,然後才感覺到b更新,這就導緻了以下情況:

雖然這可能看起來像是一緻性或因果關系維護失敗,但實際并不是的,且這種行為在一些真實的cpu上也可以觀察到(如dec alpha)。

為了處理這個問題,需要在位址load和資料load之間插入一個資料依賴屏障或一個更強的屏障:

這将迫使結果為前兩種情況之一,而防止了第三種可能性的出現。

[!]注意:這種極其有違直覺的場景,在有多個獨立緩存(split caches)的機器上很容易出現,比如:一個cache bank處理偶數編号的緩存行,另外一個cache bank處理奇數編号的緩存行。指針p可能存儲在奇數編号的緩存行,變量b可能存儲在偶數編号的緩存行中。然後,如果在讀取cpu緩存的時候,偶數的bank非常繁忙,而奇數bank處于閑置狀态,就會出現指針p(&b)是新值,但變量b(2)是舊值的情況。

另外一個需要資料依賴屏障的例子是從記憶體中讀取一個數字,然後用來計算某個數組的下标;

資料依賴屏障對rcu系統是很重要的,如,看include/linux/ rcupdate.h的rcu_dereference()函數。這個函數允許rcu的指針被替換為一個新的值,而這個新的值還沒有完全的初始化。

更多詳細的例子參見”高速緩存一緻性”小節。

控制依賴需要一個完整的read記憶體屏障來保證其正确性,而不簡單地隻是資料依賴屏障。考慮下面的代碼:

這不會産生想要的結果,因為這裡沒有實際的資料依賴,而是一個控制依賴,cpu可能會提前預測結果而使if語句短路。在這樣的情況下,實際需要的是下面的代碼:

當處理cpu-cpu之間的互動時,相應類型的記憶體屏障總應該是成對出現的。缺少相應的配對屏障幾乎可以肯定是錯誤的。

write屏障應始終與資料依賴屏障或者read屏障配對,雖然通用記憶體屏障也是可以的。同樣地,read屏障或資料依賴屏障應至少始終與write屏障配對使用,雖然通用屏障仍然也是可以的:

或者:

基本上,那個位置的read屏障是必不可少的,盡管可以是“更弱“的類型。

[!]注意:write屏障之前的store指令通常與read屏障或資料依賴屏障後的load指令相比對,反之亦然:

首先,write屏障確定store操作的偏序關系。考慮以下事件序列:

這一連串的事件送出給記憶體一緻性系統的順序,可以使系統其它部分感覺到無序集合{ store a,store b, store c } 中的操作都發生在無序集合{ store d, store e}中的操作之前:

其次,資料依賴屏障確定于有資料依賴關系的load指令間的偏序關系。考慮以下事件序列: 

在沒有其它幹涉時,盡管cpu 1發出了write屏障,cpu2感覺到的cpu1上事件的順序也可能是随機的:

在上述的例子中,盡管load *c(可能是b)在load c之後,但cpu 2感覺到的b卻是7;

然而,在cpu2中,如果資料依賴屏障放置在loadc和load *c(即:b)之間:

将發生以下情況:

第三,read屏障確定load指令上的偏序關系。考慮以下的事件序列:

在沒有其它幹涉時,盡管cpu1發出了一個write屏障,cpu 2感覺到的cpu 1中事件的順序也可能是随機的:

然而,如果在cpu2上的load a和load b之間放置一個read屏障:

cpu1上的偏序關系将能被cpu2正确感覺到:

為了更徹底說明這個問題,考慮read屏障的兩側都有load a将發生什麼:

即使兩個load a都發生在loadb之後,它們仍然可能獲得不同的值:

但是,在read屏障完成之前,cpu1對a的更新就可能被cpu2看到:

如果load b == 2,可以保證第二次load a總是等于 1。但是不能保證第一次load a的值,a == 0或a == 1都可能會出現。

許多cpu都會預測并提前加載:即,當系統發現它即将需要從記憶體中加載一個條目時,系統會尋找沒有其它load指令占用總線資源的時候提前加載 —— 即使還沒有達到指令執行流中的該點。這使得實際的load指令可能會立即完成,因為cpu已經獲得了值。

也可能cpu根本不會使用這個值,因為執行到了另外的分支而繞開了這個load – 在這種情況下,它可以丢棄該值或僅是緩存該值供以後使用。

考慮下面的場景:

可能出現:

在第二個load指令之前,放置一個read屏障或資料依賴屏障:

是否強制重新擷取預取的值,在一定程度上依賴于使用的屏障類型。如果值沒有發送變化,将直接使用預取的值:

但如果另一個cpu有更新該值或者使該值失效,就必須重新加載該值:

傳遞性是有關順序的一個非常直覺的概念,但是真實的計算機系統往往并不保證。下面的例子示範傳遞性(也可稱為“積累律(cumulativity)”):

假設cpu 2 的load x傳回1、load y傳回0。這表明,從某種意義上來說,cpu 2的load x在cpu 1 store x之後,cpu 2的load y在cpu 3的store y 之前。問題是“cpu 3的 load x是否可能傳回0?”

因為,從某種意義上說,cpu 2的load x在cpu 1的store之後,我們很自然地希望cpu 3的load x必須傳回1。這就是傳遞性的一個例子:如果在cpu b上執行了一個load指令,随後cpu a 又對相同位置進行了load操作,那麼,cpu a load的值要麼和cpu b load的值相同,要麼是個更新的值。

在linux核心中,使用通用記憶體屏障能保證傳遞性。是以,在上面的例子中,如果從cpu 2的load x指令傳回1,且其load y傳回0,那麼cpu 3的load x也必須傳回1。

但是,read或write屏障不保證傳遞性。例如,将上述例子中的通用屏障改為read屏障,如下所示:

這就破壞了傳遞性:在本例中,cpu 2的load x傳回1,load y傳回0,但是cpu 3的load x傳回0是完全合法的。

關鍵點在于,雖然cpu 2的read屏障保證了cpu2上的load指令的順序,但它并不能保證cpu 1上的store順序。是以,如果這個例子運作所在的cpu 1和2共享了存儲緩沖區或某一級緩存,cpu 2可能會提前獲得到cpu 1寫入的值。是以,需要通用屏障來確定所有的cpu都遵守cpu1和cpu2的通路組合順序。

要重申的是,如果你的代碼需要傳遞性,請使用通用屏障。

linux核心有多種不同的屏障,工作在不同的層上:

編譯器屏障。

cpu記憶體屏障。

mmio write屏障。

linux核心有一個顯式的編譯器屏障函數,用于防止編譯器将記憶體通路從屏障的一側移動到另一側:

這是一個通用屏障 – 不存在弱類型的編譯屏障。

編譯屏障并不直接影響cpu,cpu依然可以按照它所希望的順序進行重排序。

linux核心有8個基本的cpu記憶體屏障:

除了資料依賴屏障之外,其它所有屏障都包含了編譯器屏障的功能。資料依賴屏障不強加任何額外的編譯順序。

旁白:在資料依賴的情況下,可能希望編譯器以正确的順序發出load指令(如:’a[b]’,将會在load a[b]之前load b),但在c規範下并不能保證如此,編譯器可能不會預先推測b的值(即,等于1),然後在load b之前先load a(即,tmp = a [1];if(b!= 1)tmp = a[b];)。還有編譯器重排序的問題,編譯器load a[b]之後重新load b,這樣,b就擁有比a[b]更新的副本。關于這些問題尚未形成共識,然而access_once宏是解決這個問題很好的開始。

在單處理器編譯系統中,smp記憶體屏障将退化為編譯屏障,因為它假定cpu可以保證自身的一緻性,并且可以正确的處理重疊通路。

[!]注意:smp記憶體屏障必須用在smp系統中來控制引用共享記憶體的順序,使用鎖也可以滿足需求。

強制性屏障不應該被用來控制smp,因為強制屏障在up系統中會産生過多不必要的開銷。但是,它們可以用于控制在通過松散記憶體i / o視窗通路的mmio操作。即使在非smp系統中,這些也是必須的,因為它們可以禁止編譯器和cpu的重排進而影響記憶體操作的順序。

下面是些更進階的屏障函數:

這個函數将值賦給變量,然後在其後插入一個完整的記憶體屏障,根據不同的實作。在up編譯器中,不能保證插入編譯器屏障之外的屏障。

這些都是用于原子加,減,遞增和遞減而不用傳回值的,主要用于引用計數。這些函數并不包含記憶體屏障。

例如,考慮下面的代碼片段,它标記死亡的對象, 然後将該對象的引用計數減1:

這可以確定設定對象的死亡标記是在引用計數遞減之前;

更多資訊參見documentation/atomic_ops.txt ,“atomic operations” 章節介紹了它的使用場景。

這些類似于用于原子自增,自減的屏障。他們典型的應用場景是按位解鎖操作,必須注意,因為這裡也沒有隐式的記憶體屏障。

考慮通過清除一個lock位來實作解鎖操作。 clear_bit()函數将需要像下面這樣使用記憶體屏障:

這可以防止在clear之前的記憶體操作跑到clear後面。unlock的參考實作見”鎖的功能”小節。

更多資訊見documentation/atomic_ops.txt , “atomic operations“章節有關于使用場景的介紹;

對于記憶體映射i / o寫操作,linux核心也有個特殊的障礙;

這是一個強制性寫屏障的變體,保證對弱序i / o區的寫操作有偏序關系。其影響可能超越cpu和硬體之間的接口,且能實際地在一定程度上影響到硬體。

更多資訊參見”鎖與i / o通路”章節。

linux核心中的一些其它的功能暗含着記憶體屏障,主要是鎖和排程功能。

該規範是一個最低限度的保證,任何特定的體系結構都可能提供更多的保證,但是在特定體系結構之外不能依賴它們。

linux核心有很多鎖結構:

自旋鎖

r / w自旋鎖

互斥

信号量

r / w信号量

rcu

所有的情況下,它們都是lock操作和unlock操作的變種。這些操作都隐含着特定的屏障:

lock操作的含義:

在lock操作之後的記憶體操作将會在lock操作結束之後完成;

在lock操作之前的記憶體操作可能在lock操作結束之後完成;

unlock操作的含義:

在unlock操作之前的記憶體操作将會在unlock操作結束之前完成;

在unlock操作之後的記憶體操作可能在unlock操作結束之前完成;

lock與lock的含義:

在一個lock之前的其它lock操作一定在該lock結束之前完成;

lock與unlock的含義:

在某個unlock之前的所有其它lock操作一定在該unlock結束之前完成;

在某個lock之前的所有其它unlock操作一定在該lock結束之前完成;

失敗的有條件鎖的含義:

某些鎖操作的變種可能會失敗,要麼是由于無法立即獲得鎖,要麼是在休眠等待鎖可用的同時收到了一個解除阻塞的信号。失敗的鎖操作并不暗含任何形式的屏障。

是以,根據(1),(2)和(4),一個無條件的lock後面跟着一個unlock操作相當于一個完整的屏障,但一個unlock後面跟着一個lock卻不是。

[!]注意:将lock和unlock作為單向屏障的一個結果是,臨界區外的指令可能會移到臨界區裡。

lock後跟着一個unlock并不認為是一個完整的屏障,因為存在lock之前的存取發生在lock之後,unlock之後的存取在unlock之前發生的可能性,這樣,兩個存取操作的順序就可能颠倒:

可能會發生:

鎖和信号量在up編譯系統中不保證任何順序,是以在這種情況下根本不能考慮為屏障 —— 尤其是對于i / o通路 —— 除非結合中斷禁用操作。

更多資訊請參閱”cpu之間的鎖屏障”章節。

考慮下面的例子:

以下的順序是可以接受的:

[+] note that {*f,*a} indicates a combined access.

但下列情形的,是不能接受的:

禁止中斷(等價于lock)和允許中斷(等價于unlock)僅可充當編譯屏障。是以,如果某些場景下需要記憶體或i / o屏障,必須通過其它的手段來提供。

一個全局資料标記的事件上的休眠和喚醒,可以被看作是兩塊資料之間的互動:正在等待的任務的狀态和标記這個事件的全局資料。為了確定正确的順序,進入休眠的原語和喚醒的原語都暗含了某些屏障。

首先,通常一個休眠任務執行類似如下的事件序列:

set_current_state()會在改變任務狀态後自動插入一個通用記憶體屏障;

set_current_state()可能包含在下面的函數中:

是以,在設定狀态後,這些函數也暗含了一個通用記憶體屏障。上面的各個函數又被封裝在其它函數中,所有這些函數都在對應的地方插入了記憶體屏障;

其次,執行正常喚醒的代碼如下:

或:

類似wake_up()的函數都暗含一個記憶體屏障。當且僅當他們喚醒某個任務的時候。任務狀态被清除之前記憶體屏障執行,也即是在設定喚醒标志事件的store操作和設定task_running的store操作之間:

可用喚醒函數包括:

[!]注意:在休眠任務執行set_current_state()之後,若要load喚醒前store指令存儲的值,休眠和喚醒所暗含的記憶體屏障都不能保證喚醒前多個store指令的順序。例如:休眠函數如下

以及喚醒函數如下:

并不能保證休眠函數在對my_data做過修改之後能夠感覺到event_indicated的變化。在這種情況下,兩側的代碼必須在隔離資料通路之間插入自己的記憶體屏障。是以,上面的休眠任務應該這樣:

以及喚醒者應該做的:

其它暗含記憶體屏障的函數:

schedule()以及類似函數暗含了完整記憶體屏障。

在smp系統中,鎖原語提供了更加豐富的屏障類型:在任意特定的鎖沖突的情況下,會影響其它cpu上的記憶體通路順序。

考慮下面的場景:系統有一對自旋鎖(m)、(q)和三個cpu,然後發生以下的事件序列:

對cpu 3來說, *a到*h的存取順序是沒有保證的,不同于單獨的鎖在單獨的cpu上的作用。例如,它可能感覺的順序如下:

但它不會看到任何下面的場景:

但是,如果發生以下情況:

cpu 3可能會看到:

但是,假設cpu 1先得到鎖,cpu 3将不會看到任何下面的場景:

在某些情況下(尤其是涉及numa),在兩個不同cpu上的兩個自旋鎖區内的i / o通路,在pci橋看來可能是交叉的,因為pci橋不一定保證緩存一緻性,此時記憶體屏障将失效。

例如:

pci橋可能看到的順序如下所示:

這可能會導緻硬體故障。

這裡有必要在釋放自旋鎖之前插入mmiowb()函數,例如:

這将確定在cpu 1上的兩次store比cpu 2上的兩次store操作先被pci感覺。

此外,相同的裝置上如果store指令後跟随一個load指令,可以省去mmiowb()函數,因為load強制在load執行前store指令必須完成:

更多資訊參見:documentation/docbook/deviceiobook.tmpl

在正常操作下,一個單線程代碼片段中記憶體操作重排序一般不會産生問題,仍然可以正常工作,即使是在一個smp核心系統中也是如此。但是,下面四種場景下,重新排序可能會引發問題:

多理器間的互動。

原子操作。

裝置通路。

中斷。

當系統具有一個以上的處理器,系統中多個cpu可能要通路同一資料集。這可能會導緻同步問題,通常處理這種場景是使用鎖。然而,鎖是相當昂貴的,是以如果有其它的選擇盡量不使用鎖。在這種情況下,能影響到多個cpu的操作可能必須仔細排序,以防止出現故障。

例如,在r / w信号量慢路徑的場景。這裡有一個waiter程序在信号量上排隊,并且它的堆棧上的一塊空間連結到信号量上的等待程序清單:

要喚醒一個特定的waiter程序,up_read()或up_write()函數必須做以下動作:

讀取waiter記錄的next指針,擷取下一個waiter記錄的位址;

讀取waiter的task結構的指針;

清除task指針,通知waiter已經擷取信号量;

在task上調用wake_up_process()函數;

釋放waiter的task結構上的引用。

換句話說,它必須執行下面的事件:

如果這些步驟的順序發生任何改變,那麼就會出問題。

一旦程序将自己排隊并且釋放信号鎖,waiter将不再獲得鎖,它隻需要等待它的任務指針被清零,然後繼續執行。由于記錄是在waiter的堆棧上,這意味着如果在清單中的next指針被讀取出之前,task指針被清零,另一個cpu可能會開始處理,up*()函數在有機會讀取next指針之前waiter的堆棧就被修改。

考慮上述事件序列可能發生什麼:

雖然這裡可以使用信号鎖來處理,但在喚醒後的down_xxx()函數不必要的再次獲得自旋鎖。

這個問題可以通過插入一個通用的smp記憶體屏障來處理:

在這種情況下,即使是在其它的cpu上,屏障確定所有在屏障之前的記憶體操作一定先于屏障之後的記憶體操作執行。但是它不能確定所有在屏障之前的記憶體操作一定先于屏障指令身執行完成時執行;

在一個up系統中, 這種場景不會産生問題 , smp_mb()僅僅是一個編譯屏障,可以確定編譯器以正确的順序發出指令,而不會實際幹預到cpu。因為隻有一個cpu,cpu的依賴順序邏輯會管理好一切。

雖然它們在技術上考慮了處理器間的互動,但是特别注意,有一些原子操作暗含了完整的記憶體屏障,另外一些卻沒有包含,但是它們作為一個整體在核心中應用廣泛。

任一原子操作,修改了記憶體中某一狀态并傳回有關狀态(新的或舊的)的資訊,這意味着在實際操作(明确的lock操作除外)的兩側暗含了一個smp條件通用記憶體屏障(smp_mb()),包括;

它們都是用于實作諸如lock和unlock的操作,以及判斷引用計數器決定對象銷毀,同樣,隐式的記憶體屏障效果是必要的。

下面的操作存在潛在的問題,因為它們并沒有包含記憶體障礙,但可能被用于執行諸如解鎖的操作:

如果有必要,這些應使用恰當的顯式記憶體屏障(例如:smp_mb__before_clear_bit())。

下面這些也沒有包含記憶體屏障,是以在某些場景下可能需要明确的記憶體屏障(例如:smp_mb__before_atomic_dec()):

如果将它們用于統計,那麼可能并不需要記憶體屏障,除非統計資料之間有耦合。

如果将它們用于對象的引用計數器來控制生命周期,也許也不需要記憶體屏障,因為可能引用計數會在鎖區域内修改,或調用方已經考慮了鎖,是以記憶體屏障不是必須的。

如果将它們用于建構一個鎖的描述,那麼确實可能需要記憶體屏障,因為鎖原語通常以特定的順序來處理事情;

基本上,每一個使用場景都必須仔細考慮是否需要記憶體屏障。

以下操作是特殊的鎖原語:

這些實作了諸如lock和unlock的操作。在實作鎖原語時應當優先考慮使用它們,因為它們的實作可以在很多架構中進行優化。

[!]注意:對于這些場景,也有特定的記憶體屏障原語可用,因為在某些cpu上原子指令暗含着完整的記憶體屏障,再使用記憶體屏障顯得多餘,在這種情況下,特殊屏障原語将是個空操作。

更多資訊見 documentation/atomic_ops.txt。

許多裝置都可以映射到記憶體上,是以對cpu來說它們隻是一組記憶體單元。為了控制這樣的裝置,驅動程式通常必須確定對應的記憶體通路順序的正确性。

然而,聰明的cpu或者聰明的編譯器可能為引發潛在的問題,如果cpu或者編譯器認為重排、合并、聯合通路更加高效,驅動程式精心編排的指令順序可能在實際通路裝置是并不是按照這個順序通路的 —— 這會導緻裝置故障。

在linux核心中,i / o通常需要适當的通路函數 —— 如inb() 或者 writel() —— 它們知道如何保持适當的順序。雖然這在大多數情況下不需要明确的使用記憶體屏障,但是下面兩個場景可能需要:

在某些系統中,i / o存儲操作并不是在所有cpu上都是嚴格有序的,是以,對所有的通用驅動,鎖是必須的,且必須在解鎖臨界區之前執行mmiowb().

如果通路函數是用來通路一個松散通路屬性的i / o存儲視窗,那麼需要強制記憶體屏障來保證順序。

更多資訊參見 documentation/docbook/deviceiobook.tmpl。

驅動可能會被自己的中斷服務例程中斷,是以,驅動程式兩個部分可能會互相幹擾,嘗試控制或通路該裝置。

通過禁用本地中斷(一種鎖的形式)可以緩和這種情況,這樣,驅動程式中關鍵的操作都包含在中斷禁止的區間中 。有時驅動的中斷例程被執行,但是驅動程式的核心不是運作在相同的cpu上,并且直到目前的中斷被處理結束之前不允許其它中斷,是以,在中斷處理器不需要再次加鎖。

但是,考慮一個驅動使用位址寄存器和資料寄存器跟以太網卡互動,如果該驅動的核心在中斷禁用下與網卡通信,然後驅動程式的中斷處理程式被調用:

如果排序規則十分寬松,資料寄存器的存儲可能發生在第二次位址寄存器之後:

如果是寬松的排序規則,它必須假設中斷禁止部分的記憶體通路可能向外洩漏,可能會和中斷部分交叉通路 – 反之亦然 – 除非使用了隐式或顯式的屏障。

通常情況下,這不會産生問題,因為這種區域中的i / o通路将在嚴格有序的io寄存器上包含同步load操作,形成隐式記憶體屏障。如果這還不夠,可能需要顯式地使用一個mmiowb()。

類似的情況可能發生在一個中斷例程和運作在不同cpu上進行通信的兩個例程的時候。這樣的情況下,應該使用中斷禁用鎖來保證順序。

通路i/o記憶體時,驅動應使用适當的存取函數:

inx(), outx():

它們都旨在跟i / o空間打交道,而不是記憶體空間,但這主要是一個特定于cpu的概念。在 i386和x86_64處理器中确實有特殊的i / o空間通路周期和指令,但許多cpu沒有這樣的概念。

包括pci總線也定義了i / o空間,比如在i386和x86_64的cpu 上很容易将它映射到cpu的i / o空間上。然而,對于那些不支援io空間的cpu,它也可能作為虛拟的io空間被映射cpu的的記憶體空間。

通路這個空間可能是完全同步的(在i386),但橋裝置(如pci主橋)可能不完全履行這一點。

可以保證它們彼此之間的全序關系。

對于其他類型的記憶體和i / o操作,不保證它們的全序關系。

readx(), writex():

無論是保證完全有序還是不合并通路取決于他們通路時定義的通路視窗屬性,例如,最新的i386架構的機器通過mtrr寄存器控制。

通常情況下,隻要不是通路預取裝置,就保證它們的全序關系且不合并。

然而,對于中間連結硬體(如pci橋)可能會傾向延遲處理,當重新整理一個store時,首選從同一位置load,但是對同一個裝置或配置空間load時,對與pci來說一次就足夠了。

[*]注意:試圖從剛寫過的相同的位置load資料可能導緻故障 – 考慮16550 rx / tx串行寄存器的例子。

對于可預取的i / o記憶體,可能需要一個mmiowb()屏障保證順序;

請參閱pci規範獲得pci事務間互動的更多資訊;

readx_relaxed()

這些類似readx(),但在任何時候都不保證順序。因為沒有i / o讀屏障。

ioreadx(), iowritex()

它們通過選擇inx()/outx() or readx()/writex()來實際操作。

首先假定概念上cpu是弱有序的,但它能維護程式因果關系。某些cpu(如i386或x86_64)比其它類型的cpu(如powerpc的或frv)受到更多的限制,是以,在考慮與具體體系結構無關的代碼時,必須假設處在最寬松的場景(即dec alpha)。

這意味着必須考慮cpu将以任何它喜歡的順序執行它的指令流 —— 甚至是并行的 —— 如果流中的某個指令依賴前面較早的指令,則該較早的指令必須在後者執行之前完全結束[*],換句話說:保持因果關系。

[*]有些指令會産生多個結果 —— 如改變條件碼,改變寄存器或修改記憶體 —— 不同的指令可能依賴于不同的結果。

cpu也可能會放棄那些最終不産生效果的指令。例如,如果兩個相鄰的指令加載一個直接值到同一個寄存器中,第一個可能被丢棄。

同樣地,必須假定編譯器可能以任何它認為合适的方式會重新排列指令流,但同樣維護程式因果關系。

緩存的記憶體操作被系統交叉感覺的方式,在一定程度上,受到cpu和記憶體之間的緩存、以及保持系統一緻狀态的記憶體一緻性系統的影響。

若cpu和系統其它部分的互動通過cache進行,記憶體系統就必須包括cpu緩存,以及cpu及其緩存之間的記憶體屏障(記憶體屏障邏輯上如下圖中的虛線):

雖然一些特定的load或store實際上可能不出現在發出這些指令的cpu之外,因為在該cpu自己的緩存内已經滿足,但是,如果其它cpu關心這些資料,那麼還是會産生完整的記憶體通路,因為高速緩存一緻性機制将遷移緩存行到需要通路的cpu,并傳播沖突。

隻要能維持程式的因果關系,cpu核心可以以任何順序執行指令。有些指令生成load和store操作,并将他們放入記憶體請求隊列等待執行。cpu核心可以以任意順序放入到隊列中,并繼續執行,直到它被強制等待某一個指令完成。

記憶體屏障關心的是控制通路穿越cpu到記憶體一邊的順序,以及系統其他組建感覺到的順序。

[!]對于一個給定的cpu,并不需要記憶體屏障,因為cpu總是可以看到自己的load和store指令,好像發生的順序就是程式順序一樣。

[!]mmio或其它裝置通路可能繞過緩存系統。這取決于通路裝置時記憶體視窗屬性,或者某些cpu支援的特殊指令。

但是事情并不像上面說的那麼簡單,雖然緩存被期望是一緻的,但是沒有保證這種一緻性的順序。這意味着在一個cpu上所做的更改最終可以被所有cpu可見,但是并不保證其它的cpu能以相同的順序感覺變化。

考慮一個系統,有一對cpu(1&2),每一個cpu有一組并行的資料緩存(cpu 1有a / b,cpu 2有c / d):

假設該系統具有以下屬性:

奇數編号的緩存行在緩存a或者c中,或它可能仍然駐留在記憶體中;

偶數編号的緩存行在緩存b或者d中,或它可能仍然駐留在記憶體中;

當cpu核心正在通路一個cache,其它的cache可能利用總線來通路該系統的其餘元件 —— 可能是取代髒緩存行或預加載;

每個cache有一個操作隊列,用來維持cache與系統其餘部分的一緻性;

正常load已經存在于緩存行中的資料時,一緻性隊列不會重新整理,即使隊列中的内容可能會影響這些load。

接下來,試想一下,第一個cpu上有兩個寫操作,并且它們之間有一個write屏障,來保證它們到達該cpu緩存的順序:

write記憶體屏障強制系統中其它cpu能以正确的順序感覺本地cpu緩存的更改。現在假設第二個cpu要讀取這些值:

基本上,雖然兩個緩存行cpu 2在最終都會得到更新,但是在不進行幹預的情況下不能保證更新的順序與在cpu 1在送出的順序一緻。

是以我們需要在load之間插入一個資料依賴屏障或read屏障。這将迫使緩存在處理其他任務之前強制送出一緻性隊列;

dec alpha處理器上可能會遇到這類問題,因為他們有一個分列緩存,通過更好地利用資料總線以提高性能。雖然大部分的cpu在讀操作需要讀取記憶體的時候會使用資料依賴屏障,但并不都這樣,是以不能依賴這些。

其它cpu也可能頁有分列緩存,但是對于正常的記憶體通路,它們會協調各個緩存列。在缺乏記憶體屏障的時候,alpha 的語義會移除這種協作。

對于dma的裝置,并不是所有的系統都維護緩存一緻性。這時通路dma的裝置可能從ram中得到髒資料,因為髒的緩存行可能駐留在各個cpu的緩存中,并且可能還沒有被寫入到ram。為了處理這種情況,核心必須重新整理每個cpu緩存上的重疊位(或是也可以直接廢棄它們)。

此外,當裝置以及加載自己的資料之後,可能被來自cpu緩存的髒緩存行寫回ram所覆寫,或者目前cpu緩存的緩存行可能直接忽略ram已被更新,直到緩存行從cpu的緩存被丢棄和重載。為了處理這個問題,核心必須廢棄每個cpu緩存的重疊位。

更多資訊參見documentation/cachetlb.txt。

記憶體映射i / o通常作為cpu記憶體空間視窗中的一部分位址,它們與直接通路ram的的視窗有不同的屬性。

這些屬性通常是,通路繞過緩存直接進入到裝置總線。這意味着mmio的通路可能先于早些時候發出的通路被緩存的記憶體的請求到達。在這樣的情況下,一個記憶體屏障還不夠,如果緩存的記憶體寫和mmio通路存在依賴,cache必須重新整理。

程式員可能想當然的認為cpu會完全按照指定的順序執行記憶體操作,如果确實如此的話,假設cpu執行下面這段代碼:

他們會期望cpu執行下一個指令之前上一個一定執行完成,然後在系統中可以觀察到一個明确的執行順序;

當然,現實中是非常混亂的。對許多cpu和編譯器來說,上述假設都不成立,因為:

load操作可能更需要立即完成的,以保持執行進度,而推遲store往往是沒有問題的;

load操作可能預取,當結果證明是不需要的,可以丢棄;

load操作可能預取,導緻取數的時間和預期的事件序列不符合;

記憶體通路的順序可能被重排,以更好地利用cpu總線和緩存;

與記憶體和io裝置互動時,如果能批通路相鄰的位置,load和store可能會合并,以提高性能,進而減少了事務設定的開銷(記憶體和pci裝置都能夠做到這一點);

cpu的資料緩存也可能會影響順序,雖然緩存一緻性機制可以緩解 – 一旦store操作命中緩存 —— 并不能保證一緻性能正确的傳播到其它cpu。

是以對另一個cpu,上面的代碼實際觀測的結果可能是:

但是,cpu保證自身的一緻性:不需要記憶體屏障,也可以保證自己以正确的順序通路記憶體,如下面的代碼:

假設不受到外部的影響,最終的結果可能為:

上面的代碼cpu可能産生的全部的記憶體通路順序如下:

對于這個順序,如果沒有幹預,在保持一緻的前提下,一些操作也可能被合并,丢棄。

在cpu感覺這些操作之前,編譯器也可能合并、丢棄、延遲加載這些元素。

可減少到:

因為在沒有write屏障的情況下,可以假定将v寫入到*a的操作被丢棄了,同樣:

若沒有記憶體屏障,可被簡化為:

在cpu之外根本看不到load操作。

dec alpha cpu是最松散的cpu之一。不僅如此,一些版本的alpha cpu有一個分列的資料緩存,允許它們在不同的時間更新語義相關的緩存。在同步多個緩存,保證一緻性的時候,資料依賴屏障是必須的,使cpu可以正确的順序處理指針的變化和資料的獲得。

alpha定義了linux核心的記憶體屏障模型。

參見上面的“緩存一緻性”章節。

記憶體屏障可以用來實作循環緩沖,不需要用鎖來使得生産者與消費者串行。

更多詳情參考“documentation/circular-buffers.txt”

alpha axp architecture reference manual, second edition (sites & witek,

digital press)

chapter 5.2: physical address space characteristics

chapter 5.4: caches and write buffers

chapter 5.5: data sharing

chapter 5.6: read/write ordering

amd64 architecture programmer’s manual volume 2: system programming

chapter 7.1: memory-access ordering

chapter 7.4: buffering and combining memory writes

ia-32 intel architecture software developer’s manual, volume 3:

system programming guide

chapter 7.1: locked atomic operations

chapter 7.2: memory ordering

chapter 7.4: serializing instructions

the sparc architecture manual, version 9

chapter 8: memory models

appendix d: formal specification of the memory models

appendix j: programming with the memory models

ultrasparc programmer reference manual

chapter 5: memory accesses and cacheability

chapter 15: sparc-v9 memory models

ultrasparc iii cu user’s manual

chapter 9: memory models

ultrasparc iiii processor user’s manual

ultrasparc architecture 2005

chapter 9: memory

appendix d: formal specifications of the memory models

ultrasparc t1 supplement to the ultrasparc architecture 2005

appendix f: caches and cache coherency

solaris internals, core kernel architecture, p63-68:

chapter 3.3: hardware considerations for locks and

synchronization

unix systems for modern architectures, symmetric multiprocessing and caching

for kernel programmers:

chapter 13: other memory models

intel itanium architecture software developer’s manual: volume 1:

section 2.6: speculation

section 4.4: memory access

繼續閱讀