=================
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操作所感覺.
進一步舉例說明, 考慮如下的操作序列:
{ 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;
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
資料依賴屏障
------------
資料依賴屏障的使用需求有點微妙, 并不總是很明顯就能看出需要他們. 為了說明這一點,
考慮如下的操作序列:
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).
為了解決這個問題, 必須在取位址和取資料之間插入一個資料依賴或更強的屏障:
<資料依賴屏障>
這将強制最終結果是前兩種情況之一, 而避免出現第三種情況.
[!] 注意, 這種非常違反直覺的情況最容易出現在cache分列的機器上, 比如, 一個cache組
處理偶數号的cache行, 另一個cache組處理奇數号的cache行. P指針可能存儲在奇數号
的cache行中, 而B的值可能存儲在偶數号的cache行中. 這樣一來, 如果執行讀操作的
CPU的偶數号cache組非常繁忙, 而奇數号cache組空閑, 它就可能看到P已被更新成新值
(&B), 而B還是舊值(2).
另一個可能需要資料依賴屏障的例子是, 從記憶體讀取一個數值, 用于計算數組的通路偏移:
{ M[0] == 1, M[1] == 2, M[3] = 3, P == 0, Q == 3 }
M[1] = 4;
P = 1
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)"進行短路. 在這樣的情況下, 需要的是:
<讀屏障>
(譯注:
例如:
{ 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上的寫屏障是為了保證這樣的邏輯: 如果a == 3 || b == 4, 那麼必定有p == 1.
但是到了CPU 2, 可能a或b的值已更新, 而p的值未更新. 那麼這時使用資料依賴屏障就不能
保證p的更新. 因為從代碼邏輯上說, p跟任何人都沒有依賴關系. 這時必須使用讀屏障, 以
確定x = *q之前, p被更新.
原文中"短路"的意思就是, 由于p沒有資料依賴關系, CPU可以早早獲得它的值, 而不必考慮
更新.)
SMP記憶體屏障的配對使用
---------------------
在處理CPU與CPU的互動時, 對應類型的記憶體屏障總是應該配對使用. 缺乏适當配對的使用基
本上可以肯定是錯誤的.
一個寫屏障總是與一個資料依賴屏障或讀屏障相配對, 雖然通用屏障也可行. 類似的, 一個
讀屏障或資料依賴屏障也總是與一個寫屏障相配對, 盡管一個通用屏障也同樣可行:
a = 1;
b = 2; x = b;
y = a;
或:
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 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)之間:
STORE D = 4 LOAD C (獲得&B)
<資料依賴屏障>
那麼下面的情況将會發生:
| | +------+ +-------+
| |------>| B=2 |----- --->| Y->8 |
| | : +------+ \ +-------+
| CPU 1 | : | A=1 | \ --->| C->&Y |
| | X->9 |------>| |
確定STORE C之前的影響 ---> \ ddddddddddddddddd | |
都被後續的LOAD操作感 \ +-------+ | |
知到 ----->| B->2 |------>| |
+-------+ | |
: : +-------+
第三, 讀屏障用作部分有序的LOAD操作. 考慮如下事件序列:
{ A = 0, B = 9 }
STORE A=1
STORE B=2
LOAD B
LOAD A
| |------>| 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所感覺:
| : : | |
在這一時刻, 讀屏障導緻 ----> \ rrrrrrrrrrrrrrrrr | |
STORE B之前的影響都被 \ +-------+ | |
CPU 2所感覺 ---->| A->1 |------>| |
為了更全面地說明這一點, 考慮一下如果代碼在讀屏障的兩邊都有一個LOAD A的話, 會發生
什麼:
LOAD A [第一次LOAD A]
LOAD A [第二次LOAD A]
盡管兩次LOAD A都發生在LOAD B之後, 它們也可能得到不同的值:
| | A->0 |------>| 一次 |
CPU 2所感覺 ---->| A->1 |------>| 二次 |
但是也可能CPU 2在讀屏障結束之前就感覺到CPU 1對A的更新:
\ : : | |
\ +-------+ | |
---->| 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 +-------+ ~ | |
(譯注: 此時總線空閑) : : ~ | |
: : ~ | |
一旦除法結束, --> : : ~-->| |
CPU能馬上使 : : | |
LOAD指令生效 : : +-------+
如果在第二個LOAD之前放一個讀屏障或資料依賴屏障:
DIVIDE
<讀屏障>
這在一定程度上将迫使預取所獲得的值, 根據屏障的類型而被重新考慮. 如果沒有新的更新
操作作用到已經被預取的記憶體位址, 則預取到的值就會被使用:
rrrrrrrrrrrrrrrr~ | |
: : ~-->| |
: : | |
但是, 如果存在一個來自于其他CPU的更新或失效, 那麼預取将被取消, 并且重新載入值:
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的鎖的屏障作用"章節.
例如, 考慮如下代碼:
*C = c;
*D = d;
*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()在它更改程序狀态之後會自動插入一個通用記憶體屏障:
===============================
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);
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
可用的喚醒函數包括:
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;
睡眠函數并不能保證在看到my_data的修改之後才看到event_indicated的修改. 在這種情況
下, 兩邊的代碼必須在對my_data訪存之前插入自己的記憶體屏障. 是以上述的睡眠函數應該
這樣做:
if (event_indicated) {
smp_rmb();
do_something(my_data);
而喚醒函數應該這樣做:
smp_wmb();
其他函數
其他隐含了屏障的函數:
(*) schedule()和類似函數隐含了完整的記憶體屏障.
(譯注: schedule函數完成了程序的切換, 它的兩邊可能對應着兩個不同的上下文. 如
果訪存操作跨越schedule函數而進行了亂序, 那麼基本上可以肯定是錯誤的.)
===================
跨CPU的鎖的屏障作用
在SMP系統中, 鎖定原語給出了多種形式的屏障: 其中一種在一些特定的鎖沖突的情況下,
會影響其他CPU上的記憶體通路順序.
鎖與記憶體通路
假設系統中有(M)和(Q)這一對spinlock, 有三個CPU; 那麼可能發生如下操作序列:
*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
但是, 如果是下面的情形:
LOCK M [1]
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()作為幹預, 例如:
mmiowb();
這樣就能確定CPU 1的兩次STORE操作先于CPU 2的STORE操作被PCI橋所看到.
此外, 對于同一硬體裝置在進行STORE操作之後再進行LOAD操作, 可以省去mmiowb(), 因為
LOAD操作将強制STORE操作在開始LOAD之前就完成:
a = readl(DATA);
b = readl(DATA);
更多資訊請參閱"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*()函數傳回, 然後可能進行新的函數調用, 導緻棧空間被重複使用).
看看上面的事件序列可能會發生什麼:
down_xxx()
将waiter結構鍊入等待隊列
進入睡眠
up_yyy()
被CPU 1的UP事件喚醒
<被搶占了>
重新得到運作
down_xxx()函數傳回
繼續調用foo()函數
foo()重用了棧上的waiter結構
<搶占傳回>
--- OOPS ---
對付這個問題可以使用信号量中的鎖, 但是當程序被喚醒後, down_xxx()函數其實沒必要重
新獲得這個spinlock.
實際的解決辦法是插入一個通用SMP記憶體屏障:
smp_mb();
這樣, 對于系統中的其他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 | | |--->| 裝置 |
| | | 隊列 | : | | | | | |
一些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要去讀取這些值的情形:
q = p;
上面這一對讀操作可能不會在預期的順序下執行, 比如持有p的cache行可能被更新到另一個
CPU的cache, 而持有v的cache行因為其他一些cache事件的影響而延遲了對那個CPU的cache
的更新:
<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>
<C:read *q> 在v被更新到cache之前讀取v
<C:unbusy>
<C:commit v=2>
基本上, 雖然最終CPU 2的兩個cache行都将得到更新, 但是在沒有幹預的情況下, 并不能保
證更新的順序跟CPU 1送出的順序一緻.
我們需要在兩次LOAD之間插入一個資料依賴屏障或讀屏障, 以作為幹預. 這将強制cache在
處理後續的請求之前, 先讓它的一緻性隊列得到送出:
smp_read_barrier_depends()
<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;
c = *C;
d = *D;
對于每一條指令, 他們會期望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看到這些操作之前, 編譯器也可能會合并, 丢棄或推遲序列中的一些操作.
可能削減為:
于是, 在沒有使用寫屏障的情況下, 可以認為将V寫入*A的STORE操作丢失了. 類似的:
在沒有記憶體屏障的情況下, 可能削減為:
Z = Y;
于是在該CPU之外, 根本就看不到有LOAD操作存在.
特别值得一提的Alpha處理器
-------------------------
DEC Alpha是現有的最為松散的CPU之一. 不僅如此, 許多版本的Alpha CPU擁有分列的資料
cache, 允許他們在不同的時間更新兩個語義相關的緩存. 因為記憶體一緻性系統需要同步更
新系統的兩個cache, 資料依賴屏障在這裡就真正成為了必要, 以使得CPU能夠按正确的順序
來處理指針的更新和新資料的擷取.
Alpha處理器定義了Linux核心的記憶體屏障模型. (譯注: 體系結構無關的代碼需要以最壞情
況為基準來考慮.)
參閱前面的"Cache一緻性"章節.
========
使用示例
環型緩沖區
記憶體屏障可以用于實作環型緩沖區, 不需要使用鎖, 就能使生産者和消費者串行化. 參閱:
Documentation/circular-buffers.txt
以獲得更多細節.
====
引用
Alpha AXP Architecture Reference Manual, Second Edition (Sites & Witek,
Digital Press)
Chapter 5.2: Physical Address Space Characteristics
Chapter 5.4: Caches and Write Buffers
Chapter 5.5: Data Sharing
Chapter 5.6: Read/Write Ordering
AMD64 Architecture Programmer's Manual Volume 2: System Programming
Chapter 7.1: Memory-Access Ordering
Chapter 7.4: Buffering and Combining Memory Writes
IA-32 Intel Architecture Software Developer's Manual, Volume 3:
System Programming Guide
Chapter 7.1: Locked Atomic Operations
Chapter 7.2: Memory Ordering
Chapter 7.4: Serializing Instructions
The SPARC Architecture Manual, Version 9
Chapter 8: Memory Models
Appendix D: Formal Specification of the Memory Models
Appendix J: Programming with the Memory Models
UltraSPARC Programmer Reference Manual
Chapter 5: Memory Accesses and Cacheability
Chapter 15: Sparc-V9 Memory Models
UltraSPARC III Cu User's Manual
Chapter 9: Memory Models
UltraSPARC IIIi Processor User's Manual
UltraSPARC Architecture 2005
Chapter 9: Memory
Appendix D: Formal Specifications of the Memory Models
UltraSPARC T1 Supplement to the UltraSPARC Architecture 2005
Appendix F: Caches and Cache Coherency
Solaris Internals, Core Kernel Architecture, p63-68:
Chapter 3.3: Hardware Considerations for Locks and
Synchronization
Unix Systems for Modern Architectures, Symmetric Multiprocessing and Caching
for Kernel Programmers:
Chapter 13: Other Memory Models
Intel Itanium Architecture Software Developer's Manual: Volume 1:
Section 2.6: Speculation
Section 4.4: Memory Access