內存屏障的分類:
-
編譯器引起的內存屏障
-
緩存引起的內存屏障
-
亂序執行引起的內存屏障
1、編譯器引起的內存屏障:
我們都知道,從寄存器裏面取一個數要比從內存中取快的多,所以有時候編譯器爲了編譯出優化度更高的程序,就會把一些常用變量放到寄存器中,下次使用該變量的時候就直接從寄存器中取,而不再訪問內存,這就出現了問題,當其他線程把內存中的值改變了怎麼辦?也許你會想,編譯器怎麼會那麼笨,犯這種低級錯誤呢!是的,編譯器沒你想象的那麼聰明!讓我們看下面的代碼:(代碼摘自《獨闢蹊徑品內核》)
intflag=0; voidwait(){ while(flag ==0) sleep(1000); ...... } voidwakeup(){ flag=1; } |
這段代碼表示一個線程在循環等待另一個線程修改flag。Gcc等編譯器在編譯的時候發現,sleep()不會修改flag的值,所以,爲了提高效率,它就會把某個寄存器分配給flag,於是編譯後就生成了這樣的僞彙編代碼:
voidwait(){ movl flag,%eax; while(%eax==0) sleep(1000); } |
這時,當wakeup函數修改了flag的值,wait函數還在傻乎乎的讀寄存器的值而不知道其實flag已經改變了,線程就會死循環下去。由此可見,編譯器的優化帶來了相反的效果!
但是,你又不能說是讓編譯器放棄這種優化,因爲在很多場合下,這種優化帶來的性能是十分可觀的!那我們該怎麼辦呢?有沒有什麼辦法可以避免這種情況?答案必須是肯定的,我們可以使用關鍵字volatile來避免這種情況。
volatileintflag =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之前對內存進行的讀寫操作指令全部完成