當前位置:網站首頁>內存屏障簡介

內存屏障簡介

2022-01-27 11:09:44 TABE_

內存屏障是硬件之上與操作系統之下對並發作出的最後一層支持。

內存屏障是由於編譯器的優化和緩存的使用,導致對內存的寫入操作不能及時的反應出來的一種現象,也就是說當完成對內存的寫入操作之後,讀取出來的可能是舊的內容。內存屏障本質上是由於CPU重排序指令引起的。重排序問題無時無刻不在發生,主要源自以下幾種場景:

  1. 編譯器編譯時的優化。
  2. 緩存同步順序(導致可見性問題)。
  3. 處理器執行時的多發射和亂序優化。
  4. 讀取和存儲指令的優化。

編譯器編譯時的優化

我們都知道,從寄存器裏面取一個數要比從內存中取快的多,所以有時候編譯器為了編譯出優化度更高的程序,就會把一些常用變量放到寄存器中,下次使用該變量的時候就直接從寄存器中取,而不再訪問內存。但是,當其他線程把內存中的值改變時,這樣的優化就會出現問題,讓我們看下面的代碼:

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來避免這種情况。

緩存同步順序

好,既然寄存器能够引起這樣的問題,那麼緩存呢?我們都知道,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()。用的也是以上那些指令,感興趣可以去看下內核代碼。

處理器執行時的多發射和亂序優化

現代處理器基本上都是支持多發射的,也就是在一個指令周期內可以同時執行多條指令。但是,處理器的資源就那麼多,可能不能同時滿足處理這些指令的要求。比如,處理器就只有一個加法器,如果同時有兩條指令都需要算加法,那麼有一條指令必須等待。如果這時候再下一條指令是讀取指令,並且和前兩條指令無關,那麼這條指令將在前面某條加法指令之前完成。還有一種可能,就是前後指令之間具有相關性,比如對同一個地址先讀取再寫入,後面的寫入操作必須等待前面的讀取操作完成後才能執行。但是如果這時候第三條指令是寫入一個無關的地址,那它可以在前面的寫入操作之前被執行,執行順序再次被打亂了。

所以,一般情况下指令亂序並不是CPU在執行指令之前刻意去調整順序。CPU總是順序的去內存裏面取指令,然後將其順序的放入指令流水線。但是指令執行時的各種條件,指令與指令之間的相互影響,可能導致順序放入流水線的指令,最終不是按照放入的順序執行完成,在外邊看起來仿佛是“亂序”一樣,這就是所謂的“順序流入,亂序流出”。

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

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

讀取和存儲指令的優化

CPU有可能根據情况,將相臨的兩條讀取或寫入操作合並成一條。

例如,對於如下的兩條讀取操作:

X = *A; Y = *(A + 4);

可能被合並成一條讀取操作:

{
    X, Y} = LOAD {
    *A, *(A + 4) };

同樣的,對於如下兩條寫入操作:

*A = X; *(A + 4) = Y;

有可能會被合並成一條:

STORE {
    *A, *(A + 4) } = {
    X, Y};

版權聲明
本文為[TABE_]所創,轉載請帶上原文鏈接,感謝
https://cht.chowdera.com/2022/01/202201271109434868.html

隨機推薦