天天看點

Linux 核心記憶體屏障15 引用

linux 核心記憶體屏障

By: David Howells [email protected]

Paul E. McKenney [email protected]

Will Deacon [email protected]

Peter Zijlstra [email protected]

1 免責聲明

這個文檔不是一個規範,這是為了讓文檔更加簡潔,但不是為了文檔的不完整。

這個文檔為使用linux提供的各種記憶體屏障功能提供了指南,但是如果有任何疑問請詢問。

強調,這個文檔不是一個linux對硬體的期望的規範。

文檔的目的有兩個:

(1) 指定對于任何記憶體屏障,我們能夠期望的最低功能。

(2) 提供使用記憶體屏障的指南

注意:一個體系結構可以提供比最低功能更多的功能,但是如果少于該功能,這個體系結構就是有錯誤的。在某個體系結構下記憶體屏障可以是一個空的操作,因為在該體系結構的工作方式明确記憶體屏障是沒必要的

2 目錄

linux 核心記憶體屏障 1

1 免責聲明 1

2 目錄 1

3 記憶體通路抽象模型 2

4 操作裝置 4

4.1 保證 4

5 什麼是記憶體屏障? 6

5.1 記憶體屏障的種類 7

5.2 關于記憶體屏障, 不能保證什麼? 9

5.3 資料依賴屏障 10

5.4 控制依賴 12

5.5 SMP 屏障配對使用 17

5.6 記憶體屏障舉例 18

5.7 讀記憶體屏障與記憶體預取 24

5.8 傳遞性 26

6 核心中的顯式記憶體屏障 29

6.1 編譯優化屏障 29

6.2 CPU記憶體屏障 34

6.3 MMIO寫屏障 37

7 核心中隐式的記憶體屏障 37

7.1 鎖定擷取函數 37

7.2 中斷函數 40

7.3 睡眠喚醒函數 40

7.4 其他函數 43

8 跨CPU的擷取的屏障作用 43

8.1 ACQUIRES與記憶體通路 43

8.2 ACQUIRES與IO通路 44

9 什麼地方需要記憶體屏障? 45

9.1 處理器間互動 45

9.2 原子操作 47

9.3 通路裝置 49

9.4 中斷 50

10 核心中I/O屏障的作用 51

11 最小限度有序的假想模型 52

12 CPU cache的影響 52

12.1 CACHE一緻性 54

12.2 cache一緻性與DMA 57

12.3 cache一緻性與MMIO 57

13 CPU所能做到的 58

13.1 特别值得一提的Alpha處理器 60

13.2 VIRTUAL MACHINE GUESTS 60

14 使用示例 60

14.1 環型緩沖區 61

15 引用 61

3 記憶體通路抽象模型

考慮如下抽象系統模型:

假設每個CPU執行一個産生記憶體通路操作的程式。 在抽象CPU中,存儲器操作順序是非常松散的,在保證程式上下文邏輯關系的前提下,

CPU可以按照其所喜歡的任何順序來執行記憶體操作。 類似的,編譯器也可以将它輸出的指令安排成任何它喜歡的順序, 隻要保證不

影響程式表面的執行邏輯.

在上面的圖示中, 一個CPU執行記憶體操作所産生的影響, 一直要到該操作穿越該CPU與系統中

其他部分的界面(見圖中的虛線)之後, 才能被其他部分所感覺.

舉例來說, 考慮如下的操作序列:

CPU 1       CPU 2
=============== ===============
{ A == 1; B == 2 }
A = 3;      x = B;
B = 4;      y = A;
           

這一組通路指令(見上圖的中間部分)在記憶體系統上生效的順序, 可以有24種不同的組合:

STORE A=3, STORE B=4, y=LOAD A->3, x=LOAD B->4

STORE A=3, STORE B=4, x=LOAD B->4, y=LOAD A->3

STORE A=3, y=LOAD A->3, STORE B=4, x=LOAD B->4

STORE A=3, y=LOAD A->3, x=LOAD B->2, STORE B=4

STORE A=3, x=LOAD B->2, STORE B=4, y=LOAD A->3

STORE A=3, x=LOAD B->2, y=LOAD A->3, STORE B=4

STORE B=4, STORE A=3, y=LOAD A->3, x=LOAD B->4

STORE B=4, …

然後這就産生四種不同組合的結果值:

x == 2, y == 1

x == 2, y == 3

x == 4, y == 1

x == 4, y == 3

此外,一個CPU向記憶體系統送出的STORE操作還可能不會以相同的順序被其他CPU所執行的LOAD操作所感覺。

進一步舉例說明子,考慮如下事件序列:

CPU 1 CPU 2

=============== ===============

{ A == 1, B == 2, C == 3, P == &A, Q == &C }

B = 4; Q = P;

P = &B D = *Q;

在這裡存在明顯的資料依賴,因為在CPU 2上,LOAD到D中的值取決于從P中擷取的位址。

在操作序列結束時,可能獲得以下幾種結果:

(Q == &A) and (D == 1)

(Q == &B) and (D == 2)

(Q == &B) and (D == 4)

注意,CPU 2将永遠不會嘗試将C加載到D中,因為(資料依賴)CPU将在發出* Q的加載之前将P加載到Q中。

4 操作裝置

一些裝置将其控制寄存器映射到一組記憶體位址集合上,但這些控制寄存器的被通路順序非常

重要。 例如,想像一個帶有一組内部的以太網卡

通過位址端口寄存器(A)通路的寄存器和資料

端口寄存器(D)。 要讀取内部寄存器5,則可能會執行以下代碼

使用:

*A = 5;

x = *D;

但這可能會執行為以下兩個序列之一:

STORE *A = 5, x = LOAD *D

x = LOAD *D, STORE *A = 5

其中第二個幾乎肯定會導緻錯誤,因為它在嘗試讀取寄存器後設定位址。

4.1 保證

CPU可能會有一些最低限度的保證:

(*)對于一個CPU, 在它上面出現的有上下文依賴關系的記憶體通路将被按順序執行。 這意味着:

Q = READ_ONCE(P); smp_read_barrier_depends(); D = READ_ONCE(*Q);

CPU将順序執行以下記憶體操作:

Q = LOAD P, D = LOAD *Q

并始終按這個順序。 在大多數CPU上,smp_read_barrier_depends()不執行任何操作,但它是DEC Alpha所必需的。 
           

READ_ONCE()用于防止編譯器的惡作劇。 請注意,您通常應該使用rcu_dereference()而不是開放式編碼smp_read_barrier_depends()。

(*) 對于一個CPU,重疊的LOAD和STORE将在該CPU内按照順序執行。 這意味着:

a = READ_ONCE(*X); WRITE_ONCE(*X, b);

CPU隻會按照以下的操作順序操作記憶體:

a = LOAD *X, STORE *X = b

對于:

WRITE_ONCE(*X, c); d = READ_ONCE(*X);

CPU隻會按照以下的操作順序操作記憶體:

STORE *X = c, d = LOAD *X

(如果LOAD和STORE的目标指向同一塊記憶體位址, 則認為是重疊的操作)

還有一些事情必須被假定或者必須不被假定的:

(*) 在使用不被READ_ONCE()和WRITE_ONCE()保護的記憶體引用時,不要假設編譯器将執行你想要的操作(通常使用volatile來實作保護)。

沒有它們,編譯器在其權利範圍内進行各種“創造性”轉換,這些轉換将在“編譯屏障”章節說明

(*)不能假定無關的加載和存儲操作将按照給定的順序發出。 這意味着:

X = *A; Y = *B; *D = Z;

我們可能會得到以下任何執行順序:

X = LOAD *A, Y = LOAD *B, STORE *D = Z

X = LOAD *A, STORE *D = Z, Y = LOAD *B

Y = LOAD *B, X = LOAD *A, STORE *D = Z

Y = LOAD *B, STORE *D = Z, X = LOAD *A

STORE *D = Z, X = LOAD *A, Y = LOAD *B

STORE *D = Z, Y = LOAD *B, X = LOAD *A

(*)必須假設連續的存儲器通路可以被合并或丢棄。 這意味着:

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

我們可能會得到以下任何一個執行順序:

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

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

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

同樣,對于

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

我們可能會得到以下任何一個執行順序:

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

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

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

本節描述的保證在一些情況下無效(不能保證操作順序、原子性等):

(*) 保證對位域操作無效,因為編譯器通常生成的代碼以使用非原子的

讀-修改-寫 指令順序來實作這些功能。 是以不要嘗試使用位域同步并行邏輯。

(*) 即使在通過鎖保護位域的情況下,給定位域中的所有字段都必須被同一個鎖保護。

如果給定位域中的兩個字段受到不同的鎖的保護,則編譯器的非原子操作

讀-修改-寫入 順序可能導緻一個字段的更新破壞相鄰字段的值。

(*) 保證僅适用于正确對齊和正确大小的變量。

“正确大小”在這裡是指與“char”,“short”,“int”和“long”大小相同的變量。“正确對齊”是指自然對齊,是以“char”沒有對齊限制,“short”是兩位元組對齊,“int”是四位元組對齊,“long”在32位和64位系統上分别是四位元組或八位元組對齊。

請注意,這些保證被引入了C11标準,是以請在使用C11标準之前的編譯器時注意(例如gcc 4.6)。 C11标準的第3.14節包含此保證的内容,章節名稱是“memory location存儲單元”,如下所示:

存儲單元

存儲單元的定義:對于标量類型的對象,或者相鄰位字段(具有非零寬度)的最大序列

注1:存儲單元的特性:兩個執行線程可以更新和通路獨立的存儲單元,不會産生互相的幹擾。

注2:一個位域和相鄰的非位域成員将位于分開的存儲單元中。

這同樣适用于兩個位域,

如果一個在嵌套的結構中聲明,而另一個不在,

或者兩個位域被一個零長度的位域聲明分隔開,

或者它們被一個非位域的成員分開。

在一個資料結構中如果兩個位字段之間聲明的所有成員也是位字段,則同時更新這兩個位字段是不安全的,無論中間這些位字段的大小是多少。

例如一個資料結構聲明如下

struct {

char a;

int b:5, c:11, :0, d:8;

struct { int ee:8; } e;

}

該資料結構包含四個獨立的存儲單元:成員a、位字段d和e.ee分别位于各自獨立的存儲單元,他們可以并發的修改而不會彼此幹擾。

位域b和c一起構成第四個存儲單元。位域b和c不能并發修改,但b和a可以并發修改。

5 什麼是記憶體屏障?

正如上文所說,無關的記憶體操作實際是以随機順序執行的,但這對于CPU-CPU互動或者CPU與I/O互動可能是一個問題。

需要一些手段來幹預編譯器和CPU,使其限制指令順序。

記憶體屏障就是這樣的幹預手段。 他們保證處于屏障兩側的記憶體操作滿足部分有序。

(部分有序的:記憶體屏障之前的操作都會先于屏障之後的操作,

但是如果幾個操作出現在屏障的同一邊, 則不保證它們的順序. 這一點下文将多次提到)

這樣的幹預是非常重要的,因為系統中的CPU和其他裝置可以使用各種各樣的優化政策來提高性能,包括記憶體操作重新排序,

延遲和記憶體操作的合并執行; 預取、分支預測和各種類型的緩存。

記憶體屏障用于禁止或抑制這些政策,使代碼正确的控制多個CPU或CPU與裝置的互動。

5.1 記憶體屏障的種類

記憶體屏障有四個基本種類

(1) STORE記憶體屏障

寫記憶體屏障提供這樣的保證: 所有出現在屏障之前的STORE操作都将先于所有出現在屏障之後的STORE操作被系統中的其他元件所感覺.

寫屏障僅保證針對STORE操作的部分有序; 不要求對LOAD操作沒有任何影響。

CPU可以被視為一個随着時間的推移向存儲系統送出一系列STORE操作的裝置。在寫記憶體屏障之前的所有STORE

操作将出現在寫入屏障之後的所有STORE操作之前。

[!]請注意,寫記憶體屏障通常應與讀記憶體屏障配對; 請參閱“SMP屏障配對”小節。

(2) 資料依賴屏障

8資料依賴屏障是讀屏障的弱化版本。 假設有兩個LOAD操作的場景,其中第二個LOAD操作依賴于第一個操作的結果

(例如:第一個LOAD擷取位址,而第二個LOAD使用該位址去取資料),這時候需要資料依賴屏障

確定第一個LOAD獲得的位址被用于通路之前,第二個LOAD的目标被更新。

資料依賴屏障僅對互相依賴的LOAD操作産生部分排序; 不對STORE操作、獨立LOAD操作或重疊的LOAD操作産生影響。

如(1)中所述,系統中的CPU可以感覺到其他CPU送出到存儲器系統的STORE操作序列。

而在該CPU上觸發的資料依賴屏障将保證, 對于在屏障之前發生的LOAD操作,

如果這個LOAD操作的目标被其他CPU的STORE操作所修改,那麼在屏障完成的時候,

這個LOAD操作之前的所有STORE操作所産生的影響,将被資料依賴屏障之後執行的任何LOAD操作所感覺.

有關排序限制的圖表,請參見:“記憶體屏障序列示例”。

請注意,第一個LOAD實際上必須具有資料依賴關系,而不是控制依賴。

如果第二個LOAD的位址依賴于第一個LOAD,但是依賴關系是通過一個條件語句而不是實際加載位址本身,

那麼它是一個控制依賴關系,最好需要一個完整的讀屏障。 有關詳細資訊,請參閱“控制依賴關系”小節。

[!] 請注意,資料依賴障礙通常應與寫入障礙配對; 請參閱“SMP障礙配對”小節。

(3) LOAD記憶體屏障

讀屏障包含資料依賴屏障的功能, 并且保證所有出現在屏障之前的LOAD操作都将先于所有出現在屏障之後的LOAD操作被系統中的其他元件所感覺.

讀屏障僅保證針對LOAD操作的部分有序; 不要求對STORE操作産生影響.

讀記憶體屏障隐含了資料依賴屏障, 是以可以用于替代資料依賴屏障.

[!] 注意, 讀屏障一般要跟寫屏障配對使用; 參閱”SMP記憶體屏障的配對使用”章節.

(4)通用記憶體屏障.

通用記憶體屏障保證所有出現在屏障之前的LOAD和STORE操作都将先于所有出現在屏障

之後的LOAD和STORE操作被系統中的其他元件所感覺.

通用記憶體屏障是針對LOAD和STORE操作的部分有序.

通用記憶體屏障隐含了讀屏障和寫屏障, 是以可以用于替代它們.

記憶體屏障還有兩種隐式類型:

(5) ACQUIRE操作

這是一個單向的可滲透的屏障。它保證所有出現在ACQUIRE之後的記憶體操作都将在

ACQUIRE操作被系統中的其他元件所感覺之後才能發生.ACQUIRE包括LOCK操作、

smp_load_acquire()和smp_cond_acquire()操作。後來的版本ACQUIRE語義包括控制依賴和smp_rmb()。

出現在ACQUIRE之前的記憶體操作可能在ACQUIRE之後才發生

ACQUIRE操作應該總是跟RELEASE操作成對出現的。

(6) RELEASE操作

這是一個單向的可滲透的屏障。它保證所有出現在RELEASE之前的記憶體操作都将在

RELEASE操作被系統中的其他元件所感覺之前發生.RELEASE操作包括UNLOCK操作和smp_store_release()操作。

出現在RELEASE之後的記憶體操作可能在RELEASE完成之前就發生了.
 使用ACQUIRE和RELEASE操作通常不需要其他種類的記憶體屏障(但請注意“MMIO寫屏障”一節中提到的例外情況)。
 此外,RELEASE + ACQUIRE對不能保證能替代完整的記憶體屏障。
 然而,在ACQUIRE後的給定的變量,ACQUIRE之前的任何RELEASE之前的該變量的所有存儲器通路都保證是可見的。
 換句話說,在給定變量的關鍵部分中,該變量的所有先前關鍵部分的所有通路都将保證已完成。
 這意味着ACQUIRE操作是一個最小的“擷取”操作(擷取之前釋出的記憶體通路狀态),
 RELEASE操作時一個最小的“釋出”操作(釋出目前記憶體狀态)。
           

在atomic_ops.txt中描述的原子操作的子集除了完全有序和自由排序(無屏障語義)之外還有ACQUIRE和RELEASE變體。

對于複合的原子操作LOAD和STORE,ACQUIRE語義僅應用于LOAD,RELEASE語義僅應用于操作的STORE部分。

隻有在存在多CPU互動或CPU與裝置互動的情況下才可能需要用到記憶體屏障。

如果可以確定某段代碼中不存在這樣的互動,那麼這段代碼就不需要記憶體屏障。

注意:對于前邊提到的都是最低限度的保證,不同的體系結構可能提供更多的保證,

但是在特定體系結構的代碼之外,不能依賴于這些額外的保證。

5.2 關于記憶體屏障, 不能保證什麼?

Linux核心的記憶體屏障不保證下面這些事情:

(*) 不能保證記憶體屏障之前出現的任何記憶體通路都會在記憶體屏障指令之前完成。

記憶體屏障相當于在該CPU的通路隊列中畫一條線, 使得相關訪存類型的請求不能跨越記憶體屏障。

(*) 不保證在一個CPU上執行的記憶體屏障會對其他系統中的CPU或硬體裝置産生任何直接影響。

間接影響就是第二個CPU感覺到第一個CPU通路記憶體的順序,不過請看下一點:

(*) 不能保證CPU能夠觀察到第二個CPU的通路記憶體的正确順序,即使第二個CPU使用記憶體屏障,

除非第一個CPU也使用了與之比對的記憶體屏障(參閱”SMP記憶體屏障的配對使用”部分)

(*) 不能保證一些CPU外硬體不會對記憶體通路重新排序。CPU cache一緻性機制會在CPU間傳播記憶體屏障所帶來的間接影響,

但是可能不是按照原順序的。

[*]更多關于總線主要DMA和一緻性的問題請參閱:

Documentation/PCI/pci.txt

Documentation/DMA-API-HOWTO.txt

Documentation/DMA-API.txt

5.3 資料依賴屏障

資料依賴屏障的使用要求有點微妙, 并不總是很明顯就能看出是否需要他們。

為了說明這點,考慮如下的操作隊列:

CPU 1             CPU 2
===============       ===============
{ A == 1, B == 2, C == 3, P == &A, Q == &C }
B = 4;
<write barrier>
WRITE_ONCE(P, &B)
              Q = READ_ONCE(P);
              D = *Q;
           

這裡有明顯的資料依賴, 在序列執行完之後,Q的值一定是&A和&B之一,執行結果可能是:

(Q == &A) implies (D == 1)

(Q == &B) implies (D == 4)

但是! CPU 2可能在看的P被更新之後, 才看到B被更新, 這就導緻下面的情況:

(Q == &B) and (D == 2) ????

雖然這看起來似乎是一緻性錯誤或邏輯關系錯誤,但其實不是,這種現象可以在特定的cpu上觀察到(比如DEC Alpha)。

為了解決這個問題, 必須在取位址和取資料之間插入一個資料依賴或更強的屏障:

CPU 1 CPU 2

=============== ===============

{ A == 1, B == 2, C == 3, P == &A, Q == &C }

B = 4;

WRITE_ONCE(P, &B);

Q = READ_ONCE(P);

D = *Q;

這就将執行結果強制為前兩種結果之一,避免了第三種結果的産生。

資料依賴性屏障還必須對依賴寫入進行順序操作

CPU 1 CPU 2

=============== ===============

{ A == 1, B == 2, C = 3, P == &A, Q == &C }

B = 4;

WRITE_ONCE(P, &B);

Q = READ_ONCE(P);

*Q = 5;

資料依賴性屏障必須将讀入Q值和寫入*Q順序操作。 這樣避免了以下結果:

(Q == &B) && (B == 4)

請注意,這種模式應該很少見。畢竟,依賴排序的主要目的是防止産生對資料結構的寫入,以及這些寫入導緻的高速緩存未命中的昂貴開銷。

該模式可用于記錄罕見的錯誤條件等,并且排序可以防止這些記錄丢失。????????????????

[!] 注意, 這種非常違反直覺的情況最容易出現在獨立cache的機器上,

例如一個高速緩存處理偶數編号的cache行,而另一個處理奇數編号的cache行。

P指針可能存儲在奇數号的cache行中, 而B的值可能存儲在偶數号的cache行中。

這樣一來,如果正在進行讀取操作的CPU的偶數編号cache組非常繁忙,而奇數編号的cache組空閑。

則CPU可以看到指針P是新值(&B),但看的的變量B還是舊值(2),

資料依賴屏障對于RCU系統非常重要,

舉例來說請參閱include/linux/rcupdate.h檔案中的rcu_assign_pointer()和rcu_dereference()函數.

這個函數使得目前RCU指針指向的對象被替換成新的對象時, 不會發生新對象尚未初始化完成的情況.

更詳盡的例子請參見“Cache一緻性”章節。

5.4 控制依賴

控制依賴可能有點棘手,因為目前的編譯器不了解它們。本節的目的是幫助您預防編譯器的無知破壞你的代碼。

為了使LOAD-LOAD控制依賴正确工作,需要完整的讀記憶體屏障,而不僅僅是一個資料依賴障礙。

考慮以下代碼:

q = READ_ONCE(a);

if (q) {

p = READ_ONCE(b);

}

這段代碼可能達不到預期的效果因為這裡其實并不是資料依賴, 而是控制依賴,CPU可能

試圖通過提前預測結果而對”if (p)”進行短路,其他cpu也可以看到b的LOAD發生在a的load之前。

在這樣的情況下, 需要的是:

q = READ_ONCE(a);

if (q) {

p = READ_ONCE(b);

}

然而,對于STORE操作不能這麼認為。這意味着針對LOAD-STORE控制依賴關系提供了排序,如下例所示:

q = READ_ONCE(a);

if (q) {

WRITE_ONCE(b, 1);

}

控制依賴關系通常與其他類型的屏障配對。也就是說,請注意,READ_ONCE()和WRITE_ONCE()都不是可選的

沒有READ_ONCE(),編譯器可能将’a’的LOAD與其他LOAD操作合并。

沒有WRITE_ONCE(),編譯器可能将‘b’STORE與其他STORE操作合并。

這可能會對排序的特别違反直覺的影響。

更糟糕的是,如果編譯器能夠證明變量’a’的值總是非零值,

編譯器将在它的權利範圍内通過删除“if”條件判斷語句對原示例進行優化,結果如下:

q = a;

b = 1;

是以不要丢棄READ_ONCE()。

在“if”語句的兩個分支上執行相同STORE操作進行強制排序是非常誘人的。代碼如下:

q = READ_ONCE(a);

if (q) {

barrier();

WRITE_ONCE(b, 1);

do_something();

} else {

barrier();

WRITE_ONCE(b, 1);

do_something_else();

}

不幸的是,現在的編譯器會在高優化等級的時候進行如下優化:

q = READ_ONCE(a);

barrier();

WRITE_ONCE(b, 1);

if (q) {

do_something();

} else {

do_something_else();

}

現在從LOAD“A”和STORE“b”之間沒有條件語句,這意味着CPU有權限對他們進行重新排序:

條件語句是絕對必需的,即使在使用所有編譯器優化之後,它也必須存在于彙編代碼中。

是以,如果在本例中需要固定排序,則需要顯式的記憶體障礙,例如smp_store_release()

q = READ_ONCE(a);

if (q) {

smp_store_release(&b, 1);

do_something();

} else {

smp_store_release(&b, 1);

do_something_else();

}

相比之下,如果沒有明确的記憶體屏障,隻有當條件語句的兩條腿中的STORE操作不同時,

控制排序才能有效。例如:

q = READ_ONCE(a);

if (q) {

WRITE_ONCE(b, 1);

do_something();

} else {

WRITE_ONCE(b, 2);

do_something_else();

}

例子中的READ_ONCE()仍然是必要的,以防止編譯器計算’a’的值

另外,你需要注意的是用局部變量’q’做了什麼操作,否則編譯器可能能夠預測該值并再次删除所需的條件。例如:

q = READ_ONCE(a);

if (q % MAX) {

WRITE_ONCE(b, 1);

do_something();

} else {

WRITE_ONCE(b, 2);

do_something_else();

}

如果MAX被定義為1,則編譯器知道(q%MAX)等于零,在這種情況下,編譯器将在它的權利範圍将上述代碼轉換為以下代碼:

q = READ_ONCE(a);

WRITE_ONCE(b, 1);

do_something_else();

這頁,CPU就不再需要保證LOAD’a’和STORE’b’的順序。

添加一個barrier()是很有吸引力的,但這沒有幫助。

條件語句沒了,控制屏障不會再回來了。

是以,如果你需要這個執行順序,你應該確定MAX大于1,如下:

q = READ_ONCE(a);

BUILD_BUG_ON(MAX <= 1);

if (q % MAX) {

WRITE_ONCE(b, 1);

do_something();

} else {

WRITE_ONCE(b, 2);

do_something_else();

}

請再次注意,STORE“b”的兩個參數不同。如果他們相同的,正像前面提到的,

編譯器可以将這個STORE操作移動到if語句外。

你還必須小心,不要太多依賴于布爾短路評估(或運算時隻有第一個條件為假時才會計算第二個條件)

考慮如下例子:

q = READ_ONCE(a);

if (q || 1 > 0)

WRITE_ONCE(b, 1);

因為第一個條件不能錯誤,第二個條件總是為真,編譯器可以将此示例轉換為以下内容:

q = READ_ONCE(a);

WRITE_ONCE(b, 1);

此示例強調了確定編譯器無法猜測您的代碼的需要。????????????????????

更一般來說,雖然READ_ONCE()強制編譯器執行給定的LOAD代碼,但并不強制編譯器使用該執行結果。

另外,控制依賴隻适用于所讨論的if語句的then分支和else分支。

特别地,控制依賴不一定适用于if語句後面的代碼。

q = READ_ONCE(a);

if (q) {

WRITE_ONCE(b, 1);

} else {

WRITE_ONCE(b, 2);

}

WRITE_ONCE(c, 1);

人們很容易認為這個代碼實際上是有序的,因為編譯器不能對volatile修飾的操作(READ_ONCE、WRITE_ONCE操作)重新排序

也不能對條件語句中的WRITE操作排序。

不幸的是,對于這種推理,編譯器可能将兩個寫入“b”編譯為條件移動指令,就像在這個奇怪的僞彙編代碼:

ld r1,a

cmp r1, 0cmov,ner4, 1

cmov,eq r4, 2str4,bst 1,c

一個弱排序的CPU認為STORE’a’和LOAD’c’之間沒有任何依賴關系。

控制依賴關系隻會展開成一對cmov指令和依賴這兩個指令的存儲操作。

簡而言之,控制依賴僅适用于所讨論的if語句的then分支和else分支中的STORE操作(包括這兩個分支所包含的函數調用),

但不包括if語句後面的代碼。

最後,控制依賴不提供傳遞性。這通過兩個相關示例來證明,初始值“x”和“y”都為零:

CPU 0 CPU 1

======================= =======================

r1 = READ_ONCE(x); r2 = READ_ONCE(y);

if (r1 > 0) if (r2 > 0)

WRITE_ONCE(y, 1); WRITE_ONCE(x, 1);

assert(!(r1 == 1 && r2 == 1));
           

上述包含兩個CPU的示例将永遠不會觸發斷言。

但是,如果控制依賴性保證了傳輸性(它們沒有),則添加以下CPU及執行代碼将保證觸發斷言。

CPU 2

=====================

WRITE_ONCE(x, 2);

assert(!(r1 == 2 && r2 == 1 && x == 2)); /* FAILS!!! */
           

但是由于控制依賴關系不能提供傳輸性,是以在包含三個CPU的示例完成後,上述斷言可能會失敗。

如果需要在三CPU的例子中來保證順序,那麼需要在CPU 0和CPU 1代碼片段中的LOAD和STORE之間(if語句之前或之後)增加smp_mb()

此外,原來的兩CPU執行個體非常脆弱,應該避免這樣。

These two examples are the LB and WWC litmus tests from this paper:

http://www.cl.cam.ac.uk/users/pes20/ppc-supplemental/test6.pdf and this

site: https://www.cl.cam.ac.uk/~pes20/ppcmem/index.html.

這兩個例子來自于http://www.cl.cam.ac.uk/users/pes20/ppc-supplemental/test6.pdf和https://www.cl.cam.ac.uk/~pes20/ppcmem/index.html

網站的litmus測試程式的LB和WWC測試例程。

In summary:

綜上所述:

(*)控制依賴可以對LOAD-STORE順序操作進行排序。然而控制依賴不保證其他種類的操作按照順序執行:

不保證LOAD-LOAD操作,也不保證先STORE與後來的任何操作的執行順序。

如果您需要這些其他形式的順序保證,請使用smp_rmb(),smp_wmb(),或者在先STORE和後LOAD的情況下使用smp_mb()

(*) 如果“if”語句的兩條分支以同一變量的相同STORE開始,那麼必須在STORE前面增加的smp_mb()或smp_store_release()來保證STORE順序。

請注意,在“if”語句的每個分支的開始處使用barrier()是不夠的,

因為上邊的例子說明,優化編譯器可以在遵守barrier()規定的情況下破壞控制依賴關系。

(*) 控制依賴關系要求在前邊的LOAD和後續的STORE之間至少有一個執行時的條件語句,而這個條件語句必須與前面的LOAD有關聯。

如果編譯器能夠優化條件語句,那麼它也将優化代碼順序。 使用READ_ONCE()和WRITE_ONCE()可以幫助程式保留所需的條件語句。

(*)使用控制依賴性需要避免編譯器重新排序導緻依賴關系不存在。

小心的使用 READ_ONCE()和 atomic_read()可以保護控制依賴關系。

有關的更多資訊,請參閱編譯屏障章節。

(*)控制依賴僅适用于包含控件依賴關系的if語句的then分支和else分支(包括這兩個分支所包含的函數調用)。

控制依賴關系不适用于包含控件依賴關系的if語句之後的代碼

(*)控制依賴關系通常與其他類型的屏障配對使用。

(*)控制依賴不提供傳遞性。 如果需要傳遞性,請使用smp_mb()。

(*) 編譯器不了解控制依賴。 是以,您的工作是確定編譯器不會破壞您的代碼。

5.5 SMP 屏障配對使用

處理CPU-CPU互動時,某些類型的記憶體屏障應該始終配對使用。 缺乏适當的配對使用基本上可以肯定是錯誤的。

盡管沒有傳遞性,一般屏障應該配對使用,雖然他們與其他類型的屏障也能配對。

acquire屏障與release屏障配對,但是他們又都能與其他類型的屏障配對(當然包括通用屏障)。

write屏障可以與資料依賴屏障、控制依賴屏障、acquire屏障、release屏障、read屏障或者通用屏障配對。

同樣的read屏障、控制依賴屏障或資料依賴屏障與write屏障、acquire屏障、release屏障或者通用屏障配對。

CPU 1 CPU 2

=============== ===============

WRITE_ONCE(a, 1);

WRITE_ONCE(b, 2); x = READ_ONCE(b);

y = READ_ONCE(a);

Or:

CPU 1             CPU 2
===============       ===============================
a = 1;
<write barrier>
WRITE_ONCE(b, &a);    x = READ_ONCE(b);
              <data dependency barrier>
              y = *x;
           

Or even:

CPU 1             CPU 2
===============       ===============================
r1 = READ_ONCE(y);
<general barrier>
WRITE_ONCE(y, 1);     if (r2 = READ_ONCE(x)) {
                 <implicit control dependency>
                 WRITE_ONCE(y, 1);
              }

assert(r1 == 0 || r2 == 0);
           

基本上,read屏障總是必須存在,盡管它可能是“較弱”的類型。

[!]注意,在write屏障之前出現的STORE操作通常總是期望比對讀屏障或資料依賴屏障之後出現的LOAD操作,反之亦然:

CPU 1 CPU 2

=================== ===================

WRITE_ONCE(a, 1); }—- —>{ v = READ_ONCE(c);

WRITE_ONCE(b, 2); } \ / { w = READ_ONCE(d);

\

WRITE_ONCE(c, 3); } / \ { x = READ_ONCE(a);

WRITE_ONCE(d, 4); }—- —>{ y = READ_ONCE(b);

5.6 記憶體屏障舉例

第一,write屏障用作将STORE操作部分有序。請考慮以下操作順序:

CPU 1

=======================

STORE A = 1

STORE B = 2

STORE C = 3

STORE D = 4

STORE E = 5

這個操作序列會按照順序被送出到記憶體一緻性系統,而系統中的其他元件可以看到

{STORE A,STORE B,STORE C}集合都發生在{STORE D,STORE E}集合之前,而集合内部可能亂序。

第二,資料依賴屏障對有資料依賴關系的LOAD操作進行部分有序的限制。 考慮以下事件序列:

CPU 1 CPU 2

======================= =======================

{ B = 7; X = 9; Y = 8; C = &Y }

STORE A = 1

STORE B = 2

STORE C = &B LOAD X

STORE D = 4 LOAD C (gets &B)

LOAD *C (reads B)

沒有幹預的話, CPU 1的操作被CPU 2感覺到的順序是随機的, 盡管CPU 1執行了寫屏障:

在上面的例子中, CPU 2看到的B的值是7, 盡管對LOAD*C(值應該是B)發生在LOAD C之

後.

但是,如果在CPU 2的LOAD C 和LOAD* C(即:B)之間放置資料依賴障礙的話:

CPU 1 CPU 2

======================= =======================

{ B = 7; X = 9; Y = 8; C = &Y }

STORE A = 1

STORE B = 2

STORE C = &B LOAD X

STORE D = 4 LOAD C (gets &B)

LOAD *C (reads B)

那麼下面的情況将會發生:

第三,讀取屏障用作LOAD上的部分順序。考慮如下事件序列:

CPU 1           CPU 2
======================= =======================
    { A = 0, B = 9 }
STORE A=1
<write barrier>
STORE B=2
            LOAD B
            LOAD A
           

沒有幹預的話, CPU 1的操作被CPU 2感覺到的順序是随機的, 盡管CPU 1使用了寫屏障:

但是, 如果在CPU 2的LOAD B和LOAD A之間增加一個讀屏障:

CPU 1 CPU 2

======================= =======================

{ A = 0, B = 9 }

STORE A=1

STORE B=2

LOAD B

LOAD A

那麼CPU 1的部分有序将正确的被CPU 2所感覺:

為了更全面地說明這一點, 考慮一下如果代碼在讀屏障的兩邊都有一個LOAD A的話, 會發生

什麼:

CPU 1 CPU 2

======================= =======================

{ A = 0, B = 9 }

STORE A=1

STORE B=2

LOAD B

LOAD A [first load of A]

LOAD A [second load of A]

盡管兩次LOAD A都發生在LOAD B之後, 它們也可能得到不同的值:

但是也可能CPU 2在讀屏障結束之前就感覺到CPU 1對A的更新:

這裡保證, 如果LOAD B得到的值是2的話, 第二個LOAD A總是能得到的值是1.

但是對于第一個LOAD A的值是沒有保證的,可能得到的值是0或者1.

5.7 讀記憶體屏障與記憶體預取

許多CPU會對LOAD操作進行預取: 那就是CPU發現它可能需要從記憶體中LOAD一個資料,

同時CPU尋找一個不需要使用總線進行其他LOAD操作的時機,來進行這個LOAD操作

(雖然CPU的指令執行流程還沒有執行到該LOAD指令)。

這可能使得某些LOAD指令執行時會立即完成,因為CPU已經預取到了所需要LOAD的值。

可能會出現因為一個分支語句導緻CPU實際上并不需要執行該LOAD語句,在這種情況下CPU可以丢棄該值或者緩存該值供以後使用。

Consider:

考慮如下場景:

CPU 1 CPU 2

======================= =======================

LOAD B

DIVIDE } 除法指令通常消耗

DIVIDE } 很長的執行時間

LOAD A

Which might appear as this:

這可能将表現為如下情況:

如果在第二個LOAD之前放一個讀屏障或資料依賴屏障:

CPU 1           CPU 2
======================= =======================
            LOAD B
            DIVIDE
            DIVIDE
            <read barrier>
            LOAD A
           

這将迫使CPU對所推測的任何值進行更新檢查,這取決于所使用的障礙物的類型。

如果沒有對推測的記憶體位置進行更改,那麼隻會使用推測值:

但是如果有其他CPU更新或者删除該值,則記憶體預取将失效,CPU重新加載該值:

5.8 傳遞性

傳遞性是對真實計算機系統并不總是提供的排序的深刻直覺的概念。 以下示例示範了傳遞性:

CPU 1           CPU 2           CPU 3
======================= ======================= =======================
    { X = 0, Y = 0 }
STORE X=1       LOAD X          STORE Y=1
            <general barrier>   <general barrier>
            LOAD Y          LOAD X
           

假設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 X之後,是以肯定期望CPU 3的LOAD X傳回1.

這個很自然的期望就是傳遞性的一個例子:如果在CPU A上執行的LOAD操作在CPU B上執行相同變量的LOAD操作之後,

則CPU A的LOAD必須傳回與CPU B的LOAD相同的值,或者必須傳回一些更新的值。

在Linux核心中,使用通用記憶體障礙保證傳遞性。

是以,在上述示例中,如果CPU 2 LOAD X傳回1,并且LOAD Y傳回0,那麼CPU 3從LOAD X肯定傳回1。

但是,讀寫障礙不能保證傳遞性。例如,将上述示例中的CPU 2的通用屏障替換為讀屏障,如下所示:

CPU 1 CPU 2 CPU 3

======================= ======================= =======================

{ X = 0, Y = 0 }

STORE X=1 LOAD X STORE Y=1

LOAD Y LOAD X

這種替換會破壞傳遞性:在這個例子中,

從CPU 2 LOAD X傳回1,并且LOAD Y傳回0,那麼CPU 3從LOAD X肯定傳回0是完全可能的。

關鍵是,雖然CPU 2的讀屏障指令對LOAD進行排序,但不能保證對CPU 1的STORE進行排序。

是以,如果此示例在CPU 1和2共享存儲緩沖區或進階緩存的系統上運作,則CPU 2可能會更早通路到CPU 1的寫入。

是以需要通用障礙,以確定所有CPU對CPU 1和CPU 2的通路的順序意見一緻。?????????????????

通用屏障提供“全局傳遞性”,是以所有的CPU都會就操作順序達成一緻。

相比之下,一組release-acquire僅提供“局部傳遞性”,是以隻有那些鍊上的CPU才能保證通路的組合順序一緻。

例如,切換到依賴于Herman Hollerith的C代碼:

int u, v, x, y, z;

void cpu0(void)
{
    r0 = smp_load_acquire(&x);
    WRITE_ONCE(u, 1);
    smp_store_release(&y, 1);
}

void cpu1(void)
{
    r1 = smp_load_acquire(&y);
    r4 = READ_ONCE(v);
    r5 = READ_ONCE(u);
    smp_store_release(&z, 1);
}

void cpu2(void)
{
    r2 = smp_load_acquire(&z);
    smp_store_release(&x, 1);
}

void cpu3(void)
{
    WRITE_ONCE(v, 1);
    smp_mb();
    r3 = READ_ONCE(u);
}
           

因為cpu0(),cpu1()和cpu2()參與smp_store_release()/ smp_load_acquire()的本地依賴鍊,

是以以下結果不可能發生:

r0 == 1 && r1 == 1 && r2 == 1

此外,由于cpu0()和cpu1()之間的依賴關系,

r1等于1時cpu1()肯定看到cpu0()的寫入,是以以下結果不可能發生:

r1 == 1 && r5 == 0

但是,release-acquisition的傳遞性隻涉及到參與release-acquisition操作的CPU,不适用于cpu3()。

是以,以下結果是可能發生:

r0 == 0 && r1 == 1 && r2 == 1 && r3 == 0 && r4 == 0

As an aside, the following outcome is also possible:

除此之外,以下結果也是可能的:

r0 == 0 && r1 == 1 && r2 == 1 && r3 == 0 && r4 == 0 && r5 == 1

雖然cpu0(),cpu1()和cpu2()将按順序看到它們各自的讀寫操作,

但是沒有涉及release-acquisition鍊的CPU在順序上可能不太符合要求。

這種分歧源于以下事實:

在所有情況下,用于實作smp_load_acquire()和smp_store_release()的

弱記憶體屏障指令不需要在先前存儲之後對後續加載進行排序。 ????????????????????????????????????不了解

這意味着cpu3()可以看到cpu0()的STORE u發生在cpu1的LOAD v後面,盡管cpu0()和cpu1()都認為這兩個操作按照預定的順序發生。

但是,請記住,smp_load_acquire()不是魔術。特别是,它隻是從其參數中讀取。

它不能確定任何特定的值被讀取。 是以,以下結果是可能的:

r0 == 0 && r1 == 0 && r2 == 0 && r5 == 0

請注意,這一結果甚至可能發生在一個神話般的順序一緻的系統,沒有任何重新排序。

To reiterate, if your code requires global transitivity, use general

barriers throughout.

要重申,如果您的代碼需要全局傳遞性,請在整個過程中使用一般障礙。

6 核心中的顯式記憶體屏障

The Linux kernel has a variety of different barriers that act at different

levels:

Linux核心具有各種各樣的屏障,在不同層次上起作用:

(*)編譯優化屏障

(*)CPU記憶體屏障

(*)MMIO寫屏障

6.1 編譯優化屏障

Linux核心有一個明确的編譯器屏障功能,可以防止編譯器将屏障任意一側的記憶體通路移動到另一側:

barrier();

這是通用的障礙 - 沒有read-read或write-write的屏障變體。

然而,READ_ONCE()和WRITE_ONCE()可以被認為是僅影響由READ_ONCE()或WRITE_ONCE()

标記的特定通路的barrier()的弱形式。

barrier()函數具有以下效果:

(*) 阻止編譯器将barrier()之後的通路重新排序到barrier()之前的任何通路之前。

這個性質的一個示例使用是簡化中斷處理程式代碼與被中斷代碼之間的通信。

(*)在一個循環中,強制編譯器加載該循環中使用的變量,每個周遊該循環的條件是有條件的。

有許多優化使單線程代碼可以安全執行而在多線程代碼中可能是緻命的,

READ_ONCE()和WRITE_ONCE()函數可以防止這些優化,

以下是這些優化的一些示例:

(*)編譯器在其權限内對加載和存儲重新排序到同一個變量,在某些情況下,CPU在其權限内可以将加載重新排序到同一個變量。

這意味着以下代碼:

a[0] = x;

a[1] = x;

可能導緻存儲在a[1]中的x值比存儲在a[0]中的x值要舊。預先編譯器和CPU都不執行如下操作:

a[0] = READ_ONCE(x);

a[1] = READ_ONCE(x);

簡而言之,READ_ONCE()和WRITE_ONCE()為從多個CPU到單個變量的通路提供高速緩存一緻性。

(*) 編譯器有權限合并來自相同變量的連續LOAD操作。 這種合并可能導緻編譯器“優化”以下代碼:

while (tmp = a)

do_something_with(tmp);

優化成以下代碼,盡管對于單線程代碼來說合法,但幾乎肯定不是開發者的意圖:

if (tmp = a)

for (;;)

do_something_with(tmp);

Use READ_ONCE() to prevent the compiler from doing this to you:
           

使用READ_ONCE()來防止編譯器這樣做:

while (tmp = READ_ONCE(a))

do_something_with(tmp);

(*) 編譯器有權限控制重新加載變量。

例如,在高寄存器壓力阻止編譯器将所有感興趣的資料儲存在寄存器中的情況下。

是以,編譯器可能會優化我們前面示例中的變量“tmp”:

while (tmp = a)

do_something_with(tmp);

這可能導緻以下代碼,這在單線程代碼中是完全安全的,但可能在并發代碼中是緻命的:

while (a)

do_something_with(a);

例如,在“while”語句和對do_something_with()的調用之間變量a被某個其他CPU修改的情況下,

該代碼的優化版本可能導緻将零傳遞給do_something_with()。

Again, use READ_ONCE() to prevent the compiler from doing this:

再次,使用READ_ONCE()來防止編譯器這樣做:

while (tmp = READ_ONCE(a))

do_something_with(tmp);

請注意,如果編譯器運作的寄存器不足,則可能會将tmp儲存到堆棧中。

這種節省和後期恢複的開銷是為什麼編譯器重新加載變量。

這樣做對于單線程代碼是完全安全的,是以您需要告訴編譯器有關不安全的情況。

(*)如果編譯器知道該值是什麼,則編譯器有權限完全省略LOAD。

例如,如果編譯器可以證明變量’a’的值始終為零,則可以優化此代碼:

while (tmp = a)

do_something_with(tmp);

優化成這樣:

do { } while (0);
           

這種轉換是單線程代碼的勝利,因為它擺脫了一個LOAD和一個分支語句。

問題是編譯器進行了假設,假設目前的CPU是唯一一個更新變量’a’。

如果變量’a’被共享,則編譯器的假設将是錯誤的。 使用READ_ONCE()來告訴編譯器它不知道它認為是多少:

while (tmp = READ_ONCE(a))

do_something_with(tmp);

但請注意,編譯器還會仔細觀察您使用READ_ONCE()之後的值。

例如,假設您執行以下操作,MAX是一個值為1的預處理器宏:

while ((tmp = READ_ONCE(a)) % MAX)

do_something_with(tmp);

然後,編譯器知道使用“%”運算符跟着MAX結果将始終為零,

這将再次允許編譯器将代碼優化。 (它仍将從變量’a’加載。)

(*) 類似地,如果編譯器知道變量已經具有存儲的值,則在編譯器有權限省略STORE操作。

同樣,編譯器假定目前的CPU是唯一STORE該變量的CPU,這可能導緻編譯器對共享變量做錯了事情。

例如,假設您有以下内容:

a = 0;

… Code that does not store to variable a …

沒有STORE a操作的代碼

a = 0;

編譯器看到變量’a’的值已經為零,是以可能會省略第二個STORE操作。

如果其他CPU可能同時STORE“a”,這将是一個緻命的錯誤。

使用WRITE_ONCE()來防止編譯器發生這種錯誤的猜測:

WRITE_ONCE(a, 0);

沒有STORE a操作的代碼

WRITE_ONCE(a, 0);

(*) 編譯器有權重新排序記憶體通路,除非你告訴它不應該這麼做。

例如,考慮過程級代碼和中斷處理程式之間的以下互動:

void process_level(void)

{

msg = get_message();

flag = true;

}

void interrupt_handler(void)
{
    if (flag)
        process_message(msg);
}
           

沒有什麼可以阻止編譯器将process_level()轉換為以下内容,實際上這可能是單線程代碼的勝利:

void process_level(void)

{

flag = true;

msg = get_message();

}

如果這兩個語句之間發生中斷,那麼interrupt_handler()可能會傳遞一個亂碼的msg。 使用WRITE_ONCE()來防止如下:

void process_level(void)

{

WRITE_ONCE(msg, get_message());

WRITE_ONCE(flag, true);

}

void interrupt_handler(void)
{
    if (READ_ONCE(flag))
        process_message(READ_ONCE(msg));
}
           

請注意,如果該中斷處理程式本身可以被通路“flag”和“msg”的中斷處理程式中斷,

例如嵌套中斷或NMI,則需要在interrupt_handler()中使用READ_ONCE()和WRITE_ONCE()。

否則,除了用于文檔的目的,interrupt_handler()中不需要使用READ_ONCE()和WRITE_ONCE()。

(另請注意,嵌套中斷通常不會在現代Linux核心中出現,實際上,如果中斷處理程式傳回中斷使能,

您将獲得一個WARN_ONCE()splat。)

您應該假設編譯器移動READ_ONCE()和WRITE_ONCE()代碼不能

越過包含READ_ONCE(),WRITE_ONCE(),barrier()或類似原語的代碼。

這種效果也可以使用barrier()實作,但READ_ONCE()和WRITE_ONCE()更具選擇性:

使用READ_ONCE()和WRITE_ONCE(),編譯器隻需要忘記指定的記憶體位置的内容,

而barrier() 編譯器必須丢棄其在任何機器寄存器中緩存的所有存儲器位置的值。

當然,編譯器也必須遵守READ_ONCE()和WRITE_ONCE()發生的順序,盡管CPU不需要這樣做。

(*) 編譯器有權産生STORE操作,如以下示例所示:

if (a)

b = a;

else

b = 42;

編譯器可以通過如下優化來節省一個分支:

b = 42;

if (a)

b = a;

在單線程代碼中,這不僅安全,而且還節省了一個分支。

不幸的是,在并發代碼中這種優化可能會導緻,當加載變量’b’時,即使變量’a’從不為零,一些其他CPU看到b等于值42。

使用WRITE_ONCE()來防止如下:

if (a)

WRITE_ONCE(b, a);

else

WRITE_ONCE(b, 42);

編譯器也可以産生LOAD操作。 這些通常不那麼有害,但是它們可能會導緻高速緩存行彈跳,

進而導緻性能和可擴充性的降低。 使用READ_ONCE()來防止發明的負載

(*) 對于對齊的存儲器位置,其尺寸允許通過單個存儲器引用指令通路它們,

防止“LOAD撕裂”和“STORE撕裂”,其中單個大記憶體塊的通路被多個較小的記憶體通路代替。

例如,給定一個具有7位立即字段的16位存儲指令的架構,

編譯器可能會試圖使用兩個16位存儲立即指令來實作以下32位存儲:

p = 0x00010002;

請注意,GCC真的使用這種優化,這并不奇怪,因為它可能需要兩個以上的指令來建構常量,然後存儲它。

是以,這種優化可以在單線程代碼中成功。 事實上,最近的錯誤(自從修複)導緻GCC錯誤地在易失性存儲中使用這種優化。

在沒有此類錯誤的情況下,使用WRITE_ONCE()可以防止在以下示例中出現STORE撕裂:

WRITE_ONCE(p, 0x00010002);

:

使用結構也可能導緻LOAD和STORE撕裂,如本例所示:

struct attribute((packed)) foo {

short a;

int b;

short c;

};

struct foo foo1, foo2;

foo2.a = foo1.a;
foo2.b = foo1.b;
foo2.c = foo1.c;
           

因為沒有使用READ_ONCE()或WRITE_ONCE()也沒有使用volatile标記,

編譯器在他的權限内用一對32位加載,後跟一對32位存儲來實作這三個指派語句的行為。

這将導緻對“b”的加載和存儲操作分為兩個指令。

在此示例中,READ_ONCE()和WRITE_ONCE()再次防止撕裂:

foo2.a = foo1.a;

WRITE_ONCE(foo2.b, READ_ONCE(foo1.b));

foo2.c = foo1.c;

除此之外,對已标記為volatile的變量,不必使用READ_ONCE()和WRITE_ONCE()。

例如,因為’jiffies’被标記為volatile,是以不需要說READ_ONCE(jiffies)。

這樣做的原因是READ_ONCE()和WRITE_ONCE()使用volatile cast來實作的,這樣對變量沒有影響。

Please note that these compiler barriers have no direct effect on the CPU,

which may then reorder things however it wishes.

請注意,這些編譯器屏障對CPU沒有直接的影響,然後它可能會重新排序。

6.2 CPU記憶體屏障

Linux核心有八個基本的CPU記憶體障礙:

類型 強制屏障 SMP環境生效指令

=============== ======================= ===========================

GENERAL mb() smp_mb()

WRITE wmb() smp_wmb()

READ rmb() smp_rmb()

除資料依賴屏障之外的所有記憶體屏障隐含了編譯器屏障。 資料依賴不會強加任何額外的編譯器排序。

另外:在有資料相關性的情況下,編譯器會将LOAD指令按正确的順序輸出

(例如, 在

a[b]

語句中, load b必須放在load a[b]之前),

但是在C規範中不保證編譯器不預測b的值(例如等于1)于是先load a再load b

(例如tmp = a [1]; if(b!= 1)tmp = a [ b];)。

編譯器在load a[b]之後又重新load b, 也可能會存在問題,因為b比a[b]更新。

對于這種一緻性問題,應該先使用READ_ONCE()宏。

在UP系統中, SMP記憶體屏障将退化成編譯器優化屏障, 因為它假定CPU能夠保證自身的一緻性

, 并能以正确的順序處理重疊的記憶體通路.

但是,請參閱下面的“虛拟機訪客”一節。

[!] 請注意,SMP記憶體障礙必須用于控制在SMP系統上引用共享記憶體的順序,盡管使用鎖定是足夠的。

強制屏障不應該用于控制SMP的影響,因為強制性障礙可能對SMP和UP系統造成不必要的開銷。

然而,使用MMIO來通路松散屬性的IO記憶體視窗時, 強制屏障可以用來控制這些訪存的影響。

強制屏障即使在非SMP系統上也可能需要,因為它們通過禁止編譯器和CPU重新排序來影響裝置

感覺到的對存儲器操作的順序。

還有一些更進階的屏障函數

(*) smp_store_mb(var, value)

這将value指派給var變量,然後插入一個完整的記憶體屏障。 在UP編譯中不能保證會插入編譯優化屏障以外其他東西。

(*) smp_mb__before_atomic();

(*) smp_mb__after_atomic();

這些用于原子操作(如加,減,遞增和遞減)不傳回值的函數,特别是用于引用計數時。

這些函數并不意味着記憶體障礙。

也用于不傳回值的原子位操作函數(如set_bit和clear_bit)。

例如,考慮将一個對象标記為死亡的代碼,然後減少對象的引用計數:

obj->dead = 1;

smp_mb__before_atomic();

atomic_dec(&obj->ref_count);

這確定在感覺到引用計數器遞減之前感覺到對象上的死亡标記被設定。

有關詳細資訊,請參閱Documentation / atomic_ops.txt。 有關在哪裡使用這些資訊,請參閱“原子操作”小節。

(*) lockless_dereference();

這可以被認為是圍繞smp_read_barrier_depends()資料依賴性屏障的指針擷取包裝器。

這也類似于rcu_dereference(),但是在對象生命周期由除RCU之外的其他機制處理的情況下,

例如,僅當系統關閉時才删除對象。

另外,在一些資料結構中使用了lockless_dereference(),可以使用和不使用RCU。

(*) dma_wmb();

(*) dma_rmb();

這些用于一緻的記憶體,以確定CPU和具有DMA能力的裝置可通路的共享記憶體的寫入或讀取順序。

例如,考慮與裝置共享記憶體的裝置驅動程式,并使用描述符狀态值來訓示描述符是否屬于裝置或CPU,

以及當新的描述符可用時的門鈴機制來通知它:

if (desc->status != DEVICE_OWN) {

dma_rmb();

/* read/modify data */
    read_data = desc->data;
    desc->data = write_data;

    /* flush modifications before status update */
    dma_wmb();

    /* assign ownership */
    desc->status = DEVICE_OWN;

    /* force memory to sync before notifying device via MMIO */
    wmb();

    /* notify device of new descriptors */
    writel(DESC_NOTIFY, doorbell);
}
           

在我們從描述符讀取資料之前,dma_rmb()允許我們保證裝置已經釋放了所有權,

并且dma_wmb()允許我們保證将資料寫入描述符,然後裝置可以看到它現在擁有所有權。

在嘗試寫入高速緩存不相幹的MMIO區域之前,需要wmb()來保證高速緩存一緻存儲器寫入已經完成。

有關一緻記憶體的更多資訊,請參閱文檔/ DMA-API.txt。

6.3 MMIO寫屏障

對于記憶體映射IO的寫操作, Linux核心還有一個特别的屏障:

mmiowb();

這是強制寫入障礙的一個變化,導緻對弱有序I / O區域的寫入被部分排序。

其影響可能超出CPU->硬體接口,實際上會在某種程度上影響硬體。

有關詳細資訊,請參閱“擷取與IO通路”章節。

7 核心中隐式的記憶體屏障

linux核心中的其他一些函數意味着記憶體屏障,其中包括鎖函數和排程函數。

本規範是最低保證; 任何特定的架構可以提供更實質的保證,但是在特定體

系結構的代碼之外, 不能依賴于這些額外保證.

7.1 鎖定擷取函數

Linux核心有很多鎖結構:

(*)自旋鎖

(*)讀寫自旋鎖

(*)互斥體

(*)信号量

(*)讀寫信号量

在所有情況下, 它們都是”ACQUIRE”操作和”RELEASE”操作的變種. 這些操作都隐含一定的屏障:

(1) ACQUIRE操作所隐含的操作:

ACQUIRE之後發出的記憶體操作将在ACQUIRE操作完成後完成。

ACQUIRE之前發出的記憶體操作,可能會在ACQUIRE操作完成後完成。

将smp_mb__before_spinlock()與以下ACQUIRE操作相結合,可以根據對先STORE與随後的LOAD和STORE進行排序。

請注意,這比smp_mb()更弱! 許多架構上的smp_mb__before_spinlock()原語都是不受限制的。

(2) RELEASE操作所隐含的

在RELEASE操作之前出現的記憶體操作, 一定在RELEASE操作完成之前完成.

而在RELEASE操作之後出現的記憶體操作, 可能在RELEASE操作完成之前就完成了.

(3) ACQUIRE操作+ACQUIRE操作所隐含的:

在某個ACQUIRE操作之前出現的所有ACQUIRE操作都将在後面這個ACQUIRE之前完成.

(4) ACQUIRE操作+RELEASE操作所隐含的:

在RELEASE操作之前出現的所有ACQUIRE操作都将在這個RELEASE之前完成.

(5) ACQUIRE失敗所隐含的

某些變種的ACQUIRE操作可能會失敗,比如可能由于無法立即獲得鎖,或者由于在睡眠等待鎖可用時收到未阻塞的信号。

失敗的鎖操作不隐含任何屏障.

[!] 注意:鎖ACQUIRE和RELEASE隻是單向屏障,其結果是, 臨界區之外的指令可能會在臨界區中産生影響.

一個ACQUIRE後跟RELEASE可能不會被認為是完全的記憶體障礙,因為在ACQUIRE之前的通路可能發生在

ACQUIRE之後,以及RELEASE之後的通路可能發生在RELEASE之前,并且這兩次通路可以互相交叉

*A = a;

ACQUIRE M

RELEASE M

*B = b;

可能表現為:

ACQUIRE M, STORE *B, STORE *A, RELEASE M

當ACQUIRE和RELEASE分别是鎖定擷取和釋放時,如果鎖的ACQUIRE和RELEASE是相同的鎖定變量,

但是僅從不具有該鎖的另一個CPU的角度來看,則可能會發生相同的重新排序。

簡而言之,接下來是RELEASE的ACQUIRE可能不被認為是一個完整的記憶體障礙。

類似地,RELEASE後跟ACQUIRE的情況并不意味着完整的記憶體屏障。

是以,CPU對與RELEASE和ACQUIRE相對應的關鍵部分的執行可能會交叉,以便:

*A = a;

RELEASE M

ACQUIRE N

*B = b;

可能表現為:

ACQUIRE N, STORE *B, STORE *A, RELEASE M

可能看來,這種重新排序可能會導緻死鎖。

但是,這不可能發生,因為如果有這樣的一個死鎖威脅,RELEASE就會完成,進而避免死鎖。

為什麼這個工作有用?

一個關鍵點是,我們隻是在談論CPU進行重新排序,而不是編譯器。 如果編譯器(或者開發者)切換操作,可能會發生死鎖。

但是假設CPU重新排序操作。 在這種情況下,解鎖先于彙編代碼中的鎖定。

CPU隻是選擇嘗試先執行以後的鎖定操作。

如果有一個死鎖,這個鎖定操作将簡單地自旋(或者嘗試睡覺,但稍後會更多)。

CPU将最終執行解鎖操作(在彙編代碼中的鎖定操作之前),這将解開潛在的死鎖,進而允許鎖定操作成功。

但是如果鎖是睡眠鎖怎麼辦? 在這種情況下,代碼将嘗試進入排程程式,最終将遇到記憶體屏障,這将迫使較早的解鎖操作完成,

再次解開死鎖。 可能會有一個睡眠解鎖的一類情況,但是鎖定原語需要在任何情況下正确地解決這樣的一類情況。

鎖和信号量可能不會對UP編譯系統提供任何排序的保證,

是以在這種情況下,根本不能指望實際執行任何操作,特别是在I / O通路方面,

除非與中斷禁用操作相結合。

另請參閱“Intel-CPU擷取屏障的影響”部分。

例如,考慮以下幾點:

*A = a;

*B = b;

ACQUIRE

*C = c;

*D = d;

RELEASE

*E = e;

*F = f;

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

ACQUIRE, {*F,*A}, *E, {*C,*D}, *B, RELEASE

[+] 注意, {*F,*A} 代表一次合并通路.

但是下面的執行順序都不可接受:

{*F,*A}, *B, ACQUIRE, *C, *D, RELEASE, *E

*A, *B, *C, ACQUIRE, *D, RELEASE, *E, *F

*A, *B, ACQUIRE, *C, RELEASE, *D, *E, *F

*B, ACQUIRE, *C, *D, RELEASE, {*F,*A}, *E

7.2 中斷函數

禁止中斷(類似于ACQUIRE)和啟用中斷(類似于RELEASE)的函數隻會起到編譯優化屏障的作用. 所

以, 如果在這種情況下需要使用記憶體或IO屏障, 必須采取其他手段.

7.3 睡眠喚醒函數

在全局資料中标記的事件上睡眠和喚醒可以被視為兩塊資料之間的互動:

等待事件的任務的任務狀态和用于訓示事件的全局資料。

為了確定這些似乎以正确的順序發生,開始進入睡眠過程的原語和啟動喚醒的原語隐含着某些屏障。

首先,睡眠者通常遵循這樣的事件序列:

for (;;) {

set_current_state(TASK_UNINTERRUPTIBLE);

if (event_indicated)

break;

schedule();

}

在更改任務狀态後,set_current_state()會自動插入通用記憶體屏障:

CPU 1

===============================

set_current_state();

smp_store_mb();

STORE current->state

LOAD event_indicated

set_current_state()可能被包裝在以下函數中:

prepare_to_wait();

prepare_to_wait_exclusive();

是以,這也意味着設定狀态之後的通用記憶障礙。以上的各個函數又被包

裝在其他一些函數中, 所有這些包裝函數都相當于在對應的位置插入了記憶體屏障:

wait_event();

wait_event_interruptible();

wait_event_interruptible_exclusive();

wait_event_interruptible_timeout();

wait_event_killable();

wait_event_timeout();

wait_on_bit();

wait_on_bit_lock();

其次,執行喚醒的代碼通常是這樣的:

event_indicated = 1;

wake_up(&event_wait_queue);

or:

event_indicated = 1;
wake_up_process(event_daemon);
           

類似wake_up()的函數會隐含一個寫記憶體屏障。 當且僅當它們的确喚醒了某個程序時。

屏障出現在程序的睡眠狀态被清除之前, 也就是在設定喚醒事件标記的STORE操作和将程序狀态

修改為TASK_RUNNING的STORE操作之間:

CPU 1 CPU 2

=============================== ===============================

set_current_state(); STORE event_indicated

smp_store_mb(); wake_up();

STORE current->state

STORE current->state

LOAD event_indicated

要重複的是,當且僅當事物被實際喚醒時,這種寫入記憶障礙才是存在的。

要看到這一點,請考慮以下事件序列,其中X和Y都初始為零:

CPU 1 CPU 2

=============================== ===============================

X = 1; STORE event_indicated

smp_mb(); wake_up();

Y = 1; wait_event(wq, Y == 1);

wake_up(); load from Y sees 1, no memory barrier

load from X might see 0

相反,如果确實發生了喚醒,則CPU 2從X的LOAD将被保證看到1。

可用的喚醒函數包括:

complete();

wake_up();

wake_up_all();

wake_up_bit();

wake_up_interruptible();

wake_up_interruptible_all();

wake_up_interruptible_nr();

wake_up_interruptible_poll();

wake_up_interruptible_sync();

wake_up_interruptible_sync_poll();

wake_up_locked();

wake_up_locked_poll();

wake_up_nr();

wake_up_poll();

wake_up_process();

[!]請注意,在睡眠者調用set_current_state()之後,

睡眠者和喚醒者隐含的記憶體障礙在喚醒之前不會對這些存儲值的LOAD進行排序。

例如,如果睡眠者:

set_current_state(TASK_INTERRUPTIBLE);

if (event_indicated)

break;

__set_current_state(TASK_RUNNING);

do_something(my_data);

and the waker does:

而喚醒函數這樣做:

my_data = value;

event_indicated = 1;

wake_up(&event_wait_queue);

睡眠函數并不能保證在看到my_data的修改之後才看到event_indicated的修改. 在這種情況

下, 兩邊的代碼必須在對my_data訪存之前插入自己的記憶體屏障. 是以上述的睡眠函數應該

這樣做:

set_current_state(TASK_INTERRUPTIBLE);

if (event_indicated) {

smp_rmb();

do_something(my_data);

}

而喚醒函數應該這樣做:

my_data = value;

smp_wmb();

event_indicated = 1;

wake_up(&event_wait_queue);

7.4 其他函數

其他隐含了屏障的函數:

(*)schedule()和類似函數隐含了完整的記憶體屏障.

8 跨CPU的擷取的屏障作用

在SMP系統中, 鎖定原語給出了多種形式的屏障: 其中一種在一些特定的鎖沖突的情況下,

會影響其他CPU上的記憶體通路順序.

8.1 ACQUIRES與記憶體通路

考慮以下幾點:系統有一對自旋鎖(M)和(Q),三個CPU; 那麼應該發生以下事件序列:

CPU 1 CPU 2

=============================== ===============================

WRITE_ONCE(*A, a); WRITE_ONCE(*E, e);

ACQUIRE M ACQUIRE Q

WRITE_ONCE(*B, b); WRITE_ONCE(*F, f);

WRITE_ONCE(*C, c); WRITE_ONCE(*G, g);

RELEASE M RELEASE Q

WRITE_ONCE(*D, d); WRITE_ONCE(*H, h);

那麼對于CPU 3來說, 從*A到*H的通路順序是沒有保證的, 不像單獨的鎖對應單獨的CPU有

那樣的限制. 例如, CPU 3可能看到的順序是:

*E, ACQUIRE M, ACQUIRE Q, *G, *C, *F, *A, *B, RELEASE Q, *D, *H, RELEASE M
           

但是它不會看到如下情況:

*B, *C or *D preceding ACQUIRE M

*A, *B or *C following RELEASE M

*F, *G or *H preceding ACQUIRE Q

*E, *F or *G following RELEASE Q

8.2 ACQUIRES與IO通路

在某些情況下(特别是涉及到NUMA的情況),兩個CPU上發起的屬于兩個spinlock臨界區的IO

通路可能被PCI橋看成是交錯發生的,為PCI橋并不一定參與cache一緻性協定, 以至于無

法響應讀記憶體屏障。

例如:

CPU 1 CPU 2

=============================== ===============================

spin_lock(Q)

writel(0, ADDR)

writel(1, DATA);

spin_unlock(Q);

spin_lock(Q);

writel(4, ADDR);

writel(5, DATA);

spin_unlock(Q);

PCI橋可能看到的是:

STORE *ADDR = 0, STORE *ADDR = 4, STORE *DATA = 1, STORE *DATA = 5

這可能會引起硬體操作的錯誤.

這裡所需要的是, 在釋放spinlock之前, 使用mmiowb()作為幹預, 例如:

CPU 1 CPU 2

=============================== ===============================

spin_lock(Q)

writel(0, ADDR)

writel(1, DATA);

mmiowb();

spin_unlock(Q);

spin_lock(Q);

writel(4, ADDR);

writel(5, DATA);

mmiowb();

spin_unlock(Q);

這樣就能確定CPU 1的兩次STORE操作先于CPU 2的STORE操作被PCI橋所看到.

此外,對于同一硬體裝置在進行STORE操作之後再進行LOAD操作, 可以省去mmiowb()

LOAD操作将強制STORE操作在開始LOAD之前就完成:

CPU 1 CPU 2

=============================== ===============================

spin_lock(Q)

writel(0, ADDR)

a = readl(DATA);

spin_unlock(Q);

spin_lock(Q);

writel(4, ADDR);

b = readl(DATA);

spin_unlock(Q);

更多資訊請參閱”Documentation/DocBook/deviceiobook.tmpl”.

9 什麼地方需要記憶體屏障?

在正常操作下, 記憶體操作的亂序一般并不會成為問題, 即使是在SMP核心中, 一段單線程的

線性代碼也總是能夠正确工作. 但是, 有四種情況, 亂序絕對可能是一個問題:

(*)處理器間互動.

(*)原子操作.

(*)通路裝置.

(*) Interrupts.

中斷.

INTERPROCESSOR INTERACTION

9.1 處理器間互動

當有一個具有多個處理器的系統時,系統中的多個CPU可能同時在同一個資料集上工作。

這可能會導緻同步問題,而通常的處理方式是使用鎖。

然而,鎖是相當昂貴的,是以如果可能的話,可以可能的話最好使用操作而不使用鎖。

在這種情況下,可能需要仔細安排那些影響兩個CPU的操作,以防止故障。

例如,考慮R / W信号量慢速路徑。 這裡一個等待的程序在信号量上排隊,

由于信号量的特點,程序的堆棧連結到信号量的等待程序清單:

struct rw_semaphore {

spinlock_t lock;

struct list_head waiters;

};

struct rwsem_waiter {
    struct list_head list;
    struct task_struct *task;
};
           

要喚醒這樣一個等待程序, up_read()函數或up_write()函數需要這樣做:

(1) 讀取該等待程序所對應的waiter結構的next指針, 以記錄下一個等待程序是誰;

(2) 讀取waiter結構中的task指針, 以擷取對應程序的程序控制塊;

(3) 清空waiter結構中的task指針, 以表示這個程序正在獲得信号量;

(4)對這個程序調用wake_up_process()函數;

(5) 釋放waiter結構對程序控制塊的引用計數.

換句話說, 這個過程會執行如下事件序列:

LOAD waiter->list.next;

LOAD waiter->task;

STORE waiter->task;

CALL wakeup

RELEASE task

而如果其中一些步驟發生了亂序, 那麼整個過程可能會産生錯誤.

一旦waiter程序将自己挂入等待隊列, 并釋放了信号量裡的鎖, 這個等待程序就不會再獲得這

個鎖了; 它要做的事情就是在繼續工作之前, 等待waiter結構中的task指針被清空。

而既然waiter結構存在于等待程序的棧上, 這就意味着, 如果在waiter結構中的next指針被讀

取之前, task指針先被清空了的話,那麼, 這個等待程序可能已經在另一個CPU上開始運作了

并且在up*()函數有機會讀取到next指針之前, 棧空間上對應的waiter結構可能已經被複用了.

看看上面的事件序列可能會發生什麼:

CPU 1 CPU 2

=============================== ===============================

down_xxx()

Queue waiter

Sleep

up_yyy()

LOAD waiter->task;

STORE waiter->task;

Woken up by other event

Resume processing

down_xxx() returns

call foo()

foo() clobbers *waiter

LOAD waiter->list.next;

— OOPS —

對付這個問題可以使用信号量中的鎖, 但是當程序被喚醒後, down_xxx()函數其實沒必要重

新獲得這個spinlock.

實際的解決辦法是插入一個通用SMP記憶體屏障:

LOAD waiter->list.next;

LOAD waiter->task;

smp_mb();

STORE waiter->task;

CALL wakeup

RELEASE task

這樣, 對于系統中的其他CPU來說, 屏障将保證屏障之前的所有記憶體通路先于屏障之後的所

有記憶體通路發生。屏障并不保證屏障之前的所有記憶體通路都在屏障指令結束之前完成.。

在UP系統中 - 這種情況将不是問題 - smp_mb()函數隻是一個編譯優化屏障, 這就確定了編

譯器生成順序正确的指令, 而不需要幹預CPU. 既然隻有一個CPU, 該CPU的資料依賴邏輯将

處理所有事情.

ATOMIC OPERATIONS

9.2 原子操作

雖然原子操作在技術上實作了處理器之間的互動, 然而特别注意一些原子操作隐含了完整的

記憶體屏障, 而另外一些則沒有, 但是它們卻作為一個被整個核心嚴重依賴群體.

許多原子操作修改記憶體中的一些狀态, 并且傳回該狀态相關的資訊(舊狀态或新狀态), 就在

其中實際操作記憶體的兩邊各隐含一個SMP環境下的通用記憶體屏障(smp_mb())(除顯式的鎖操作

之外, 稍後說明). 它們包括:

xchg();

atomic_xchg(); atomic_long_xchg();

atomic_inc_return(); atomic_long_inc_return();

atomic_dec_return(); atomic_long_dec_return();

atomic_add_return(); atomic_long_add_return();

atomic_sub_return(); atomic_long_sub_return();

atomic_inc_and_test(); atomic_long_inc_and_test();

atomic_dec_and_test(); atomic_long_dec_and_test();

atomic_sub_and_test(); atomic_long_sub_and_test();

atomic_add_negative(); atomic_long_add_negative();

test_and_set_bit();

test_and_clear_bit();

test_and_change_bit();

/* when succeeds */
cmpxchg();
atomic_cmpxchg();       atomic_long_cmpxchg();
atomic_add_unless();        atomic_long_add_unless();
           

它們被用于作為ACQUIRE類和RELEASE類操作的實作, 和用于控制對象删除的引用計數, 這些情況

下, 隐含記憶體屏障是有必要的.

以下操作由于沒有隐含記憶體屏障, 會有潛在的問題, 但有可能被用于實作RELEASE類這樣的操

atomic_set();

set_bit();

clear_bit();

change_bit();

如果需要, 對應于這些函數, 可以使用相應的顯式記憶體屏障(比如

smp_mb__before_atomic()).

下面這些函數也不隐含記憶體屏障, 并且在一些情況下, 可能也需要用到顯式記憶體屏障(比如

smp_mb__before_atomic()):

atomic_add();

atomic_sub();

atomic_inc();

atomic_dec();

如果它們用于産生一般統計, 那麼他們可能就不需要記憶體屏障, 除非統計資料之間存在耦合.

如果它們被用作控制對象生命周期的引用計數,則它們可能不需要記憶體屏障,

因為要麼引用計數需要在一個鎖的臨界區裡面進行調整, 要麼調用者已經持有足夠的引用而相當于擁

有了鎖(引用計數足夠多,不可能在這種情況下析構), 是以不需要記憶體屏障。

如果它們用于構成鎖的一些描述資訊, 那麼他們可能就需要記憶體屏障, 因為鎖原語一般需要

按一定的順序來操作.

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

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

test_and_set_bit_lock();

clear_bit_unlock();

__clear_bit_unlock();

它們都執行了ACQUIRE類和RELEASE類的操作. 相比其他操作, 它們應該優先被用于實作鎖原語,

因為它們的實作可以在許多體系結構下得到優化.

[!] 注意, 這些特殊的記憶體屏障原語對一些情況也是有用的, 因為在一些體系結構的CPU上,

使用的原子操作本身就隐含了完整的記憶體屏障功能, 是以屏障指令在這裡是多餘的, 在

這樣的情況下, 這些特殊的屏障原語将不使用額外的屏障操作.

更多資訊請參閱Documentation/atomic_ops.txt.

9.3 通路裝置

許多裝置都可以被映射到記憶體, 是以在CPU看來, 它們隻是一組記憶體位址. 為了控制這些設

備, 驅動程式通常需要確定正确的記憶體通路按正确的順序來執行.

但是, 聰明的CPU或者聰明的編譯器卻導緻了潛在的問題, 如果CPU或編譯器認為亂序, 或合

并通路更有利于效率的話, 驅動程式代碼中仔細安排的訪存序列可能并不會按正确的順序被

送到裝置上 - 進而可能導緻裝置的錯誤.

在Linux核心裡面, IO通路應該使用适當的通路函數 - 例如inb()或writel() - 它們知道如

何得到恰當的通路順序. 大多數情況下, 在使用這些函數之後就不必再顯式的使用記憶體屏障

, 但是在兩種情況下, 記憶體屏障可能還是需要的:

(1) 在一些系統中, IO存儲操作對于所有CPU來說并不是嚴格有序的, 是以對于所有的通用驅動程式(譯注: 通用驅動程式需要适應各種體系結構的系統), 需要使用鎖, 并且一定要在解鎖臨界區之前執行mmiowb()函數.

(2) 如果訪存函數通路松散屬性的IO記憶體視窗, 那麼需要使用強制記憶體屏障來確定執行順序.

更多資訊請參閱Documentation/DocBook/deviceiobook.tmpl.

9.4 中斷

驅動程式可能被它自己的中斷處理程式所打斷, 然後驅動程式中的這兩個部分可能會互相幹

擾對方控制或通路裝置的意圖.

通過禁用本地中斷(一種形式的鎖)可能至少部分緩解這種情況, 這樣的話, 驅動程式中的關

鍵操作都将包含在禁用中斷的區間中. 于是當驅動程式的中斷處理程式正在執行時, 驅動程

序的核心代碼不可能在相同的CPU上運作, 并且在目前中斷被處理完之前中斷處理程式不允

許再次被調用, 于是中斷處理程式就不需要再對這種情況使用鎖.

但是, 考慮一個驅動程式正通過一個位址寄存器和一個資料寄存器跟以太網卡互動的情況.

假設驅動程式的核心代碼在禁用中斷的情況下操作了網卡, 然後驅動程式的中斷處理程式

被調用:

LOCAL IRQ DISABLE

writew(ADDR, 3);

writew(DATA, y);

LOCAL IRQ ENABLE

writew(ADDR, 4);

q = readw(DATA);

如果執行順序的規則足夠松散, 對資料寄存器的寫操作可能發生在第二次對位址寄存器的寫

操作之後:

STORE *ADDR = 3, STORE *ADDR = 4, STORE *DATA = y, q = LOAD *DATA

如果執行順序像這樣松散, 就需要假定在禁用中斷區間内應該完成的通路可能洩漏到區間之

外, 并且可能漏到中斷過程中進行通路 - 反之亦然 - 除非使用隐式或顯式的屏障

通常這并不是一個問題, 因為禁用中斷區間内完成的IO訪存将會包含嚴格有序的同步LOAD操

作, 形成隐式的IO屏障. 如果這還不夠, 那麼需要顯式的調用一下mmiowb().

在一個中斷服務程式與兩個運作在不同CPU的程式互相通信的情況下, 類似的情況也可能發

生. 如果出現這樣的情況, 那麼禁用中斷的鎖操作需要用于確定執行順序. (譯注: 也就是

類似于spinlock_irq這樣的操作.)

10 核心中I/O屏障的作用

在對IO記憶體進行存取的時候, 驅動程式應該使用适當的存取函數:

(*) inX(), outX():

它們都是傾向于跟IO空間打交道, 而不是普通記憶體空間, 不過這主要取決于具體CPU的

邏輯. i386和x86_64處理器确實有特殊的IO空間存取周期和指令, 但是許多系統結構

的CPU卻并沒有這些概念.

包括PCI總線也可能會定義成IO空間 - 比如在i386和x86_64的CPU上 - 很容易将它映

射到CPU的IO空間上. 但是, 它也可能作為虛拟的IO空間被映射到CPU的記憶體空間上,

特别對于那些不支援IO空間的CPU.

通路這些空間可能是完全同步的(比如在i386上), 但是對于橋裝置(比如PCI主橋)可能

并不完全是這樣.

他們能保證完全遵守IO操作之間的通路順序.

他們不能保證完全遵從IO操作與其他類型的記憶體操作之間的通路順序.

(*) readX(), writeX():

在發起調用的CPU上, 這些函數是否保證完全遵從記憶體通路順序而且不進行合并通路,

取決于它們所通路的記憶體視窗上定義的屬性. 例如, 較新的i386體系結構的機器, 可

以通過MTRR寄存器來控制記憶體視窗的屬性.

通常, 隻要不是通路預取裝置, 這些函數将保證完全有序并且不進行合并通路.

但是對于橋裝置(比如PCI橋), 如果它們願意的話, 可能會傾向于對記憶體操作進行延遲

處理; 要重新整理一個STORE操作, 首選是對相同位址進行一次LOAD[*], 但是對于PCI來說

, 對相同裝置或相同的配置的IO空間進行一次LOAD就足夠了.

[*] 注意! 試圖從剛寫過的位址LOAD資料, 可能會導緻錯誤 - 比如對于16550 Rx/Tx

序列槽寄存器.

遇到帶預取的IO記憶體, 可能需要使用mmiowb()屏障來強制讓STORE操作有序.

關于PCI事務互動方面的更多資訊, 請參閱PCI規範.

(*) readX_relaxed(), writeX_relaxed()

這些函數類似于readX()和writeX(), 但是他們提供更弱的記憶體有序保證.特别的,

他們不保證一般的記憶體通路順序,也不保證LOCK和UNLOCK操作的順序。

如果後者需要保證則應該使用mmiowb()記憶體屏障。注意松散的外部裝置的通路順序與其他裝置看到的順序相同。

(*) ioreadX(), iowriteX()

這些函數在進行訪存的時候會根據訪存類型選擇适當的操作, inX()/outX()或

readX()/writeX().

11 最小限度有序的假想模型

從概念上說, 必須假定的CPU是弱有序的, 但是它會保持程式上下文邏輯關系的外觀. 一些

CPU(比如i386或x86_64)比另一些(比如powerpc或frv)更具有限制力, 而在體系結構無關的

代碼中, 必須假定為最松散的情況(也就是DEC Alpha).

也就是說, 必須考慮到CPU可能會按它喜歡的順序來執行操作 - 甚至并行執行 - 隻是當指

令流中的一條指令依賴于之前的一條指令時, 之前的這條指定才必須在後面這條指令可能被

處理之前完全結束; 換句話說: 保持程式的上下文邏輯關系.

[*] 一些指令會産生不止一處影響 - 比如會修改條件碼, 修改寄存器或修改記憶體 - 不同

的指令可能依賴于不同的影響

CPU也可能丢棄那些最終不産生任何影響的操作序列. 比如, 如果兩個相鄰的指令都将一個

立即數LOAD到寄存器, 那麼第一個LOAD指令可能被丢棄.

類似的, 也需要假設編譯器可能按它覺得舒服的順序來調整指令流, 但同樣也會保持程式的

上下文邏輯關系

12 CPU cache的影響

操作cache中緩存的記憶體之後, 相應的影響會在整個系統間得到傳播. 位于CPU和記憶體之間的

cache, 和保持系統狀态一緻的記憶體一緻性機構, 在一定程度上影響了傳播的方法.

自從通過使用cache來實作CPU與系統中其他部分的互動以來, 記憶體系統就包含了CPU的緩存,

而記憶體屏障基本上就工作在CPU和其cache之間的界面上(邏輯上說, 記憶體屏障工作在下圖中

虛線所示的地方):

一些LOAD和STORE可能不會實際出現在發起操作的CPU之外, 因為在CPU自己的cache上就能滿

足需要, 盡管如此, 如果其他CPU關心這些資料, 那麼完整的記憶體通路還是會發生, 因為

在保持程式所期望的上下文邏輯的前提下, CPU核心可能會按它認為合适的順序來執行指令.

一些指令會産生LOAD和STORE操作, 并且将它們放到記憶體請求隊列中, 等待被執行. CPU核心

可能會按它喜歡的順序來将這些操作放進隊列, 然後繼續運作, 直到它必須等待這些訪存指

令完成的時候為止.

記憶體屏障所需要關心的是訪存操作從CPU一側穿越到記憶體一側的順序, 和系統中的其他部件

感覺到的操作發生的順序.

[!] 對于一個CPU自己的LOAD和STORE來說, 并不需要使用記憶體屏障, 因為CPU總是能按程式

執行順序看到它們所執行的LOAD和STORE操作.

[!] MMIO或其他裝置存取可能繞開cache系統. 這取決于通路裝置所經過的記憶體視窗的屬性

和/或是否使用了CPU所特有的與裝置進行互動的指令.

12.1 CACHE一緻性

但是, 事情并不是像上面所說的那樣簡單: 因為雖然可以期望cache是一緻的, 但是一緻性

傳播的順序卻是沒有保證的. 也就是說, 雖然一個CPU所做出的更新将最終被其它CPU都看到

, 但是卻不保證其他CPU所看到的都是相同的順序.

考慮這樣一個系統, 它具有雙CPU(1和2), 每個CPU有一對并行的資料cache(CPU 1對應A/B,

CPU 2對應C/D):

想象一下該系統有如下屬性:

(*) 一個奇數号的cache行可能被緩存在cache A, cache C, 或者可能依然駐留在記憶體中;

(*) 一個偶數号的cache行可能被緩存在cache B, cache D, 或者可能依然駐留在記憶體中;

(*) 而當CPU核心通路一個cache時, 另一個cache可以同時利用總線來通路系統中的其他部

分 - 可能是替換一個髒的cache行或者進行預取;

(*) 每個cache都有一個操作隊列, 被用于保持cache與系統中的其他部分的一緻性;

(*) 當LOAD命中了已經存在于cache中的行時, 該一緻性隊列并不會得到沖刷, 盡管隊列中的内容可能會影響這些LOAD操作. (譯注: 也就是說, 隊列中有針對某一cache行的更新操作正在等待被執行, 而這時LOAD操作需要讀這個cache行. 這種情況下, LOAD并不會等待隊列中的這個更新完成, 而是直接擷取了更新前的值.)

接下來, 想象一下在第一個CPU上執行兩個寫操作, 并在它們之間使用一個寫屏障, 以保證

它們按要求的順序到達該CPU的cache:

CPU 1       CPU 2       COMMENT
=============== =============== =======================================
                u == 0, v == 1 and p == &u, q == &u
v = 2;
smp_wmb();          Make sure change to v is visible before
                 change to p
<A:modify v=2>          v is now in cache A exclusively
p = &v;
<B:modify p=&v>         p is now in cache B exclusively
           

寫記憶體屏障保證系統中的其他CPU會按正确的順序看到本地CPU cache的更新. 但是設想一下

第二個CPU要去讀取這些值的情形:

CPU 1       CPU 2       COMMENT
=============== =============== =======================================
...
        q = p;
        x = *q;
           

上面這一對讀操作可能不會在預期的順序下執行, 比如持有p的cache行可能被更新到另一個

CPU的cache, 而持有v的cache行因為其他一些cache事件的影響而延遲了對那個CPU的cache

的更新:

CPU 1       CPU 2       COMMENT
=============== =============== =======================================
                u == 0, v == 1 and p == &u, q == &u
v = 2;
smp_wmb();
<A:modify v=2>  <C:busy>
        <C:queue v=2>
p = &v;     q = p;
        <D:request p>
<B:modify p=&v> <D:commit p=&v>
        <D:read p>
        x = *q;
        <C:read *q> Reads from v before v updated in cache
        <C:unbusy>
        <C:commit v=2>
           

基本上, 雖然最終CPU 2的兩個cache行都将得到更新, 但是在沒有幹預的情況下, 并不能保

證更新的順序跟CPU 1送出的順序一緻.

我們需要在兩次LOAD之間插入一個資料依賴屏障或讀屏障, 以作為幹預. 這将強制cache在

處理後續的請求之前, 先讓它的一緻性隊列得到送出:

CPU 1 CPU 2 COMMENT

=============== =============== =======================================

u == 0, v == 1 and p == &u, q == &u

v = 2;

smp_wmb();

p = &v; q = p;

smp_read_barrier_depends()

x = *q;

15 引用

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

Chapter 8: Memory Models

UltraSPARC Architecture 2005

Chapter 9: Memory

Appendix D: Formal Specifications of the Memory Models

UltraSPARC T1 Supplement to the UltraSPARC Architecture 2005

Chapter 8: Memory Models

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

繼續閱讀