內存屏障

當你看到“內存屏障”四個字的時候,你的第一反應是什麼?寄存器裏取出了錯誤的值?ifence,sfence之類的指令?還是諸如volatile之類的關鍵字?好吧,我第一次看到這四個字的時候,腦子裏浮現出的是魔獸爭霸裏綠油油的鋪滿苔蘚的岩石屏障- -#,並且,當我搞明白內存屏障具體是什麼,而且自認爲對其很熟悉之後,我的第一反應依然是那幾塊綠油油的石頭,而且很想上去A一把!

言歸正傳,先解釋下什麼是內存屏障。內存屏障是指“由於編譯器的優化和緩存的使用,導致對內存的寫入操作不能及時的反應出來,也就是說當完成對內存的寫入操作之後,讀取出來的可能是舊的內容”(摘自《獨闢蹊徑品內核》)。(這裏概念貌似不是很準確,正確的定義:爲了防止編譯器和硬件的不正確優化,使得對存儲器的訪問順序(其實就是變量)和書寫程序時的訪問順序不一致而提出的一種解決辦法。 它不是一種錯誤的現象,而是一種對錯誤現象提出的解決方發----歡迎指正!!)

概念就是概念,生硬的東西,懂的人能從中悟出點什麼,不懂的人還是一頭霧水。不要着急,我們先給內存屏障分下類,然後挨個來研究一番,等看完這篇文章,再回來讀讀概念,你就懂了!

內存屏障的分類:

  1. 編譯器引起的內存屏障
  2. 緩存引起的內存屏障
  3. 亂序執行引起的內存屏障

1、編譯器引起的內存屏障:

我們都知道,從寄存器裏面取一個數要比從內存中取快的多,所以有時候編譯器爲了編譯出優化度更高的程序,就會把一些常用變量放到寄存器中,下次使用該變量的時候就直接從寄存器中取,而不再訪問內存,這就出現了問題,當其他線程把內存中的值改變了怎麼辦?也許你會想,編譯器怎麼會那麼笨,犯這種低級錯誤呢!是的,編譯器沒你想象的那麼聰明!讓我們看下面的代碼:(代碼摘自《獨闢蹊徑品內核》)

int flag=0;
 
void wait(){
    while ( flag == 0 )
        sleep(1000);
    ......
}
 
void wakeup(){
    flag=1;
}

這段代碼表示一個線程在循環等待另一個線程修改flag。 Gcc等編譯器在編譯的時候發現,sleep()不會修改flag的值,所以,爲了提高效率,它就會把某個寄存器分配給flag,於是編譯後就生成了這樣的僞彙編代碼:

void wait(){
    movl  flag, %eax;
 
    while ( %eax == 0)
        sleep(1000);
}

這時,當wakeup函數修改了flag的值,wait函數還在傻乎乎的讀寄存器的值而不知道其實flag已經改變了,線程就會死循環下去。由此可見,編譯器的優化帶來了相反的效果!

但是,你又不能說是讓編譯器放棄這種優化,因爲在很多場合下,這種優化帶來的性能是十分可觀的!那我們該怎麼辦呢?有沒有什麼辦法可以避免這種情況?答案必須是肯定的,我們可以使用關鍵字volatile來避免這種情況

volatile int flag = 0;

這樣,我們就能避免編譯器把某個寄存器分配給flag了。

好,上面所描述這些,就叫做“編譯器優化引起的內存屏障”,是不是懂了點什麼?再回去看看概念?

2、緩存引起的內存屏障

好,既然寄存器能夠引起這樣的問題,那麼緩存呢?我們都知道,CPU會把數據取到一個叫做cache的地方,然後下次取的時候直接訪問cache,寫入的時候,也先將值寫入cache。

那麼,先讓我們考慮,在單核的情況下會不會出現問題呢?先想一下,單核情況下,除了CPU還會有什麼會修改內存?對了,是外部設備的DMA!那麼,DMA修改內存,會不會引起內存屏障的問題呢?答案是,在現在的體系結構中,不會。

當外部設備的DMA操作結束的時候,會有一種機制保證CPU知道他對應的緩存行已經失效了;而當CPU發動DMA操作時,在想外部設備發送啓動命令前,需要把對應cache中的內容寫回內存。在大多數RISC的架構中,這種機制是通過一寫個特殊指令來實現的。在X86上,採用一種叫做總線監測技術的方法來實現。就是CPU和外部設備訪問內存的時候都需要經過總線的仲裁,有一個專門的硬件模塊用於記錄cache中的內存區域,當外部設備對內存寫入的時候,就通過這個硬件來判斷下改內存區域是否在cache中,然後再進行相應的操作。

那麼,什麼時候才能產生cache引起的內存屏障呢?多CPU? 是的,在多CPU的系統裏面,每個CPU都有自己的cache,當同一個內存區域同時存在於兩個CPU的cache中時,CPU1改變了自己cache中的值,但是CPU2卻仍然在自己的cache中讀取那個舊值,這種結果是不是很杯具呢?因爲沒有訪存操作,總線也是沒有辦法監測的,這時候怎麼辦?

對阿,怎麼辦呢?我們需要在CPU2讀取操作之前使自己的cache失效,x86下,很多指令能做到這點,如lock前綴的指令,cpuid, iret等。內核中使用了一些函數來完成這個功能:mb(), rmb(), wmb()。用的也是以上那些指令,感興趣可以去看下內核代碼。

3、亂序執行引起的內存屏障:

我們都知道,超標量處理器越來越流行,連龍芯都是四發射的。超標量實際上就是一個CPU擁有多條獨立的流水線,一次可以發射多條指令,因此,很多允許指令的亂序執行,具體怎麼個亂序方法,可以去看體系結構方面的書,這裏只說內存屏障。

指令亂序執行了,就會出現問題,假設指令1給某個內存賦值,指令2從該內存取值用來運算。如果他們兩個顛倒了,指令2先從內存中取值運算,是不是就錯了?

對於這種情況,x86上專門提供了lfence,sfence,和mfence 指令來停止流水線:

lfence:停止相關流水線,知道lfence之前對內存進行的讀取操作指令全部完成

sfence:停止相關流水線,知道lfence之前對內存進行的寫入操作指令全部完成

mfence:停止相關流水線,知道lfence之前對內存進行的讀寫操作指令全部完成

好,將完這三種類型,再回去看看概念,清晰了麼?如果還不明白,那就是我的表達能力太有限了,自己網上再搜搜把!

 

FROM:http://www.spongeliu.com/clanguage/memorybarrier/

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章