首頁  >  文章  >  系統教程  >  詳解Linux核心中的記憶體屏障

詳解Linux核心中的記憶體屏障

WBOY
WBOY轉載
2024-02-10 15:00:24488瀏覽

前言

我之前閱讀了一篇關於順序一致性和快取一致性的討論文章,對於這兩個概念的區別和聯繫有了更加清晰的認識。在Linux核心中,有許多同步和屏障機制,我想在此做一些總結。

詳解Linux核心中的記憶體屏障

#快取一致性

之前我一直認為Linux中的許多機制是為了保證快取一致性,但實際上,絕大部分的快取一致性是由硬體機制實現的。只有在使用帶有lock前綴的指令時,才與緩存有些關係(雖然這話絕對並不嚴謹,但是從目前來看,大多數情況下是如此的)。我們更多的時候,是為了確保順序​​一致性。

快取一致性是指在多處理器系統中,每個CPU都有自己的L1快取。由於不同CPU的L1快取中可能快取了同一片記憶體的內容,當一個CPU更改了自己被快取的內容時,它必須確保另一個CPU讀取這塊資料時也能夠讀取到最新的內容。但是,別擔心,這個複雜的工作完全由硬體來完成,透過實作MESI協議,硬體可以輕鬆地完成快取一致性的工作。即使是多個CPU同時寫入,也不會有問題。無論是在自己的快取中、其他CPU的快取中或記憶體中,一個CPU總是能夠讀入最新的數據,這就是快取一致性的工作原理。

順序一致性

#所謂順序一致性,說的則是與快取一致性完全不同的概念,雖然它們都是處理器發展的產物。因為編譯器的技術不斷發展,它可能為了最佳化你的程式碼,而將某些操作的順序變更執行。處理器中也早就有了多發射、亂序執行的概念。這樣的結果,就是實際執行的指令順序會與程式設計時程式碼的執行順序略有不同。這在單處理器下當然沒什麼,畢竟只要自己的程式碼不過問,就沒人過問,編譯器和處理器就是在保證自己的程式碼發現不了的情況下打亂執行順序的。但多處理器不是這樣,可能一個處理器上指令的完成順序,會對其它處理器上執行的程式碼造成很大影響。所以就有了順序一致性的概念,就是保證一個處理器上執行緒的執行順序,在其它的處理器上的執行緒看來,都是一樣的。這個問題的解決不是光靠處理器或編譯器就能解決的,需要軟體的介入。

記憶體屏障

軟體介入的方式也非常簡單,那就是插入記憶體屏障(memory barrier)。其實記憶體屏障這個詞,是搞處理器的人造的,弄得我們很不好理解。記憶體屏障,很容易讓我們串到快取一致性去,乃至懷疑是否這樣做才能讓它cpu看到被修改過的cache,這樣想就錯了。所謂記憶體屏障,從處理器角度來說,是用來串列化讀寫操作的,從軟體角度來講,就是用來解決順序一致性問題的。編譯器不是要打亂程式碼執行順序嗎,處理器不是要亂序執行嗎,你插入一個記憶體屏障,就等於告訴編譯器,屏障前後的指令順序不能顛倒,告訴處理器,只有等屏障前的指令執行完了,屏障後的指令才能開始執行。當然,記憶體屏障能阻擋編譯器亂來,但處理器還是有辦法。處理器中不是有多發射、亂序執行、順序完成的概念嗎,它在內存屏障時只要保證前面指令的讀寫操作,一定在後面指令的讀寫操作完成之前完成,就可以了。所以記憶體屏障才會對應有讀屏障、寫屏障和讀寫屏障三類。如x86之前保證寫入操作都是順序完成的,所以不需要寫屏障,但現在也有部分ia32處理器的寫入操作變成亂序完成,所以也需要寫屏障。
其實,除了專門的讀寫屏障指令,還有很多指令的執行是帶有讀寫屏障功能的,例如帶有lock前綴的指令。在專門的讀寫屏障指令出現之前,linux就是靠lock撐過來的。
至於在那裡插入讀寫屏障,則視軟體的需求而定。讀寫屏障無法完全實現順序一致性,但多處理器上的線程也不會一直盯著你的執行順序看,只要保證在它看過的時候,認為你符合順序一致性,執行不會出現你程式碼中沒有預料到的情況。所謂預料外的情況,舉例而言,你的線程是先給變量a賦值,再給變量b賦值,結果別的處理器上運行的線程看過來,發現b賦值了,a卻沒有賦值,(注意這種不一致不是快取不一致造成的,而是處理器寫入作業完成的順序不一致所造成的),這時就要在a賦值與b賦值之間,加上一個寫入屏障。

多處理器間同步

#有了SMP之後,執行緒就開始同時在多個處理器上運作。只要是線程就有通訊和同步的要求。幸好SMP系統是共享記憶體的,也就是所有處理器看到的記憶體內容都一樣,雖然有獨立的L1 cache,但還是由硬體完成了快取一致性處理的問題。那不同處理器上的執行緒要存取相同數據,需要臨界區,需要同步。靠什麼同步?之前在UP系統中,我們上靠信號量,下靠關中斷和讀取修改寫指令。現在在SMP系統中,關卡中斷已經廢了,雖然為了同步同一處理器上的執行緒還是需要的,但只靠它已經不行了。讀修改寫指令?也不行了。在你指令中讀取操作完成寫入操作還沒進行時,就可能有另外的處理器進行了讀取操作或寫入操作。快取一致性協定是先進,但還沒先進到預測這條讀取操作是哪一種指令發出來的。所以x86又發明了帶有lock前綴的指令。在此指令執行時,會將所有包含指令中讀寫位址的cache line失效,並鎖定記憶體匯流排。這樣別的處理器要想對同樣的位址或同一個cache line上的位址讀寫,既無法從cache中進行(cache中相關line已經失效了),也無法從記憶體匯流排上進行(整個記憶體匯流排都鎖了),終於達到原子性執行的目的了。當然,從P6處理器開始,如果帶lock前綴指令要存取的位址本來就在cache中,就不需要鎖記憶體匯流排,也能完成原子性操作了(雖然我懷疑這是因為加了多處理器內部公共的L2 cache的緣故)。

因為會鎖定記憶體匯流排,所以帶lock前綴指令執行前,也會先將未完成的讀寫操作完成,也起到記憶體屏障的功能。
現在多處理器間執行緒的同步,上用自旋鎖,下用這種帶了lock前綴的讀取修改寫指令。當然,實際的同步還有加上禁止本處理器任務調度的,有加上任務關中斷的,還會在外面加上信號量的外衣。 linux中對這種自旋鎖的實現,已歷經四代發展,變得愈發高效強大。

内存屏障的实现

\#ifdef CONFIG_SMP  
\#define smp_mb()  mb()  
\#define smp_rmb()  rmb()  
\#define smp_wmb()  wmb()  
\#else  
\#define smp_mb()  barrier()  
\#define smp_rmb()  barrier()  
\#define smp_wmb()  barrier()  
\#endif 

CONFIG_SMP就是用来支持多处理器的。如果是UP(uniprocessor)系统,就会翻译成barrier()。

#define barrier() asm volatile(“”: : :”memory”)
barrier()的作用,就是告诉编译器,内存的变量值都改变了,之前存在寄存器里的变量副本无效,要访问变量还需再访问内存。这样做足以满足UP中所有的内存屏障。

\#ifdef CONFIG_X86_32  
/* 
 \* Some non-Intel clones support out of order store. wmb() ceases to be a 
 \* nop for these. 
 */  
\#define mb() alternative("lock; addl $0,0(%%esp)", "mfence", X86_FEATURE_XMM2)  
\#define rmb() alternative("lock; addl $0,0(%%esp)", "lfence", X86_FEATURE_XMM2)  
\#define wmb() alternative("lock; addl $0,0(%%esp)", "sfence", X86_FEATURE_XMM)  
\#else  
\#define mb()  asm volatile("mfence":::"memory")  
\#define rmb()  asm volatile("lfence":::"memory")  
\#define wmb()  asm volatile("sfence" ::: "memory")  
\#endif 

如果是SMP系统,内存屏障就会翻译成对应的mb()、rmb()和wmb()。这里CONFIG_X86_32的意思是说这是一个32位x86系统,否则就是64位的x86系统。现在的linux内核将32位x86和64位x86融合在同一个x86目录,所以需要增加这个配置选项。

可以看到,如果是64位x86,肯定有mfence、lfence和sfence三条指令,而32位的x86系统则不一定,所以需要进一步查看cpu是否支持这三条新的指令,不行则用加锁的方式来增加内存屏障。

SFENCE,LFENCE,MFENCE指令提供了高效的方式来保证读写内存的排序,这种操作发生在产生弱排序数据的程序和读取这个数据的程序之间。 
  SFENCE——串行化发生在SFENCE指令之前的写操作但是不影响读操作。 
  LFENCE——串行化发生在SFENCE指令之前的读操作但是不影响写操作。 
  MFENCE——串行化发生在MFENCE指令之前的读写操作。 
sfence:在sfence指令前的写操作当必须在sfence指令后的写操作前完成。 
lfence:在lfence指令前的读操作当必须在lfence指令后的读操作前完成。 
mfence:在mfence指令前的读写操作当必须在mfence指令后的读写操作前完成。

至于带lock的内存操作,会在锁内存总线之前,就把之前的读写操作结束,功能相当于mfence,当然执行效率上要差一些。

说起来,现在写点底层代码真不容易,既要注意SMP问题,又要注意cpu乱序读写问题,还要注意cache问题,还有设备DMA问题,等等。

多处理器间同步的实现
多处理器间同步所使用的自旋锁实现,已经有专门的文章介绍

以上是詳解Linux核心中的記憶體屏障的詳細內容。更多資訊請關注PHP中文網其他相關文章!

陳述:
本文轉載於:lxlinux.net。如有侵權,請聯絡admin@php.cn刪除