================= LINUX內(nèi)核內(nèi)存屏障 ================= By: David Howells <[email protected]> Paul E. McKenney <[email protected]> 譯: kouu <[email protected]> 出 處: Linux內(nèi)核文檔 -- Documentation/memory-barriers.txt 目錄: (*) 內(nèi)存訪問抽象模型. - 操作設(shè)備. - 保證. (*) 什么是內(nèi)存屏障? - 各式各樣的內(nèi)存屏障. - 關(guān)于內(nèi)存屏障, 不能假定什么? - 數(shù)據(jù)依賴屏障. - 控制依賴. - SMP內(nèi)存屏障的配對(duì)使用. - 內(nèi)存屏障舉例. - 讀內(nèi)存屏障與內(nèi)存預(yù)取. (*) 內(nèi)核中顯式的內(nèi)存屏障. - 編譯優(yōu)化屏障. - CPU內(nèi)存屏障. - MMIO寫屏障. (*) 內(nèi)核中隱式的內(nèi)存屏障. - 鎖相關(guān)函數(shù). - 禁止中斷函數(shù). - 睡眠喚醒函數(shù). - 其他函數(shù). (*) 跨CPU的鎖的屏障作用. - 鎖與內(nèi)存訪問. - 鎖與IO訪問. (*) 什么地方需要內(nèi)存屏障? - 處理器間交互. - 原子操作. - 訪問設(shè)備. - 中斷. (*) 內(nèi)核中I/O屏障的作用. (*) 最小限度有序的假想模型. (*) CPU cache的影響. - Cache一致性. - Cache一致性與DMA. - Cache一致性與MMIO. (*) CPU所能做到的. - 特別值得一提的Alpha處理器. (*) 使用示例. - 環(huán)型緩沖區(qū). (*) 引用. ================ 內(nèi)存訪問抽象模型 ================ 考 慮如下抽象系統(tǒng)模型: : : : : : : +-------+ : +--------+ : +-------+ | | : | | : | | | | : | | : | | | CPU 1 |<----->| 內(nèi)存 |<----->| CPU 2 | | | : | | : | | | | : | | : | | +-------+ : +--------+ : +-------+ ^ : ^ : ^ | : | : | | : | : | | : v : | | : +--------+ : | | : | | : | | : | | : | +---------->| 設(shè)備 |<----------+ : | | : : | | : : +--------+ : : : 假設(shè)每個(gè)CPU都分別運(yùn)行著一個(gè)會(huì) 觸發(fā)內(nèi)存訪問操作的程序. 對(duì)于這樣一個(gè)CPU, 其內(nèi)存訪問 順序是非常松散的, 在保證程序上下文邏輯關(guān)系的前提下, CPU可以按它所喜歡的順序來執(zhí) 行內(nèi)存操作. 類似的, 編譯器也可以將它輸出的指令安排成任何它喜歡的順序, 只要保證不 影響程序表面 的執(zhí)行邏輯. (譯注: 內(nèi)存屏障是為應(yīng)付內(nèi)存訪問操作的亂序執(zhí)行而生的. 那么, 內(nèi)存訪問為什么會(huì)亂序呢? 這里 先簡(jiǎn)要介紹一下: 現(xiàn)在的CPU一般采用流水線來執(zhí)行指令. 一個(gè)指令的執(zhí)行被分成: 取指, 譯碼, 訪存, 執(zhí)行, 寫回, 等若干個(gè)階段. 指令流水線并不是串行化的, 并不會(huì)因?yàn)橐粋€(gè)耗時(shí)很長(zhǎng)的指令在"執(zhí)行"階段呆很長(zhǎng)時(shí)間, 而 導(dǎo)致后續(xù)的指令都卡在"執(zhí)行"之前的階段上. 相反, 流水線中的多個(gè)指令是可以同時(shí)處于一個(gè)階段的, 只要CPU內(nèi)部相應(yīng)的處理部件未被 占滿. 比如說CPU有一個(gè)加法器和一個(gè)除法器, 那么一條加法指令和一條除法指令就可能同 時(shí)處于"執(zhí)行"階段, 而兩條加法指令在"執(zhí)行"階段就只能串行工作. 這樣一 來, 亂序可能就產(chǎn)生了. 比如一條加法指令出現(xiàn)在一條除法指令的后面, 但是由于除 法的執(zhí)行時(shí)間很長(zhǎng), 在它執(zhí)行完之前, 加法可能先執(zhí)行完了. 再比如兩條訪存指令, 可能由 于第二條指令命中了cache(或其他原因)而導(dǎo)致它先于第一條指令完成. 一 般情況下, 指令亂序并不是CPU在執(zhí)行指令之前刻意去調(diào)整順序. CPU總是順序的去內(nèi)存里 面取指令, 然后將其順序的放入指令流水線. 但是指令執(zhí)行時(shí)的各種條件, 指令與指令之間 的相互影響, 可能導(dǎo)致順序放入流水線的指令, 最終亂序執(zhí)行完成. 這就是所謂的"順序流 入, 亂序流出". 指令流水線除了在資源不足的情況下會(huì)卡住之外(如前所述的一個(gè)加法器應(yīng)付兩條加法指令) , 指令之間的相關(guān)性才是導(dǎo)致流水線阻塞的主要原因. 下文中也會(huì)多次提到, CPU的亂序執(zhí)行并不是任意的亂序, 而必須保證上下文依賴邏輯的正 確性. 比如: a++; b=f(a); 由于b=f(a)這條指令依賴于第一條指令(a++)的執(zhí)行結(jié)果, 所以 b=f(a)將在"執(zhí)行"階段之前被阻塞, 直到a++的執(zhí)行結(jié)果被生成出來. 如果兩條像這樣有依賴關(guān)系的指令挨得 很近, 后一條指令必定會(huì)因?yàn)榈却耙粭l執(zhí)行的結(jié)果 , 而在流水線中阻塞很久. 而編譯器的亂序, 作為編譯優(yōu)化的一種手段, 則試圖通過指令重 排 將這樣的兩條指令拉開距離, 以至于后一條指令執(zhí)行的時(shí)候前一條指令結(jié)果已經(jīng)得到了, 那么也就不再需要阻塞等待了. 相 比于CPU的亂序, 編譯器的亂序才是真正對(duì)指令順序做了調(diào)整. 但是編譯器的亂序也必須 保證程序上下文的依賴邏輯. 由 于指令執(zhí)行存在這樣的亂序, 那么自然, 由指令執(zhí)行而引發(fā)的內(nèi)存訪問勢(shì)必也可能亂序. ) 在上面的圖示中, 一個(gè)CPU執(zhí)行內(nèi)存操作所產(chǎn)生的影響, 一直要到該操作穿越該CPU與系統(tǒng)中 其他部分的界面(見圖中的虛線)之后, 才能被其他部分所感知. 舉 例來說, 考慮如下的操作序列: CPU 1 CPU 2 =============== =============== { A == 1; B == 2 } A = 3; x = A; B = 4; y = B; 這一 組訪問指令在內(nèi)存系統(tǒng)(見上圖的中間部分)上生效的順序, 可以有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, ... ... 然后這就產(chǎn)生四種不同組合的結(jié)果值: x == 1, y == 2 x == 1, y == 4 x == 3, y == 2 x == 3, y == 4 甚至于, 一個(gè)CPU在內(nèi)存系統(tǒng)上提交的STORE操作還可能不會(huì)以相同的順序被其他CPU所執(zhí)行 的LOAD操作所感知. 進(jìn)一步舉 例說明, 考慮如下的操作序列: CPU 1 CPU 2 =============== =============== { A == 1, B == 2, C = 3, P == &A, Q == &C } B = 4; Q = P; P = &B D = *Q; 這里有一處明顯的數(shù)據(jù)依賴, 因?yàn)樵贑PU2上, LOAD到D里面的值依賴于從P獲取到的地址. 在 操作序列的最后, 下面的幾種結(jié)果都是有可能出現(xiàn)的: (Q == &A) 且 (D == 1) (Q == &B) 且 (D == 2) (Q == &B) 且 (D == 4) 注意, CPU2決不會(huì)將C的值LOAD到D, 因?yàn)镃PU保證在將P的值裝載到Q之后才會(huì)執(zhí)行對(duì)*Q的 LOAD操作(譯注: 因?yàn)榇嬖跀?shù)據(jù)依賴). 操作設(shè)備 -------- 對(duì) 于一些設(shè)備, 其控制寄存器被映射到一組內(nèi)存地址集合上, 而這些控制寄存器被訪問的順 序是至關(guān)重要的. 假設(shè), 一個(gè)以太網(wǎng)卡擁有一些內(nèi)部寄存器, 通過一個(gè)地址端口寄存器(A) 和一個(gè)數(shù)據(jù)端口寄存器(D)來訪問它們. 要讀取編號(hào)為5的內(nèi)部寄存器, 可能使用如下代碼: *A = 5; x = *D; 但是這可能會(huì)表現(xiàn)為以下兩個(gè) 序列之一(譯注: 因?yàn)閺某绦虮砻婵? A和D是不存在依賴的): STORE *A = 5, x = LOAD *D x = LOAD *D, STORE *A = 5 其中的第二種幾乎肯定會(huì)導(dǎo)致錯(cuò)誤, 因?yàn)樗谧x取寄存器之后才設(shè)置寄存器的編號(hào). 保證 ---- 對(duì)于一個(gè)CPU, 它最低限度會(huì)提供如下的保證: (*) 對(duì)于一個(gè)CPU, 在它上面出現(xiàn)的有上下文依賴關(guān)系的內(nèi)存訪問將被按順序執(zhí)行. 這意味 著: Q = P; D = *Q; CPU會(huì)順序執(zhí)行以下訪存: Q = LOAD P, D = LOAD *Q 并且總是按這樣的順序. (*) 對(duì)于一個(gè)CPU, 重疊的LOAD和STORE操作將被按順序執(zhí)行. 這意味著: a = *X; *X = b; CPU只會(huì)按以下順序執(zhí)行訪存: a = LOAD *X, STORE *X = b 同樣, 對(duì)于: *X = c; d = *X; CPU只會(huì)按以下順序執(zhí)行訪存: STORE *X = c, d = LOAD *X (如果LOAD和STORE的目標(biāo)指向同一塊內(nèi)存地址, 則認(rèn)為是重疊). 還有一些事情是必須被假定或者必須不被假定的: (*) 必須不能假定無關(guān)的LOAD和STORE會(huì)按給定的順序被執(zhí)行. 這意味著: X = *A; Y = *B; *D = Z; 可能會(huì)得到如下幾種執(zhí)行序列之一: 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 (*) 必須假定重疊內(nèi)存訪問可能被合并或丟棄. 這意味著: X = *A; Y = *(A + 4); 可能會(huì)得到如下幾種執(zhí)行序列之一: X = LOAD *A; Y = LOAD *(A + 4); Y = LOAD *(A + 4); X = LOAD *A; {X, Y} = LOAD {*A, *(A + 4) }; 同樣, 對(duì)于: *A = X; Y = *A; 可能會(huì)得到如下幾種執(zhí)行序列之一: STORE *A = X; Y = LOAD *A; STORE *A = Y = X; =============== 什么是內(nèi)存屏障? =============== 正 如上面所說, 無關(guān)的內(nèi)存操作會(huì)被按隨機(jī)順序有效的得到執(zhí)行, 但是在CPU與CPU交互時(shí)或 CPU與IO設(shè)備交互時(shí), 這可能會(huì)成為問題. 我們需要一些手段來干預(yù)編譯器和CPU, 使其限制 指令順序. 內(nèi)存屏障就是這樣的干預(yù)手段. 他們能保證處于內(nèi)存屏障兩邊的內(nèi)存操作滿足部分有序. ( 譯注: 這里"部分有序"的意思是, 內(nèi)存屏障之前的操作都會(huì)先于屏障之后的操作, 但是如果 幾個(gè)操作出現(xiàn)在屏障的同一邊, 則不保證它們的順序. 這一點(diǎn)下文將多次提到.) 這樣的強(qiáng)制措施是非常重要的, 因?yàn)橄到y(tǒng)中的CPU和其他設(shè)備可以使用各種各樣的策略來提 高性能, 包括對(duì)內(nèi)存操作的亂序, 延遲和合并執(zhí)行; 預(yù)取; 投機(jī)性的分支預(yù)測(cè)和各種緩存. 內(nèi)存屏障用于禁用或抑制這些策略, 使代碼能夠清楚的控制多個(gè)CPU和/或設(shè)備的交互. 各 式各樣的內(nèi)存屏障 ------------------ 內(nèi)存屏障有四種基本類型: (1) 寫(STORE)內(nèi)存屏障. 寫內(nèi)存屏障提供這樣的保證: 所有出現(xiàn)在屏障之前的STORE操作都將先于所有出現(xiàn)在屏 障之后的STORE操作被系統(tǒng)中的其他組件所感知. 寫屏障僅保證針對(duì)STORE操作的部分有序; 不要求對(duì)LOAD操作產(chǎn)生影響. 隨著時(shí)間的推移, 一個(gè)CPU提交的STORE操作序列將被存儲(chǔ)系統(tǒng)所感知. 所有在寫屏障 之前的STORE操作將先于所有在寫屏障之后的STORE操作出現(xiàn)在被感知的序列中. [!] 注意, 寫屏障一般需要與讀屏障或數(shù)據(jù)依賴屏障配對(duì)使用; 參閱"SMP內(nèi)存屏障配 對(duì)"章節(jié). (譯注: 因?yàn)閷懫琳现槐WC自己提交的順序, 而無法干預(yù)其他代碼讀內(nèi) 存的順序. 所以配對(duì)使用很重要. 其他類型的屏障亦是同理.) (2) 數(shù)據(jù)依賴屏障. 數(shù)據(jù)依賴屏障是讀屏障的弱化版本. 假設(shè)有兩個(gè)LOAD操作的場(chǎng)景, 其中第二個(gè)LOAD操 作的結(jié)果依賴于第一個(gè)操作(比如, 第一個(gè)LOAD獲取地址, 而第二個(gè)LOAD使用該地址去 取數(shù)據(jù)), 數(shù)據(jù)依賴屏障確保在第一個(gè)LOAD獲取的地址被用于訪問之前, 第二個(gè)LOAD的 目標(biāo)內(nèi)存已經(jīng)更新. (譯注: 因?yàn)榈诙€(gè)LOAD要使用第一個(gè)LOAD的結(jié)果來作為L(zhǎng)OAD的目標(biāo), 這里存在著數(shù) 據(jù)依賴. 由前面的"保證"章節(jié)可知, 第一個(gè)LOAD必定會(huì)在第二個(gè)LOAD之前執(zhí)行, 不需 要使用讀屏障來保證順序, 只需要使用數(shù)據(jù)依賴屏障來保證內(nèi)存已刷新.) 數(shù)據(jù)依賴屏障僅保證針對(duì)相互依賴的LOAD操作的部分有序; 不要求對(duì)STORE操作, 獨(dú)立的LOAD操作, 或重疊的LOAD操作產(chǎn)生影響. 正如(1)中所提到的, 在一個(gè)CPU看來, 系統(tǒng)中的其他CPU提交到內(nèi)存系統(tǒng)的STORE操作 序列在某一時(shí)刻可以被其感知到. 而在該CPU上觸發(fā)的數(shù)據(jù)依賴屏障將保證, 對(duì)于在屏 障之前發(fā)生的LOAD操作, 如果一個(gè)LOAD操作的目標(biāo)被其他CPU的STORE操作所修改, 那 么在屏障完成之時(shí), 這個(gè)對(duì)應(yīng)的STORE操作之前的所有STORE操作所產(chǎn)生的影響, 將被 數(shù)據(jù)依賴屏障之后執(zhí)行的LOAD操作所感知. 參閱"內(nèi)存屏障舉例"章節(jié)所描述的時(shí)序圖. [!] 注意, 對(duì)第一個(gè)LOAD的依賴的確是一個(gè)數(shù)據(jù)依賴而不是控制依賴. 而如果第二個(gè) LOAD的地址依賴于第一個(gè)LOAD, 但并不是通過實(shí)際加載的地址本身這樣的依賴條 件, 那么這就是控制依賴, 需要一個(gè)完整的讀屏障或更強(qiáng)的屏障. 參閱"控制依 賴"相關(guān)章節(jié). [!] 注意, 數(shù)據(jù)依賴屏障一般要跟寫屏障配對(duì)使用; 參閱"SMP內(nèi)存屏障的配對(duì)使用"章 節(jié). (3) 讀(LOAD)內(nèi)存屏障. 讀屏障包含數(shù)據(jù)依賴屏障的功能, 并且保證所有出現(xiàn)在屏障之前的LOAD操作都將先于 所有出現(xiàn)在屏障之后的LOAD操作被系統(tǒng)中的其他組件所感知. 讀屏障僅保證針對(duì)LOAD操作的部分有序; 不要求對(duì)STORE操作產(chǎn)生影響. 讀內(nèi)存屏障隱含了數(shù)據(jù)依賴屏障, 因此可以用于替代它們. [!] 注意, 讀屏障一般要跟寫屏障配對(duì)使用; 參閱"SMP內(nèi)存屏障的配對(duì)使用"章節(jié). (4) 通用內(nèi)存屏障. 通用內(nèi)存屏障保證所有出現(xiàn)在屏障之前的LOAD和STORE操作都將先于所有出現(xiàn)在屏障 之后的LOAD和STORE操作被系統(tǒng)中的其他組件所感知. 通用內(nèi)存屏障是針對(duì)LOAD和STORE操作的部分有序. 通用內(nèi)存屏障隱含了讀屏障和寫屏障, 因此可以用于替代它們. 內(nèi)存屏障還有兩種隱式類型: (5) LOCK操作. 它的作用相當(dāng)于一個(gè)單向滲透屏障. 它保證所有出現(xiàn)在LOCK之后的內(nèi)存操作都將在 LOCK操作被系統(tǒng)中的其他組件所感知之后才能發(fā)生. 出現(xiàn)在LOCK之前的內(nèi)存操作可能在LOCK完成之后才發(fā)生. LOCK操作總是跟UNLOCK操作配對(duì)出現(xiàn)的. (6) UNLOCK操作. 它的作用也相當(dāng)于一個(gè)單向滲透屏障. 它保證所有出現(xiàn)在UNLOCK之前的內(nèi)存操作都將 在UNLOCK操作被系統(tǒng)中的其他組件所感知之前發(fā)生. 出現(xiàn)在UNLOCK之后的內(nèi)存操作可能在UNLOCK完成之前就發(fā)生了. 需要保證LOCK和UNLOCK操作嚴(yán)格按照相互影響的正確順序出現(xiàn). (譯注: LOCK和UNLOCK的這種單向屏障作用, 確保臨界區(qū)內(nèi)的訪存操作不能跑到臨界區(qū) 外, 否則就起不到"保護(hù)"作用了.) 使用LOCK和UNLOCK之后, 一般就不再需要其他內(nèi)存屏障了(但是注意"MMIO寫屏障"章節(jié) 中所提到的例外). 只 有在存在多CPU交互或CPU與設(shè)備交互的情況下才可能需要用到內(nèi)存屏障. 如果可以確保某 段代碼中不存在這樣的交互, 那么這段代碼就不需要使用內(nèi)存屏障. (譯注: CPU亂序執(zhí)行指 令, 同樣會(huì)導(dǎo)致寄存器的存取順序被打亂, 但是為什么不需要寄存器屏障呢? 就是因?yàn)榧拇?br>器是CPU私有的, 不存在跟其他CPU或設(shè)備的交互.) 注意, 對(duì)于前面提到的最低限度保證. 不同的體系結(jié)構(gòu)可能提供更多的保證, 但是在特定體 系結(jié)構(gòu)的代碼之外, 不能依賴于這些額外的保證. 關(guān)于內(nèi)存屏障, 不能假定什么? --------------------------- Linux內(nèi)核的內(nèi)存屏障不保證下面這些事情: (*) 在內(nèi)存屏障之前出現(xiàn)的內(nèi)存訪問不保證在內(nèi)存屏障指令完成之前完成; 內(nèi)存屏障相當(dāng) 于在該CPU的訪問隊(duì)列中畫一條線, 使得相關(guān)訪存類型的請(qǐng)求不能相互跨越. (譯注: 用于實(shí)現(xiàn)內(nèi)存屏障的指令, 其本身并不作為參考對(duì)象, 其兩邊的訪存操作才被當(dāng)作參 考對(duì)象. 所以屏障指令執(zhí)行完成并不表示出現(xiàn)在屏障之前的訪存操作已經(jīng)完成. 而如 果屏障之后的某一個(gè)訪存操作已經(jīng)完成, 則屏障之前的所有訪存操作必定都已經(jīng)完成 了.) (*) 在一個(gè)CPU上執(zhí)行的內(nèi)存屏障不保證會(huì)直接影響其他系統(tǒng)中的CPU或硬件設(shè)備. 只會(huì)間 接影響到第二個(gè)CPU感知第一個(gè)CPU產(chǎn)生訪存效果的順序, 不過請(qǐng)看下一點(diǎn): (*) 不能保證一個(gè)CPU能夠按順序看到另一個(gè)CPU的訪存效果, 即使另一個(gè)CPU使用了內(nèi)存屏 障, 除非這個(gè)CPU也使用了與之配對(duì)的內(nèi)存屏障(參閱"SMP內(nèi)存屏障的配對(duì)使用"章節(jié)). (*) 不保證一些與CPU相關(guān)的硬件不會(huì)亂序訪存. CPU cache一致性機(jī)構(gòu)會(huì)在CPU之間傳播內(nèi) 存屏障所帶來的間接影響, 但是可能不是按順序的. [*] 更多關(guān)于總線主控DMA和一致性的問題請(qǐng)參閱: Documentation/PCI/pci.txt Documentation/PCI/PCI-DMA-mapping.txt Documentation/DMA-API.txt 數(shù)據(jù)依賴屏障 ------------ 數(shù) 據(jù)依賴屏障的使用需求有點(diǎn)微妙, 并不總是很明顯就能看出需要他們. 為了說明這一點(diǎn), 考慮如下的操作序列: CPU 1 CPU 2 =============== =============== { A == 1, B == 2, C = 3, P == &A, Q == &C } B = 4; <寫屏障> P = &B Q = P; D = *Q; 這里有明顯的數(shù)據(jù)依賴, 在序列執(zhí)行完之后, Q的值一定是&A和&B之一, 也就是: (Q == &A) 那么 (D == 1) (Q == &B) 那么 (D == 4) 但是! CPU 2可能在看到P被更新之后, 才看到B被更新, 這就導(dǎo)致下面的情況: (Q == &B) 且 (D == 2) ???? 雖然這看起來似乎是一個(gè)一致性錯(cuò)誤或邏輯關(guān)系錯(cuò)誤, 但其實(shí)不是, 并且在一些真實(shí)的CPU 中就能 看到這樣的行為(就比如DEC Alpha). 為了解決這個(gè)問題, 必須在取地址和取數(shù)據(jù)之間插入一個(gè)數(shù)據(jù)依賴或更強(qiáng)的屏障: CPU 1 CPU 2 =============== =============== { A == 1, B == 2, C = 3, P == &A, Q == &C } B = 4; <寫屏障> P = &B Q = P; <數(shù)據(jù)依賴屏障> D = *Q; 這將強(qiáng)制最終結(jié)果是前兩種情況之一, 而避免出現(xiàn)第三種情況. [!] 注意, 這種非常違反直覺的情況最容易出現(xiàn)在cache分列的機(jī)器上, 比如, 一個(gè)cache組 處理偶數(shù)號(hào)的cache行, 另一個(gè)cache組處理奇數(shù)號(hào)的cache行. P指針可能存儲(chǔ)在奇數(shù)號(hào) 的cache行中, 而B的值可能存儲(chǔ)在偶數(shù)號(hào)的cache行中. 這樣一來, 如果執(zhí)行讀操作的 CPU的偶數(shù)號(hào)cache組非常繁忙, 而奇數(shù)號(hào)cache組空閑, 它就可能看到P已被更新成新值 (&B), 而B還是舊值(2). 另一個(gè)可 能需要數(shù)據(jù)依賴屏障的例子是, 從內(nèi)存讀取一個(gè)數(shù)值, 用于計(jì)算數(shù)組的訪問偏移: CPU 1 CPU 2 =============== =============== { M[0] == 1, M[1] == 2, M[3] = 3, P == 0, Q == 3 } M[1] = 4; <寫屏障> P = 1 Q = P; <數(shù)據(jù)依賴屏障> D = M[Q]; 數(shù) 據(jù)依賴屏障對(duì)于RCU非常重要, 舉例來說. 參閱include/linux/rcupdate.h文件中的 rcu_dereference() 函數(shù). 這個(gè)函數(shù)使得當(dāng)前RCU指針指向的對(duì)象被替換成新的對(duì)象時(shí), 不會(huì) 發(fā)生新對(duì)象尚未初始化完成的情況. (譯注: 更新RCU對(duì)象時(shí), 一般步驟是: 1-為新對(duì)象分配 空間; 2-初始化新對(duì)象; 3-調(diào)用rcu_dereference()函數(shù), 將對(duì)象指針指到新的對(duì)象上, 這 就 意味著新的對(duì)象已生效. 這個(gè)過程中如果出現(xiàn)亂序訪存, 可能導(dǎo)致對(duì)象指針的更新發(fā)生在 新對(duì)象初始化完成之前. 也就是說, 新對(duì)象尚未初始化完成就已經(jīng)生效了. 那么別的CPU就 可能引用到一個(gè)尚未初始化完成的新對(duì)象, 從而出現(xiàn)錯(cuò)誤.) 更詳 盡的例子請(qǐng)參閱"Cache一致性"章節(jié). 控制依賴 -------- 控制依賴需要使用一個(gè)完整 的讀內(nèi)存屏障, 簡(jiǎn)單的數(shù)據(jù)依賴屏障不能使其正確工作. 考慮 下面的代碼: q = &a; if (p) q = &b; <數(shù)據(jù)依賴屏障> x = *q; 這段代碼可能達(dá)不到預(yù)期的效果, 因?yàn)檫@里其實(shí)并不是數(shù)據(jù)依賴, 而是控制依賴, CPU可能 試 圖通過提前預(yù)測(cè)結(jié)果而對(duì)"if (p)"進(jìn)行短路. 在這樣的情況下, 需要的是: 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; <數(shù)據(jù)依賴屏障> x = *q; CPU 1上的寫屏障是為了保證這樣的邏輯: 如果p == 1, 那么必定有a == 3 && b == 4. 但是到了CPU 2, 可能p的值已更新(==1), 而a和b的值未更新, 那么這時(shí)數(shù)據(jù)依賴屏障可以 起作用, 確保x = *q時(shí)a和b的值更新. 因?yàn)閺拇a邏輯上說, q跟a或b是有所依賴的, 數(shù)據(jù) 依賴屏障能保證這些有依賴關(guān)系的值都已更新. 然而, 換一個(gè)寫法: 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的值未更新. 那么這時(shí)使用數(shù)據(jù)依賴屏障就不能 保證p的更新. 因?yàn)閺拇a邏輯上說, p跟任何人都沒有依賴關(guān)系. 這時(shí)必須使用讀屏障, 以 確保x = *q之前, p被更新. 原文中"短路"的意思就是, 由于p沒有數(shù)據(jù)依賴關(guān)系, CPU可以早早獲得它的值, 而不必考慮 更新.) SMP 內(nèi)存屏障的配對(duì)使用 --------------------- 在處理CPU與CPU的交互時(shí), 對(duì)應(yīng)類型的內(nèi)存屏障總是應(yīng)該配對(duì)使用. 缺乏適當(dāng)配對(duì)的使用基 本上可以肯定是錯(cuò)誤的. 一個(gè)寫屏障總是與一個(gè)數(shù)據(jù)依賴屏障 或讀屏障相配對(duì), 雖然通用屏障也可行. 類似的, 一個(gè) 讀屏障或數(shù)據(jù)依賴屏障也總是與一個(gè)寫屏障相配對(duì), 盡管一個(gè)通用屏障也同樣可行: CPU 1 CPU 2 =============== =============== a = 1; <寫屏障> b = 2; x = b; <讀屏障> y = a; 或: CPU 1 CPU 2 =============== =============== a = 1; <寫屏障> b = &a; x = b; <數(shù)據(jù)依賴屏障> y = *x; 基本上, 讀屏障總是需要用在這些地方的, 盡管可以使用"弱"類型. [!] 注意, 在寫屏障之前出現(xiàn)的STORE操作通??偸瞧谕ヅ渥x屏障或數(shù)據(jù)依賴屏障之后出 現(xiàn)的LOAD操作, 反之亦然: CPU 1 CPU 2 =============== =============== a = 1; }---- --->{ v = c b = 2; } \ / { w = d <寫屏障> \ <讀屏障> c = 3; } / \ { x = a; d = 4; }---- --->{ y = b; 內(nèi)存屏障舉例 ------------ 首 先, 寫屏障用作部分有序的STORE操作. 考慮如下的操作序列: CPU 1 ======================= STORE A = 1 STORE B = 2 STORE C = 3 <寫屏障> STORE D = 4 STORE E = 5 這個(gè)操作序列會(huì)按順序被提交到內(nèi)存一致性系統(tǒng), 而系統(tǒng)中的其他組件可能看到 { STORE A, STORE B, STORE C }的組合出現(xiàn)在{ STORE D, STORE E }的組合之前, 而組合 內(nèi)部可能亂序: +-------+ : : | | +------+ | |------>| C=3 | } /\ | | : +------+ }----- \ -----> 操作被系統(tǒng)中的其他 | | : | A=1 | } \/ 組件所感知 | | : +------+ } | CPU 1 | : | B=2 | } | | +------+ } | | wwwwwwwwwwwwwwww } <--- 在這一時(shí)刻, 寫屏障要求在它之 | | +------+ } 前出現(xiàn)的STORE操作都先于在它 | | : | E=5 | } 之后出現(xiàn)的STORE操作被提交 | | : +------+ } | |------>| D=4 | } | | +------+ +-------+ : : | | CPU 1發(fā)起的STORE操作被提交到內(nèi)存系統(tǒng)的順序 | V 其次, 數(shù)據(jù)依賴屏障用作部分有序的數(shù)據(jù)依賴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) 沒有干預(yù)的話, CPU 1的操作被CPU 2感知到的順序是隨機(jī)的, 盡管CPU 1執(zhí)行了寫屏障: +-------+ : : : : | | +------+ +-------+ | CPU 2所看到的 | |------>| B=2 |----- --->| Y->8 | | 更新序列 | | : +------+ \ +-------+ | | CPU 1 | : | A=1 | \ --->| C->&Y | V | | +------+ | +-------+ | | wwwwwwwwwwwwwwww | : : | | +------+ | : : | | : | C=&B |--- | : : +-------+ | | : +------+ \ | +-------+ | | | |------>| D=4 | ----------->| C->&B |------>| | | | +------+ | +-------+ | | +-------+ : : | : : | | | : : | | | : : | CPU 2 | | +-------+ | | 對(duì)B的取值顯然不正確 ---> | | B->7 |------>| | | +-------+ | | | : : | | | +-------+ | | 對(duì)X的LOAD延誤了B的 ---> \ | X->9 |------>| | 一致性更新 \ +-------+ | | ----->| B->2 | +-------+ +-------+ : : 在 上面的例子中, CPU 2看到的B的值是7, 盡管對(duì)*C(值應(yīng)該是B)的LOAD發(fā)生在對(duì)C的LOAD之 后. 但是, 如果一個(gè)數(shù)據(jù)依賴屏障被放到CPU 2的LOAD C和LOAD *C(假設(shè)值是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) <數(shù)據(jù)依賴屏障> LOAD *C (讀取B) 那 么下面的情況將會(huì)發(fā)生: +-------+ : : : : | | +------+ +-------+ | |------>| B=2 |----- --->| Y->8 | | | : +------+ \ +-------+ | CPU 1 | : | A=1 | \ --->| C->&Y | | | +------+ | +-------+ | | wwwwwwwwwwwwwwww | : : | | +------+ | : : | | : | C=&B |--- | : : +-------+ | | : +------+ \ | +-------+ | | | |------>| D=4 | ----------->| C->&B |------>| | | | +------+ | +-------+ | | +-------+ : : | : : | | | : : | | | : : | CPU 2 | | +-------+ | | | | X->9 |------>| | | +-------+ | | 確保STORE C之前的影響 ---> \ ddddddddddddddddd | | 都被后續(xù)的LOAD操作感 \ +-------+ | | 知到 ----->| B->2 |------>| | +-------+ | | : : +-------+ 第三, 讀屏障用作部分有序的LOAD操作. 考慮如下事件序列: CPU 1 CPU 2 ======================= ======================= { A = 0, B = 9 } STORE A=1 <寫屏障> STORE B=2 LOAD B LOAD A 沒有干預(yù)的話, CPU 1的操作被CPU 2感知到的順序是隨機(jī)的, 盡管CPU 1執(zhí)行了寫屏障: +-------+ : : : : | | +------+ +-------+ | |------>| A=1 |------ --->| A->0 | | | +------+ \ +-------+ | CPU 1 | wwwwwwwwwwwwwwww \ --->| B->9 | | | +------+ | +-------+ | |------>| B=2 |--- | : : | | +------+ \ | : : +-------+ +-------+ : : \ | +-------+ | | ---------->| B->2 |------>| | | +-------+ | CPU 2 | | | A->0 |------>| | | +-------+ | | | : : +-------+ \ : : \ +-------+ ---->| A->1 | +-------+ : : 但是, 如果一個(gè)讀屏障被放到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所施加的部分有序?qū)⒄_的被CPU 2所感知: +-------+ : : : : | | +------+ +-------+ | |------>| A=1 |------ --->| A->0 | | | +------+ \ +-------+ | CPU 1 | wwwwwwwwwwwwwwww \ --->| B->9 | | | +------+ | +-------+ | |------>| B=2 |--- | : : | | +------+ \ | : : +-------+ +-------+ : : \ | +-------+ | | ---------->| B->2 |------>| | | +-------+ | CPU 2 | | : : | | | : : | | 在這一時(shí)刻, 讀屏障導(dǎo)致 ----> \ rrrrrrrrrrrrrrrrr | | STORE B之前的影響都被 \ +-------+ | | CPU 2所感知 ---->| A->1 |------>| | +-------+ | | : : +-------+ 為 了更全面地說明這一點(diǎn), 考慮一下如果代碼在讀屏障的兩邊都有一個(gè)LOAD A的話, 會(huì)發(fā)生 什么: 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都發(fā)生在LOAD B之后, 它們也可能得到不同的值: +-------+ : : : : | | +------+ +-------+ | |------>| A=1 |------ --->| A->0 | | | +------+ \ +-------+ | CPU 1 | wwwwwwwwwwwwwwww \ --->| B->9 | | | +------+ | +-------+ | |------>| B=2 |--- | : : | | +------+ \ | : : +-------+ +-------+ : : \ | +-------+ | | ---------->| B->2 |------>| | | +-------+ | CPU 2 | | : : | | | : : | | | +-------+ | | | | A->0 |------>| 一次 | | +-------+ | | 在這一時(shí)刻, 讀屏障導(dǎo)致 ----> \ rrrrrrrrrrrrrrrrr | | STORE B之前的影響都被 \ +-------+ | | CPU 2所感知 ---->| A->1 |------>| 二次 | +-------+ | | : : +-------+ 但是也可能CPU 2在讀屏障結(jié)束之前就感知到CPU 1對(duì)A的更新: +-------+ : : : : | | +------+ +-------+ | |------>| A=1 |------ --->| A->0 | | | +------+ \ +-------+ | CPU 1 | wwwwwwwwwwwwwwww \ --->| B->9 | | | +------+ | +-------+ | |------>| B=2 |--- | : : | | +------+ \ | : : +-------+ +-------+ : : \ | +-------+ | | ---------->| B->2 |------>| | | +-------+ | CPU 2 | | : : | | \ : : | | \ +-------+ | | ---->| A->1 |------>| 一次 | +-------+ | | rrrrrrrrrrrrrrrrr | | +-------+ | | | A->1 |------>| 二次 | +-------+ | | : : +-------+ 這里只保證, 如果LOAD B得到的值是2的話, 第二個(gè)LOAD A能得到的值是1. 對(duì)于第一個(gè) LOAD A是不存在這樣的保證的; 它可能得到A的值是0或是1. 讀內(nèi)存屏障與內(nèi)存預(yù)取 -------------------- 許 多CPU會(huì)對(duì)LOAD操作進(jìn)行預(yù)取: 作為性能優(yōu)化的一種手段, 當(dāng)CPU發(fā)現(xiàn)它們將要從內(nèi)存LOAD 一個(gè)數(shù)據(jù)時(shí), 它們會(huì)尋找一個(gè)不需要使用總線來進(jìn)行其他LOAD操作的時(shí)機(jī), 用于LOAD這個(gè)數(shù) 據(jù) - 盡管他們的指令執(zhí)行流程實(shí)際上還沒有到達(dá)該處LOAD的地方. 實(shí)際上, 這可能使得某 些LOAD指令能夠立即完成, 因?yàn)镃PU已經(jīng)預(yù)取到了所需要LOAD的值. 這也可能出現(xiàn)CPU實(shí)際上用不到這個(gè)預(yù)取的值的情況 - 可能因?yàn)橐粋€(gè)分支而避開了這次LOAD - 在這樣的情況下, CPU可以丟棄這個(gè)值或者干脆就緩存它以備后續(xù)使用. 考 慮如下場(chǎng)景: CPU 1 CPU 2 ======================= ======================= LOAD B DIVIDE } 除法指令通常消耗 DIVIDE } 很長(zhǎng)的執(zhí)行時(shí)間 LOAD A 這可能將表現(xiàn)為如下情況: : : +-------+ +-------+ | | --->| B->2 |------>| | +-------+ | CPU 2 | : :DIVIDE | | +-------+ | | CPU在執(zhí)行除法指令 ---> -->| A->0 |~~~~ | | 的同時(shí), 預(yù)取A +-------+ ~ | | (譯注: 此時(shí)總線空閑) : : ~ | | : :DIVIDE | | : : ~ | | 一旦除法結(jié)束, --> : : ~-->| | CPU能馬上使 : : | | LOAD指令生效 : : +-------+ 如果在第二個(gè)LOAD之前放一個(gè)讀屏障或數(shù)據(jù)依賴屏障: CPU 1 CPU 2 ======================= ======================= LOAD B DIVIDE DIVIDE <讀屏障> LOAD A 這在一定程度上將迫使預(yù)取所獲得的值, 根據(jù)屏障的類型而被重新考慮. 如果沒有新的更新 操作作用到已經(jīng)被預(yù)取的內(nèi)存地址, 則預(yù)取到的值就會(huì)被使用: : : +-------+ +-------+ | | --->| B->2 |------>| | +-------+ | CPU 2 | : :DIVIDE | | +-------+ | | CPU在執(zhí)行除法指令 ---> -->| A->0 |~~~~ | | 的同時(shí), 預(yù)取A +-------+ ~ | | : : ~ | | : :DIVIDE | | : : ~ | | : : ~ | | rrrrrrrrrrrrrrrr~ | | : : ~ | | : : ~-->| | : : | | : : +-------+ 但是, 如果存在一個(gè)來自于其他CPU的更新或失效, 那么預(yù)取將被取消, 并且重新載入值: : : +-------+ +-------+ | | --->| B->2 |------>| | +-------+ | CPU 2 | : :DIVIDE | | +-------+ | | CPU在執(zhí)行除法指令 ---> -->| A->0 |~~~~ | | 的同時(shí), 預(yù)取A +-------+ ~ | | : : ~ | | : :DIVIDE | | : : ~ | | : : ~ | | rrrrrrrrrrrrrrrrr | | +-------+ | | 預(yù)取被丟棄, 并且更 --> -->| A->1 |------>| | 新后的值被重新獲取 +-------+ | | : : +-------+ ==================== 內(nèi) 核中顯式的內(nèi)存屏障 ==================== linux內(nèi)核擁有各式各樣的屏障, 作用在不同層次上: (*) 編譯優(yōu)化屏障. (*) CPU內(nèi)存屏障. (*) MMIO寫屏障. 編 譯優(yōu)化屏障 ------------ Linux內(nèi)核有一個(gè)顯式的編譯器屏障函數(shù), 能夠防止編譯器優(yōu)化將訪存操作從它的任一側(cè)移 到另一側(cè): barrier(); 這 是一個(gè)通用屏障 - 弱類型的編譯優(yōu)化屏障并不存在. 編譯優(yōu)化屏障并不直接作用到CPU, CPU依然可以按其意愿亂序執(zhí)行代碼. (譯注: 既然編譯優(yōu)化屏障并不能限制CPU的亂序訪存, 那么單純的編譯優(yōu)化屏障能起到什么作用呢? 以內(nèi)核中的preempt_disable宏為例: #define preempt_disable() \ do { \ inc_preempt_count(); \ barrier(); \ } while (0) preempt_disable()和對(duì)應(yīng)的preempt_enable()之間的代碼是禁止內(nèi)核搶占的, 通過對(duì)當(dāng)前 進(jìn)程的preempt_count進(jìn)行++, 以標(biāo)識(shí)進(jìn)入禁止搶占狀態(tài)(preempt_count==0時(shí)可搶占). 這 里 在對(duì)preempt_count自增之后, 使用了編譯優(yōu)化屏障. 如果不使用屏障, 本該在不可搶占狀態(tài)下執(zhí)行的指令可能被重排到preempt_count++之前(因 為這些指令基本上不會(huì)對(duì)preempt_count有依 賴). 而搶占可能是由中斷處理程序來觸發(fā)的, 在那些應(yīng)該在不可搶占狀態(tài)下執(zhí)行的指令被執(zhí)行之后, preempt_count++之前, 可能發(fā)生中 斷. 中斷來了, preempt_count的值還是0, 于是進(jìn)程可能會(huì)被錯(cuò)誤的搶占掉. 究其原因, 是因?yàn)榫幾g器看到的上下文依賴邏輯是靜態(tài)的, 它不知道這段代碼跟中斷處理程 序還存在依賴關(guān)系, 所以沒法限制自己的亂序行為. 所以, 這里的編譯優(yōu)化屏障是必要的. 但是, 僅僅使用編譯優(yōu)化屏障就足夠了么? 是的, 因?yàn)閜reempt_count這個(gè)變量是屬于當(dāng)前 進(jìn)程的, 僅會(huì)被當(dāng)前CPU訪問. CPU亂序可能導(dǎo)致后面應(yīng)該在禁止 搶占狀態(tài)下執(zhí)行的指令先于preempt_disable()執(zhí)行完, 但 是沒有關(guān)系, 因?yàn)榍懊嬉蔡岬竭^, CPU是"順序流入, 亂序流出"的, 就算后面的指令先執(zhí)行 完, preempt_disable()也必定已經(jīng)存在于流水線中了, CPU知道preempt_count變量將要被 修改. 而觸發(fā)搶占的代碼肯定會(huì)檢查preempt_count是否為0, 而這里的檢查又將依賴于 preempt_disable()的修改結(jié)果, 必定在preempt_disable()完成之后才會(huì)進(jìn)行. 究 其原因, 是因?yàn)镃PU看到的上下文依賴邏輯是動(dòng)態(tài)的, 它不管指令是來自于普通的處理流 程, 還是來自于中斷處理程序, 只要指令存在依賴, 它都能發(fā)現(xiàn). 所以, 對(duì)于類似這樣的只 被一個(gè)CPU所關(guān)注的內(nèi)存訪問, CPU的亂序訪存并不會(huì)存在問題. ) CPU 內(nèi)存屏障 ----------- Linux內(nèi)核有8種基本的CPU內(nèi)存屏障: 類型 強(qiáng)制 SMP環(huán)境 =============== ======================= =========================== 通用 mb() smp_mb() 寫 wmb() smp_wmb() 讀 rmb() smp_rmb() 數(shù)據(jù)依賴 read_barrier_depends() smp_read_barrier_depends() (譯注: 這里所說有SMP屏障是只在SMP環(huán)境下才生效的屏障, 而強(qiáng)制屏障則是不管在不在SMP 環(huán)境下都生效的屏障. 這里所謂的SMP環(huán)境, 確切的說, 其實(shí)是內(nèi)核的編譯選項(xiàng)指定為SMP的 情況, 并不是指實(shí)際運(yùn)行內(nèi)核的機(jī)器的環(huán)境. 不過既然編譯選項(xiàng)指定了SMP環(huán)境, 那么編譯 生成的內(nèi)核也基本上將會(huì)運(yùn)行在SMP環(huán)境. 下面提到的UP環(huán)境亦是同理.) 除了數(shù)據(jù)依賴屏障之外, 所有的內(nèi)存屏障都隱含了編譯優(yōu)化屏障的功能. 數(shù)據(jù)依賴屏障不對(duì) 編 譯器輸出的代碼順序造成任何額外的影響. 注: 在存在數(shù)據(jù)依賴關(guān)系的情況下, 編譯器預(yù)期會(huì)將LOAD指令按正確的順序輸出(例如, 在 `a[b]`語句中, 對(duì)b的load必須放在對(duì)a[b]的load之前), 但在C規(guī)范下, 并不保證編譯器不 去預(yù)測(cè)B的值(比如預(yù)測(cè)它等于1), 于是先load a再load b(比如, tmp = a[1]; if (b != 1) tmp = a[b];). 編譯器在load a[b]之后又重新load b, 也可能 會(huì)存在問題, 因?yàn)閎擁有比a[b]更新的副本. 這些問題的解決尚未達(dá)成共識(shí), 然而內(nèi)核中的 ACCESS_ONCE宏是解決問題的一個(gè)好的開始. 在 UP系統(tǒng)中, SMP內(nèi)存屏障將退化成編譯器優(yōu)化屏障, 因?yàn)樗俣–PU能夠保證自身的一致性 , 并本身就能以正確的順序處理重疊的內(nèi)存訪問. [!] 注意, SMP內(nèi)存屏障必須用于控制在SMP系統(tǒng)中的共享內(nèi)存的引用順序, 而使用鎖也能夠 滿足需求. 強(qiáng)制屏 障不應(yīng)該用來控制SMP的影響, 因?yàn)閺?qiáng)制屏障會(huì)過多地增加UP系統(tǒng)的開銷. 不過, 在 使用MMIO來訪問松散屬性的IO內(nèi)存窗口時(shí), 強(qiáng)制屏障可以用來控制這些訪存的影響. (譯注: 這里所指的內(nèi)存窗口, 是假定對(duì)于CPU來說, 可以設(shè)置屬于不同區(qū)間的內(nèi)存地址擁有不同的 屬 性. 這些屬性可以指示一個(gè)內(nèi)存段是否可以松散訪問, 即亂序訪問.) 強(qiáng)制屏障即使在非 SMP環(huán)境下也可能需要, 因?yàn)樗鼈兛梢酝ㄟ^禁止編譯器和CPU的亂序訪存, 從而影響設(shè)備感知 到內(nèi)存操作的順序. 還有一些更高級(jí)的屏障函數(shù): (*) set_mb(var, value) 該函數(shù)將value賦值到var變量中, 然后取決于具體編譯參數(shù)下的函數(shù)實(shí)現(xiàn), 可能在之 后插入一個(gè)內(nèi)存屏障. 在UP系統(tǒng)中, 它不能保證會(huì)插入編譯優(yōu)化屏障以外的其他屏障. (*) smp_mb__before_atomic_dec(); (*) smp_mb__after_atomic_dec(); (*) smp_mb__before_atomic_inc(); (*) smp_mb__after_atomic_inc(); 它們跟一些進(jìn)行原子操作的函數(shù)配合使用, 這些函數(shù)進(jìn)行了原子加法, 減法, 自增和 自減, 而又不將原子變量的值返回, 特別被用于引用計(jì)數(shù). 這些原子操作本身并不隱 含內(nèi)存屏障. (譯注: 像這樣被操作的原子變量, 多半是孤立而沒有數(shù)據(jù)依賴的. 如果 有數(shù)據(jù)依賴, 那么依賴關(guān)系將在一定程度上限制CPU的亂序. 否則, CPU的亂序就完全 要靠?jī)?nèi)存屏障來限制了.) 舉個(gè)例子, 考慮如下代碼段, 它將object標(biāo)識(shí)為已刪除, 然后將其引用計(jì)數(shù)自減: obj->dead = 1; smp_mb__before_atomic_dec(); atomic_dec(&obj->ref_count); 這樣可以確保設(shè)置刪除標(biāo)記在自減引用計(jì)數(shù)之前生效. 更多信息請(qǐng)參閱Documentation/atomic_ops.txt. 想知道什么地方需要用到這些函數(shù), 參閱"原子操作"章節(jié). (*) smp_mb__before_clear_bit(void); (*) smp_mb__after_clear_bit(void); 它們的用途類似于原子加減的屏障. 它們通常是跟一些進(jìn)行按位解鎖操作的函數(shù)配合 使用, 必須小心, 因?yàn)槲徊僮鞅旧硪膊⒉浑[含內(nèi)存屏障. 考慮這樣一個(gè)場(chǎng)景, 程序通過清除鎖定位來實(shí)施一些解鎖性質(zhì)的操作. clear_bit()函 數(shù)需要像這樣的屏障: smp_mb__before_clear_bit(); clear_bit( ... ); 這樣可以防止應(yīng)該在鎖定位被清除之前發(fā)生的內(nèi)存操作漏到位清除之后去(譯注: 注意 UNLOCK的屏障作用就是要保證它之前的訪存操作一定先于它而完成). 關(guān)于UNLOCK操作 的實(shí)現(xiàn), 請(qǐng)參閱"鎖相關(guān)函數(shù)"章節(jié). 更多信息請(qǐng)參閱Documentation/atomic_ops.txt. 想知道什么地方需要用到這些函 數(shù), 參閱"原子操作"章節(jié). MMIO寫屏障 ---------- 對(duì)于內(nèi)存映射IO的寫操作, Linux內(nèi)核還有一個(gè)特別的屏障: mmiowb(); 這是一個(gè)強(qiáng)制寫屏障的變體, 能夠?qū)⑷跤行虻腎O內(nèi)存窗口變成部分有序. 它的作用可能超出 CPU與硬件的界面, 從而影響到許多層次上的硬件設(shè)備. 更 多信息請(qǐng)參閱"鎖與IO訪問"章節(jié). ==================== 內(nèi)核中隱式的內(nèi)存屏障 ==================== Linux 內(nèi)核中有一些其他的方法也隱含了內(nèi)存屏障, 包括鎖和調(diào)度方法. 這個(gè)范圍是一個(gè)最低限度的保證; 一些特定的體系結(jié)構(gòu)可能提供更多的保證, 但是在特定體 系結(jié)構(gòu)的代碼之外, 不能依賴于它們. 鎖相關(guān)函數(shù) ---------- Linux 內(nèi)核有很多鎖結(jié)構(gòu): (*) spin locks (*) R/W spin locks (*) mutexes (*) semaphores (*) R/W semaphores (*) RCU 在 所有情況下, 它們都是LOCK操作和UNLOCK操作的變種. 這些操作都隱含一定的屏障: (1) LOCK操作所隱含的: 在LOCK操作之后出現(xiàn)的內(nèi)存操作, 一定在LOCK操作完成之后才會(huì)完成. 而在LOCK操作之前出現(xiàn)的內(nèi)存操作, 可能在LOCK操作完成之后才完成. (2) UNLOCK操作所隱含的: 在UNLOCK操作之前出現(xiàn)的內(nèi)存操作, 一定在UNLOCK操作完成之前完成. 而在UNLOCK操作之后出現(xiàn)的內(nèi)存操作, 可能在LOCK操作完成之前就完成了. (3) LOCK操作+LOCK操作所隱含的: 在某個(gè)LOCK操作之前出現(xiàn)的所有LOCK操作都將在這個(gè)LOCK之前完成. (4) LOCK操作+UNLOCK操作所隱含的: 在UNLOCK操作之前出現(xiàn)的所有LOCK操作都將在這個(gè)UNLOCK之前完成. 在LOCK操作之前出現(xiàn)的所有UNLOCK操作都將在這個(gè)LOCK之前完成. (5) LOCK失敗所隱含的: 某些變種的LOCK操作可能會(huì)失敗, 比如可能因?yàn)椴荒芰⒖太@得鎖(譯注: 如try_lock操 作), 再比如因?yàn)樵谒叩却i變?yōu)榭捎玫倪^程中接收到了未被阻塞的信號(hào)(譯注: 如 semaphores的down_interruptible操作). 失敗的鎖操作不隱含任何屏障. 因此, 根據(jù)(1), (2)和(4), 一個(gè)無條件的LOCK跟在一個(gè)UNLOCK之后, 鎖相當(dāng)于一個(gè)完整的 屏障, 而一個(gè)UNLOCK跟在一個(gè)LOCK之后并非如此. [!] 注意: LOCK和UNLOCK只是單向的屏障, 其結(jié)果是, 臨界區(qū)之外的指令可能會(huì)在臨界區(qū)中 執(zhí)行. 一個(gè)UNLOCK跟在一個(gè)LOCK之后并不能認(rèn)為是一個(gè)完整的屏障, 因?yàn)槌霈F(xiàn)在LOCK之前的訪存可 能在LOCK之后才執(zhí)行, 而出現(xiàn)在UNLOCK之后的訪存可能在UNLOCK之前執(zhí)行, 這兩次訪存可能 會(huì) 交叉: *A = a; LOCK UNLOCK *B = b; 可能表現(xiàn)為: LOCK, STORE *B, STORE *A, UNLOCK 鎖和信號(hào)量在UP環(huán)境下可能不提供順序保證, 在這種情況下不能被認(rèn)作是真正的屏障 - 特 別是對(duì) 于IO訪問 - 除非結(jié)合中斷禁用操作. 參閱"跨CPU的鎖的屏障作用"章節(jié). 例如, 考慮如下代碼: *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 禁止中斷函數(shù) ------------ 禁止中斷(類 似于LOCK)和啟用中斷(類似于UNLOCK)的函數(shù)只會(huì)起到編譯優(yōu)化屏障的作用. 所 以, 如果在這種情況下需要使用內(nèi)存或IO屏障, 必須采取其他手段. 睡眠喚醒函數(shù) ------------ 在一個(gè)全局事件標(biāo)記上的睡眠和喚醒可 以被看作是兩條數(shù)據(jù)之間的交互: 正在等待事件的進(jìn) 程的狀態(tài), 和用于表示事件發(fā)生的全局?jǐn)?shù)據(jù). 為了確保它們按正確的順序發(fā)生, 進(jìn)入睡眠的 原 語和發(fā)起喚醒的原語都隱含了某些屏障. 首先, 睡眠進(jìn)程通常執(zhí)行類似于如下的代碼序列: for (;;) { set_current_state(TASK_UNINTERRUPTIBLE); if (event_indicated) break; schedule(); } set_current_state()在它更 改進(jìn)程狀態(tài)之后會(huì)自動(dòng)插入一個(gè)通用內(nèi)存屏障: CPU 1 =============================== set_current_state(); set_mb(); STORE current->state <通用屏障> LOAD event_indicated set_current_state() 可能被包裝在以下函數(shù)中: prepare_to_wait(); prepare_to_wait_exclusive(); 因此這些函數(shù)也隱含了一個(gè)在設(shè)置了進(jìn)程狀態(tài)之后的通用內(nèi)存屏障. 以上的各個(gè)函數(shù)又被包 裝在其他一些函數(shù)中, 所有這些包裝函數(shù)都相當(dāng)于在對(duì)應(yīng)的位置插入了內(nèi)存屏障: 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()的函數(shù)會(huì)隱含一個(gè)寫內(nèi)存屏障. 當(dāng)且僅當(dāng)它們的確喚醒了某個(gè)進(jìn)程時(shí). 屏障 出現(xiàn)在進(jìn)程的睡眠狀態(tài)被清除之前, 也就是在設(shè)置喚醒事件標(biāo)記的STORE操作和將進(jìn)程狀態(tài) 修改為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 可用的喚醒函數(shù)包括: 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(); [!] 注意, 對(duì)于喚醒函數(shù)讀寫事件之前, 睡眠函數(shù)調(diào)用set_current_state()之后的那些 STORE操作, 睡眠和喚醒所隱含的內(nèi)存屏障并不保證它們的順序. 比如說, 如果睡眠 函數(shù)這樣做: set_current_state(TASK_INTERRUPTIBLE); if (event_indicated) break; __set_current_state(TASK_RUNNING); do_something(my_data); 而喚醒函數(shù)這樣做: my_data = value; event_indicated = 1; wake_up(&event_wait_queue); 睡眠函數(shù)并不能保證在看到my_data的修改之后才看到 event_indicated的修改. 在這種情況 下, 兩邊的代碼必須在對(duì)my_data訪存之前插入自己的內(nèi)存屏障. 因此上述的睡眠函數(shù)應(yīng)該 這樣做: set_current_state(TASK_INTERRUPTIBLE); if (event_indicated) { smp_rmb(); do_something(my_data); } 而喚醒函數(shù)應(yīng)該這樣做: my_data = value; smp_wmb(); event_indicated = 1; wake_up(&event_wait_queue); 其他函數(shù) -------- 其 他隱含了屏障的函數(shù): (*) schedule()和類似函數(shù)隱含了完整的內(nèi)存屏障. (譯注: schedule函數(shù)完成了進(jìn)程的切換, 它的兩邊可能對(duì)應(yīng)著兩個(gè)不同的上下文. 如 果訪存操作跨越schedule函數(shù)而進(jìn)行了亂序, 那么基本上可以肯定是錯(cuò)誤的.) =================== 跨 CPU的鎖的屏障作用 =================== 在SMP系統(tǒng)中, 鎖定原語給出了多種形式的屏障: 其中一種在一些特定的鎖沖突的情況下, 會(huì)影響其他CPU上的內(nèi)存訪問順序. 鎖與內(nèi)存訪問 ------------ 假 設(shè)系統(tǒng)中有(M)和(Q)這一對(duì)spinlock, 有三個(gè)CPU; 那么可能發(fā)生如下操作序列: 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; 那么對(duì)于CPU 3來說, 從*A到*H的訪問順序是沒有保證的, 不像單獨(dú)的鎖對(duì)應(yīng)單獨(dú)的CPU有 那樣的限制. 例如, CPU 3可能看到的順序是: *E, LOCK M, LOCK Q, *G, *C, *F, *A, *B, UNLOCK Q, *D, *H, UNLOCK M 但是它不會(huì)看到如下情況: *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不會(huì)看到下面的情況: *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的情況), 兩個(gè)CPU上發(fā)起的屬于兩個(gè)spinlock臨界區(qū)的IO 訪問可能被PCI橋看成是交錯(cuò)發(fā)生的, 因?yàn)镻CI橋并不一定參與cache一致性協(xié)議, 以至于無 法響應(yīng)讀內(nèi)存屏障. 例如: 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 這可能會(huì)引起硬件 操作的錯(cuò)誤. 這里所需要的是, 在釋放spinlock之前, 使用mmiowb()作為干預(yù), 例如: 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橋所看到. 此 外, 對(duì)于同一硬件設(shè)備在進(jìn)行STORE操作之后再進(jìn)行LOAD操作, 可以省去mmiowb(), 因?yàn)?br>LOAD操作將強(qiáng)制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); 更多信息請(qǐng)參 閱"Documentation/DocBook/deviceiobook.tmpl". ===================== 什 么地方需要內(nèi)存屏障? ===================== 在正常操作下, 內(nèi)存操作的亂序一般并不會(huì)成為問題, 即使是在SMP內(nèi)核中, 一段單線程的 線性代碼也總是能夠正確工作. 但是, 有四種情況, 亂序絕對(duì)可能是一個(gè)問題: (*) 處理器間交互. (*) 原子操作. (*) 訪問設(shè)備. (*) 中斷. 處 理器間交互 ------------ 當(dāng)系統(tǒng)中擁有不止一個(gè)CPU時(shí), 系統(tǒng)中的多個(gè)CPU可能在同一時(shí)間工作在同樣的數(shù)據(jù)集上. 這 將產(chǎn)生同步問題, 并且這樣的問題通常要靠使用鎖來解決. 但是, 鎖是昂貴的, 所以不是萬 不得已的情況下最好不要使用鎖. 在這種情況下, 為防止錯(cuò)誤, 導(dǎo)致兩個(gè)CPU相互影響的那 些內(nèi)存操作可能需要仔細(xì)協(xié)調(diào)好順 序. 比如, 考慮一下讀寫信號(hào)量的slow path. 信號(hào)量的等待隊(duì)列里有一個(gè)進(jìn)程正在等待, 這個(gè) 等待進(jìn)程??臻g 上的一段內(nèi)存(譯注: 也就是棧上分配的waiter結(jié)構(gòu))被鏈到信號(hào)量的等待鏈 表里: struct rw_semaphore { ... spinlock_t lock; struct list_head waiters; }; struct rwsem_waiter { struct list_head list; struct task_struct *task; }; 要喚醒這樣一個(gè) 等待進(jìn)程, up_read()函數(shù)或up_write()函數(shù)需要這樣做: (1) 讀取該等待進(jìn)程所對(duì)應(yīng)的waiter結(jié)構(gòu)的next指針, 以記錄下一個(gè)等待進(jìn)程是誰; (2) 讀取waiter結(jié)構(gòu)中的task指針, 以獲取對(duì)應(yīng)進(jìn)程的進(jìn)程控制塊; (3) 清空waiter結(jié)構(gòu)中的task指針, 以表示這個(gè)進(jìn)程正在獲得信號(hào)量; (4) 對(duì)這個(gè)進(jìn)程調(diào)用wake_up_process()函數(shù); 并且 (5) 釋放waiter結(jié)構(gòu)對(duì)進(jìn)程控制塊的引用計(jì)數(shù). 換句話說, 這個(gè)過程會(huì)執(zhí)行如下事件序列: LOAD waiter->list.next; LOAD waiter->task; STORE waiter->task; CALL wakeup RELEASE task 而 如果其中一些步驟發(fā)生了亂序, 那么整個(gè)過程可能會(huì)產(chǎn)生錯(cuò)誤. 一旦等待進(jìn)程將自己掛入等待隊(duì)列, 并釋放了信號(hào)量里的鎖, 這個(gè)等待進(jìn)程就不會(huì)再獲得這 個(gè)鎖了(譯注: 參閱信號(hào)量的代碼, 它內(nèi)部使用了一個(gè)spinlock來進(jìn)行同步); 它要做的事情 就是在 繼續(xù)工作之前, 等待waiter結(jié)構(gòu)中的task指針被清空(譯注: 然后自己會(huì)被喚醒). 而 既然waiter結(jié)構(gòu)存在于等待進(jìn)程的棧上, 這就意味著, 如果在waiter結(jié)構(gòu)中的next指針被讀 取之前, task指針先被清空了的話(譯注: 等待進(jìn)程先被喚醒了), 那么, 這個(gè)等待進(jìn)程可能 已經(jīng)在另一個(gè)CPU上開始運(yùn)行了(譯注: 相對(duì)于喚醒進(jìn)程所運(yùn)行的CPU), 并且在up*()函數(shù)有 機(jī)會(huì)讀取到 next指針之前, 棧空間上對(duì)應(yīng)的waiter結(jié)構(gòu)可能已經(jīng)被復(fù)用了(譯注: 被喚醒的 進(jìn)程從down*()函數(shù)返回, 然后可能進(jìn)行新的函數(shù)調(diào)用, 導(dǎo)致??臻g被重復(fù)使用). 看看上面的事件序列可能會(huì)發(fā)生什么: CPU 1 CPU 2 =============================== =============================== down_xxx() 將waiter結(jié)構(gòu)鏈入等待隊(duì)列 進(jìn)入睡眠 up_yyy() LOAD waiter->task; STORE waiter->task; 被CPU 1的UP事件喚醒 <被搶占了> 重新得到運(yùn)行 down_xxx()函數(shù)返回 繼續(xù)調(diào)用foo()函數(shù) foo()重用了棧上的waiter結(jié)構(gòu) <搶占返回> LOAD waiter->list.next; --- OOPS --- 對(duì)付這個(gè)問題可以 使用信號(hào)量中的鎖, 但是當(dāng)進(jìn)程被喚醒后, down_xxx()函數(shù)其實(shí)沒必要重 新獲得這個(gè)spinlock. 實(shí)際的 解決辦法是插入一個(gè)通用SMP內(nèi)存屏障: LOAD waiter->list.next; LOAD waiter->task; smp_mb(); STORE waiter->task; CALL wakeup RELEASE task 這 樣, 對(duì)于系統(tǒng)中的其他CPU來說, 屏障將保證屏障之前的所有內(nèi)存訪問先于屏障之后的所 有內(nèi)存訪問發(fā)生. 屏障并不保證屏障之前的所有內(nèi)存訪問都在屏障指令結(jié)束之前完成. 在UP系統(tǒng)中 - 這種情況將不是問題 - smp_mb()函數(shù)只是一個(gè)編譯優(yōu)化屏障, 這就確保了編 譯器生成順序正確的指令, 而不需要干預(yù)CPU. 既然只有一個(gè)CPU, 該CPU的數(shù)據(jù)依賴邏輯將 處理所有事情. 原子操作 -------- 雖然原子操作在技術(shù) 上實(shí)現(xiàn)了處理器之間的交互, 然而特別注意一些原子操作隱含了完整的 內(nèi)存屏障, 而另外一些則沒有, 但是它們卻作為一個(gè)群體被整個(gè)內(nèi)核嚴(yán)重依賴. 許多原子操作修改內(nèi)存中的一些狀態(tài), 并且返回該狀態(tài)相關(guān)的信息(舊狀態(tài)或新狀態(tài)), 就在 其中實(shí)際操作內(nèi)存的兩邊各隱含一個(gè)SMP環(huán)境下的通用內(nèi)存屏障(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操作的實(shí)現(xiàn), 和用于控制對(duì)象析構(gòu)的引用計(jì)數(shù), 這些情況 下, 隱含內(nèi)存屏障是有必要的. 以 下操作由于沒有隱含內(nèi)存屏障, 會(huì)有潛在的問題, 但有可能被用于實(shí)現(xiàn)類UNLOCK這樣的操 作: atomic_set(); set_bit(); clear_bit(); change_bit(); 如果需要, 對(duì)應(yīng)于這些函數(shù), 可以使用相應(yīng)的顯式內(nèi)存屏障(比如 smp_mb__before_clear_bit()). 下 面這些函數(shù)也不隱含內(nèi)存屏障, 并且在一些情況下, 可能也需要用到顯式內(nèi)存屏障(比如 smp_mb__before_atomic_dec()): atomic_add(); atomic_sub(); atomic_inc(); atomic_dec(); 如果它們用于產(chǎn)生統(tǒng)計(jì), 那么他們可能就不需要內(nèi)存屏障, 除非統(tǒng)計(jì)數(shù)據(jù)之間存在耦合. 如 果它們被用作控制對(duì)象生命周期的引用計(jì)數(shù), 那么它們可能并不需要內(nèi)存屏障, 因?yàn)橐?br>引用計(jì)數(shù)需要在一個(gè)鎖的臨界區(qū)里面進(jìn)行調(diào)整, 要么調(diào)用者已經(jīng)持有足夠的引用而相當(dāng)于擁 有了鎖(譯注: 一般在引用計(jì)數(shù)減為0的時(shí)候需要將對(duì)應(yīng)的對(duì)象析構(gòu), 如果調(diào)用者知道引用計(jì) 數(shù)在 某些情況下不可能減為0, 那么這個(gè)對(duì)象也就不可能在這些情況下被析構(gòu), 也就不需要 通過內(nèi)存屏障來避免訪存亂序?qū)е碌膶?duì)象在析構(gòu)之后還被訪問的 情況), 這樣的情況下并不 需要內(nèi)存屏障. 如果它們用于構(gòu)成鎖的一些描述信息, 那么他們可能就需要內(nèi)存屏障, 因?yàn)殒i原語一般需要 按一定的順序來操作. 基本上, 每個(gè)場(chǎng)景都需要仔細(xì)考慮是否需要使用內(nèi)存屏障. 以 下操作是特殊的鎖原語: test_and_set_bit_lock(); clear_bit_unlock(); __clear_bit_unlock(); 它們都執(zhí)行了類 LOCK和類UNLOCK的操作. 相比其他操作, 它們應(yīng)該優(yōu)先被用于實(shí)現(xiàn)鎖原語, 因?yàn)樗鼈兊膶?shí)現(xiàn)可以在許多體系結(jié)構(gòu)下得到優(yōu)化. [!] 注意, 這些特殊的內(nèi)存屏障原語對(duì)一些情況也是有用的, 因?yàn)樵谝恍w系結(jié)構(gòu)的CPU上, 使用的原子操作本身就隱含了完整的內(nèi)存屏障功能, 所以屏障指令在這里是多余的, 在 這樣的情況下, 這些特殊的屏障原語將不使用額外的屏障操作. 更多信息請(qǐng)參閱Documentation/atomic_ops.txt. 訪 問設(shè)備 -------- 許多設(shè)備都可以被映射到內(nèi)存, 因此在CPU看來, 它們只是一組內(nèi)存地址. 為了控制這些設(shè) 備, 驅(qū)動(dòng)程序通常需要確保正確的內(nèi)存訪問按正確的順序來執(zhí)行. 但是, 聰明的CPU或者聰明的編譯器卻導(dǎo)致了潛在的問題, 如果CPU或編譯器認(rèn)為亂序, 或合 并訪問更有利于效率的話, 驅(qū)動(dòng)程序代碼中仔細(xì)安排的訪存序列可能并不會(huì)按正確的順序被 送到設(shè)備上 - 從而可能導(dǎo)致設(shè)備的錯(cuò)誤. 在Linux內(nèi)核里面, IO訪問應(yīng)該使用適當(dāng)?shù)脑L問函數(shù) - 例如inb()或writel() - 它們知道如 何得到恰當(dāng)?shù)脑L問順序. 大多數(shù)情況下, 在使用這些函數(shù)之后就不必再顯式的使用內(nèi)存屏障 , 但是在兩種情況下, 內(nèi)存屏障可能還是需要的: (1) 在一些系統(tǒng)中, IO存儲(chǔ)操作對(duì)于所有CPU來說并不是嚴(yán)格有序的, 所以對(duì)于所有的通用 驅(qū)動(dòng)程序(譯注: 通用驅(qū)動(dòng)程序需要適應(yīng)各種體系結(jié)構(gòu)的系統(tǒng)), 需要使用鎖, 并且一 定要在解鎖臨界區(qū)之前執(zhí)行mmiowb()函數(shù). (2) 如果訪存函數(shù)訪問松散屬性的IO內(nèi)存窗口, 那么需要使用強(qiáng)制內(nèi)存屏障來確保執(zhí)行順 序. 更多信息請(qǐng)參閱Documentation/DocBook /deviceiobook.tmpl. 中斷 ---- 驅(qū)動(dòng)程序可能被它自己的中斷處理程序所打 斷, 然后驅(qū)動(dòng)程序中的這兩個(gè)部分可能會(huì)相互干 擾對(duì)方控制或訪問設(shè)備的意圖. 通過禁用本地中斷(一種形式的鎖)可能至少 部分緩解這種情況, 這樣的話, 驅(qū)動(dòng)程序中的關(guān) 鍵操作都將包含在禁用中斷的區(qū)間中. 于是當(dāng)驅(qū)動(dòng)程序的中斷處理程序正在執(zhí)行時(shí), 驅(qū)動(dòng)程 序 的核心代碼不可能在相同的CPU上運(yùn)行, 并且在當(dāng)前中斷被處理完之前中斷處理程序不允 許再次被調(diào)用, 于是中斷處理程序就不需要再對(duì)這種情況使用鎖. 但是, 考慮一個(gè)驅(qū)動(dòng)程序正通過一個(gè)地址寄存器和一個(gè)數(shù)據(jù)寄存器跟以太網(wǎng)卡交互的情況. 假設(shè)驅(qū)動(dòng)程序的核心代碼在禁用中斷的情況下操作了網(wǎng)卡, 然后驅(qū)動(dòng)程序的中斷處理程序 被調(diào)用: LOCAL IRQ DISABLE writew(ADDR, 3); writew(DATA, y); LOCAL IRQ ENABLE <進(jìn)入中斷> writew(ADDR, 4); q = readw(DATA); <退出中斷> 如果執(zhí)行順序的規(guī)則足夠松散, 對(duì)數(shù)據(jù)寄存器的寫操作可能發(fā)生在第二次對(duì)地址寄存器的寫 操作之后: STORE *ADDR = 3, STORE *ADDR = 4, STORE *DATA = y, q = LOAD *DATA 如果執(zhí)行順序像這樣松散, 就需要假定在禁用中斷區(qū)間內(nèi)應(yīng)該完成的訪問可能泄漏到區(qū)間之 外, 并且可能漏到中斷過程中進(jìn)行訪問 - 反之亦然 - 除非使用隱式或顯式的屏障. 通常這并不是一個(gè)問題, 因?yàn)榻弥袛鄥^(qū)間內(nèi)完成的IO訪存將會(huì)包含嚴(yán)格有序的同步LOAD操 作, 形成隱式的IO屏障. 如果這還不夠, 那么需要顯式的調(diào)用一下mmiowb(). 在一個(gè)中斷服務(wù)程序與兩個(gè)運(yùn)行在不同CPU 的程序相互通信的情況下, 類似的情況也可能發(fā) 生. 如果出現(xiàn)這樣的情況, 那么禁用中斷的鎖操作需要用于確保執(zhí)行順序. (譯注: 也就是 類 似于spinlock_irq這樣的操作.) =================== 內(nèi)核中I/O屏障 的作用 =================== 在對(duì)IO內(nèi)存進(jìn)行存取的時(shí)候, 驅(qū)動(dòng)程序應(yīng)該使用適當(dāng)?shù)拇嫒『瘮?shù): (*) inX(), outX(): 它們都是傾向于跟IO空間打交道, 而不是普通內(nèi)存空間, 不過這主要取決于具體CPU的 邏輯. i386和x86_64處理器確實(shí)有特殊的IO空間存取周期和指令, 但是許多系統(tǒng)結(jié)構(gòu) 的CPU卻并沒有這些概念. 包括PCI總線也可能會(huì)定義成IO空間 - 比如在i386和x86_64的CPU上 - 很容易將它映 射到CPU的IO空間上. 但是, 它也可能作為虛擬的IO空間被映射到CPU的內(nèi)存空間上, 特別對(duì)于那些不支持IO空間的CPU. 訪問這些空間可能是完全同步的(比如在i386上), 但是對(duì)于橋設(shè)備(比如PCI主橋)可能 并不完全是這樣. 他們能保證完全遵守IO操作之間的訪問順序. 他們不能保證完全遵從IO操作與其他類型的內(nèi)存操作之間的訪問順序. (*) readX(), writeX(): 在發(fā)起調(diào)用的CPU上, 這些函數(shù)是否保證完全遵從內(nèi)存訪問順序而且不進(jìn)行合并訪問, 取決于它們所訪問的內(nèi)存窗口上定義的屬性. 例如, 較新的i386體系結(jié)構(gòu)的機(jī)器, 可 以通過MTRR寄存器來控制內(nèi)存窗口的屬性. 通常, 只要不是訪問預(yù)取設(shè)備, 這些函數(shù)將保證完全有序并且不進(jìn)行合并訪問. 但是對(duì)于橋設(shè)備(比如PCI橋), 如果它們?cè)敢獾脑? 可能會(huì)傾向于對(duì)內(nèi)存操作進(jìn)行延遲 處理; 要沖刷一個(gè)STORE操作, 首選是對(duì)相同地址進(jìn)行一次LOAD[*], 但是對(duì)于PCI來說 , 對(duì)相同設(shè)備或相同的配置的IO空間進(jìn)行一次LOAD就足夠了. [*] 注意! 試圖從剛寫過的地址LOAD數(shù)據(jù), 可能會(huì)導(dǎo)致錯(cuò)誤 - 比如對(duì)于16550 Rx/Tx 串口寄存器. 遇到帶預(yù)取的IO內(nèi)存, 可能需要使用mmiowb()屏障來強(qiáng)制讓STORE操作有序. 關(guān)于PCI事務(wù)交互方面的更多信息, 請(qǐng)參閱PCI規(guī)范. (*) readX_relaxed() 這些函數(shù)類似于readX(), 但是任何情況下都不保證有序. 請(qǐng)注意, 這里沒有用到IO讀 屏障. (*) ioreadX(), iowriteX() 這些函數(shù)在進(jìn)行訪存的時(shí)候會(huì)根據(jù)訪存類型選擇適當(dāng)?shù)牟僮? inX()/outX()或 readX()/writeX(). ====================== 最 小限度有序的假想模型 ====================== 從概念上說, 必須假定的CPU是弱有序的, 但是它會(huì)保持程序上下文邏輯關(guān)系的外觀. 一些 CPU(比如i386或x86_64)比另一些(比如powerpc或frv)更具有約束力, 而在體系結(jié)構(gòu)無關(guān)的 代碼中, 必須假定為最松散的情況(也就是DEC Alpha). 也就是說, 必須考慮到CPU可能會(huì)按它喜歡的順序來執(zhí)行操作 - 甚至并行執(zhí)行 - 只是當(dāng)指 令流中的一條指令依賴于之前的一條指令時(shí), 之前的這條指定才必須在后面這條指令可能被 處理之前完全結(jié)束; 換句話說: 保持程序的上下文邏輯關(guān)系. [*] 一些指令會(huì)產(chǎn)生不止一處影響 - 比如會(huì)修改條件碼, 修改寄存器或修改內(nèi)存 - 不同 的指令可能依賴于不同的影響. CPU 也可能丟棄那些最終不產(chǎn)生任何影響的操作序列. 比如, 如果兩個(gè)相鄰的指令都將一個(gè) 立即數(shù)LOAD到寄存器, 那么第一個(gè)LOAD指令可能被丟棄. 類似的, 也需要假設(shè)編譯器可能按它覺得舒服的順序來調(diào)整指令流, 但同樣也會(huì)保持程序的 上 下文邏輯關(guān)系. =============== CPU cache的影響 =============== 操 作cache中緩存的內(nèi)存之后, 相應(yīng)的影響會(huì)在整個(gè)系統(tǒng)間得到傳播. 位于CPU和內(nèi)存之間的 cache, 和保持系統(tǒng)狀態(tài)一致的內(nèi)存一致性機(jī)構(gòu), 在一定程度上影響了傳播的方法. 自從CPU與系統(tǒng)中其他部分的交互通過使用cache來 實(shí)現(xiàn)以來, 內(nèi)存系統(tǒng)就包含了CPU的緩存, 而內(nèi)存屏障基本上就工作在CPU和其cache之間的界面上(邏輯上說, 內(nèi)存屏障工作在下圖中 虛 線所示的地方): <--- CPU ---> : <----------- 內(nèi)存 -----------> : +--------+ +--------+ : +--------+ +-----------+ | | | | : | | | | +--------+ | CPU | | 內(nèi)存 | : | CPU | | | | | | 核心 |--->| 請(qǐng)求 |----->| Cache |<-->| | | | | | | 隊(duì)列 | : | | | |--->| 內(nèi)存 | | | | | : | | | | | | +--------+ +--------+ : +--------+ | | | | : | Cache | +--------+ : | 一致性 | : | 機(jī)構(gòu) | +--------+ +--------+ +--------+ : +--------+ | | | | | | | | : | | | | | | | CPU | | 內(nèi)存 | : | CPU | | |--->| 設(shè)備 | | 核心 |--->| 請(qǐng)求 |----->| Cache |<-->| | | | | | | 隊(duì)列 | : | | | | | | | | | | : | | | | +--------+ +--------+ +--------+ : +--------+ +-----------+ : : 一些LOAD和STORE可能不會(huì)實(shí)際出現(xiàn)在發(fā)起操作的 CPU之外, 因?yàn)樵贑PU自己的cache上就能滿 足需要, 盡管如此, 如果其他CPU關(guān)心這些數(shù)據(jù), 那么完整的內(nèi)存訪問還是會(huì)發(fā)生, 因?yàn)?br>cache一致性機(jī)構(gòu)將遷移相應(yīng)的cache行到訪問它的CPU, 使一致性得到傳播. 在保持程序所期望的上下文邏 輯的前提下, CPU核心可能會(huì)按它認(rèn)為合適的順序來執(zhí)行指令. 一些指令會(huì)產(chǎn)生LOAD和STORE操作, 并且將它們放到內(nèi)存請(qǐng)求隊(duì)列中, 等待被執(zhí)行. CPU核心 可能會(huì)按它喜歡的順序來將這些操作放進(jìn)隊(duì)列, 然后繼續(xù)運(yùn)行, 直到它必須等待這些訪存指 令完成的時(shí)候?yàn)橹? 內(nèi) 存屏障所需要關(guān)心的是訪存操作從CPU一側(cè)穿越到內(nèi)存一側(cè)的順序, 和系統(tǒng)中的其他部件 感知到的操作發(fā)生的順序. [!] 對(duì)于一個(gè)CPU自己的LOAD和STORE來說, 并不需要使用內(nèi)存屏障, 因?yàn)镃PU總是能按程序 執(zhí)行順序看到它們所執(zhí)行的LOAD和STORE操作. [!] MMIO或其他設(shè)備存取可能繞開cache系統(tǒng). 這取決于訪問設(shè)備所經(jīng)過的內(nèi)存窗口的屬性 和/或是否使用了CPU所特有的與設(shè)備進(jìn)行交互的指令. CACHE一致 性 ----------- 但是, 事情并不是像上面所說的那樣簡(jiǎn)單: 因?yàn)殡m然可以期望cache是一致的, 但是一致性 傳播的順序卻是沒有保證的. 也就是說, 雖然一個(gè)CPU所做出的更新將最終被其它CPU都看到 , 但是卻不保證其他CPU所看到的都是相同的順序. 考慮這樣一個(gè)系統(tǒng), 它具有雙CPU(1和2), 每個(gè)CPU有一對(duì)并行的數(shù)據(jù)cache(CPU 1對(duì)應(yīng)A/B, CPU 2對(duì)應(yīng)C/D): : : +--------+ : +---------+ | | +--------+ : +--->| Cache A |<------->| | | | : | +---------+ | | | CPU 1 |<---+ | | | | : | +---------+ | | +--------+ : +--->| Cache B |<------->| | : +---------+ | | : | 內(nèi)存 | : +---------+ | 系統(tǒng) | +--------+ : +--->| Cache C |<------->| | | | : | +---------+ | | | CPU 2 |<---+ | | | | : | +---------+ | | +--------+ : +--->| Cache D |<------->| | : +---------+ | | : +--------+ : 想象一下該系統(tǒng)有如下屬性: (*) 一個(gè)奇數(shù)號(hào)的cache行可能被緩存在cache A, cache C, 或者可能依然駐留在內(nèi)存中; (*) 一個(gè)偶數(shù)號(hào)的cache行可能被緩存在cache B, cache D, 或者可能依然駐留在內(nèi)存中; (*) 而當(dāng)CPU核心訪問一個(gè)cache時(shí), 另一個(gè)cache可以同時(shí)利用總線來訪問系統(tǒng)中的其他部 分 - 可能是替換一個(gè)臟的cache行或者進(jìn)行預(yù)取; (*) 每個(gè)cache都有一個(gè)操作隊(duì)列, 被用于保持cache與系統(tǒng)中的其他部分的一致性; (*) 當(dāng)LOAD命中了已經(jīng)存在于cache中的行時(shí), 該一致性隊(duì)列并不會(huì)得到?jīng)_刷, 盡管隊(duì)列中 的內(nèi)容可能會(huì)影響這些LOAD操作. (譯注: 也就是說, 隊(duì)列中有針對(duì)某一cache行的更 新操作正在等待被執(zhí)行, 而這時(shí)LOAD操作需要讀這個(gè)cache行. 這種情況下, LOAD并不 會(huì)等待隊(duì)列中的這個(gè)更新完成, 而是直接獲取了更新前的值.) 接下來, 想象一下在第一個(gè)CPU上執(zhí)行兩個(gè)寫操作, 并在它們之間使用一個(gè)寫屏障, 以保證 它們按要求的順序到達(dá)該CPU的cache: CPU 1 CPU 2 說明 =============== =============== ======================================= u == 0, v == 1 并且 p == &u, q == &u v = 2; smp_wmb(); 確保對(duì)v的修改先于對(duì)p的修改被感知 <A:modify v=2> v的值只存在于cache A中 p = &v; <B:modify p=&v> p的值只存在于cache B中 寫內(nèi)存屏障保證系統(tǒng)中的其他CPU會(huì)按正確的順序看到本地CPU cache的更新. 但是設(shè)想一下 第二個(gè)CPU要去讀取這些值的情形: CPU 1 CPU 2 說明 =============== =============== ======================================= ... q = p; x = *q; 上面這一對(duì)讀操作可 能不會(huì)在預(yù)期的順序下執(zhí)行, 比如持有p的cache行可能被更新到另一個(gè) CPU的cache, 而持有v的cache行因?yàn)槠渌恍ヽache事件的影響而延遲了對(duì)那個(gè)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的兩個(gè)cache行都將得到更新, 但是在沒有干預(yù)的情況下, 并不能保 證更新的順序跟CPU 1提交的順序一致. 我們需要在兩次LOAD之間插入一個(gè)數(shù)據(jù) 依賴屏障或讀屏障, 以作為干預(yù). 這將強(qiáng)制cache在 處理后續(xù)的請(qǐng)求之前, 先讓它的一致性隊(duì)列得到提交: 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 這 些問題會(huì)在DEC Alpha處理器上遇到, 這些處理器使用了分列cache, 通過提高數(shù)據(jù)總線的 利用率以提升性能. 雖然大部分的CPU在讀操作依賴于讀操作的時(shí)候, 會(huì)在第二個(gè)讀操作中 隱含一個(gè)數(shù)依賴屏障, 但是并不是所有CPU都這樣, 因此不能依賴這一點(diǎn). 其他的CPU也可能使用分列cache, 但對(duì)于普通的內(nèi)存訪問, 他們會(huì)協(xié)調(diào)各個(gè)cache列. 而 Alpha 處理器的處理邏輯則取消了這樣的協(xié)調(diào)動(dòng)作, 除非使用內(nèi)存屏障. cache一致性與DMA ---------------- 對(duì) 于進(jìn)行DMA操作的設(shè)備, 并不是所有系統(tǒng)都保持它們的cache一致性. 在這種情況下, 準(zhǔn)備 進(jìn)行DMA的設(shè)備可能從RAM得到陳舊的數(shù)據(jù), 因?yàn)榕K的cache行可能還駐留在各個(gè)CPU的cache 中, 而尚未寫回到RAM. 為了解決這個(gè)問題, 內(nèi)核的相應(yīng)部分必須將cache中重疊的數(shù)據(jù)沖刷 掉(或者使它們失效)(譯注: 沖刷掉cache中的相應(yīng)內(nèi)容, 以保持cache與RAM的一致). 此外, 在設(shè)備已經(jīng)通過DMA將數(shù)據(jù)寫入RAM之后, 這些數(shù)據(jù)可能被cache寫回RAM的臟的cache 行所覆蓋, 或者CPU已緩存的cache行可能直接掩蓋了RAM被更新的事實(shí)(譯注: 使得對(duì)應(yīng)的 LOAD操作只能獲得cache中的舊值, 而無法得到RAM中的新值), 直到cache行被從CPU cache 中丟棄并 且重新由RAM載入. 為解決這個(gè)問題, 內(nèi)核的相應(yīng)部分必須將cache中重疊的數(shù)據(jù)失 效. 更多關(guān)于cache管理的 信息請(qǐng)參閱: Documentation/cachetlb.txt. cache一致性與MMIO ----------------- 內(nèi) 存映射IO通常通過內(nèi)存地址來觸發(fā), 這些地址是CPU內(nèi)存空間的某個(gè)窗口中的一部分, 而 這個(gè)窗口相比于普通RAM對(duì)應(yīng)的窗口會(huì)有著不同的屬 性. 這些屬性通常包含這樣的情況: 訪存會(huì)完全繞過cache, 而直接到達(dá)設(shè)備總線. 這意味著在 效果上, MMIO可能超越先前發(fā)出的對(duì)被緩存內(nèi)存的訪問(譯注: 意思是, MMIO后執(zhí)行, 但是 先到達(dá)內(nèi)存; 而先執(zhí)行的寫內(nèi)存操作則可能被緩存在cache上, 之后才能沖刷到內(nèi)存). 這種 情況下, 如果這兩者有某種依賴的話, 使用一個(gè)內(nèi)存屏障并不足夠, 而需要在寫被緩存內(nèi)存 和MMIO訪存之間將cache沖刷掉. ============= CPU 所能做到的 ============= 程序員可能會(huì)想當(dāng)然地認(rèn)為CPU將完全按照指定的順序執(zhí)行內(nèi)存操作, 如果CPU是這樣的話, 比方說讓它執(zhí)行下面的代碼: a = *A; *B = b; c = *C; d = *D; *E = e; 對(duì) 于每一條指令, 他們會(huì)期望CPU在完成內(nèi)存操作之后, 才會(huì)去執(zhí)行下一條指令, 于是系統(tǒng) 中的其他組件將看到這樣一個(gè)明確的操作序列: LOAD *A, STORE *B, LOAD *C, LOAD *D, STORE *E. 當(dāng)然, 實(shí)際情況是混亂的. 對(duì)于許多CPU和編譯器來說, 上述假設(shè)不成立, 因?yàn)? (*) LOAD操作可能更加需要立即完成以確保程序的執(zhí)行速度(譯注: 因?yàn)橥鶗?huì)有后續(xù)指令 需要等待LOAD的結(jié)果), 而STORE操作推遲一下往往并不會(huì)有問題; (*) LOAD操作可以通過預(yù)取來完成, 并且在確認(rèn)數(shù)據(jù)已經(jīng)不需要之后, 預(yù)取結(jié)果可以丟棄; (*) LOAD操作可以通過預(yù)取來完成, 導(dǎo)致結(jié)果被獲取的時(shí)機(jī)可能并不符合期望的執(zhí)行順序; (*) 內(nèi)存訪問的順序可能被重新排列, 以促進(jìn)更好的使用CPU總線和cache; (*) 有一些內(nèi)存或IO設(shè)備支持對(duì)相鄰地址的批量訪問, 在跟它們打交道的時(shí)候, LOAD和 STORE操作可能被合并, 從而削減訪存事務(wù)建立的成本, 以提高性能(內(nèi)存和PCI設(shè)備可 能都可以這樣做); 并且 (*) CPU的數(shù)據(jù)cache可能影響訪問順序, 盡管cache一致性機(jī)構(gòu)可以緩解這個(gè)問題 - 一旦 STORE操作命中了cache - 但并不能保證一致性將按順序傳播到其他CPU(譯注: 如果 STORE操作命中了cache, 那么被更新過的臟數(shù)據(jù)可能會(huì)在cache中停留一段時(shí)間, 而不 會(huì)立刻沖刷到內(nèi)存中); 所以說, 另一個(gè)CPU可能將上面的代碼看作是: LOAD *A, ..., LOAD {*C,*D}, STORE *E, STORE *B ("LOAD {*C,*D}"是一個(gè)合并的LOAD) 但 是, CPU將保證自身的一致性: 它將按正確的順序看到自己的內(nèi)存操作, 而不需要使用內(nèi) 存屏障. 以下面的代碼為例: U = *A; *A = V; *A = W; X = *A; *A = Y; Z = *A; 假設(shè)不存在外部的干擾, 那么可以肯定最終的結(jié)果一定是: U == the original value of *A U == *A的初始值 X == W Z == Y *A == Y 對(duì)于上面的代碼, CPU可能產(chǎn)生的全部?jī)?nèi)存訪問序列如下: U=LOAD *A, STORE *A=V, STORE *A=W, X=LOAD *A, STORE *A=Y, Z=LOAD *A 然而, 對(duì)于這個(gè)序列, 如果沒有干預(yù), 在保證一致性的前提下, 序列中的一些操作也很可能 會(huì)被合并或丟棄. 在CPU看到這些操作之前, 編譯器也可能會(huì)合并, 丟棄或推遲序列中的一些操作. 例如: *A = V; *A = W; 可能削減為: *A = W; 于是, 在沒有使用寫屏障的情況下, 可以認(rèn)為將V寫入*A的STORE操作丟失了. 類似的: *A = Y; Z = *A; 在沒有內(nèi)存屏障的情況下, 可能削減為: *A = Y; Z = Y; 于是在該CPU之外, 根本就看不到有LOAD操作存在. 特別值得一提的 Alpha處理器 ------------------------- DEC Alpha是現(xiàn)有的最為松散的CPU之一. 不僅如此, 許多版本的Alpha CPU擁有分列的數(shù)據(jù) cache, 允許他們?cè)诓煌臅r(shí)間更新兩個(gè)語義相關(guān)的緩存. 因?yàn)閮?nèi)存一致性系統(tǒng)需要同步更 新系統(tǒng)的兩個(gè)cache, 數(shù)據(jù)依賴屏障在這里就真正成為了必要, 以使得CPU能夠按正確的順序 來處理指針的更新和新數(shù)據(jù)的獲取. Alpha處理器定義了Linux內(nèi)核的內(nèi)存屏障模 型. (譯注: 體系結(jié)構(gòu)無關(guān)的代碼需要以最壞情 況為基準(zhǔn)來考慮.) 參閱前面的"Cache一致性"章節(jié). ======== 使 用示例 ======== 環(huán)型緩沖區(qū) ---------- 內(nèi)存屏障可以用于實(shí)現(xiàn)環(huán)型緩沖 區(qū), 不需要使用鎖, 就能使生產(chǎn)者和消費(fèi)者串行化. 參閱: Documentation/circular-buffers.txt 以獲得更多細(xì)節(jié). ==== 引 用 ==== Alpha AXP Architecture Reference Manual, Second Edition (Sites & Witek, Digital Press) Chapter 5.2: Physical Address Space Characteristics Chapter 5.4: Caches and Write Buffers Chapter 5.5: Data Sharing Chapter 5.6: Read/Write Ordering AMD64 Architecture Programmer's Manual Volume 2: System Programming Chapter 7.1: Memory-Access Ordering Chapter 7.4: Buffering and Combining Memory Writes IA-32 Intel Architecture Software Developer's Manual, Volume 3: System Programming Guide Chapter 7.1: Locked Atomic Operations Chapter 7.2: Memory Ordering Chapter 7.4: Serializing Instructions The SPARC Architecture Manual, Version 9 Chapter 8: Memory Models Appendix D: Formal Specification of the Memory Models Appendix J: Programming with the Memory Models UltraSPARC Programmer Reference Manual Chapter 5: Memory Accesses and Cacheability Chapter 15: Sparc-V9 Memory Models UltraSPARC III Cu User's Manual Chapter 9: Memory Models UltraSPARC IIIi Processor User's Manual Chapter 8: Memory Models UltraSPARC Architecture 2005 Chapter 9: Memory Appendix D: Formal Specifications of the Memory Models UltraSPARC T1 Supplement to the UltraSPARC Architecture 2005 Chapter 8: Memory Models Appendix F: Caches and Cache Coherency Solaris Internals, Core Kernel Architecture, p63-68: Chapter 3.3: Hardware Considerations for Locks and Synchronization Unix Systems for Modern Architectures, Symmetric Multiprocessing and Caching for Kernel Programmers: Chapter 13: Other Memory Models Intel Itanium Architecture Software Developer's Manual: Volume 1: Section 2.6: Speculation Section 4.4: Memory Access |
|