天天看點

Linux核心中的記憶體屏障

編譯器有時會對代碼做一些優化,例如嘗試在保證程式執行正确的前提下修改指令順序或優化ldr/str指令,讓程式執行地更快。但是編譯器畢竟不能完全猜透人的心思,有時候它做的優化會導緻程式運作不符我們的預期。

是以,核心中提供了一些額外的函數,可以插在某段代碼裡,告訴編譯器不要在這裡做指令優化。這些函數分為兩種:

記憶體屏障:rmb(), wmb(), mb(),可以防止硬體上的指令重排。除了編譯器,有的CPU也支援對指令進行重排來優化程式執行效率,這幾個函數就是去防止CPU去做這些事情。rmb()是讀通路記憶體屏障,它保證在屏障(調用rmb()的位置處)之後的任何讀操作在執行之前,屏障之前的所有讀操作都已經完成。wmb()對應寫操作,意思同上。mb()就同時包含讀和寫操作,意思同上。

優化屏障:barrier(),防止編譯器對記憶體通路的優化,類似volatile關鍵字對于通路變量的作用。它告訴編譯器,在插入barrier()的位置處,記憶體中的内容都被更新了,你想讀變量、映射到記憶體的寄存器等内容都需要真正到記憶體裡去讀,這樣就能保證barrier之後的讀指令不會被優化掉。

上面第1種屏障是和硬體即CPU特性相關的,那麼,如果你的CPU沒有指令重排的能力,也就沒有必要防止指令重排了。例如,一款CPU不支援寫指令重排,那麼系統中的wmb()就直接被定義成了barrier()。

還有SMP系統中使用的smp_rmb(), smp_wmb(), smp_mb(),它們隻用于SMP系統。在單處理器上它們被定義成barrier()。

當然,防止優化後,受影響的代碼執行效率會降低,但為了保證正确性,犧牲一點性能是值得的。

優化屏障的一個特定應用是核心的搶占機制。我們看到preempt_disable()/preempt_enable()并不是簡單的修改搶占計數:

#define preempt_disable() \
do { \
    inc_preempt_count(); \
    barrier(); \
} while (0)

#define preempt_enable_no_resched() \
do { \
    barrier(); \
    dec_preempt_count(); \
} while (0)

#define preempt_enable() \
do { \
    preempt_enable_no_resched(); \
    barrier(); \
    preempt_check_resched(); \
} while (0)           

正常的用法我們都很熟悉:

preempt_disable()
//臨界區,不能被搶占。
preempt_enable()           

但如果不加屏障,誰也不知道編譯器是否會優化成這樣:

//臨界區,不能被搶占。
preempt_disable()
preempt_enable()           

preempt_disable()
preempt_enable()
//臨界區,不能被搶占。           

這樣臨界區代碼就沒有受到保護。是以,需要在關搶占時增加preempt_count之後增加一個屏障,告訴編譯器在這之前要完成寫請求,接着再執行臨界區代碼,在開搶占時遞減preempt_count之前增加一個屏障,告訴編譯器要真正去記憶體裡擷取這個值,不能偷懶。

volatile:

上面提到了volatile,我也簡單說一下,volatile的作用是防止編譯器對訪存指令做優化,例如,在一個線程的一段代碼裡要定期讀一個變量a,根據讀到的不同值做不同僚情,但這個a的修改是在另一個線程裡做的,那編譯器可能就認為a沒有被改過進而不是去每次從記憶體裡去讀新的a(把a放在一個臨時寄存器裡,每次讀寄存器)。

用volatile關鍵字修飾a的作用就是讓使用a的代碼每次都真正從記憶體裡去讀。volatile的作用僅此而已(沒有保證原子性之類的作用,保證原子性該加鎖還是要加鎖)。

繼續閱讀