linux 內核記憶體屏障 By: David Howells [email protected] Paul E. McKenney [email protected] Will Deacon [email protected] Peter Zijlstra peterz@infrad ...
linux 內核記憶體屏障
By: David Howells [email protected]
Paul E. McKenney [email protected]
Will Deacon [email protected]
Peter Zijlstra [email protected]
翻譯:反光 [email protected]
免責聲明
這個文檔不是一個規範,這是為了讓文檔更加簡潔,但不是為了文檔的不完整。
這個文檔為使用linux提供的各種記憶體屏障功能提供了指南,但是如果有任何疑問請詢問。
參考tools/memory-model/中的形式化記憶體一致性模型和相關文檔,可以解決一些疑問。儘管如此,即使是這種記憶體模型也應該被視為其維護者的集體意見,而不是絕對正確的預言。
強調,這個文檔不是一個linux對硬體的期望的規範。
文檔的目的有兩個:
(1) 指定對於任何記憶體屏障,我們能夠期望的最低功能。
(2) 提供使用記憶體屏障的指南
註意:一個體繫結構可以提供比最低功能更多的功能,但是如果少於該功能,這個體繫結構就是有錯誤的。
在某個體繫結構下記憶體屏障可以是一個空的操作,因為在該體繫結構的工作方式明確記憶體屏障是沒必要的
目錄
- 抽象記憶體訪問模型。
- 操作設備
- CPU基本保證
- 什麼是記憶體屏障?
- 記憶體屏障的種類
- 關於記憶體屏障, 不能保證什麼?
- 數據依賴屏障(歷史上的)
- 控制依賴
- SMP屏障配對使用
- 記憶體屏障舉例
- 讀記憶體屏障與記憶體預取
- 多副本原子性
- 內核中的顯式記憶體屏障
- 編譯優化屏障
- CPU記憶體屏障
- 內核中隱式的記憶體屏障
- 獲取鎖的功能
- 中斷禁用功能
- 睡眠和喚醒功能
- 其他功能
- 跨CPU的ACQUIRING的屏障的效果
- ACQUIRES與記憶體訪問
- 什麼地方需要記憶體屏障?
- 處理器間交互
- 原子操作
- 訪問設備
- 中斷
- 內核中I/O屏障的影響。
- 最小限度有序的假想模型
- CPU cache的影響
- cache一致性與DMA
- cache一致性與MMIO
- CPU能做到的
- 特別值得一提的Alpha處理器
- 虛擬機中的客戶機
- 示例使用
- 迴圈緩衝區
- 參考
1. 抽象記憶體訪問模型
考慮如下抽象系統模型:
: :
: :
: :
+-------+ : +--------+ : +-------+
| | : | | : | |
| | : | | : | |
| CPU 1 |<----->| Memory |<----->| CPU 2 |
| | : | | : | |
| | : | | : | |
+-------+ : +--------+ : +-------+
^ : ^ : ^
| : | : |
| : | : |
| : v : |
| : +--------+ : |
| : | | : |
| : | | : |
+---------->| Device |<----------+
: | | :
: | | :
: +--------+ :
: :
假設每個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中。
1.1 操作設備
一些設備將其控制寄存器映射到一組記憶體地址集合上,但這些控制寄存器的被訪問順序非常
重要。 例如,想像一個帶有一組內部的乙太網卡
通過地址埠寄存器(A)訪問的寄存器和數據
埠寄存器(D)。 要讀取內部寄存器5,則可能會執行以下代碼
使用:
*A = 5;
x = *D;
但這可能會執行為以下兩個序列之一:
STORE *A = 5, x = LOAD *D
x = LOAD *D, STORE *A = 5
其中第二個幾乎肯定會導致錯誤,因為它在嘗試讀取寄存器後設置地址。
1.2 CPU基本保證
CPU有一些最低限度的保證:
1.2.1 上下文依賴
對於一個CPU, 在它上面出現的有上下文依賴關係的記憶體訪問將被按順序執行。 這意味著:
Q = READ_ONCE(P); D = READ_ONCE(*Q);
CPU將順序執行以下記憶體操作:
Q = LOAD P, D = LOAD *Q
而且總是按照這個順序。但是,在DEC Alpha上,READ_ONCE()也會發出記憶體屏障指令,因此DEC Alpha CPU將發出以下記憶體操作:
Q = LOAD P, MEMORY_BARRIER, D = LOAD *Q, MEMORY_BARRIER
無論是否在DEC Alpha上,READ_ONCE()也可以防止編譯器的破壞。
1.2.2 重疊區域LOAD STORE
在特定CPU內重疊的載入和存儲看起來像是在該CPU內有序的。這意味著for:
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的目標指向同一塊記憶體地址, 則認為是重疊的操作)
有很多事情必須或者不能假設:
1.2.3 沒有被READ_ONCE()和WRITE_ONCE()保護的記憶體
一定不能假設編譯器會對沒有被READ_ONCE()和WRITE_ONCE()保護的記憶體引用做你想做的事情。
如果沒有它們,編譯器就有權利進行各種“創造性”的轉換,這些轉換將在“編譯屏障”章節說明
1.2.4 不能假定獨立的載入和存儲將按照給定的順序發出
必須不能假定獨立的載入和存儲將按照給定的順序發出。這意味著
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
1.2.5 必須假定重疊的記憶體訪問可能被合併或丟棄
這意味著:
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)時要小心。包含這種保證的標準部分是第3.14節,其中對“記憶體位置”的定義如下::
記憶體位置
要麼是標量類型的對象,要麼是寬度都不為零的相鄰位域的最大序列
註1:兩個執行線程可以更新和訪問獨立的記憶體位置,而不會相互干擾。
註2:位域和相鄰的非位域成員位於獨立的記憶體位置。這同樣適用於兩個位欄位,如果一個聲明在嵌套結構聲明中,而另一個不是,或者兩個位欄位之間用零長度的位欄位聲明分隔,或者用非位欄位成員聲明分隔。如果同一個結構中兩個位欄位之間聲明的所有成員都是位欄位,而不管中間的位欄位長度是多少,那麼併發地更新這兩個位欄位是不安全的。
2 什麼是記憶體屏障?
如上所述,獨立的記憶體操作實際上是隨機執行的,但這對於CPU-CPU交互和I/O來說可能是一個問題。所需要的是某種干預方式,以指示編譯器和CPU限制順序。
記憶障礙就是這樣的干預措施。它們對屏障兩側的記憶體操作施加了一種可感知的部分排序。
這樣的干預是非常重要的,因為系統中的CPU和系統中的其他設備可以使用各種各樣的優化策略來提高性能,包括記憶體操作重新排序,延遲和記憶體操作的合併執行; 預取、分支預測和各種類型的緩存。
記憶體屏障用於禁止或抑制這些策略,使代碼正確的控制多個CPU或CPU與設備的交互。
2.1 記憶體屏障的種類
記憶體屏障有四種基本類型:
2.1.1 寫(或存儲)記憶體屏障。
寫記憶體屏障保證了在屏障之前指定的所有存儲操作在屏障之後指定的所有存儲操作發生之前,被系統的其他組件所感知。
寫屏障僅保證針對STORE操作的部分排序; 不要求對LOAD操作沒有任何影響。
隨著時間的推移,CPU可以被視為向記憶體系統提交一系列存儲操作。寫屏障之前的所有存儲將發生在寫屏障之後的所有存儲之前。
[!]請註意,寫記憶體屏障通常應與讀記憶體屏障配對; 請參閱“SMP屏障配對”小節。
2.1.2 數據依賴屏障
數據依賴屏障是讀屏障的弱化版本。在執行兩個載入的情況下,第二個載入依賴於第一個載入的結果(例如:第一個載入檢索第二個載入將指向的地址),需要一個數據依賴屏障,以確保在第一個載入獲得的地址被訪問後,第二個載入的目標被更新。
數據依賴屏障僅對相互依賴的LOAD操作產生部分排序;不對STORE操作、獨立LOAD操作或重疊的LOAD操作產生影響。
如(1)中所述,系統中的CPU可以感知到其他CPU提交到存儲器系統的STORE操作序列。
而在該CPU上觸發的數據依賴屏障將保證, 對於在屏障之前發生的LOAD操作,
如果這個LOAD操作的目標被其他CPU的STORE操作所修改,那麼在屏障完成的時候,
這個LOAD操作之前的所有STORE操作所產生的影響,將被數據依賴屏障之後執行的任何LOAD操作所感知.
有關排序約束的圖表,請參見:“記憶體屏障序列示例”。
- 容易混淆的控制依賴
[!]請註意,第一個LOAD實際上必須具有數據依賴關係,而不是控制依賴。
如果第二個LOAD的地址依賴於第一個LOAD,但是依賴關係是通過一個條件語句而不是實際載入地址本身,
那麼它是一個控制依賴關係,最好需要一個完整的讀屏障。 有關詳細信息,請參閱“控制依賴關係”小節。 - SMP屏障配對
[!] 請註意,數據依賴屏障通常應與寫屏障配對; 請參閱“SMP屏障配對”小節。
2.1.3 讀取(或載入)記憶體屏障。
讀屏障包含數據依賴屏障的功能, 並且保證所有出現在屏障之前的LOAD操作都將先於所有出現在屏障之後的LOAD操作被系統中的其他組件所感知.
讀屏障僅保證針對LOAD操作的部分有序; 不要求對STORE操作產生影響.
讀記憶體屏障隱含了數據依賴屏障, 因此可以用於替代數據依賴屏障.
[!] 註意, 讀屏障一般要跟寫屏障配對使用; 參閱"SMP記憶體屏障的配對使用"章節.
2.1.4 通用記憶體屏障.
通用記憶體屏障保證所有出現在屏障之前的LOAD和STORE操作都將先於所有出現在屏障之後的LOAD和STORE操作被系統中的其他組件所感知.
通用記憶體屏障是針對LOAD和STORE操作的部分有序.
通用記憶體屏障隱含了讀屏障和寫屏障, 因此可以用於替代它們.
記憶體屏障還有兩種隱式類型:
2.1.5 ACQUIRE操作
這是一個單向的可滲透的屏障。它保證所有出現在ACQUIRE之後的記憶體操作都將在ACQUIRE操作被系統中的其他組件所感知之後才能發生.ACQUIRE包括LOCK操作、
smp_load_acquire()和smp_cond_load_acquire()操作。
出現在ACQUIRE之前的記憶體操作可能在ACQUIRE之後才發生
ACQUIRE操作應該總是跟RELEASE操作成對出現的。
2.1.6 RELEASE操作
這是一個單向的可滲透的屏障。它保證所有出現在RELEASE之前的記憶體操作都將在
RELEASE操作被系統中的其他組件所感知之前發生.RELEASE操作包括UNLOCK操作和smp_store_release()操作。
出現在RELEASE之後的記憶體操作可能看起來是在RELEASE完成之前就發生了.
使用ACQUIRE和RELEASE操作通常不需要其他種類的記憶體屏障。
此外,RELEASE + ACQUIRE對不能保證能替代完整的記憶體屏障。
然而,在ACQUIRE後的給定的變數,ACQUIRE之前的任何RELEASE前對該變數的所有存儲器訪問都保證是可見的。
換句話說,在給定變數的臨界區中,對該變數之前所有臨界區的訪問都保證已經完成。
使用獲取和釋放操作通常可以避免使用其他類型的記憶體屏障。此外,釋放+獲取對不能保證充當完全的記憶體屏障。但是,在獲取給定變數之後,該變數在發佈之前的所有記憶體訪問都保證是可見的。換句話說,在給定變數的臨界區中,對該變數之前所有臨界區的訪問都保證已經完成。
這意味著ACQUIRE操作是一個最小的“獲取”操作(獲取之前發佈的記憶體訪問狀態),
RELEASE操作時一個最小的“釋放”操作(發佈當前記憶體狀態)。
在atomic_t.txt中描述的原子操作的子集,除了完全有序和寬鬆(無語義障礙)定義之外,還有ACQUIRE和RELEASE變體。對於複合的原子操作LOAD和STORE,ACQUIRE語義僅應用於LOAD,RELEASE語義僅應用於操作的STORE部分。
只有在兩個CPU之間或CPU和設備之間有可能交互時,才需要記憶體屏障。如果可以保證在任何特定的代碼段中不會有任何這樣的交互,那麼在該代碼段中就沒有必要使用記憶體屏障。
註意:對於前邊提到的都是最低限度的保證,不同的體繫結構可能提供更多的保證,
但是在特定體繫結構的代碼之外,不能依賴於這些額外的保證。
2.2 關於記憶體屏障, 不能保證什麼?
Linux內核的記憶體屏障不保證下麵這些事情:
-
不能保證記憶體屏障之前出現的任何記憶體訪問都會在記憶體屏障指令之前完成。記憶體屏障相當於在該CPU的訪問隊列中畫一條線,使得相關訪存類型的請求不能跨越記憶體屏障。
-
不保證在一個CPU上執行的記憶體屏障會對其他系統中的CPU或硬體設備產生任何直接影響。
間接影響就是第二個CPU感知到第一個CPU訪問記憶體的順序,不過請看下一點: -
不能保證CPU能夠觀察到第二個CPU的訪問記憶體的正確順序,即使第二個CPU使用記憶體屏障,除非第一個CPU也使用了與之匹配的記憶體屏障(參閱"SMP記憶體屏障的配對使用"部分)
-
不能保證某些處於中間位置的非cpu硬體不會對記憶體訪問進行重新排序。
CPU cache一致性機制會在CPU間傳播記憶體屏障所帶來的間接影響,但是可能不是按照原順序的。 -
更多關於匯流排主控DMA和一致性的問題請參閱:
Documentation/driver-api/pci/pci.rst
Documentation/core-api/dma-api-howto.rst
Documentation/core-api/dma-api.rst
2.3 數據依賴屏障(歷史上的)
從Linux內核的v4.15開始,在DEC Alpha的READ_ONCE()中添加了一個smp_mb(),這意味著需要註意本節的人只有那些工作於DEC Alpha特定於體繫結構的代碼和那些工作於READ_ONCE()本身的人。對於那些需要它的人,以及那些對歷史感興趣的人,這裡有一個關於數據依賴障礙的故事。
2.3.1 通常情況下有數據依賴的LOAD-LOAD是保序的
數據依賴屏障的使用要求有點微妙, 並不總是很明顯就能看出是否需要他們。
為了說明這點,考慮如下的操作隊列:
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)
2.3.2 DEC Alpha上的特殊情況
但是! 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 barrier>
WRITE_ONCE(P, &B);
Q = READ_ONCE(P);
<data dependency barrier>
D = *Q;
這就將執行結果強製為前兩種結果之一,避免了第三種結果的產生。
[!請註意,這種極端違反直覺的情況最容易發生在分離緩存的機器上,例如,一個緩存組處理偶數號的緩存行,而另一個緩存組處理奇數號的緩存行。指針P可能存儲在奇數的緩存行中,變數B可能存儲在偶數的緩存行中。然後,如果正在讀取的CPU的偶數銀行的緩存非常繁忙,而奇數銀行是空閑的,可以看到指針P (&B)的新值,但變數B(2)的舊值。
2.3.3 有數據依賴的LOAD-STORE是保序的
對於依賴順序的寫操作,不需要數據依賴屏障,因為Linux內核支持的cpu在確定下麵三項之前不會寫操作
(1)寫操作確實會發生,
(2)確定寫操作的位置,
(3)確定要寫的值
但請仔細閱讀“CONTROL DEPENDENCIES”一節和文檔/RCU/rcu_dereference.rst文件
編譯器可以並且確實以許多極具創造性的方式打破依賴關係。
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);
WRITE_ONCE(*Q, 5);
因此,將讀取到Q的操作與存儲到*Q的操作進行排序時,不需要任何數據依賴障礙。換句話說,即使沒有數據依賴障礙,這種結果也是被禁止的:
(Q == &B) && (B == 4)
請註意,這種模式應該很少見。畢竟,依賴排序的主要目的是防止產生對數據結構的寫入,以及這些寫入導致的高速緩存未命中的昂貴開銷。
該模式可用於記錄罕見的錯誤條件等,而cpu自然發生的順序可防止此類記錄丟失。
請註意,數據依賴項提供的排序對於包含它的CPU是本地的。有關更多信息,請參閱“多副本原子性”一節。
例如,數據依賴障礙對RCU系統非常重要。請參閱include/linux/rcupdate.h中的rcu_assign_pointer()和rcu_dereference()。這允許將RCU'd指針的當前目標替換為一個新的修改後的目標,而不會使替換的目標看起來不完全初始化。
更詳細的例子請參見“Cache一致性”小節。
2.4 控制依賴
控制依賴可能有點棘手,因為目前的編譯器不瞭解它們。本節的目的是幫助您預防編譯器的無知破壞你的代碼。
2.4.1 控制依賴可能被CPU短路導致重排(被誤認為是數據依賴的控制依賴)
為了使LOAD-LOAD控制依賴正確工作,需要完整的讀記憶體屏障,而不僅僅是一個數據依賴障礙。
考慮以下代碼:
q = READ_ONCE(a);
if (q) {
<data dependency barrier> /* BUG: 沒有數據依賴!!! */
p = READ_ONCE(b);
}
這段代碼可能達不到預期的效果因為這裡其實並不是數據依賴, 而是控制依賴,CPU可能
試圖通過提前預測結果而對"if (p)"進行短路,其他cpu也可以看到b的LOAD發生在a的load之前。
在這樣的情況下, 需要的是:
q = READ_ONCE(a);
if (q) {
<read barrier>
p = READ_ONCE(b);
}
2.4.2 控制依賴的LOAD-STORE保證順序
然而,對於STORE操作不能預取。這意味著針對LOAD-STORE控制依賴關係提供了排序,如下例所示:
q = READ_ONCE(a);
if (q) {
WRITE_ONCE(b, 1);
}
例子中的READ_ONCE()WRITE_ONCE()不是可選的
控制依賴關係通常與其他類型的屏障配對。也就是說,請註意,READ_ONCE()和WRITE_ONCE()都不是可選的
沒有READ_ONCE(),編譯器可能將'a'的LOAD與其他LOAD操作合併。
沒有WRITE_ONCE(),編譯器可能將‘b’STORE與其他STORE操作合併。
這可能會對排序的特別違反直覺的影響。
更糟糕的是,如果編譯器能夠證明變數'a'的值總是非零值,
編譯器將在它的權利範圍內通過刪除“if”條件判斷語句對原示例進行優化,結果如下:
q = a;
b = 1; /* BUG: 編譯器和CPU都能對指令進行重排!!! */
所以不要丟棄READ_ONCE()。
2.4.3 控制依賴可能被編譯器優化掉》CPU對沒有控制依賴的指令重排
在“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); /* BUG: No ordering vs. load from a!!! */
if (q) {
/* WRITE_ONCE(b, 1); -- moved up, BUG!!! */
do_something();
} else {
/* WRITE_ONCE(b, 1); -- moved up, BUG!!! */
do_something_else();
}
2.4.4 顯式記憶體屏障防止問題
現在從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();
}
2.4.5 分支操作不同防止問題
相比之下,如果沒有明確的記憶體屏障,只有當條件語句的兩條腿中的STORE操作不同時,
控制排序才能有效。例如:
q = READ_ONCE(a);
if (q) {
WRITE_ONCE(b, 1);
do_something();
} else {
WRITE_ONCE(b, 2);
do_something_else();
}
例子中的READ_ONCE()仍然是必要的,以防止編譯器計算'a'的值
2.4.6 編譯器優化掉控制依賴
另外,你需要註意的是用局部變數'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, 2);
do_something_else();
這頁,CPU就不再需要保證LOAD'a'和STORE'b'的順序。
添加一個barrier()是很有吸引力的,但這沒有幫助。
條件語句沒了,控制屏障不會再回來了。
因此,如果你需要這個執行順序,你應該確保MAX大於1,如下:
q = READ_ONCE(a);
BUILD_BUG_ON(MAX <= 1); /* 順序執行從a LOAD和STORE到b. */
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代碼,但它不會強制編譯器使用返回的結果。
2.4.7 條件語句之後的語句沒有控制依賴關係
另外,控制依賴只適用於所討論的if語句的then分支和else分支。
特別地,控制依賴不一定適用於if語句後面的代碼。
q = READ_ONCE(a);
if (q) {
WRITE_ONCE(b, 1);
} else {
WRITE_ONCE(b, 2);
}
WRITE_ONCE(c, 1); /* BUG: No ordering against the read from 'a'. */
人們很容易認為這個代碼實際上是有序的,因為編譯器不能對volatile修飾的操作(READ_ONCE、WRITE_ONCE操作)重新排序
也不能對條件語句中的WRITE操作排序。
不幸的是,對於這種推理,編譯器可能將兩個寫入“b”編譯為條件移動指令,就像在這個奇怪的偽彙編代碼:
ld r1,a
cmp r1,$0
cmov,ne r4,$1
cmov,eq r4,$2
st r4,b
st $1,c
一個弱排序的CPU認為STORE'a'和LOAD'c'之間沒有任何依賴關係。
控制依賴關係只會展開成一對cmov指令和依賴這兩個指令的存儲操作。
簡而言之,控制依賴僅適用於所討論的if語句的then分支和else分支中的STORE操作(包括這兩個分支所包含的函數調用),
但不包括if語句後面的代碼。
請註意,控制依賴項提供的順序對於包含它的CPU來說是本地的。更多信息請參閱“多副本原子性”一節。
2.4.8 總結:
綜上所述:
- 控制依賴可以對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{,64}_read()可以保護控制依賴關係。有關的更多信息,請參閱編譯屏障章節。
- 控制依賴僅適用於包含控制依賴關係的if語句的then分支和else分支(包括這兩個分支所包含的函數調用)。控制依賴關係不適用於包含控制依賴關係的if語句之後的代碼
- 控制依賴關係通常與其他類型的屏障配對使用。
- 控制依賴不提供多副本原子性。如果需要所有cpu同時查看一個給定的存儲,可以使用smp_mb()。
- 編譯器不理解控制依賴。 因此,您的工作是確保編譯器不會破壞您的代碼。
2.5 SMP屏障配對使用
處理CPU-CPU交互時,某些類型的記憶體屏障應該始終配對使用。 缺乏適當的配對使用基本上可以肯定是錯誤的。
通用的屏障是成對的,儘管它們也會與大多數其他類型的屏障配對,儘管沒有多拷貝的原子性。
acquire屏障與release屏障配對,但是他們又都能與其他類型的屏障配對(當然包括通用屏障)。
write屏障可以與數據依賴屏障、控制依賴屏障、acquire屏障、release屏障、read屏障或者通用屏障配對。
同樣的read屏障、控制依賴屏障或數據依賴屏障與write屏障、acquire屏障、release屏障或者通用屏障配對。
CPU 1 CPU 2
=============== ===============
WRITE_ONCE(a, 1);
<write barrier>
WRITE_ONCE(b, 2); x = READ_ONCE(b);
<read barrier>
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(x, 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 barrier> \ <read barrier>
WRITE_ONCE(c, 3); } / \ { x = READ_ONCE(a);
WRITE_ONCE(d, 4); }---- --->{ y = READ_ONCE(b);
2.6 記憶體屏障舉例
第一,write屏障用作將STORE操作部分有序。請考慮以下操作順序:
CPU 1
=======================
STORE A = 1
STORE B = 2
STORE C = 3
<write barrier>
STORE D = 4
STORE E = 5
這個操作序列會按照順序被提交到記憶體一致性系統,而系統中的其他組件可以看到
{STORE A,STORE B,STORE C}集合都發生在{STORE D,STORE E}集合之前,而集合內部可能亂序。
+-------+ : :
| | +------+
| |------>| C=3 | } /\
| | : +------+ }----- \ -----> Events perceptible to
| | : | A=1 | } \/ the rest of the system
| | : +------+ }
| CPU 1 | : | B=2 | }
| | +------+ }
| | wwwwwwwwwwwwwwww } <--- At this point the write barrier
| | +------+ } requires all stores prior to the
| | : | E=5 | } barrier to be committed before
| | : +------+ } further stores may take place
| |------>| D=4 | }
| | +------+
+-------+ : :
|
| Sequence in which stores are committed to the
| memory system by CPU 1
V
第二,數據依賴屏障對有數據依賴關係的LOAD操作進行部分有序的限制。 考慮以下事件序列:
CPU 1 CPU 2
======================= =======================
{ B = 7; X = 9; Y = 8; C = &Y }
STORE A = 1
STORE B = 2
<write barrier>
STORE C = &B LOAD X
STORE D = 4 LOAD C (gets &B)
LOAD *C (reads B)
沒有干預的話, CPU 1的操作被CPU 2感知到的順序是隨機的, 儘管CPU 1執行了寫屏障:
+-------+ : : : :
| | +------+ +-------+ | Sequence of update
| |------>| B=2 |----- --->| Y->8 | | of perception on
| | : +------+ \ +-------+ | CPU 2
| CPU 1 | : | A=1 | \ --->| C->&Y | V
| | +------+ | +-------+
| | wwwwwwwwwwwwwwww | : :
| | +------+ | : :
| | : | C=&B |--- | : : +-------+
| | : +------+ \ | +-------+ | |
| |------>| D=4 | ----------->| C->&B |------>| |
| | +------+ | +-------+ | |
+-------+ : : | : : | |
| : : | |
| : : | CPU 2 |
| +-------+ | |
Apparently incorrect ---> | | B->7 |------>| |
perception of B (!) | +-------+ | |
| : : | |
| +-------+ | |
The load of X holds ---> \ | X->9 |------>| |
up the maintenance \ +-------+ | |
of coherence of B ----->| B->2 | +-------+
+-------+
: :
在上面的例子中, CPU 2看到的B的值是7, 儘管對LOADC(值應該是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
<write barrier>
STORE C = &B LOAD X
STORE D = 4 LOAD C (gets &B)
<data dependency barrier>
LOAD *C (reads B)
那麼下麵的情況將會發生:
+-------+ : : : :
| | +------+ +-------+
| |------>| B=2 |----- --->| Y->8 |
| | : +------+ \ +-------+
| CPU 1 | : | A=1 | \ --->| C->&Y |
| | +------+ | +-------+
| | wwwwwwwwwwwwwwww | : :
| | +------+ | : :
| | : | C=&B |--- | : : +-------+
| | : +------+ \ | +-------+ | |
| |------>| D=4 | ----------->| C->&B |------>| |
| | +------+ | +-------+ | |
+-------+ : : | : : | |
| : : | |
| : : | CPU 2 |
| +-------+ | |
| | X->9 |------>| |
| +-------+ | |
Makes sure all effects ---> \ ddddddddddddddddd | |
prior to the store of C \ +-------+ | |
are perceptible to ----->| B->2 |------>| |
subsequent loads +-------+ | |
: : +-------+
第三,讀取屏障用作LOAD上的部分順序。考慮如下事件序列:
CPU 1 CPU 2
======================= =======================
{ A = 0, B = 9 }
STORE A=1
<write barrier>
STORE B=2
LOAD B
LOAD A
在沒有干預的情況下,CPU 2可以選擇以某種隨機的順序感知CPU 1上的事件,儘管CPU 1發出了寫屏障:
+-------+ : : : :
| | +------+ +-------+
| |------>| A=1 |------ --->| A->0 |
| | +------+ \ +-------+
| CPU 1 | wwwwwwwwwwwwwwww \ --->| B->9 |
| | +------+ | +-------+
| |------>| B=2 |--- | : :
| | +------+ \ | : : +-------+
+-------+ : : \ | +-------+ | |
---------->| B->2 |------>| |
| +-------+ | CPU 2 |
| | A->0 |------>| |
| +-------+ | |
| : : +-------+
\ : :
\ +-------+
---->| A->1 |
+-------+
: :
但是, 如果在CPU 2的LOAD B和LOAD A之間增加一個讀屏障:
CPU 1 CPU 2
======================= =======================
{ A = 0, B = 9 }
STORE A=1
<write barrier>
STORE B=2
LOAD B
<read barrier>
LOAD A
那麼CPU 1的部分有序將正確的被CPU 2所感知:
+-------+ : : : :
| | +------+ +-------+
| |------>| A=1 |------ --->| A->0 |
| | +------+ \ +-------+
| CPU 1 | wwwwwwwwwwwwwwww \ --->| B->9 |
| | +------+ | +-------+
| |------>| B=2 |--- | : :
| | +------+ \ | : : +-------+
+-------+ : : \ | +-------+ | |
---------->| B->2 |------>| |
| +-------+ | CPU 2 |
| : : | |
| : : | |
At this point the read ----> \ rrrrrrrrrrrrrrrrr | |
barrier causes all effects \ +-------+ | |
prior to the storage of B ---->| A->1 |------>| |
to be perceptible to CPU 2 +-------+ | |
: : +-------+
為了更全面地說明這一點, 考慮一下如果代碼在讀屏障的兩邊都有一個LOAD A的話, 會發生
什麼:
CPU 1 CPU 2
======================= =======================
{ A = 0, B = 9 }
STORE A=1
<write barrier>
STORE B=2
LOAD B
LOAD A [first load of A]
<read barrier>
LOAD A [second load of A]
儘管兩次LOAD A都發生在LOAD B之後, 它們也可能得到不同的值:
+-------+ : : : :
| | +------+ +-------+
| |------>| A=1 |------ --->| A->0 |
| | +------+ \ +-------+
| CPU 1 | wwwwwwwwwwwwwwww \ --->| B->9 |
| | +------+ | +-------+
| |------>| B=2 |--- | : :
| | +------+ \ | : : +-------+
+-------+ : : \ | +-------+ | |
---------->| B->2 |------>| |
| +-------+ | CPU 2 |
| : : | |
| : : | |
| +-------+ | |
| | A->0 |------>| 1st |
| +-------+ | |
At this point the read ----> \ rrrrrrrrrrrrrrrrr | |
barrier causes all effects \ +-------+ | |
prior to the storage of B ---->| A->1 |------>| 2nd |
to be perceptible to CPU 2 +-------+ | |
: : +-------+
但是也可能CPU 2在讀屏障結束之前就感知到CPU 1對A的更新:
+-------+ : : : :
| | +------+ +-------+
| |------>| A=1 |------ --->| A->0 |
| | +------+ \ +-------+
| CPU 1 | wwwwwwwwwwwwwwww \ --->| B->9 |
| | +------+ | +-------+
| |------>| B=2 |--- | : :
| | +------+ \ | : : +-------+
+-------+ : : \ | +-------+ | |
---------->| B->2 |------>| |
| +-------+ | CPU 2 |
| : : | |
\ : : | |
\ +-------+ | |
---->| A->1 |------>| 1st |
+-------+ | |
rrrrrrrrrrrrrrrrr | |
+-------+ | |
| A->1 |------>| 2nd |
+-------+ | |
: : +-------+
這裡保證, 如果LOAD B得到的值是2的話, 第二個LOAD A總是能得到的值是1.
但是對於第一個LOAD A的值是沒有保證的,可能得到的值是0或者1.
2.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
- 這可能將表現為如下情況:
-
: +-------+
+-------+ | |
--->| B->2 |------>| |
+-------+ | CPU 2 |
: :DIVIDE | |
+-------+ | |
The CPU being busy doing a ---> --->| A->0 |~~~~ | |
division speculates on the +-------+ ~ | |
LOAD of A : : ~ | |
: :DIVIDE | |
: : ~ | |
Once the divisions are complete --> : : ~-->| |
the CPU can then perform the : : | |
LOAD with immediate effect : : +-------+
如果在第二個LOAD之前放一個讀屏障或數據依賴屏障:
CPU 1 CPU 2
======================= =======================
LOAD B
DIVIDE
DIVIDE
<read barrier>
LOAD A
這將迫使CPU對所推測的任何值進行更新檢查,這取決於所使用的屏障的類型。
如果沒有對推測的記憶體位置進行更改,那麼只會使用推測值:
: : +-------+
+-------+ | |
--->| B->2 |------>| |
+-------+ | CPU 2 |
: :DIVIDE | |
+-------+ | |
The CPU being busy doing a ---> --->| A->0 |~~~~ | |
division speculates on the +-------+ ~ | |
LOAD of A : : ~ | |
: :DIVIDE | |
: : ~ | |
: : ~ | |
rrrrrrrrrrrrrrrr~ | |
: : ~ | |
: : ~-->| |
: : | |
: : +-------+
但是如果有其他CPU更新或者刪除該值,則記憶體預取將失效,CPU重新載入該值:
: : +-------+
+-------+ | |
--->| B->2 |------>| |
+-------+ | CPU 2 |
: :DIVIDE | |
+-------+ | |
The CPU being busy doing a ---> --->| A->0 |~~~~ | |
division speculates on the +-------+ ~ | |
LOAD of A : : ~ | |
: :DIVIDE | |
: : ~ | |
: : ~ | |
rrrrrrrrrrrrrrrrr | |
+-------+ | |
The speculation is discarded ---> --->| A->1 |------>| |
and an updated value is +-------+ | |
retrieved : : +-------+
2.8 多副本原子性
多副本原子性是一個非常直觀的關於排序的概念,但實際的電腦系統並不總是提供這種特性。也就是說,一個給定的數據存儲在同一時間對所有cpu可見,或者,所有cpu對所有數據存儲可見的順序達成一致。然而,對完全多副本原子性的支持將拒絕有價值的硬體優化,因此一種稱為“其他多副本原子性”的較弱形式只保證給定的存儲在同一時間對所有其他cpu可見。本文檔的其餘部分將討論這種較弱的形式,但為了簡潔起見,我們將其簡單稱為“多副本原子性”。
下麵的例子演示了多副本的原子性:
CPU 1 CPU 2 CPU 3
======================= ======================= =======================
{ X = 0, Y = 0 }
STORE X=1 r1=LOAD X (reads 1) LOAD Y (reads 1)
<general barrier> <read barrier>
STORE Y=r1 LOAD X
假設CPU 2的LOAD X返回1,並將其STORE到Y,而CPU 3的LOAD Y返回1。這表明CPU 1的STORE X先於CPU 2的LOAD X ,CPU 2的STORE Y先於CPU 3的LOAD Y。此外,記憶體屏障保證CPU 2在STORE Y之前執行它的LOAD X,CPU 3在LOAD X之前從LOAD Y 。那麼問題是“CPU 3的LOAD X可以返回0嗎?”
因為CPU 3的LOAD X在某種意義上是在CPU 2的LOAD X之後,所以很自然地認為CPU 3的LOAD X必然返回1。這個期望來自於多副本的原子性:如果CPU B上執行的load語句跟CPU a上執行的load語句是同一個變數(而CPU a一開始並沒有存儲它讀取的值),那麼在多副本原子系統上,CPU B的load語句必須返回與CPU a的load語句相同的值,或者稍後的某個值。但Linux內核並不要求系統是多副本原子性的。
上面例子中使用的通用記憶體屏障彌補了多副本原子性的不足。在這個例子中,如果CPU 2的LOAD X返回1,CPU 3的LOAD Y返回1,那麼CPU 3的LOAD X肯定也返回1。
然而,依賴關係、讀屏障和寫屏障並不總是能夠補償非多副本原子性。例如,假設上面的例子去掉了CPU 2的一般屏障,只留下如下所示的數據依賴:
CPU 1 CPU 2 CPU 3
======================= ======================= =======================
{ X = 0, Y = 0 }
STORE X=1 r1=LOAD X (reads 1) LOAD Y (reads 1)
<data dependency> <read barrier>
STORE Y=r1 LOAD X (reads 0)
這種替換會破壞多副本原子性:在這個例子中,CPU 2的LOAD X返回1、CPU 3的LOAD Y返回1、CPU 3的LOAD X返回0都是完全合法的。
關鍵在於,儘管CPU 2的數據依賴會對其LOAD和STRORE進行排序,但它並不保證CPU 1的STRORE也會排序。因此,如果這個例子運行在非多副本原子系統上,CPU 1和CPU 2共用一個存儲緩衝區或某個緩存級別,那麼CPU 2可能會提前訪問CPU 1的STORE操作。因而需要通用屏障來確保所有cpu對多次訪問的組合順序達成一致。
通用屏障不僅可以彌補非多副本的原子性,還可以生成額外的順序,以確保所有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()之間的release-acquire關係,cpu1()必須看到cpu0()的寫操作,因此禁止出現以下結果:
r1 == 1 && r5 == 0
然而,release-acquire鏈提供的順序對於參與該鏈的cpu來說是本地的,並且不適用於cpu3(),至少除了stores。因此,可能產生以下結果:
不理解這種情況發生的原因
r0 == 0 && r1 == 1 && r2 == 1 && r3 == 0 && r4 == 0
除此之外,以下結果也是可能的:
不理解這種情況發生的原因
r0 == 0 && r1 == 1 && r2 == 1 && r3 == 0 && r4 == 0 && r5 == 1
儘管cpu0()、cpu1()和cpu2()會按順序查看它們各自的讀寫操作,但未參與release-acquire鏈的cpu可能不同意這種順序。這種分歧源於這樣一個事實:在所有情況下,用於實現smp_load_acquire()和smp_store_release()的弱記憶體壁壘指令都不需要對之前的STORE和之後的LOAD進行排序。這意味著cpu3()可以將cpu0()的WRITE_ONCE(u, 1);視為發生在cpu1()的 READ_ONCE(v);之後,即使cpu0()和cpu1()都認為這兩個操作是按照預期的順序進行的。
但請記住,smp_load_acquire()不是魔法。特別是,它只是從它的排序參數中讀取。它不會確保讀取任何特定的值。因此,可能產生以下結果:
r0 == 0 && r1 == 0 && r2 == 0 && r5 == 0
請註意,這種結果甚至可能發生在神話中的順序一致系統中,其中沒有任何東西是重新排序的。
重申一下,如果你的代碼需要對所有操作進行完全排序,請始終使用通用屏障。
3 內核中的顯式記憶體屏障
Linux內核具有各種各樣的屏障,在不同層次上起作用:
- 編譯優化屏障
- CPU記憶體屏障
3.1 編譯優化屏障
Linux內核有一個明確的編譯器屏障功能,可以防止編譯器將屏障任意一側的記憶體訪問移動到另一側:
barrier();
這是通用的屏障 - 沒有read-read或write-write的屏障變體。然而,READ_ONCE()和WRITE_ONCE()可以被認為是僅影響由READ_ONCE()或WRITE_ONCE()標記的特定訪問的barrier()的弱形式。
barrier()函數具有以下效果:
- 阻止編譯器將barrier()之後的記憶體訪問重新排序到barrier()之前的任何記憶體訪問之前。這個性質的一個示例用途是簡化中斷處理程式代碼與被中斷代碼之間的通信。
- 在迴圈內部,強制編譯器每次執行迴圈時都載入迴圈條件語句中使用的變數。
READ_ONCE()和WRITE_ONCE()函數可以防止任何優化,儘管這些優化在單線程代碼中是完全安全的,但在併發代碼中可能是致命的。下麵是這類優化的一些例子:
3.1.1 重新排序LOAD和STORE
編譯器有權利對同一個變數重新排序LOAD和STORE,在某些情況下,CPU也有權利對同一個變數重新排序載入。這意味著下麵的代碼:
a[0] = x;
a[1] = x;
可能導致存儲在[1]中的x值比存儲在[0]中的x值更舊。防止編譯器和CPU這樣做,如下所示:
a[0] = READ_ONCE(x);
a[1] = READ_ONCE(x);
簡而言之,READ_ONCE()和WRITE_ONCE()為多個cpu對同一個變數的訪問提供了緩存一致性。
3.1.2 並來自同一個變數的連續LOAD
編譯器有權合併來自同一個變數的連續LOAD。這樣的合併會導致編譯器“優化”以下代碼:
while (tmp = a)
do_something_with(tmp);
下麵這段代碼雖然在某種意義上適合單線程代碼,但幾乎肯定不是開發人員想要的:
if (tmp = a)
for (;;)
do_something_with(tmp);
使用READ_ONCE()來防止編譯器對你這樣做:
while (tmp = READ_ONCE(a))
do_something_with(tmp);
3.1.3 重新LOAD變數
編譯器有重新載入變數的權利,例如,在高寄存器壓力導致編譯器無法將所有感興趣的數據保存在寄存器中時。因此,編譯器可能會根據我們之前的例子優化變數tmp
:
while (tmp = a)
do_something_with(tmp);
這可能導致以下代碼,這在單線程代碼中是完全安全的,但在併發代碼中可能是致命的:
while (a)
do_something_with(a);
例如,這段代碼的優化版本在執行“while”語句和調用do_something_with()之間修改了變數a的情況下,可能會向do_something_with()傳遞一個0。
再次,使用READ_ONCE()來防止編譯器這樣做:
while (tmp = READ_ONCE(a))
do_something_with(tmp);
註意,如果編譯器運行時缺少寄存器,它可能會將tmp保存到堆棧上。這種保存和稍後恢復的開銷是編譯器重新載入變數的原因。這樣做對於單線程代碼是完全安全的,所以您需要告訴編譯器在哪些情況下不安全。
3.1.4 省略LOAD
如果編譯器知道裝載的值是多少,它就有權利完全忽略裝載。例如,如果編譯器可以證明變數'a'的值總是0,它可以優化這段代碼:
while (tmp = a)
do_something_with(tmp);
優化成這樣:
do { } while (0);
這種轉換是單線程代碼的勝利,因為它擺脫了一個LOAD和一個分支語句。
問題是編譯器進行了假設,假設當前的CPU是唯一一個更新變數'a'的CPU。
如果變數'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'載入。)
3.1.5 省略STORE
類似地,如果編譯器知道變數已經具有存儲的值,則在編譯器有許可權省略STORE操作。
同樣,編譯器假定當前的CPU是唯一STORE該變數的CPU,這可能導致編譯器對共用變數做錯了事情。
例如,假設您有以下內容:
a = 0;
... Code that does not store to variable a ...
a = 0;
編譯器看到變數'a'的值已經為零,所以可能會省略第二個STORE操作。
如果其他CPU可能同時STORE“a”,這將是一個致命的錯誤。
使用WRITE_ONCE()來防止編譯器發生這種錯誤的猜測:
WRITE_ONCE(a, 0);
... Code that does not store to variable a ...
WRITE_ONCE(a, 0);
3.1.6 重排記憶體訪問
編譯器有權重新排序記憶體訪問,除非你告訴它不應該這麼做。
例如,考慮過程級代碼和中斷處理程式之間