LINUX內核內存屏障
=================
LINUX內核內存屏障
=================
By: David Howells <[email protected]>
Paul E. McKenney <[email protected]>
譯: kouu <[email protected]>
出處: Linux內核文檔 -- Documentation/memory-barriers.txt
目錄:
(*) 內存訪問抽象模型.
- 操作設備.
- 保證.
(*) 什麼是內存屏障?
- 各式各樣的內存屏障.
- 關於內存屏障, 不能假定什麼?
- 數據依賴屏障.
- 控制依賴.
- SMP內存屏障的配對使用.
- 內存屏障舉例.
- 讀內存屏障與內存預取.
(*) 內核中顯式的內存屏障.
- 編譯優化屏障.
- CPU內存屏障.
- MMIO寫屏障.
(*) 內核中隱式的內存屏障.
- 鎖相關函數.
- 禁止中斷函數.
- 睡眠喚醒函數.
- 其他函數.
(*) 跨CPU的鎖的屏障作用.
- 鎖與內存訪問.
- 鎖與IO訪問.
(*) 什麼地方需要內存屏障?
- 處理器間交互.
- 原子操作.
- 訪問設備.
- 中斷.
(*) 內核中I/O屏障的作用.
(*) 最小限度有序的假想模型.
(*) CPU cache的影響.
- Cache一致性.
- Cache一致性與DMA.
- Cache一致性與MMIO.
(*) CPU所能做到的.
- 特別值得一提的Alpha處理器.
(*) 使用示例.
- 環型緩衝區.
(*) 引用.
================
內存訪問抽象模型
================
考慮如下抽象系統模型:
: :
: :
: :
+-------+ : +--------+ : +-------+
| | : | | : | |
| | : | | : | |
| CPU 1 |<----->| 內存 |<----->| CPU 2 |
| | : | | : | |
| | : | | : | |
+-------+ : +--------+ : +-------+
^ : ^ : ^
| : | : |
| : | : |
| : v : |
| : +--------+ : |
| : | | : |
| : | | : |
+---------->| 設備 |<----------+
: | | :
: | | :
: +--------+ :
: :
假設每個CPU都分別運行着一個會觸發內存訪問操作的程序. 對於這樣一個CPU, 其內存訪問
順序是非常鬆散的, 在保證程序上下文邏輯關係的前提下, CPU可以按它所喜歡的順序來執
行內存操作. 類似的, 編譯器也可以將它輸出的指令安排成任何它喜歡的順序, 只要保證不
影響程序表面的執行邏輯.
(譯註:
內存屏障是爲應付內存訪問操作的亂序執行而生的. 那麼, 內存訪問爲什麼會亂序呢? 這裏
先簡要介紹一下:
現在的CPU一般採用流水線來執行指令. 一個指令的執行被分成: 取指, 譯碼, 訪存, 執行,
寫回, 等若干個階段.
指令流水線並不是串行化的, 並不會因爲一個耗時很長的指令在"執行"階段呆很長時間, 而
導致後續的指令都卡在"執行"之前的階段上.
相反, 流水線中的多個指令是可以同時處於一個階段的, 只要CPU內部相應的處理部件未被
佔滿. 比如說CPU有一個加法器和一個除法器, 那麼一條加法指令和一條除法指令就可能同
時處於"執行"階段, 而兩條加法指令在"執行"階段就只能串行工作.
這樣一來, 亂序可能就產生了. 比如一條加法指令出現在一條除法指令的後面, 但是由於除
法的執行時間很長, 在它執行完之前, 加法可能先執行完了. 再比如兩條訪存指令, 可能由
於第二條指令命中了cache(或其他原因)而導致它先於第一條指令完成.
一般情況下, 指令亂序並不是CPU在執行指令之前刻意去調整順序. CPU總是順序的去內存裏
面取指令, 然後將其順序的放入指令流水線. 但是指令執行時的各種條件, 指令與指令之間
的相互影響, 可能導致順序放入流水線的指令, 最終亂序執行完成. 這就是所謂的"順序流
入, 亂序流出".
指令流水線除了在資源不足的情況下會卡住之外(如前所述的一個加法器應付兩條加法指令)
, 指令之間的相關性纔是導致流水線阻塞的主要原因.
下文中也會多次提到, CPU的亂序執行並不是任意的亂序, 而必須保證上下文依賴邏輯的正
確性. 比如: a++; b=f(a); 由於b=f(a)這條指令依賴於第一條指令(a++)的執行結果, 所以
b=f(a)將在"執行"階段之前被阻塞, 直到a++的執行結果被生成出來.
如果兩條像這樣有依賴關係的指令捱得很近, 後一條指令必定會因爲等待前一條執行的結果
, 而在流水線中阻塞很久. 而編譯器的亂序, 作爲編譯優化的一種手段, 則試圖通過指令重
排將這樣的兩條指令拉開距離, 以至於後一條指令執行的時候前一條指令結果已經得到了,
那麼也就不再需要阻塞等待了.
相比於CPU的亂序, 編譯器的亂序纔是真正對指令順序做了調整. 但是編譯器的亂序也必須
保證程序上下文的依賴邏輯.
由於指令執行存在這樣的亂序, 那麼自然, 由指令執行而引發的內存訪問勢必也可能亂序.
)
在上面的圖示中, 一個CPU執行內存操作所產生的影響, 一直要到該操作穿越該CPU與系統中
其他部分的界面(見圖中的虛線)之後, 才能被其他部分所感知.
舉例來說, 考慮如下的操作序列:
CPU 1 CPU 2
=============== ===============
{ A == 1; B == 2 }
A = 3; x = A;
B = 4; y = B;
這一組訪問指令在內存系統(見上圖的中間部分)上生效的順序, 可以有24種不同的組合:
STORE A=3, STORE B=4, x=LOAD A->3, y=LOAD B->4
STORE A=3, STORE B=4, y=LOAD B->4, x=LOAD A->3
STORE A=3, x=LOAD A->3, STORE B=4, y=LOAD B->4
STORE A=3, x=LOAD A->3, y=LOAD B->2, STORE B=4
STORE A=3, y=LOAD B->2, STORE B=4, x=LOAD A->3
STORE A=3, y=LOAD B->2, x=LOAD A->3, STORE B=4
STORE B=4, STORE A=3, x=LOAD A->3, y=LOAD B->4
STORE B=4, ...
...
然後這就產生四種不同組合的結果值:
x == 1, y == 2
x == 1, y == 4
x == 3, y == 2
x == 3, y == 4
甚至於, 一個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;
這裏有一處明顯的數據依賴, 因爲在CPU2上, LOAD到D裏面的值依賴於從P獲取到的地址. 在
操作序列的最後, 下面的幾種結果都是有可能出現的:
(Q == &A) 且 (D == 1)
(Q == &B) 且 (D == 2)
(Q == &B) 且 (D == 4)
注意, CPU2決不會將C的值LOAD到D, 因爲CPU保證在將P的值裝載到Q之後纔會執行對*Q的
LOAD操作(譯註: 因爲存在數據依賴).
操作設備
--------
對於一些設備, 其控制寄存器被映射到一組內存地址集合上, 而這些控制寄存器被訪問的順
序是至關重要的. 假設, 一個以太網卡擁有一些內部寄存器, 通過一個地址端口寄存器(A)
和一個數據端口寄存器(D)來訪問它們. 要讀取編號爲5的內部寄存器, 可能使用如下代碼:
*A = 5;
x = *D;
但是這可能會表現爲以下兩個序列之一(譯註: 因爲從程序表面看, A和D是不存在依賴的):
STORE *A = 5, x = LOAD *D
x = LOAD *D, STORE *A = 5
其中的第二種幾乎肯定會導致錯誤, 因爲它在讀取寄存器之後才設置寄存器的編號.
保證
----
對於一個CPU, 它最低限度會提供如下的保證:
(*) 對於一個CPU, 在它上面出現的有上下文依賴關係的內存訪問將被按順序執行. 這意味
着:
Q = P; D = *Q;
CPU會順序執行以下訪存:
Q = LOAD P, D = LOAD *Q
並且總是按這樣的順序.
(*) 對於一個CPU, 重疊的LOAD和STORE操作將被按順序執行. 這意味着:
a = *X; *X = b;
CPU只會按以下順序執行訪存:
a = LOAD *X, STORE *X = b
同樣, 對於:
*X = c; d = *X;
CPU只會按以下順序執行訪存:
STORE *X = c, d = LOAD *X
(如果LOAD和STORE的目標指向同一塊內存地址, 則認爲是重疊).
還有一些事情是必須被假定或者必須不被假定的:
(*) 必須不能假定無關的LOAD和STORE會按給定的順序被執行. 這意味着:
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; Y = *A;
可能會得到如下幾種執行序列之一:
STORE *A = X; Y = LOAD *A;
STORE *A = Y = X;
===============
什麼是內存屏障?
===============
正如上面所說, 無關的內存操作會被按隨機順序有效的得到執行, 但是在CPU與CPU交互時或
CPU與IO設備交互時, 這可能會成爲問題. 我們需要一些手段來干預編譯器和CPU, 使其限制
指令順序.
內存屏障就是這樣的干預手段. 他們能保證處於內存屏障兩邊的內存操作滿足部分有序. (
譯註: 這裏"部分有序"的意思是, 內存屏障之前的操作都會先於屏障之後的操作, 但是如果
幾個操作出現在屏障的同一邊, 則不保證它們的順序. 這一點下文將多次提到.)
這樣的強制措施是非常重要的, 因爲系統中的CPU和其他設備可以使用各種各樣的策略來提
高性能, 包括對內存操作的亂序, 延遲和合並執行; 預取; 投機性的分支預測和各種緩存.
內存屏障用於禁用或抑制這些策略, 使代碼能夠清楚的控制多個CPU和/或設備的交互.
各式各樣的內存屏障
------------------
內存屏障有四種基本類型:
(1) 寫(STORE)內存屏障.
寫內存屏障提供這樣的保證: 所有出現在屏障之前的STORE操作都將先於所有出現在屏
障之後的STORE操作被系統中的其他組件所感知.
寫屏障僅保證針對STORE操作的部分有序; 不要求對LOAD操作產生影響.
隨着時間的推移, 一個CPU提交的STORE操作序列將被存儲系統所感知. 所有在寫屏障
之前的STORE操作將先於所有在寫屏障之後的STORE操作出現在被感知的序列中.
[!] 注意, 寫屏障一般需要與讀屏障或數據依賴屏障配對使用; 參閱"SMP內存屏障配
對"章節. (譯註: 因爲寫屏障只保證自己提交的順序, 而無法干預其他代碼讀內
存的順序. 所以配對使用很重要. 其他類型的屏障亦是同理.)
(2) 數據依賴屏障.
數據依賴屏障是讀屏障的弱化版本. 假設有兩個LOAD操作的場景, 其中第二個LOAD操
作的結果依賴於第一個操作(比如, 第一個LOAD獲取地址, 而第二個LOAD使用該地址去
取數據), 數據依賴屏障確保在第一個LOAD獲取的地址被用於訪問之前, 第二個LOAD的
目標內存已經更新.
(譯註: 因爲第二個LOAD要使用第一個LOAD的結果來作爲LOAD的目標, 這裏存在着數
據依賴. 由前面的"保證"章節可知, 第一個LOAD必定會在第二個LOAD之前執行, 不需
要使用讀屏障來保證順序, 只需要使用數據依賴屏障來保證內存已刷新.)
數據依賴屏障僅保證針對相互依賴的LOAD操作的部分有序; 不要求對STORE操作,
獨立的LOAD操作, 或重疊的LOAD操作產生影響.
正如(1)中所提到的, 在一個CPU看來, 系統中的其他CPU提交到內存系統的STORE操作
序列在某一時刻可以被其感知到. 而在該CPU上觸發的數據依賴屏障將保證, 對於在屏
障之前發生的LOAD操作, 如果一個LOAD操作的目標被其他CPU的STORE操作所修改, 那
麼在屏障完成之時, 這個對應的STORE操作之前的所有STORE操作所產生的影響, 將被
數據依賴屏障之後執行的LOAD操作所感知.
參閱"內存屏障舉例"章節所描述的時序圖.
[!] 注意, 對第一個LOAD的依賴的確是一個數據依賴而不是控制依賴. 而如果第二個
LOAD的地址依賴於第一個LOAD, 但並不是通過實際加載的地址本身這樣的依賴條
件, 那麼這就是控制依賴, 需要一個完整的讀屏障或更強的屏障. 參閱"控制依
賴"相關章節.
[!] 注意, 數據依賴屏障一般要跟寫屏障配對使用; 參閱"SMP內存屏障的配對使用"章
節.
(3) 讀(LOAD)內存屏障.
讀屏障包含數據依賴屏障的功能, 並且保證所有出現在屏障之前的LOAD操作都將先於
所有出現在屏障之後的LOAD操作被系統中的其他組件所感知.
讀屏障僅保證針對LOAD操作的部分有序; 不要求對STORE操作產生影響.
讀內存屏障隱含了數據依賴屏障, 因此可以用於替代它們.
[!] 注意, 讀屏障一般要跟寫屏障配對使用; 參閱"SMP內存屏障的配對使用"章節.
(4) 通用內存屏障.
通用內存屏障保證所有出現在屏障之前的LOAD和STORE操作都將先於所有出現在屏障
之後的LOAD和STORE操作被系統中的其他組件所感知.
通用內存屏障是針對LOAD和STORE操作的部分有序.
通用內存屏障隱含了讀屏障和寫屏障, 因此可以用於替代它們.
內存屏障還有兩種隱式類型:
(5) LOCK操作.
它的作用相當於一個單向滲透屏障. 它保證所有出現在LOCK之後的內存操作都將在
LOCK操作被系統中的其他組件所感知之後才能發生.
出現在LOCK之前的內存操作可能在LOCK完成之後才發生.
LOCK操作總是跟UNLOCK操作配對出現的.
(6) UNLOCK操作.
它的作用也相當於一個單向滲透屏障. 它保證所有出現在UNLOCK之前的內存操作都將
在UNLOCK操作被系統中的其他組件所感知之前發生.
出現在UNLOCK之後的內存操作可能在UNLOCK完成之前就發生了.
需要保證LOCK和UNLOCK操作嚴格按照相互影響的正確順序出現.
(譯註: LOCK和UNLOCK的這種單向屏障作用, 確保臨界區內的訪存操作不能跑到臨界區
外, 否則就起不到"保護"作用了.)
使用LOCK和UNLOCK之後, 一般就不再需要其他內存屏障了(但是注意"MMIO寫屏障"章節
中所提到的例外).
只有在存在多CPU交互或CPU與設備交互的情況下才可能需要用到內存屏障. 如果可以確保某
段代碼中不存在這樣的交互, 那麼這段代碼就不需要使用內存屏障. (譯註: CPU亂序執行指
令, 同樣會導致寄存器的存取順序被打亂, 但是爲什麼不需要寄存器屏障呢? 就是因爲寄存
器是CPU私有的, 不存在跟其他CPU或設備的交互.)
注意, 對於前面提到的最低限度保證. 不同的體系結構可能提供更多的保證, 但是在特定體
繫結構的代碼之外, 不能依賴於這些額外的保證.
關於內存屏障, 不能假定什麼?
---------------------------
Linux內核的內存屏障不保證下面這些事情:
(*) 在內存屏障之前出現的內存訪問不保證在內存屏障指令完成之前完成; 內存屏障相當
於在該CPU的訪問隊列中畫一條線, 使得相關訪存類型的請求不能相互跨越. (譯註:
用於實現內存屏障的指令, 其本身並不作爲參考對象, 其兩邊的訪存操作才被當作參
考對象. 所以屏障指令執行完成並不表示出現在屏障之前的訪存操作已經完成. 而如
果屏障之後的某一個訪存操作已經完成, 則屏障之前的所有訪存操作必定都已經完成
了.)
(*) 在一個CPU上執行的內存屏障不保證會直接影響其他系統中的CPU或硬件設備. 只會間
接影響到第二個CPU感知第一個CPU產生訪存效果的順序, 不過請看下一點:
(*) 不能保證一個CPU能夠按順序看到另一個CPU的訪存效果, 即使另一個CPU使用了內存屏
障, 除非這個CPU也使用了與之配對的內存屏障(參閱"SMP內存屏障的配對使用"章節).
(*) 不保證一些與CPU相關的硬件不會亂序訪存. CPU cache一致性機構會在CPU之間傳播內
存屏障所帶來的間接影響, 但是可能不是按順序的.
[*] 更多關於總線主控DMA和一致性的問題請參閱:
Documentation/PCI/pci.txt
Documentation/PCI/PCI-DMA-mapping.txt
Documentation/DMA-API.txt
數據依賴屏障
------------
數據依賴屏障的使用需求有點微妙, 並不總是很明顯就能看出需要他們. 爲了說明這一點,
考慮如下的操作序列:
CPU 1 CPU 2
=============== ===============
{ A == 1, B == 2, C = 3, P == &A, Q == &C }
B = 4;
<寫屏障>
P = &B
Q = P;
D = *Q;
這裏有明顯的數據依賴, 在序列執行完之後, Q的值一定是&A和&B之一, 也就是:
(Q == &A) 那麼 (D == 1)
(Q == &B) 那麼 (D == 4)
但是! CPU 2可能在看到P被更新之後, 纔看到B被更新, 這就導致下面的情況:
(Q == &B) 且 (D == 2) ????
雖然這看起來似乎是一個一致性錯誤或邏輯關係錯誤, 但其實不是, 並且在一些真實的CPU
中就能看到這樣的行爲(就比如DEC Alpha).
爲了解決這個問題, 必須在取地址和取數據之間插入一個數據依賴或更強的屏障:
CPU 1 CPU 2
=============== ===============
{ A == 1, B == 2, C = 3, P == &A, Q == &C }
B = 4;
<寫屏障>
P = &B
Q = P;
<數據依賴屏障>
D = *Q;
這將強制最終結果是前兩種情況之一, 而避免出現第三種情況.
[!] 注意, 這種非常違反直覺的情況最容易出現在cache分列的機器上, 比如, 一個cache組
處理偶數號的cache行, 另一個cache組處理奇數號的cache行. P指針可能存儲在奇數號
的cache行中, 而B的值可能存儲在偶數號的cache行中. 這樣一來, 如果執行讀操作的
CPU的偶數號cache組非常繁忙, 而奇數號cache組空閒, 它就可能看到P已被更新成新值
(&B), 而B還是舊值(2).
另一個可能需要數據依賴屏障的例子是, 從內存讀取一個數值, 用於計算數組的訪問偏移:
CPU 1 CPU 2
=============== ===============
{ M[0] == 1, M[1] == 2, M[3] = 3, P == 0, Q == 3 }
M[1] = 4;
<寫屏障>
P = 1
Q = P;
<數據依賴屏障>
D = M[Q];
數據依賴屏障對於RCU非常重要, 舉例來說. 參閱include/linux/rcupdate.h文件中的
rcu_dereference()函數. 這個函數使得當前RCU指針指向的對象被替換成新的對象時, 不會
發生新對象尚未初始化完成的情況. (譯註: 更新RCU對象時, 一般步驟是: 1-爲新對象分配
空間; 2-初始化新對象; 3-調用rcu_dereference()函數, 將對象指針指到新的對象上, 這
就意味着新的對象已生效. 這個過程中如果出現亂序訪存, 可能導致對象指針的更新發生在
新對象初始化完成之前. 也就是說, 新對象尚未初始化完成就已經生效了. 那麼別的CPU就
可能引用到一個尚未初始化完成的新對象, 從而出現錯誤.)
更詳盡的例子請參閱"Cache一致性"章節.
控制依賴
--------
控制依賴需要使用一個完整的讀內存屏障, 簡單的數據依賴屏障不能使其正確工作. 考慮
下面的代碼:
q = &a;
if (p)
q = &b;
<數據依賴屏障>
x = *q;
這段代碼可能達不到預期的效果, 因爲這裏其實並不是數據依賴, 而是控制依賴, CPU可能
試圖通過提前預測結果而對"if (p)"進行短路. 在這樣的情況下, 需要的是:
q = &a;
if (p)
q = &b;
<讀屏障>
x = *q;
(譯註:
例如:
CPU 1 CPU 2
=============== ===============
{ a == 1, b == 2, p == 0}
a = 3;
b = 4;
<寫屏障>
p = 1;
q = &a;
if (p)
q = &b;
<數據依賴屏障>
x = *q;
CPU 1上的寫屏障是爲了保證這樣的邏輯: 如果p == 1, 那麼必定有a == 3 && b == 4.
但是到了CPU 2, 可能p的值已更新(==1), 而a和b的值未更新, 那麼這時數據依賴屏障可以
起作用, 確保x = *q時a和b的值更新. 因爲從代碼邏輯上說, q跟a或b是有所依賴的, 數據
依賴屏障能保證這些有依賴關係的值都已更新.
然而, 換一個寫法:
CPU 1 CPU 2
=============== ===============
{ a == 1, b == 2, p == 0}
p = 1;
<寫屏障>
a = 3;
b = 4;
q = &a;
if (p)
q = &b;
<讀屏障>
x = *q;
CPU 1上的寫屏障是爲了保證這樣的邏輯: 如果a == 3 || b == 4, 那麼必定有p == 1.
但是到了CPU 2, 可能a或b的值已更新, 而p的值未更新. 那麼這時使用數據依賴屏障就不能
保證p的更新. 因爲從代碼邏輯上說, p跟任何人都沒有依賴關係. 這時必須使用讀屏障, 以
確保x = *q之前, p被更新.
原文中"短路"的意思就是, 由於p沒有數據依賴關係, CPU可以早早獲得它的值, 而不必考慮
更新.)
SMP內存屏障的配對使用
---------------------
在處理CPU與CPU的交互時, 對應類型的內存屏障總是應該配對使用. 缺乏適當配對的使用基
本上可以肯定是錯誤的.
一個寫屏障總是與一個數據依賴屏障或讀屏障相配對, 雖然通用屏障也可行. 類似的, 一個
讀屏障或數據依賴屏障也總是與一個寫屏障相配對, 儘管一個通用屏障也同樣可行:
CPU 1 CPU 2
=============== ===============
a = 1;
<寫屏障>
b = 2; x = b;
<讀屏障>
y = a;
或:
CPU 1 CPU 2
=============== ===============
a = 1;
<寫屏障>
b = &a; x = b;
<數據依賴屏障>
y = *x;
基本上, 讀屏障總是需要用在這些地方的, 儘管可以使用"弱"類型.
[!] 注意, 在寫屏障之前出現的STORE操作通常總是期望匹配讀屏障或數據依賴屏障之後出
現的LOAD操作, 反之亦然:
CPU 1 CPU 2
=============== ===============
a = 1; }---- --->{ v = c
b = 2; } \ / { w = d
<寫屏障> \ <讀屏障>
c = 3; } / \ { x = a;
d = 4; }---- --->{ y = b;
內存屏障舉例
------------
首先, 寫屏障用作部分有序的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 }的組合之前, 而組合
內部可能亂序:
+-------+ : :
| | +------+
| |------>| C=3 | } /\
| | : +------+ }----- \ -----> 操作被系統中的其他
| | : | A=1 | } \/ 組件所感知
| | : +------+ }
| CPU 1 | : | B=2 | }
| | +------+ }
| | wwwwwwwwwwwwwwww } <--- 在這一時刻, 寫屏障要求在它之
| | +------+ } 前出現的STORE操作都先於在它
| | : | E=5 | } 之後出現的STORE操作被提交
| | : +------+ }
| |------>| D=4 | }
| | +------+
+-------+ : :
|
| CPU 1發起的STORE操作被提交到內存系統的順序
|
V
其次, 數據依賴屏障用作部分有序的數據依賴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 (得到&B)
LOAD *C (讀取B)
沒有干預的話, CPU 1的操作被CPU 2感知到的順序是隨機的, 儘管CPU 1執行了寫屏障:
+-------+ : : : :
| | +------+ +-------+ | CPU 2所看到的
| |------>| B=2 |----- --->| Y->8 | | 更新序列
| | : +------+ \ +-------+ |
| CPU 1 | : | A=1 | \ --->| C->&Y | V
| | +------+ | +-------+
| | wwwwwwwwwwwwwwww | : :
| | +------+ | : :
| | : | C=&B |--- | : : +-------+
| | : +------+ \ | +-------+ | |
| |------>| D=4 | ----------->| C->&B |------>| |
| | +------+ | +-------+ | |
+-------+ : : | : : | |
| : : | |
| : : | CPU 2 |
| +-------+ | |
對B的取值顯然不正確 ---> | | B->7 |------>| |
| +-------+ | |
| : : | |
| +-------+ | |
對X的LOAD延誤了B的 ---> \ | X->9 |------>| |
一致性更新 \ +-------+ | |
----->| B->2 | +-------+
+-------+
: :
在上面的例子中, CPU 2看到的B的值是7, 儘管對*C(值應該是B)的LOAD發生在對C的LOAD之
後.
但是, 如果一個數據依賴屏障被放到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 (獲得&B)
<數據依賴屏障>
LOAD *C (讀取B)
那麼下面的情況將會發生:
+-------+ : : : :
| | +------+ +-------+
| |------>| B=2 |----- --->| Y->8 |
| | : +------+ \ +-------+
| CPU 1 | : | A=1 | \ --->| C->&Y |
| | +------+ | +-------+
| | wwwwwwwwwwwwwwww | : :
| | +------+ | : :
| | : | C=&B |--- | : : +-------+
| | : +------+ \ | +-------+ | |
| |------>| D=4 | ----------->| C->&B |------>| |
| | +------+ | +-------+ | |
+-------+ : : | : : | |
| : : | |
| : : | CPU 2 |
| +-------+ | |
| | X->9 |------>| |
| +-------+ | |
確保STORE C之前的影響 ---> \ ddddddddddddddddd | |
都被後續的LOAD操作感 \ +-------+ | |
知到 ----->| B->2 |------>| |
+-------+ | |
: : +-------+
第三, 讀屏障用作部分有序的LOAD操作. 考慮如下事件序列:
CPU 1 CPU 2
======================= =======================
{ A = 0, B = 9 }
STORE A=1
<寫屏障>
STORE B=2
LOAD B
LOAD A
沒有干預的話, CPU 1的操作被CPU 2感知到的順序是隨機的, 儘管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
<寫屏障>
STORE B=2
LOAD B
<讀屏障>
LOAD A
那麼CPU 1所施加的部分有序將正確的被CPU 2所感知:
+-------+ : : : :
| | +------+ +-------+
| |------>| A=1 |------ --->| A->0 |
| | +------+ \ +-------+
| CPU 1 | wwwwwwwwwwwwwwww \ --->| B->9 |
| | +------+ | +-------+
| |------>| B=2 |--- | : :
| | +------+ \ | : : +-------+
+-------+ : : \ | +-------+ | |
---------->| B->2 |------>| |
| +-------+ | CPU 2 |
| : : | |
| : : | |
在這一時刻, 讀屏障導致 ----> \ rrrrrrrrrrrrrrrrr | |
STORE B之前的影響都被 \ +-------+ | |
CPU 2所感知 ---->| A->1 |------>| |
+-------+ | |
: : +-------+
爲了更全面地說明這一點, 考慮一下如果代碼在讀屏障的兩邊都有一個LOAD A的話, 會發生
什麼:
CPU 1 CPU 2
======================= =======================
{ A = 0, B = 9 }
STORE A=1
<寫屏障>
STORE B=2
LOAD B
LOAD A [第一次LOAD A]
<讀屏障>
LOAD A [第二次LOAD A]
儘管兩次LOAD A都發生在LOAD B之後, 它們也可能得到不同的值:
+-------+ : : : :
| | +------+ +-------+
| |------>| A=1 |------ --->| A->0 |
| | +------+ \ +-------+
| CPU 1 | wwwwwwwwwwwwwwww \ --->| B->9 |
| | +------+ | +-------+
| |------>| B=2 |--- | : :
| | +------+ \ | : : +-------+
+-------+ : : \ | +-------+ | |
---------->| B->2 |------>| |
| +-------+ | CPU 2 |
| : : | |
| : : | |
| +-------+ | |
| | A->0 |------>| 一次 |
| +-------+ | |
在這一時刻, 讀屏障導致 ----> \ rrrrrrrrrrrrrrrrr | |
STORE B之前的影響都被 \ +-------+ | |
CPU 2所感知 ---->| A->1 |------>| 二次 |
+-------+ | |
: : +-------+
但是也可能CPU 2在讀屏障結束之前就感知到CPU 1對A的更新:
+-------+ : : : :
| | +------+ +-------+
| |------>| A=1 |------ --->| A->0 |
| | +------+ \ +-------+
| CPU 1 | wwwwwwwwwwwwwwww \ --->| B->9 |
| | +------+ | +-------+
| |------>| B=2 |--- | : :
| | +------+ \ | : : +-------+
+-------+ : : \ | +-------+ | |
---------->| B->2 |------>| |
| +-------+ | CPU 2 |
| : : | |
\ : : | |
\ +-------+ | |
---->| A->1 |------>| 一次 |
+-------+ | |
rrrrrrrrrrrrrrrrr | |
+-------+ | |
| A->1 |------>| 二次 |
+-------+ | |
: : +-------+
這裏只保證, 如果LOAD B得到的值是2的話, 第二個LOAD A能得到的值是1. 對於第一個
LOAD A是不存在這樣的保證的; 它可能得到A的值是0或是1.
讀內存屏障與內存預取
--------------------
許多CPU會對LOAD操作進行預取: 作爲性能優化的一種手段, 當CPU發現它們將要從內存LOAD
一個數據時, 它們會尋找一個不需要使用總線來進行其他LOAD操作的時機, 用於LOAD這個數
據 - 儘管他們的指令執行流程實際上還沒有到達該處LOAD的地方. 實際上, 這可能使得某
些LOAD指令能夠立即完成, 因爲CPU已經預取到了所需要LOAD的值.
這也可能出現CPU實際上用不到這個預取的值的情況 - 可能因爲一個分支而避開了這次LOAD
- 在這樣的情況下, CPU可以丟棄這個值或者乾脆就緩存它以備後續使用.
考慮如下場景:
CPU 1 CPU 2
======================= =======================
LOAD B
DIVIDE } 除法指令通常消耗
DIVIDE } 很長的執行時間
LOAD A
這可能將表現爲如下情況:
: : +-------+
+-------+ | |
--->| B->2 |------>| |
+-------+ | CPU 2 |
: :DIVIDE | |
+-------+ | |
CPU在執行除法指令 ---> -->| A->0 |~~~~ | |
的同時, 預取A +-------+ ~ | |
(譯註: 此時總線空閒) : : ~ | |
: :DIVIDE | |
: : ~ | |
一旦除法結束, --> : : ~-->| |
CPU能馬上使 : : | |
LOAD指令生效 : : +-------+
如果在第二個LOAD之前放一個讀屏障或數據依賴屏障:
CPU 1 CPU 2
======================= =======================
LOAD B
DIVIDE
DIVIDE
<讀屏障>
LOAD A
這在一定程度上將迫使預取所獲得的值, 根據屏障的類型而被重新考慮. 如果沒有新的更新
操作作用到已經被預取的內存地址, 則預取到的值就會被使用:
: : +-------+
+-------+ | |
--->| B->2 |------>| |
+-------+ | CPU 2 |
: :DIVIDE | |
+-------+ | |
CPU在執行除法指令 ---> -->| A->0 |~~~~ | |
的同時, 預取A +-------+ ~ | |
: : ~ | |
: :DIVIDE | |
: : ~ | |
: : ~ | |
rrrrrrrrrrrrrrrr~ | |
: : ~ | |
: : ~-->| |
: : | |
: : +-------+
但是, 如果存在一個來自於其他CPU的更新或失效, 那麼預取將被取消, 並且重新載入值:
: : +-------+
+-------+ | |
--->| B->2 |------>| |
+-------+ | CPU 2 |
: :DIVIDE | |
+-------+ | |
CPU在執行除法指令 ---> -->| A->0 |~~~~ | |
的同時, 預取A +-------+ ~ | |
: : ~ | |
: :DIVIDE | |
: : ~ | |
: : ~ | |
rrrrrrrrrrrrrrrrr | |
+-------+ | |
預取被丟棄, 並且更 --> -->| A->1 |------>| |
新後的值被重新獲取 +-------+ | |
: : +-------+
====================
內核中顯式的內存屏障
====================
linux內核擁有各式各樣的屏障, 作用在不同層次上:
(*) 編譯優化屏障.
(*) CPU內存屏障.
(*) MMIO寫屏障.
編譯優化屏障
------------
Linux內核有一個顯式的編譯器屏障函數, 能夠防止編譯器優化將訪存操作從它的任一側移
到另一側:
barrier();
這是一個通用屏障 - 弱類型的編譯優化屏障並不存在.
編譯優化屏障並不直接作用到CPU, CPU依然可以按其意願亂序執行代碼.
(譯註:
既然編譯優化屏障並不能限制CPU的亂序訪存, 那麼單純的編譯優化屏障能起到什麼作用呢?
以內核中的preempt_disable宏爲例:
#define preempt_disable() \
do { \
inc_preempt_count(); \
barrier(); \
} while (0)
preempt_disable()和對應的preempt_enable()之間的代碼是禁止內核搶佔的, 通過對當前
進程的preempt_count進行++, 以標識進入禁止搶佔狀態(preempt_count==0時可搶佔). 這
裏在對preempt_count自增之後, 使用了編譯優化屏障.
如果不使用屏障, 本該在不可搶佔狀態下執行的指令可能被重排到preempt_count++之前(因
爲這些指令基本上不會對preempt_count有依賴). 而搶佔可能是由中斷處理程序來觸發的,
在那些應該在不可搶佔狀態下執行的指令被執行之後, preempt_count++之前, 可能發生中
斷. 中斷來了, preempt_count的值還是0, 於是進程可能會被錯誤的搶佔掉.
究其原因, 是因爲編譯器看到的上下文依賴邏輯是靜態的, 它不知道這段代碼跟中斷處理程
序還存在依賴關係, 所以沒法限制自己的亂序行爲. 所以, 這裏的編譯優化屏障是必要的.
但是, 僅僅使用編譯優化屏障就足夠了麼? 是的, 因爲preempt_count這個變量是屬於當前
進程的, 僅會被當前CPU訪問.
CPU亂序可能導致後面應該在禁止搶佔狀態下執行的指令先於preempt_disable()執行完, 但
是沒有關係, 因爲前面也提到過, CPU是"順序流入, 亂序流出"的, 就算後面的指令先執行
完, preempt_disable()也必定已經存在於流水線中了, CPU知道preempt_count變量將要被
修改. 而觸發搶佔的代碼肯定會檢查preempt_count是否爲0, 而這裏的檢查又將依賴於
preempt_disable()的修改結果, 必定在preempt_disable()完成之後纔會進行.
究其原因, 是因爲CPU看到的上下文依賴邏輯是動態的, 它不管指令是來自於普通的處理流
程, 還是來自於中斷處理程序, 只要指令存在依賴, 它都能發現. 所以, 對於類似這樣的只
被一個CPU所關注的內存訪問, CPU的亂序訪存並不會存在問題.
)
CPU內存屏障
-----------
Linux內核有8種基本的CPU內存屏障:
類型 強制 SMP環境
=============== ======================= ===========================
通用 mb() smp_mb()
寫 wmb() smp_wmb()
讀 rmb() smp_rmb()
數據依賴 read_barrier_depends() smp_read_barrier_depends()
(譯註: 這裏所說有SMP屏障是隻在SMP環境下才生效的屏障, 而強制屏障則是不管在不在SMP
環境下都生效的屏障. 這裏所謂的SMP環境, 確切的說, 其實是內核的編譯選項指定爲SMP的
情況, 並不是指實際運行內核的機器的環境. 不過既然編譯選項指定了SMP環境, 那麼編譯
生成的內核也基本上將會運行在SMP環境. 下面提到的UP環境亦是同理.)
除了數據依賴屏障之外, 所有的內存屏障都隱含了編譯優化屏障的功能. 數據依賴屏障不對
編譯器輸出的代碼順序造成任何額外的影響.
注: 在存在數據依賴關係的情況下, 編譯器預期會將LOAD指令按正確的順序輸出(例如, 在
`a[b]`語句中, 對b的load必須放在對a[b]的load之前), 但在C規範下, 並不保證編譯器不
去預測B的值(比如預測它等於1), 於是先load a再load b(比如,
tmp = a[1]; if (b != 1) tmp = a[b];). 編譯器在load a[b]之後又重新load b, 也可能
會存在問題, 因爲b擁有比a[b]更新的副本. 這些問題的解決尚未達成共識, 然而內核中的
ACCESS_ONCE宏是解決問題的一個好的開始.
在UP系統中, SMP內存屏障將退化成編譯器優化屏障, 因爲它假定CPU能夠保證自身的一致性
, 並本身就能以正確的順序處理重疊的內存訪問.
[!] 注意, SMP內存屏障必須用於控制在SMP系統中的共享內存的引用順序, 而使用鎖也能夠
滿足需求.
強制屏障不應該用來控制SMP的影響, 因爲強制屏障會過多地增加UP系統的開銷. 不過, 在
使用MMIO來訪問鬆散屬性的IO內存窗口時, 強制屏障可以用來控制這些訪存的影響. (譯註:
這裏所指的內存窗口, 是假定對於CPU來說, 可以設置屬於不同區間的內存地址擁有不同的
屬性. 這些屬性可以指示一個內存段是否可以鬆散訪問, 即亂序訪問.) 強制屏障即使在非
SMP環境下也可能需要, 因爲它們可以通過禁止編譯器和CPU的亂序訪存, 從而影響設備感知
到內存操作的順序.
還有一些更高級的屏障函數:
(*) set_mb(var, value)
該函數將value賦值到var變量中, 然後取決於具體編譯參數下的函數實現, 可能在之
後插入一個內存屏障. 在UP系統中, 它不能保證會插入編譯優化屏障以外的其他屏障.
(*) smp_mb__before_atomic_dec();
(*) smp_mb__after_atomic_dec();
(*) smp_mb__before_atomic_inc();
(*) smp_mb__after_atomic_inc();
它們跟一些進行原子操作的函數配合使用, 這些函數進行了原子加法, 減法, 自增和
自減, 而又不將原子變量的值返回, 特別被用於引用計數. 這些原子操作本身並不隱
含內存屏障. (譯註: 像這樣被操作的原子變量, 多半是孤立而沒有數據依賴的. 如果
有數據依賴, 那麼依賴關係將在一定程度上限制CPU的亂序. 否則, CPU的亂序就完全
要靠內存屏障來限制了.)
舉個例子, 考慮如下代碼段, 它將object標識爲已刪除, 然後將其引用計數自減:
obj->dead = 1;
smp_mb__before_atomic_dec();
atomic_dec(&obj->ref_count);
這樣可以確保設置刪除標記在自減引用計數之前生效.
更多信息請參閱Documentation/atomic_ops.txt. 想知道什麼地方需要用到這些函數,
參閱"原子操作"章節.
(*) smp_mb__before_clear_bit(void);
(*) smp_mb__after_clear_bit(void);
它們的用途類似於原子加減的屏障. 它們通常是跟一些進行按位解鎖操作的函數配合
使用, 必須小心, 因爲位操作本身也並不隱含內存屏障.
考慮這樣一個場景, 程序通過清除鎖定位來實施一些解鎖性質的操作. clear_bit()函
數需要像這樣的屏障:
smp_mb__before_clear_bit();
clear_bit( ... );
這樣可以防止應該在鎖定位被清除之前發生的內存操作漏到位清除之後去(譯註: 注意
UNLOCK的屏障作用就是要保證它之前的訪存操作一定先於它而完成). 關於UNLOCK操作
的實現, 請參閱"鎖相關函數"章節.
更多信息請參閱Documentation/atomic_ops.txt. 想知道什麼地方需要用到這些函
數, 參閱"原子操作"章節.
MMIO寫屏障
----------
對於內存映射IO的寫操作, Linux內核還有一個特別的屏障:
mmiowb();
這是一個強制寫屏障的變體, 能夠將弱有序的IO內存窗口變成部分有序. 它的作用可能超出
CPU與硬件的界面, 從而影響到許多層次上的硬件設備.
更多信息請參閱"鎖與IO訪問"章節.
====================
內核中隱式的內存屏障
====================
Linux內核中有一些其他的方法也隱含了內存屏障, 包括鎖和調度方法.
這個範圍是一個最低限度的保證; 一些特定的體系結構可能提供更多的保證, 但是在特定體
繫結構的代碼之外, 不能依賴於它們.
鎖相關函數
----------
Linux內核有很多鎖結構:
(*) spin locks
(*) R/W spin locks
(*) mutexes
(*) semaphores
(*) R/W semaphores
(*) RCU
在所有情況下, 它們都是LOCK操作和UNLOCK操作的變種. 這些操作都隱含一定的屏障:
(1) LOCK操作所隱含的:
在LOCK操作之後出現的內存操作, 一定在LOCK操作完成之後纔會完成.
而在LOCK操作之前出現的內存操作, 可能在LOCK操作完成之後才完成.
(2) UNLOCK操作所隱含的:
在UNLOCK操作之前出現的內存操作, 一定在UNLOCK操作完成之前完成.
而在UNLOCK操作之後出現的內存操作, 可能在LOCK操作完成之前就完成了.
(3) LOCK操作+LOCK操作所隱含的:
在某個LOCK操作之前出現的所有LOCK操作都將在這個LOCK之前完成.
(4) LOCK操作+UNLOCK操作所隱含的:
在UNLOCK操作之前出現的所有LOCK操作都將在這個UNLOCK之前完成.
在LOCK操作之前出現的所有UNLOCK操作都將在這個LOCK之前完成.
(5) LOCK失敗所隱含的:
某些變種的LOCK操作可能會失敗, 比如可能因爲不能立刻獲得鎖(譯註: 如try_lock操
作), 再比如因爲在睡眠等待鎖變爲可用的過程中接收到了未被阻塞的信號(譯註: 如
semaphores的down_interruptible操作). 失敗的鎖操作不隱含任何屏障.
因此, 根據(1), (2)和(4), 一個無條件的LOCK跟在一個UNLOCK之後, 鎖相當於一個完整的
屏障, 而一個UNLOCK跟在一個LOCK之後並非如此.
[!] 注意: LOCK和UNLOCK只是單向的屏障, 其結果是, 臨界區之外的指令可能會在臨界區中
執行.
一個UNLOCK跟在一個LOCK之後並不能認爲是一個完整的屏障, 因爲出現在LOCK之前的訪存可
能在LOCK之後才執行, 而出現在UNLOCK之後的訪存可能在UNLOCK之前執行, 這兩次訪存可能
會交叉:
*A = a;
LOCK
UNLOCK
*B = b;
可能表現爲:
LOCK, STORE *B, STORE *A, UNLOCK
鎖和信號量在UP環境下可能不提供順序保證, 在這種情況下不能被認作是真正的屏障 - 特
別是對於IO訪問 - 除非結合中斷禁用操作.
參閱"跨CPU的鎖的屏障作用"章節.
例如, 考慮如下代碼:
*A = a;
*B = b;
LOCK
*C = c;
*D = d;
UNLOCK
*E = e;
*F = f;
如下的事件序列都是可接受的:
LOCK, {*F,*A}, *E, {*C,*D}, *B, UNLOCK
[+] 注意, {*F,*A} 代表一次合併訪問.
但是下面的序列都不可接受:
{*F,*A}, *B, LOCK, *C, *D, UNLOCK, *E
*A, *B, *C, LOCK, *D, UNLOCK, *E, *F
*A, *B, LOCK, *C, UNLOCK, *D, *E, *F
*B, LOCK, *C, *D, UNLOCK, {*F,*A}, *E
禁止中斷函數
------------
禁止中斷(類似於LOCK)和啓用中斷(類似於UNLOCK)的函數只會起到編譯優化屏障的作用. 所
以, 如果在這種情況下需要使用內存或IO屏障, 必須採取其他手段.
睡眠喚醒函數
------------
在一個全局事件標記上的睡眠和喚醒可以被看作是兩條數據之間的交互: 正在等待事件的進
程的狀態, 和用於表示事件發生的全局數據. 爲了確保它們按正確的順序發生, 進入睡眠的
原語和發起喚醒的原語都隱含了某些屏障.
首先, 睡眠進程通常執行類似於如下的代碼序列:
for (;;) {
set_current_state(TASK_UNINTERRUPTIBLE);
if (event_indicated)
break;
schedule();
}
set_current_state()在它更改進程狀態之後會自動插入一個通用內存屏障:
CPU 1
===============================
set_current_state();
set_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);
或:
event_indicated = 1;
wake_up_process(event_daemon);
類似wake_up()的函數會隱含一個寫內存屏障. 當且僅當它們的確喚醒了某個進程時. 屏障
出現在進程的睡眠狀態被清除之前, 也就是在設置喚醒事件標記的STORE操作和將進程狀態
修改爲TASK_RUNNING的STORE操作之間:
CPU 1 CPU 2
=============================== ===============================
set_current_state(); STORE event_indicated
set_mb(); wake_up();
STORE current->state <寫屏障>
<通用屏障> STORE current->state
LOAD event_indicated
可用的喚醒函數包括:
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()之後的那些
STORE操作, 睡眠和喚醒所隱含的內存屏障並不保證它們的順序. 比如說, 如果睡眠
函數這樣做:
set_current_state(TASK_INTERRUPTIBLE);
if (event_indicated)
break;
__set_current_state(TASK_RUNNING);
do_something(my_data);
而喚醒函數這樣做:
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);
其他函數
--------
其他隱含了屏障的函數:
(*) schedule()和類似函數隱含了完整的內存屏障.
(譯註: schedule函數完成了進程的切換, 它的兩邊可能對應着兩個不同的上下文. 如
果訪存操作跨越schedule函數而進行了亂序, 那麼基本上可以肯定是錯誤的.)
===================
跨CPU的鎖的屏障作用
===================
在SMP系統中, 鎖定原語給出了多種形式的屏障: 其中一種在一些特定的鎖衝突的情況下,
會影響其他CPU上的內存訪問順序.
鎖與內存訪問
------------
假設系統中有(M)和(Q)這一對spinlock, 有三個CPU; 那麼可能發生如下操作序列:
CPU 1 CPU 2
=============================== ===============================
*A = a; *E = e;
LOCK M LOCK Q
*B = b; *F = f;
*C = c; *G = g;
UNLOCK M UNLOCK Q
*D = d; *H = h;
那麼對於CPU 3來說, 從*A到*H的訪問順序是沒有保證的, 不像單獨的鎖對應單獨的CPU有
那樣的限制. 例如, CPU 3可能看到的順序是:
*E, LOCK M, LOCK Q, *G, *C, *F, *A, *B, UNLOCK Q, *D, *H, UNLOCK M
但是它不會看到如下情況:
*B, *C or *D 先於 LOCK M
*A, *B or *C 後於 UNLOCK M
*F, *G or *H 先於 LOCK Q
*E, *F or *G 後於 UNLOCK Q
但是, 如果是下面的情形:
CPU 1 CPU 2
=============================== ===============================
*A = a;
LOCK M [1]
*B = b;
*C = c;
UNLOCK M [1]
*D = d; *E = e;
LOCK M [2]
*F = f;
*G = g;
UNLOCK M [2]
*H = h;
CPU 3可能看到:
*E, LOCK M [1], *C, *B, *A, UNLOCK M [1],
LOCK M [2], *H, *F, *G, UNLOCK M [2], *D
但是如果CPU 1先得到鎖, CPU 3不會看到下面的情況:
*B, *C, *D, *F, *G or *H 先於 LOCK M [1]
*A, *B or *C 後於 UNLOCK M [1]
*F, *G or *H 先於 LOCK M [2]
*A, *B, *C, *E, *F or *G 後於 UNLOCK M [2]
鎖與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".
=====================
什麼地方需要內存屏障?
=====================
在正常操作下, 內存操作的亂序一般並不會成爲問題, 即使是在SMP內核中, 一段單線程的
線性代碼也總是能夠正確工作. 但是, 有四種情況, 亂序絕對可能是一個問題:
(*) 處理器間交互.
(*) 原子操作.
(*) 訪問設備.
(*) 中斷.
處理器間交互
------------
當系統中擁有不止一個CPU時, 系統中的多個CPU可能在同一時間工作在同樣的數據集上. 這
將產生同步問題, 並且這樣的問題通常要靠使用鎖來解決. 但是, 鎖是昂貴的, 所以不是萬
不得已的情況下最好不要使用鎖. 在這種情況下, 爲防止錯誤, 導致兩個CPU相互影響的那
些內存操作可能需要仔細協調好順序.
比如, 考慮一下讀寫信號量的slow path. 信號量的等待隊列裏有一個進程正在等待, 這個
等待進程棧空間上的一段內存(譯註: 也就是棧上分配的waiter結構)被鏈到信號量的等待鏈
表裏:
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
而如果其中一些步驟發生了亂序, 那麼整個過程可能會產生錯誤.
一旦等待進程將自己掛入等待隊列, 並釋放了信號量裏的鎖, 這個等待進程就不會再獲得這
個鎖了(譯註: 參閱信號量的代碼, 它內部使用了一個spinlock來進行同步); 它要做的事情
就是在繼續工作之前, 等待waiter結構中的task指針被清空(譯註: 然後自己會被喚醒). 而
既然waiter結構存在於等待進程的棧上, 這就意味着, 如果在waiter結構中的next指針被讀
取之前, task指針先被清空了的話(譯註: 等待進程先被喚醒了), 那麼, 這個等待進程可能
已經在另一個CPU上開始運行了(譯註: 相對於喚醒進程所運行的CPU), 並且在up*()函數有
機會讀取到next指針之前, 棧空間上對應的waiter結構可能已經被複用了(譯註: 被喚醒的
進程從down*()函數返回, 然後可能進行新的函數調用, 導致棧空間被重複使用).
看看上面的事件序列可能會發生什麼:
CPU 1 CPU 2
=============================== ===============================
down_xxx()
將waiter結構鏈入等待隊列
進入睡眠
up_yyy()
LOAD waiter->task;
STORE waiter->task;
被CPU 1的UP事件喚醒
<被搶佔了>
重新得到運行
down_xxx()函數返回
繼續調用foo()函數
foo()重用了棧上的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的數據依賴邏輯將
處理所有事情.
原子操作
--------
雖然原子操作在技術上實現了處理器之間的交互, 然而特別注意一些原子操作隱含了完整的
內存屏障, 而另外一些則沒有, 但是它們卻作爲一個羣體被整個內核嚴重依賴.
許多原子操作修改內存中的一些狀態, 並且返回該狀態相關的信息(舊狀態或新狀態), 就在
其中實際操作內存的兩邊各隱含一個SMP環境下的通用內存屏障(smp_mb())(除顯式的鎖操作
之外, 稍後說明). 它們包括:
xchg();
cmpxchg();
atomic_cmpxchg();
atomic_inc_return();
atomic_dec_return();
atomic_add_return();
atomic_sub_return();
atomic_inc_and_test();
atomic_dec_and_test();
atomic_sub_and_test();
atomic_add_negative();
atomic_add_unless(); /* 如果成功 (返回 1) */
test_and_set_bit();
test_and_clear_bit();
test_and_change_bit();
它們被用於作爲類LOCK和類UNLOCK操作的實現, 和用於控制對象析構的引用計數, 這些情況
下, 隱含內存屏障是有必要的.
以下操作由於沒有隱含內存屏障, 會有潛在的問題, 但有可能被用於實現類UNLOCK這樣的操
作:
atomic_set();
set_bit();
clear_bit();
change_bit();
如果需要, 對應於這些函數, 可以使用相應的顯式內存屏障(比如
smp_mb__before_clear_bit()).
下面這些函數也不隱含內存屏障, 並且在一些情況下, 可能也需要用到顯式內存屏障(比如
smp_mb__before_atomic_dec()):
atomic_add();
atomic_sub();
atomic_inc();
atomic_dec();
如果它們用於產生統計, 那麼他們可能就不需要內存屏障, 除非統計數據之間存在耦合.
如果它們被用作控制對象生命週期的引用計數, 那麼它們可能並不需要內存屏障, 因爲要麼
引用計數需要在一個鎖的臨界區裏面進行調整, 要麼調用者已經持有足夠的引用而相當於擁
有了鎖(譯註: 一般在引用計數減爲0的時候需要將對應的對象析構, 如果調用者知道引用計
數在某些情況下不可能減爲0, 那麼這個對象也就不可能在這些情況下被析構, 也就不需要
通過內存屏障來避免訪存亂序導致的對象在析構之後還被訪問的情況), 這樣的情況下並不
需要內存屏障.
如果它們用於構成鎖的一些描述信息, 那麼他們可能就需要內存屏障, 因爲鎖原語一般需要
按一定的順序來操作.
基本上, 每個場景都需要仔細考慮是否需要使用內存屏障.
以下操作是特殊的鎖原語:
test_and_set_bit_lock();
clear_bit_unlock();
__clear_bit_unlock();
它們都執行了類LOCK和類UNLOCK的操作. 相比其他操作, 它們應該優先被用於實現鎖原語,
因爲它們的實現可以在許多體系結構下得到優化.
[!] 注意, 這些特殊的內存屏障原語對一些情況也是有用的, 因爲在一些體系結構的CPU上,
使用的原子操作本身就隱含了完整的內存屏障功能, 所以屏障指令在這裏是多餘的, 在
這樣的情況下, 這些特殊的屏障原語將不使用額外的屏障操作.
更多信息請參閱Documentation/atomic_ops.txt.
訪問設備
--------
許多設備都可以被映射到內存, 因此在CPU看來, 它們只是一組內存地址. 爲了控制這些設
備, 驅動程序通常需要確保正確的內存訪問按正確的順序來執行.
但是, 聰明的CPU或者聰明的編譯器卻導致了潛在的問題, 如果CPU或編譯器認爲亂序, 或合
並訪問更有利於效率的話, 驅動程序代碼中仔細安排的訪存序列可能並不會按正確的順序被
送到設備上 - 從而可能導致設備的錯誤.
在Linux內核裏面, IO訪問應該使用適當的訪問函數 - 例如inb()或writel() - 它們知道如
何得到恰當的訪問順序. 大多數情況下, 在使用這些函數之後就不必再顯式的使用內存屏障
, 但是在兩種情況下, 內存屏障可能還是需要的:
(1) 在一些系統中, IO存儲操作對於所有CPU來說並不是嚴格有序的, 所以對於所有的通用
驅動程序(譯註: 通用驅動程序需要適應各種體系結構的系統), 需要使用鎖, 並且一
定要在解鎖臨界區之前執行mmiowb()函數.
(2) 如果訪存函數訪問鬆散屬性的IO內存窗口, 那麼需要使用強制內存屏障來確保執行順
序.
更多信息請參閱Documentation/DocBook/deviceiobook.tmpl.
中斷
----
驅動程序可能被它自己的中斷處理程序所打斷, 然後驅動程序中的這兩個部分可能會相互幹
擾對方控制或訪問設備的意圖.
通過禁用本地中斷(一種形式的鎖)可能至少部分緩解這種情況, 這樣的話, 驅動程序中的關
鍵操作都將包含在禁用中斷的區間中. 於是當驅動程序的中斷處理程序正在執行時, 驅動程
序的核心代碼不可能在相同的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這樣的操作.)
===================
內核中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()
這些函數類似於readX(), 但是任何情況下都不保證有序. 請注意, 這裏沒有用到IO讀
屏障.
(*) ioreadX(), iowriteX()
這些函數在進行訪存的時候會根據訪存類型選擇適當的操作, inX()/outX()或
readX()/writeX().
======================
最小限度有序的假想模型
======================
從概念上說, 必須假定的CPU是弱有序的, 但是它會保持程序上下文邏輯關係的外觀. 一些
CPU(比如i386或x86_64)比另一些(比如powerpc或frv)更具有約束力, 而在體系結構無關的
代碼中, 必須假定爲最鬆散的情況(也就是DEC Alpha).
也就是說, 必須考慮到CPU可能會按它喜歡的順序來執行操作 - 甚至並行執行 - 只是當指
令流中的一條指令依賴於之前的一條指令時, 之前的這條指定才必須在後面這條指令可能被
處理之前完全結束; 換句話說: 保持程序的上下文邏輯關係.
[*] 一些指令會產生不止一處影響 - 比如會修改條件碼, 修改寄存器或修改內存 - 不同
的指令可能依賴於不同的影響.
CPU也可能丟棄那些最終不產生任何影響的操作序列. 比如, 如果兩個相鄰的指令都將一個
立即數LOAD到寄存器, 那麼第一個LOAD指令可能被丟棄.
類似的, 也需要假設編譯器可能按它覺得舒服的順序來調整指令流, 但同樣也會保持程序的
上下文邏輯關係.
===============
CPU cache的影響
===============
操作cache中緩存的內存之後, 相應的影響會在整個系統間得到傳播. 位於CPU和內存之間的
cache, 和保持系統狀態一致的內存一致性機構, 在一定程度上影響了傳播的方法.
自從CPU與系統中其他部分的交互通過使用cache來實現以來, 內存系統就包含了CPU的緩存,
而內存屏障基本上就工作在CPU和其cache之間的界面上(邏輯上說, 內存屏障工作在下圖中
虛線所示的地方):
<--- CPU ---> : <----------- 內存 ----------->
:
+--------+ +--------+ : +--------+ +-----------+
| | | | : | | | | +--------+
| CPU | | 內存 | : | CPU | | | | |
| 核心 |--->| 請求 |----->| Cache |<-->| | | |
| | | 隊列 | : | | | |--->| 內存 |
| | | | : | | | | | |
+--------+ +--------+ : +--------+ | | | |
: | Cache | +--------+
: | 一致性 |
: | 機構 | +--------+
+--------+ +--------+ : +--------+ | | | |
| | | | : | | | | | |
| CPU | | 內存 | : | CPU | | |--->| 設備 |
| 核心 |--->| 請求 |----->| Cache |<-->| | | |
| | | 隊列 | : | | | | | |
| | | | : | | | | +--------+
+--------+ +--------+ : +--------+ +-----------+
:
:
一些LOAD和STORE可能不會實際出現在發起操作的CPU之外, 因爲在CPU自己的cache上就能滿
足需要, 儘管如此, 如果其他CPU關心這些數據, 那麼完整的內存訪問還是會發生, 因爲
cache一致性機構將遷移相應的cache行到訪問它的CPU, 使一致性得到傳播.
在保持程序所期望的上下文邏輯的前提下, CPU核心可能會按它認爲合適的順序來執行指令.
一些指令會產生LOAD和STORE操作, 並且將它們放到內存請求隊列中, 等待被執行. CPU核心
可能會按它喜歡的順序來將這些操作放進隊列, 然後繼續運行, 直到它必須等待這些訪存指
令完成的時候爲止.
內存屏障所需要關心的是訪存操作從CPU一側穿越到內存一側的順序, 和系統中的其他部件
感知到的操作發生的順序.
[!] 對於一個CPU自己的LOAD和STORE來說, 並不需要使用內存屏障, 因爲CPU總是能按程序
執行順序看到它們所執行的LOAD和STORE操作.
[!] MMIO或其他設備存取可能繞開cache系統. 這取決於訪問設備所經過的內存窗口的屬性
和/或是否使用了CPU所特有的與設備進行交互的指令.
CACHE一致性
-----------
但是, 事情並不是像上面所說的那樣簡單: 因爲雖然可以期望cache是一致的, 但是一致性
傳播的順序卻是沒有保證的. 也就是說, 雖然一個CPU所做出的更新將最終被其它CPU都看到
, 但是卻不保證其他CPU所看到的都是相同的順序.
考慮這樣一個系統, 它具有雙CPU(1和2), 每個CPU有一對並行的數據cache(CPU 1對應A/B,
CPU 2對應C/D):
:
: +--------+
: +---------+ | |
+--------+ : +--->| Cache A |<------->| |
| | : | +---------+ | |
| CPU 1 |<---+ | |
| | : | +---------+ | |
+--------+ : +--->| Cache B |<------->| |
: +---------+ | |
: | 內存 |
: +---------+ | 系統 |
+--------+ : +--->| Cache C |<------->| |
| | : | +---------+ | |
| CPU 2 |<---+ | |
| | : | +---------+ | |
+--------+ : +--->| Cache 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 說明
=============== =============== =======================================
u == 0, v == 1 並且 p == &u, q == &u
v = 2;
smp_wmb(); 確保對v的修改先於對p的修改被感知
<A:modify v=2> v的值只存在於cache A中
p = &v;
<B:modify p=&v> p的值只存在於cache B中
寫內存屏障保證系統中的其他CPU會按正確的順序看到本地CPU cache的更新. 但是設想一下
第二個CPU要去讀取這些值的情形:
CPU 1 CPU 2 說明
=============== =============== =======================================
...
q = p;
x = *q;
上面這一對讀操作可能不會在預期的順序下執行, 比如持有p的cache行可能被更新到另一個
CPU的cache, 而持有v的cache行因爲其他一些cache事件的影響而延遲了對那個CPU的cache
的更新:
CPU 1 CPU 2 說明
=============== =============== =======================================
u == 0, v == 1 並且 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> 在v被更新到cache之前讀取v
<C:unbusy>
<C:commit v=2>
基本上, 雖然最終CPU 2的兩個cache行都將得到更新, 但是在沒有干預的情況下, 並不能保
證更新的順序跟CPU 1提交的順序一致.
我們需要在兩次LOAD之間插入一個數據依賴屏障或讀屏障, 以作爲干預. 這將強制cache在
處理後續的請求之前, 先讓它的一致性隊列得到提交:
CPU 1 CPU 2 說明
=============== =============== =======================================
u == 0, v == 1 並且 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>
smp_read_barrier_depends()
<C:unbusy>
<C:commit v=2>
x = *q;
<C:read *q> 在v被更新到cache之後讀取v
這些問題會在DEC Alpha處理器上遇到, 這些處理器使用了分列cache, 通過提高數據總線的
利用率以提升性能. 雖然大部分的CPU在讀操作依賴於讀操作的時候, 會在第二個讀操作中
隱含一個數依賴屏障, 但是並不是所有CPU都這樣, 因此不能依賴這一點.
其他的CPU也可能使用分列cache, 但對於普通的內存訪問, 他們會協調各個cache列. 而
Alpha處理器的處理邏輯則取消了這樣的協調動作, 除非使用內存屏障.
cache一致性與DMA
----------------
對於進行DMA操作的設備, 並不是所有系統都保持它們的cache一致性. 在這種情況下, 準備
進行DMA的設備可能從RAM得到陳舊的數據, 因爲髒的cache行可能還駐留在各個CPU的cache
中, 而尚未寫回到RAM. 爲了解決這個問題, 內核的相應部分必須將cache中重疊的數據沖刷
掉(或者使它們失效)(譯註: 沖刷掉cache中的相應內容, 以保持cache與RAM的一致).
此外, 在設備已經通過DMA將數據寫入RAM之後, 這些數據可能被cache寫回RAM的髒的cache
行所覆蓋, 或者CPU已緩存的cache行可能直接掩蓋了RAM被更新的事實(譯註: 使得對應的
LOAD操作只能獲得cache中的舊值, 而無法得到RAM中的新值), 直到cache行被從CPU cache
中丟棄並且重新由RAM載入. 爲解決這個問題, 內核的相應部分必須將cache中重疊的數據失
效.
更多關於cache管理的信息請參閱: Documentation/cachetlb.txt.
cache一致性與MMIO
-----------------
內存映射IO通常通過內存地址來觸發, 這些地址是CPU內存空間的某個窗口中的一部分, 而
這個窗口相比於普通RAM對應的窗口會有着不同的屬性.
這些屬性通常包含這樣的情況: 訪存會完全繞過cache, 而直接到達設備總線. 這意味着在
效果上, MMIO可能超越先前發出的對被緩存內存的訪問(譯註: 意思是, MMIO後執行, 但是
先到達內存; 而先執行的寫內存操作則可能被緩存在cache上, 之後才能沖刷到內存). 這種
情況下, 如果這兩者有某種依賴的話, 使用一個內存屏障並不足夠, 而需要在寫被緩存內存
和MMIO訪存之間將cache沖刷掉.
=============
CPU所能做到的
=============
程序員可能會想當然地認爲CPU將完全按照指定的順序執行內存操作, 如果CPU是這樣的話,
比方說讓它執行下面的代碼:
a = *A;
*B = b;
c = *C;
d = *D;
*E = e;
對於每一條指令, 他們會期望CPU在完成內存操作之後, 纔會去執行下一條指令, 於是系統
中的其他組件將看到這樣一個明確的操作序列:
LOAD *A, STORE *B, LOAD *C, LOAD *D, STORE *E.
當然, 實際情況是混亂的. 對於許多CPU和編譯器來說, 上述假設不成立, 因爲:
(*) LOAD操作可能更加需要立即完成以確保程序的執行速度(譯註: 因爲往往會有後續指令
需要等待LOAD的結果), 而STORE操作推遲一下往往並不會有問題;
(*) LOAD操作可以通過預取來完成, 並且在確認數據已經不需要之後, 預取結果可以丟棄;
(*) LOAD操作可以通過預取來完成, 導致結果被獲取的時機可能並不符合期望的執行順序;
(*) 內存訪問的順序可能被重新排列, 以促進更好的使用CPU總線和cache;
(*) 有一些內存或IO設備支持對相鄰地址的批量訪問, 在跟它們打交道的時候, LOAD和
STORE操作可能被合併, 從而削減訪存事務建立的成本, 以提高性能(內存和PCI設備可
能都可以這樣做); 並且
(*) CPU的數據cache可能影響訪問順序, 儘管cache一致性機構可以緩解這個問題 - 一旦
STORE操作命中了cache - 但並不能保證一致性將按順序傳播到其他CPU(譯註: 如果
STORE操作命中了cache, 那麼被更新過的髒數據可能會在cache中停留一段時間, 而不
會立刻沖刷到內存中);
所以說, 另一個CPU可能將上面的代碼看作是:
LOAD *A, ..., LOAD {*C,*D}, STORE *E, STORE *B
("LOAD {*C,*D}"是一個合併的LOAD)
但是, CPU將保證自身的一致性: 它將按正確的順序看到自己的內存操作, 而不需要使用內
存屏障. 以下面的代碼爲例:
U = *A;
*A = V;
*A = W;
X = *A;
*A = Y;
Z = *A;
假設不存在外部的干擾, 那麼可以肯定最終的結果一定是:
U == the original value of *A
U == *A的初始值
X == W
Z == Y
*A == Y
對於上面的代碼, CPU可能產生的全部內存訪問序列如下:
U=LOAD *A, STORE *A=V, STORE *A=W, X=LOAD *A, STORE *A=Y, Z=LOAD *A
然而, 對於這個序列, 如果沒有干預, 在保證一致性的前提下, 序列中的一些操作也很可能
會被合併或丟棄.
在CPU看到這些操作之前, 編譯器也可能會合並, 丟棄或推遲序列中的一些操作.
例如:
*A = V;
*A = W;
可能削減爲:
*A = W;
於是, 在沒有使用寫屏障的情況下, 可以認爲將V寫入*A的STORE操作丟失了. 類似的:
*A = Y;
Z = *A;
在沒有內存屏障的情況下, 可能削減爲:
*A = Y;
Z = Y;
於是在該CPU之外, 根本就看不到有LOAD操作存在.
特別值得一提的Alpha處理器
-------------------------
DEC Alpha是現有的最爲鬆散的CPU之一. 不僅如此, 許多版本的Alpha CPU擁有分列的數據
cache, 允許他們在不同的時間更新兩個語義相關的緩存. 因爲內存一致性系統需要同步更
新系統的兩個cache, 數據依賴屏障在這裏就真正成爲了必要, 以使得CPU能夠按正確的順序
來處理指針的更新和新數據的獲取.
Alpha處理器定義了Linux內核的內存屏障模型. (譯註: 體系結構無關的代碼需要以最壞情
況爲基準來考慮.)
參閱前面的"Cache一致性"章節.
========
使用示例
========
環型緩衝區
----------
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.