Linux內核的內存屏障
內容:
什麼是內存屏障? 顯式內核屏障 隱式內核內存屏障 CPU之間的鎖屏障效應 什麼地方需要內存障礙? 內核的I/O屏障效應 假想的最小執行順序模型 CPU緩存的影響 CPU能做到的 使用示例 引用
抽象的內存訪問模型
考慮下麵這個係統的抽象模型:
: : : : : : +-------+ : +--------+ : +-------+ | | : | | : | | | | : | | : | | | CPU 1 |<----->| Memory |<----->| CPU 2 | | | : | | : | | | | : | | : | | +-------+ : +--------+ : +-------+ ^ : ^ : ^ | : | : | | : | : | | : v : | | : +--------+ : | | : | | : | | : | | : | +---------->| Device |<----------+ : | | : : | | : : +--------+ : : :
每個CPU執行一個有內存訪問操作的程序。在這個抽象的CPU中,內存操作的順序是非常寬鬆的。假若能讓程序的因果關係看起來是保持著的,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指令時感知到的這些store的順序可能並不是第一個CPU提交的順序。
另一個例子,考慮下麵的事件序列:
CPU 1 CPU 2 =============== =============== { A == 1, B == 2, C = 3, P == &A, Q == &C } B = 4; Q = P; P = &B D = *Q;
這裏有一個明顯的數據依賴,D的值取決於CPU 2從P取得的地址。執行結束時,下麵任一結果都是有可能的;
(Q == &A) and (D == 1) (Q == &B) and (D == 2) (Q == &B) and (D == 4)
注意:CPU 2永遠不會將C的值賦給D,因為CPU在對*Q發出load指令之前會先將P賦給Q。
硬件操作
一些硬件的控製接口,是一組存儲單元,但這些控製寄存器的訪問順序是非常重要的。例如,考慮擁有一係列內部寄存器的以太網卡,它通過一個地址端口寄存器(A)和一個數據端口寄存器(D)訪問。現在要讀取編號為5的內部寄存器,可能要使用下列代碼:
*A = 5; x = *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指令在該CPU中將會看起來是有序的。這意味著對於:
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重疊了。)。
還有一些必須要和一定不能假設的東西:
- 一定不能假設無關聯的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的交互和I / O來說卻是個問題。我們需要某種方式來指導編譯器和CPU以約束執行順序。
內存屏障就是這樣一種幹預手段。它們會給屏障兩側的內存操作強加一個偏序關係。
這種強製措施是很重要的,因為一個係統中,CPU和其它硬件可以使用各種技巧來提高性能,包括內存操作的重排、延遲和合並;預取;推測執行分支以及各種類型的緩存。內存屏障是用來禁用或抑製這些技巧的,使代碼穩健地控製多個CPU和(或)設備的交互。
內存屏障的種類
內存屏障有四種基本類型:
- write(或store)內存屏障。
write內存屏障保證:所有該屏障之前的store操作,看起來一定在所有該屏障之後的store操作之前執行。
write屏障僅保證store指令上的偏序關係,不要求對load指令有什麼影響。
隨著時間推移,可以視CPU提交了一係列store操作到內存係統。在該一係列store操作中,write屏障之前的所有store操作將在該屏障後麵的store操作之前執行。
[!]注意,write屏障一般與read屏障或數據依賴障礙成對出現;請參閱“SMP屏障配對”小節。
數據依賴屏障。數據依賴屏障是read屏障的一種較弱形式。在執行兩個load指令,第二個依賴於第一個的執行結果(例如:第一個load執行獲取某個地址,第二個 load指令取該地址的值)時,可能就需要一個數據依賴屏障,來確保第二個load指令在獲取目標地址值的時候,第一個load指令已經更新過該地址。
數據依賴屏障僅保證相互依賴的load指令上的偏序關係,不要求對store指令,無關聯的load指令以及重疊的load指令有什麼影響。
如write(或store)內存屏障中 提到的,可以視係統中的其它CPU提交了一些列store指令到內存係統,然後the CPU being considered就能感知到。由該CPU發出的數據依賴屏障可以確保任何在該屏障之前的load指令,如果該load指令的目標被另一個CPU的存儲 (store)指令修改,在屏障執行完成之後,所有在該load指令對應的store指令之前的store指令的更新都會被所有在數據依賴屏障之後的 load指令感知。
參考”內存屏障順序實例”小節圖中的順序約束。
[!]注意:第一個load指令確實必須有一個數據依賴,而不是控製依賴。如果第二個load指令的目標地址依賴於第 一個load,但是這個依賴是通過一個條件語句,而不是實際加載的地址本身,那麼它是一個控製依賴,需要一個完整的read屏障或更強的屏障。查看”控製 依賴”小節,了解更多信息。
[!]注意:數據依賴屏障一般與寫障礙成對出現;看到“SMP屏障配對”章節。
read(或load)內存屏障。read屏障是數據依賴屏障外加一個保證,保證所有該屏障之前的load操作,看起來一定在所有該屏障之後的load操作之前執行。
read屏障僅保證load指令上的偏序關係,不要求對store指令有什麼影響。
read屏障包含了數據依賴屏障的功能,因此可以替代數據依賴屏障。
[!]注意:read屏障通常與write屏障成對出現;請參閱“SMP屏障配對”小節。
通用內存屏障。通用屏障確保所有該屏障之前的load和store操作,看起來一定在所有屏障之後的load和store操作之前執行。
通用屏障能保證load和store指令上的偏序關係。
通用屏障包含了read屏障和write屏障,因此可以替代它們兩者。
一對隱式的屏障變種:
- LOCK操作。
LOCK操作可以看作是一個單向滲透的屏障。它保證所有在LOCK之後的內存操作看起來一定在LOCK操作後才發生。
LOCK操作之前的內存操作可能會在LOCK完成之後發生。
LOCK操作幾乎總是與UNLOCK操作成對出現。
- UNLOCK操作。
這也是一個單向滲透屏障。它保證所有UNLOCK操作之前的內存操作看起來一定在UNLOCK操作之前發生。
UNLOCK操作之後的內存操作可能會在UNLOCK完成之前發生。
LOCK和UNLOCK操作嚴格保證自己對指令的順序。
使用了LOCK和UNLOCK操作,一般就不需要其它類型的內存屏障了(但要注意在”MMIO write屏障”一節中提到的例外情況)。
僅當兩個CPU之間或者CPU與其它設備之間有交互時才需要屏障。如果可以確保某段代碼中不會有任何這種交互,那麼這段代碼就不需要內存屏障。
注意,這些是最低限度的保證。不同的架構可能會提供更多的保證,但是它們不是必須的,不應該依賴其寫代碼(they may not be relied upon outside of arch specific code)。
什麼是內存屏障不能確保的?
有一些事情,Linux內核的內存屏障並不保證:
- 不能保證,任何在內存屏障之前的內存訪問操作能在內存屏障指令執行完成時也執行完成;內存屏障相當於在CPU的訪問隊列中劃了一條界線,相應類型的指令不能跨過該界線。
- 不能保證,一個CPU發出的內存屏障能對另一個CPU或該係統中的其它硬件有任何直接影響。隻會間接影響到第二個CPU看第一個CPU的存取操作發生的順序,但請看下一條:
- 不能保證,一個CPU看到第二個CPU存取操作的結果的順序,即使第二個CPU使用了內存屏障,除非第一個CPU也使用與第二個CPU相匹配的內存屏障(見”SMP屏障配對”小節)。
-
不能保證,一些CPU相關的硬件[*]不會對內存訪問重排序。 CPU緩存的一致性機製會在多個CPU之間傳播內存屏障的間接影響,但可能不是有序的。
[*]總線主控DMA和一致性相關信息,請參閱:
Documentation/PCI/pci.txt
Documentation/PCI/PCI-DMA-mapping.txt
Documentation/DMA-API.txt
數據依賴屏障
數據依賴屏障的使用條件有點微妙,且並不總是很明顯。為了闡明問題,考慮下麵的事件序列:
CPU 1 CPU 2 =============== =============== { A == 1, B == 2, C = 3, P == &A, Q == &C } B = 4; <write barrier> P = &B Q = P; D = *Q;
這裏很明顯存在數據依賴,看起來在執行結束後,Q不是&A就是&B,並且:
(Q == &A) 意味著 (D == 1) (Q == &B) 意味著 (D == 4)
但是,從CPU 2可能先感知到P更新,然後才感知到B更新,這就導致了以下情況:
(Q == &B) and (D == 2) ????
雖然這可能看起來像是一致性或因果關係維護失敗,但實際並不是的,且這種行為在一些真實的CPU上也可以觀察到(如DEC Alpha)。
為了處理這個問題,需要在地址load和數據load之間插入一個數據依賴屏障或一個更強的屏障:
CPU 1 CPU 2 =============== =============== { A == 1, B == 2, C = 3, P == &A, Q == &C } B = 4; <write barrier> P = &B Q = P; <data dependency barrier> D = *Q;
這將迫使結果為前兩種情況之一,而防止了第三種可能性的出現。
[!]注意:這種極其有違直覺的場景,在有多個獨立緩存(split caches)的機器上很容易出現,比如:一個cache bank處理偶數編號的緩存行,另外一個cache bank處理奇數編號的緩存行。指針P可能存儲在奇數編號的緩存行,變量B可能存儲在偶數編號的緩存行中。然後,如果在讀取CPU緩存的時候,偶數的 bank非常繁忙,而奇數bank處於閑置狀態,就會出現指針P(&B)是新值,但變量B(2)是舊值的情況。
另外一個需要數據依賴屏障的例子是從內存中讀取一個數字,然後用來計算某個數組的下標;
CPU 1 CPU 2 =============== =============== { M[0] == 1, M[1] == 2, M[3] = 3, P == 0, Q == 3 } M[1] = 4; <write barrier> P = 1 Q = P; <data dependency barrier> D = M[Q];
數據依賴屏障對RCU係統是很重要的,如,看include/linux/ rcupdate.h的rcu_dereference()函數。這個函數允許RCU的指針被替換為一個新的值,而這個新的值還沒有完全的初始化。
更多詳細的例子參見”高速緩存一致性”小節。
控製依賴
控製依賴需要一個完整的read內存屏障來保證其正確性,而不簡單地隻是數據依賴屏障。考慮下麵的代碼:
q = &a; if (p) q = &b; <data dependency barrier> x = *q;
這不會產生想要的結果,因為這裏沒有實際的數據依賴,而是一個控製依賴,CPU可能會提前預測結果而使if語句短路。在這樣的情況下,實際需要的是下麵的代碼:
q = &a; if (p) q = &b; <read barrier> x = *q;
SMP屏障配對
當處理CPU-CPU之間的交互時,相應類型的內存屏障總應該是成對出現的。缺少相應的配對屏障幾乎可以肯定是錯誤的。
write屏障應始終與數據依賴屏障或者read屏障配對,雖然通用內存屏障也是可以的。同樣地,read屏障或數據依賴屏障應至少始終與write屏障配對使用,雖然通用屏障仍然也是可以的:
CPU 1 CPU 2 =============== =============== a = 1; <write barrier> b = 2; x = b; <read barrier> y = a;
或者:
CPU 1 CPU 2 =============== =============================== a = 1; <write barrier> b = &a; x = b; <data dependency barrier> y = *x;
基本上,那個位置的read屏障是必不可少的,盡管可以是“更弱“的類型。
[!]注意:write屏障之前的store指令通常與read屏障或數據依賴屏障後的load指令相匹配,反之亦然:
CPU 1 CPU 2 =============== =============== a = 1; }---- --->{ v = c b = 2; } \ / { w = d <write barrier> \ <read barrier> c = 3; } / \ { x = a; d = 4; }---- --->{ y = b;
內存屏障序列實例
首先,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發出了write屏障,CPU2感知到的CPU1上事件的順序也可能是隨機的:
+-------+ : : : : | | +------+ +-------+ | 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 | +-------+ +-------+ : :
在上述的例子中,盡管load *C(可能是B)在load C之後,但CPU 2感知到的B卻是7;
然而,在CPU2中,如果數據依賴屏障放置在loadC和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 +-------+ | | : : +-------+
第三,read屏障確保load指令上的偏序關係。考慮以下的事件序列:
CPU 1 CPU 2 ======================= ======================= { A = 0, B = 9 } STORE A=1 <write barrier> STORE B=2 LOAD B LOAD A
在沒有其它幹涉時,盡管CPU1發出了一個write屏障,CPU 2感知到的CPU 1中事件的順序也可能是隨機的:
+-------+ : : : : | | +------+ +-------+ | |------>| A=1 |------ --->| A->0 | | | +------+ \ +-------+ | CPU 1 | wwwwwwwwwwwwwwww \ --->| B->9 | | | +------+ | +-------+ | |------>| B=2 |--- | : : | | +------+ \ | : : +-------+ +-------+ : : \ | +-------+ | | ---------->| B->2 |------>| | | +-------+ | CPU 2 | | | A->0 |------>| | | +-------+ | | | : : +-------+ \ : : \ +-------+ ---->| A->1 | +-------+ : :
然而,如果在CPU2上的load A和load B之間放置一個read屏障:
CPU 1 CPU 2 ======================= ======================= { A = 0, B = 9 } STORE A=1 <write barrier> STORE B=2 LOAD B <read barrier> LOAD A
CPU1上的偏序關係將能被CPU2正確感知到:
+-------+ : : : : | | +------+ +-------+ | |------>| 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 +-------+ | | : : +-------+
為了更徹底說明這個問題,考慮read屏障的兩側都有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都發生在loadB之後,它們仍然可能獲得不同的值:
+-------+ : : : : | | +------+ +-------+ | |------>| 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 +-------+ | | : : +-------+
但是,在read屏障完成之前,CPU1對A的更新就可能被CPU2看到:
+-------+ : : : : | | +------+ +-------+ | |------>| 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的值,A == 0或A == 1都可能會出現。
read內存屏障與load預加載
許多CPU都會預測並提前加載:即,當係統發現它即將需要從內存中加載一個條目時,係統會尋找沒有其它load指令占用總線資源的時候提前加載 —— 即使還沒有達到指令執行流中的該點。這使得實際的load指令可能會立即完成,因為CPU已經獲得了值。
也可能CPU根本不會使用這個值,因為執行到了另外的分支而繞開了這個load – 在這種情況下,它可以丟棄該值或僅是緩存該值供以後使用。
考慮下麵的場景:
CPU 1 CPU 2 ======================= ======================= LOAD B DIVIDE } Divide instructions generally DIVIDE } take a long time to perform 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指令之前,放置一個read屏障或數據依賴屏障:
CPU 1 CPU 2 ======================= ======================= LOAD B DIVIDE DIVIDE <read barrier> LOAD A
是否強製重新獲取預取的值,在一定程度上依賴於使用的屏障類型。如果值沒有發送變化,將直接使用預取的值:
: : +-------+ +-------+ | | --->| B->2 |------>| | +-------+ | CPU 2 | : : DIVIDE| | +-------+ | | The CPU being busy doing a ---> --->| A->0 |~~~~ | | division speculates on the +-------+ ~ | | LOAD of A : : ~ | | : : DIVIDE| | : : ~ | | : : ~ | | rrrrrrrrrrrrrrrr~ | | : : ~ | | : : ~-->| | : : | | : : +-------+
但如果另一個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 : : +-------+
傳遞性
傳遞性是有關順序的一個非常直觀的概念,但是真實的計算機係統往往並不保證。下麵的例子演示傳遞性(也可稱為“積累律(cumulativity)”):
CPU 1 CPU 2 CPU 3 ======================= ======================= ======================= { X = 0, Y = 0 } STORE X=1 LOAD X STORE Y=1 <general barrier> <general barrier> LOAD Y LOAD X
假設CPU 2 的load X返回1、load Y返回0。這表明,從某種意義上來說,CPU 2的LOAD X在CPU 1 store X之後,CPU 2的load y在CPU 3的store y 之前。問題是“CPU 3的 load X是否可能返回0?”
因為,從某種意義上說,CPU 2的load X在CPU 1的store之後,我們很自然地希望CPU 3的load X必須返回1。這就是傳遞性的一個例子:如果在CPU B上執行了一個load指令,隨後CPU A 又對相同位置進行了load操作,那麼,CPU A load的值要麼和CPU B load的值相同,要麼是個更新的值。
在Linux內核中,使用通用內存屏障能保證傳遞性。因此,在上麵的例子中,如果從CPU 2的load X指令返回1,且其load Y返回0,那麼CPU 3的load X也必須返回1。
但是,read或write屏障不保證傳遞性。例如,將上述例子中的通用屏障改為read屏障,如下所示:
CPU 1 CPU 2 CPU 3 ======================= ======================= ======================= { X = 0, Y = 0 } STORE X=1 LOAD X STORE Y=1 <read barrier> <general barrier> LOAD Y LOAD X
這就破壞了傳遞性:在本例中,CPU 2的load X返回1,load Y返回0,但是CPU 3的load X返回0是完全合法的。
關鍵點在於,雖然CPU 2的read屏障保證了CPU2上的load指令的順序,但它並不能保證CPU 1上的store順序。因此,如果這個例子運行所在的CPU 1和2共享了存儲緩衝區或某一級緩存,CPU 2可能會提前獲得到CPU 1寫入的值。因此,需要通用屏障來確保所有的CPU都遵守CPU1和CPU2的訪問組合順序。
要重申的是,如果你的代碼需要傳遞性,請使用通用屏障。
顯式內核屏障
Linux內核有多種不同的屏障,工作在不同的層上:
- 編譯器屏障。
- CPU內存屏障。
- MMIO write屏障。
編譯器屏障
Linux內核有一個顯式的編譯器屏障函數,用於防止編譯器將內存訪問從屏障的一側移動到另一側:
barrier();
這是一個通用屏障 – 不存在弱類型的編譯屏障。
編譯屏障並不直接影響CPU,CPU依然可以按照它所希望的順序進行重排序。
CPU內存屏障
Linux內核有8個基本的CPU內存屏障:
TYPE MANDATORY SMP CONDITIONAL =============== ======================= =========================== GENERAL mb() smp_mb() WRITE wmb() smp_wmb() READ rmb() smp_rmb() DATA DEPENDENCY read_barrier_depends() smp_read_barrier_depends()
除了數據依賴屏障之外,其它所有屏障都包含了編譯器屏障的功能。數據依賴屏障不強加任何額外的編譯順序。
旁白:在數據依賴的情況下,可能希望編譯器以正確的順序發出load指令(如:’a[b]’,將會在load a[b]之前load b),但在C規範下並不能保證如此,編譯器可能不會預先推測b的值(即,等於1),然後在load b之前先load a(即,tmp = a [1];if(b!= 1)tmp = a[b];)。還有編譯器重排序的問題,編譯器load a[b]之後重新load b,這樣,b就擁有比a[b]更新的副本。關於這些問題尚未形成共識,然而ACCESS_ONCE宏是解決這個問題很好的開始。
在單處理器編譯係統中,SMP內存屏障將退化為編譯屏障,因為它假定CPU可以保證自身的一致性,並且可以正確的處理重疊訪問。
[!]注意:SMP內存屏障必須用在SMP係統中來控製引用共享內存的順序,使用鎖也可以滿足需求。
強製性屏障不應該被用來控製SMP,因為強製屏障在UP係統中會產生過多不必要的開銷。但是,它們可以用於控製在通過鬆散內存I / O窗口訪問的MMIO操作。即使在非SMP係統中,這些也是必須的,因為它們可以禁止編譯器和CPU的重排從而影響內存操作的順序。
下麵是些更高級的屏障函數:
(*) set_mb(var, value)
這個函數將值賦給變量,然後在其後插入一個完整的內存屏障,根據不同的實現。在UP編譯器中,不能保證插入編譯器屏障之外的屏障。
(*) smp_mb__before_atomic_dec(); (*) smp_mb__after_atomic_dec(); (*) smp_mb__before_atomic_inc(); (*) smp_mb__after_atomic_inc();
這些都是用於原子加,減,遞增和遞減而不用返回值的,主要用於引用計數。這些函數並不包含內存屏障。
例如,考慮下麵的代碼片段,它標記死亡的對象, 然後將該對象的引用計數減1:
obj->dead = 1; smp_mb__before_atomic_dec(); atomic_dec(&obj->ref_count);
這可以確保設置對象的死亡標記是在引用計數遞減之前;
更多信息參見Documentation/atomic_ops.txt ,“Atomic operations” 章節介紹了它的使用場景。
(*) smp_mb__before_clear_bit(void); (*) smp_mb__after_clear_bit(void);
這些類似於用於原子自增,自減的屏障。他們典型的應用場景是按位解鎖操作,必須注意,因為這裏也沒有隱式的內存屏障。
考慮通過清除一個lock位來實現解鎖操作。 clear_bit()函數將需要像下麵這樣使用內存屏障:
smp_mb__before_clear_bit(); clear_bit( ... );
這可以防止在clear之前的內存操作跑到clear後麵。UNLOCK的參考實現見”鎖的功能”小節。
更多信息見Documentation/atomic_ops.txt , “Atomic operations“章節有關於使用場景的介紹;
MMIO write屏障
對於內存映射I / O寫操作,Linux內核也有個特殊的障礙;
mmiowb();
這是一個強製性寫屏障的變體,保證對弱序I / O區的寫操作有偏序關係。其影響可能超越CPU和硬件之間的接口,且能實際地在一定程度上影響到硬件。
更多信息參見”鎖與I / O訪問”章節。
隱式內核內存屏障
Linux內核中的一些其它的功能暗含著內存屏障,主要是鎖和調度功能。
該規範是一個最低限度的保證,任何特定的體係結構都可能提供更多的保證,但是在特定體係結構之外不能依賴它們。
鎖功能
Linux內核有很多鎖結構:
- 自旋鎖
- R / W自旋鎖
- 互斥
- 信號量
- R / W信號量
- RCU
所有的情況下,它們都是LOCK操作和UNLOCK操作的變種。這些操作都隱含著特定的屏障:
- LOCK操作的含義:
在LOCK操作之後的內存操作將會在LOCK操作結束之後完成;
在LOCK操作之前的內存操作可能在LOCK操作結束之後完成;
UNLOCK操作的含義:在UNLOCK操作之前的內存操作將會在UNLOCK操作結束之前完成;
在UNLOCK操作之後的內存操作可能在UNLOCK操作結束之前完成;
LOCK與LOCK的含義:在一個LOCK之前的其它LOCK操作一定在該LOCK結束之前完成;
LOCK與UNLOCK的含義:在某個UNLOCK之前的所有其它LOCK操作一定在該UNLOCK結束之前完成;
在某個LOCK之前的所有其它UNLOCK操作一定在該LOCK結束之前完成;
失敗的有條件鎖的含義:某些鎖操作的變種可能會失敗,要麼是由於無法立即獲得鎖,要麼是在休眠等待鎖可用的同時收到了一個解除阻塞的信號。失敗的鎖操作並不暗含任何形式的屏障。
因此,根據(1),(2)和(4),一個無條件的LOCK後麵跟著一個UNLOCK操作相當於一個完整的屏障,但一個UNLOCK後麵跟著一個LOCK卻不是。
[!]注意:將LOCK和UNLOCK作為單向屏障的一個結果是,臨界區外的指令可能會移到臨界區裏。
LOCK後跟著一個UNLOCK並不認為是一個完整的屏障,因為存在LOCK之前的存取發生在LOCK之後,UNLOCK之後的存取在UNLOCK之前發生的可能性,這樣,兩個存取操作的順序就可能顛倒:
*A = a; LOCK UNLOCK *B = b;
可能會發生:
LOCK, STORE *B, STORE *A, UNLOCK
鎖和信號量在UP編譯係統中不保證任何順序,所以在這種情況下根本不能考慮為屏障 —— 尤其是對於I / O訪問 —— 除非結合中斷禁用操作。
更多信息請參閱”CPU之間的鎖屏障”章節。
考慮下麵的例子:
*A = a; *B = b; LOCK *C = c; *D = d; UNLOCK *E = e; *F = f;
以下的順序是可以接受的:
LOCK, {*F,*A}, *E, {*C,*D}, *B, UNLOCK
[+] Note that {*F,*A} indicates a combined access.
但下列情形的,是不能接受的:
{*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)僅可充當編譯屏障。所以,如果某些場景下需要內存或I / O屏障,必須通過其它的手段來提供。
休眠和喚醒功能
一個全局數據標記的事件上的休眠和喚醒,可以被看作是兩塊數據之間的交互:正在等待的任務的狀態和標記這個事件的全局數據。為了確保正確的順序,進入休眠的原語和喚醒的原語都暗含了某些屏障。
首先,通常一個休眠任務執行類似如下的事件序列:
for (;;) { set_current_state(TASK_UNINTERRUPTIBLE); if (event_indicated) break; schedule(); }
set_current_state()會在改變任務狀態後自動插入一個通用內存屏障;
CPU 1 =============================== set_current_state(); set_mb(); STORE current->state <general barrier> 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 <write barrier> <general barrier> 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()之後,若要load喚醒前store指令存儲的值,休眠和喚醒所暗含的內存屏障都不能保證喚醒前多個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的變化。在這種情況下,兩側的代碼必須在隔離數據訪問之間插入自己的內存屏障。因此,上麵的休眠任務應該這樣:
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()以及類似函數暗含了完整內存屏障。
CPU之間的鎖屏障效應
在SMP係統中,鎖原語提供了更加豐富的屏障類型:在任意特定的鎖衝突的情況下,會影響其它CPU上的內存訪問順序。
鎖與內存訪問
考慮下麵的場景:係統有一對自旋鎖(M)、(Q)和三個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上的作用。例如,它可能感知的順序如下:
*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] 之後
鎖與I/O訪問
在某些情況下(尤其是涉及NUMA),在兩個不同CPU上的兩個自旋鎖區內的I / O訪問,在PCI橋看來可能是交叉的,因為PCI橋不一定保證緩存一致性,此時內存屏障將失效。
例如:
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
這可能會導致硬件故障。
這裏有必要在釋放自旋鎖之前插入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強製在load執行前store指令必須完成:
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的操作可能必須仔細排序,以防止出現故障。
例如,在R / W信號量慢路徑的場景。這裏有一個waiter進程在信號量上排隊,並且它的堆棧上的一塊空間鏈接到信號量上的等待進程列表:
struct rw_semaphore { ... spinlock_t lock; struct list_head waiters; }; struct rwsem_waiter { struct list_head list; struct task_struct *task; };
要喚醒一個特定的waiter進程,up_read()或up_write()函數必須做以下動作:
- 讀取waiter記錄的next指針,獲取下一個waiter記錄的地址;
-
- 讀取waiter的task結構的指針;
-
- 清除task指針,通知waiter已經獲取信號量;
-
- 在task上調用wake_up_process()函數;
-
- 釋放waiter的task結構上的引用。
-
換句話說,它必須執行下麵的事件:
LOAD waiter->list.next; LOAD waiter->task; STORE waiter->task; CALL wakeup RELEASE task
如果這些步驟的順序發生任何改變,那麼就會出問題。
一旦進程將自己排隊並且釋放信號鎖,waiter將不再獲得鎖,它隻需要等待它的任務指針被清零,然後繼續執行。由於記錄是在waiter的堆棧上,這意 味著如果在列表中的next指針被讀取出之前,task指針被清零,另一個CPU可能會開始處理,up*()函數在有機會讀取next指針之前 waiter的堆棧就被修改。
考慮上述事件序列可能發生什麼:
CPU 1 CPU 2 =============================== =============================== down_xxx() Queue waiter Sleep up_yyy() LOAD waiter->task; STORE waiter->task; Woken up by other event <preempt> Resume processing down_xxx() returns call foo() foo() clobbers *waiter </preempt> LOAD waiter->list.next; --- OOPS ---
雖然這裏可以使用信號鎖來處理,但在喚醒後的down_xxx()函數不必要的再次獲得自旋鎖。
這個問題可以通過插入一個通用的SMP內存屏障來處理:
LOAD waiter->list.next; LOAD waiter->task; smp_mb(); STORE waiter->task; CALL wakeup RELEASE task
在這種情況下,即使是在其它的CPU上,屏障確保所有在屏障之前的內存操作一定先於屏障之後的內存操作執行。但是它不能確保所有在屏障之前的內存操作一定先於屏障指令身執行完成時執行;
在一個UP係統中, 這種場景不會產生問題 , smp_mb()僅僅是一個編譯屏障,可以確保編譯器以正確的順序發出指令,而不會實際幹預到CPU。因為隻有一個CPU,CPU的依賴順序邏輯會管理好一切。
原子操作
雖然它們在技術上考慮了處理器間的交互,但是特別注意,有一些原子操作暗含了完整的內存屏障,另外一些卻沒有包含,但是它們作為一個整體在內核中應用廣泛。
任一原子操作,修改了內存中某一狀態並返回有關狀態(新的或舊的)的信息,這意味著在實際操作(明確的lock操作除外)的兩側暗含了一個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(); /* when succeeds (returns 1) */ test_and_set_bit(); test_and_clear_bit(); test_and_change_bit();
它們都是用於實現諸如LOCK和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();
如果將它們用於統計,那麼可能並不需要內存屏障,除非統計數據之間有耦合。
如果將它們用於對象的引用計數器來控製生命周期,也許也不需要內存屏障,因為可能引用計數會在鎖區域內修改,或調用方已經考慮了鎖,因此內存屏障不是必須的。
如果將它們用於構建一個鎖的描述,那麼確實可能需要內存屏障,因為鎖原語通常以特定的順序來處理事情;
基本上,每一個使用場景都必須仔細考慮是否需要內存屏障。
以下操作是特殊的鎖原語:
test_and_set_bit_lock(); clear_bit_unlock(); __clear_bit_unlock();
這些實現了諸如LOCK和UNLOCK的操作。在實現鎖原語時應當優先考慮使用它們,因為它們的實現可以在很多架構中進行優化。
[!]注意:對於這些場景,也有特定的內存屏障原語可用,因為在某些CPU上原子指令暗含著完整的內存屏障,再使用內存屏障顯得多餘,在這種情況下,特殊屏障原語將是個空操作。
更多信息見 Documentation/atomic_ops.txt。
設備訪問
許多設備都可以映射到內存上,因此對CPU來說它們隻是一組內存單元。為了控製這樣的設備,驅動程序通常必須確保對應的內存訪問順序的正確性。
然而,聰明的CPU或者聰明的編譯器可能為引發潛在的問題,如果CPU或者編譯器認為重排、合並、聯合訪問更加高效,驅動程序精心編排的指令順序可能在實際訪問設備是並不是按照這個順序訪問的 —— 這會導致設備故障。
在Linux內核中,I / O通常需要適當的訪問函數 —— 如inb() 或者 writel() —— 它們知道如何保持適當的順序。雖然這在大多數情況下不需要明確的使用內存屏障,但是下麵兩個場景可能需要:
- 在某些係統中,I / O存儲操作並不是在所有CPU上都是嚴格有序的,所以,對所有的通用驅動,鎖是必須的,且必須在解鎖臨界區之前執行mmiowb().
- 如果訪問函數是用來訪問一個鬆散訪問屬性的I / O存儲窗口,那麼需要強製內存屏障來保證順序。
更多信息參見 Documentation/DocBook/deviceiobook.tmpl。
中斷
驅動可能會被自己的中斷服務例程中斷,因此,驅動程序兩個部分可能會互相幹擾,嚐試控製或訪問該設備。
通過禁用本地中斷(一種鎖的形式)可以緩和這種情況,這樣,驅動程序中關鍵的操作都包含在中斷禁止的區間中 。有時驅動的中斷例程被執行,但是驅動程序的核心不是運行在相同的CPU上,並且直到當前的中斷被處理結束之前不允許其它中斷,因此,在中斷處理器不需要再次加鎖。
但是,考慮一個驅動使用地址寄存器和數據寄存器跟以太網卡交互,如果該驅動的核心在中斷禁用下與網卡通信,然後驅動程序的中斷處理程序被調用:
LOCAL IRQ DISABLE writew(ADDR, 3); writew(DATA, y); LOCAL IRQ ENABLE <interrupt> writew(ADDR, 4); q = readw(DATA); </interrupt>
如果排序規則十分寬鬆,數據寄存器的存儲可能發生在第二次地址寄存器之後:
STORE *ADDR = 3, STORE *ADDR = 4, STORE *DATA = y, q = LOAD *DATA
如果是寬鬆的排序規則,它必須假設中斷禁止部分的內存訪問可能向外泄漏,可能會和中斷部分交叉訪問 – 反之亦然 – 除非使用了隱式或顯式的屏障。
通常情況下,這不會產生問題,因為這種區域中的I / O訪問將在嚴格有序的IO寄存器上包含同步load操作,形成隱式內存屏障。如果這還不夠,可能需要顯式地使用一個mmiowb()。
類似的情況可能發生在一個中斷例程和運行在不同CPU上進行通信的兩個例程的時候。這樣的情況下,應該使用中斷禁用鎖來保證順序。
內核I/O屏障效應
訪問I/O內存時,驅動應使用適當的存取函數:
- inX(), outX():
它們都旨在跟I / O空間打交道,而不是內存空間,但這主要是一個特定於CPU的概念。在 i386和x86_64處理器中確實有特殊的I / O空間訪問周期和指令,但許多CPU沒有這樣的概念。
包括PCI總線也定義了I / O空間,比如在i386和x86_64的CPU 上很容易將它映射到CPU的I / O空間上。然而,對於那些不支持IO空間的CPU,它也可能作為虛擬的IO空間被映射CPU的的內存空間。
訪問這個空間可能是完全同步的(在i386),但橋設備(如PCI主橋)可能不完全履行這一點。
可以保證它們彼此之間的全序關係。
對於其他類型的內存和I / O操作,不保證它們的全序關係。
readX(), writeX():無論是保證完全有序還是不合並訪問取決於他們訪問時定義的訪問窗口屬性,例如,最新的i386架構的機器通過MTRR寄存器控製。
通常情況下,隻要不是訪問預取設備,就保證它們的全序關係且不合並。
然而,對於中間鏈接硬件(如PCI橋)可能會傾向延遲處理,當刷新一個store時,首選從同一位置load,但是對同一個設備或配置空間load時,對與PCI來說一次就足夠了。
[*]注意:試圖從剛寫過的相同的位置load數據可能導致故障 – 考慮16550 RX / TX串行寄存器的例子。
對於可預取的I / O內存,可能需要一個mmiowb()屏障保證順序;
請參閱PCI規範獲得PCI事務間交互的更多信息;
readX_relaxed()這些類似readX(),但在任何時候都不保證順序。因為沒有I / O讀屏障。
ioreadX(), iowriteX()它們通過選擇inX()/outX() or readX()/writeX()來實際操作。
假想的最小執行順序模型
首先假定概念上CPU是弱有序的,但它能維護程序因果關係。某些CPU(如i386或x86_64)比其它類型的CPU(如PowerPC的或FRV)受到更多的約束,所以,在考慮與具體體係結構無關的代碼時,必須假設處在最寬鬆
最後更新:2017-05-22 16:37:51