為什麼需要記憶體屏障【轉】

來源:互聯網
上載者:User

標籤:ber   共用   就會   sni   自己的   span   rdp   pki   [1]   

轉自:http://blog.csdn.net/chen19870707/article/details/39896655

  • Author:Echo Chen(陳斌)

  • Email:[email protected]

  • Blog:Blog.csdn.net/chen19870707

  • Date:September 30th, 2014

 

         來自一篇牆外的文章,要瞭解如何使用memory barrier,最好的方法是明白它為什麼存在。CPU硬體設計為了提高指令的執行速度,增設了兩個緩衝區(store buffer, invalidate queue)。這個兩個緩衝區可以避免CPU在某些情況下進行不必要的等待,從而提高速度,但是這兩個緩衝區的存在也同時帶來了新的問題。

要仔細分析這個問題需要先瞭解cache的工作方式。

        目前CPU的cache的工作方式很像軟體編程所使用的hash表,書上說“N路組相聯(N-way set associative)”,其中的“組”就是hash表的模值,即hash鏈的個數,而常說的“N路”,就是每個鏈表的最大長度。鏈表的表項叫做 cache-line,是一段固定大小的記憶體塊。讀操作很直接,不再贅述。如果某個CPU要寫資料項目,必須先將該資料項目從其他CPU的cache中移出, 這個操作叫做invalidation。當invalidation結束,CPU就可以安全的修改資料了。如果資料項目在該CPU的cache中,但是是只 讀的,這個過程叫做”write miss”。一旦CPU將資料從其他CPU的cache中移除,它就可以重複的讀寫該資料項目了。如果此時其他CPU試圖訪問這個資料項目,將產生一 次”cache miss”,這是因為第一個CPU已經使資料項目無效了。這種類型的cache-miss叫做”communication miss”,因為產生這種miss的資料項目通常是做在CPU之間溝通之用,比如鎖就是這樣一種資料項目。

       為了保證在多處理器的環境下cache仍然一致,需要一種協議來防止資料不一致和丟失。目前常用的協議是MESI協議。MESI是 Modified,Exclusive, Shared, Invalid這四種狀態的首字母的組合。使用該協議的cache,會在每個cache-line前加一個2位的tag,標示當前的狀態。

modified狀態:該cache-line包含修改過的資料,記憶體中的資料不會出現在其他CPU-cache中,此時該CPU的cache中包含的資料是最新的
exclusive狀態:與modified類似,但是資料沒有修改,表示記憶體中的資料是最新的。如果此時要從cache中剔除資料項目,不需要將資料寫回記憶體
shared狀態:資料項目可能在其他CPU中有重複,CPU必須在查詢了其他CPU之後才可以向該cache-line寫資料
invalid狀態:表示該cache-line空

MESI使用訊息傳遞的方式在上述幾種狀態之間切換,具體轉換過程參見[1]。如果CPU使用共用BUS,下面的訊息足夠:

read: 包含要讀取的CACHE-LINE的物理地址
read response: 包含READ請求的資料,要麼由記憶體滿足要麼由cache滿足
invalidate: 包含要invalidate的cache-line的物理地址,所有其他cache必須移除相應的資料項目
invalidate ack: 回複訊息 
read invalidate: 包含要讀取的cache-line的物理地址,同時使其他cache移除該資料。需要read response和invalidate ack訊息
writeback:包含要寫回的資料和地址,該狀態將處於modified狀態的lines寫回記憶體,為其他資料騰出空間

引用[1]中的話:

Interestingly enough, a shared-memory multiprocessor system really is a message-passing computer under the covers. This means that clusters of SMP machines that use distributed shared memory are using message passing to implement shared memory at two different levels of the system architecture.

      雖然該協議可以保證資料的一致性,但是在某種情況下並不高效。舉例來說,如果CPU0要更新一個處於CPU1-cache中的資料,那麼它必須等待 cache-line從CPU1-cache傳遞到CPU0-cache,然後再執行寫操作。cache之間的傳遞需要花費大量的時間,比執行一個簡單的 操作寄存器的指令高出幾個數量級。而事實上,花費這個時間根本毫無意義,因為不論從CPU1-cache傳遞過來的資料是什麼,CPU0都會覆蓋它。為了 解決這個問題,硬體設計者引入了store buffer,該緩衝區位於CPU和cache之間,當進行寫操作時,CPU直接將資料寫入store buffer,而不再等待另一個CPU的訊息。但是這個設計會導致一個很明顯的錯誤情況。

試考慮如下代碼:

   1: a = 1;
   2: b = a + 1;
   3: assert(b == 2);

假設初始時a和b的值都是0,a處於CPU1-cache中,b處於CPU0-cache中。如果按照下面流程執行這段代碼:

1 CPU0執行a=1; 
2 因為a在CPU1-cache中,所以CPU0發送一個read invalidate訊息來佔有資料 
3 CPU0將a存入store buffer 
4 CPU1接收到read invalidate訊息,於是它傳遞cache-line,並從自己的cache中移出該cache-line 
5 CPU0開始執行b=a+1; 
6 CPU0接收到了CPU1傳遞來的cache-line,即“a=0” 
7 CPU0從cache中讀取a的值,即“0” 
8 CPU0更新cache-line,將store buffer中的資料寫入,即“a=1” 
9 CPU0使用讀取到的a的值“0”,執行加1操作,並將結果“1”寫入b(b在CPU0-cache中,所以直接進行) 
10 CPU0執行assert(b == 2); 失敗

出現問題的原因是我們有兩份”a”的拷貝,一份在cache-line中,一份在store buffer中。硬體設計師的解決辦法是“store forwarding”,當執行load操作時,會同時從cache和store buffer裡讀取。也就是說,當進行一次load操作,如果store-buffer裡有該資料,則CPU會從store-buffer裡直接取出數 據,而不經過cache。因為“store forwarding”是硬體實現,我們並不需要太關心。

還有一中錯誤情況,考慮下面的代碼:

   1: void foo(void)
   2: {
   3: a = 1;
   4: b = 1;
   5: }
   6:  
   7: void bar(void)
   8: {
   9: while (b == 0) continue;
  10: assert(a == 1);
  11: }

假設變數a在CPU1-cache中,b在CPU0-cache中。CPU0執行foo(),CPU1執行bar(),程式執行的順序如下:

1 CPU0執行 a = 1; 因為a不在CPU0-cache中,所以CPU0將a的值放到store-buffer裡,然後發送read invalidate訊息
2 CPU1執行while(b == 0) continue; 但是因為b不再CPU1-cache中,所以它會發送一個read訊息 
3 CPU0執行 b = 1;因為b在CPU0-cache中,所以直接儲存b的值到store-buffer中 
4 CPU0收到 read 訊息,於是它將更新過的b的cache-line傳遞給CPU1,並標記為shared 
5 CPU1接收到包含b的cache-line,並安裝到自己的cache中 
6 CPU1現在可以繼續執行while(b == 0) continue;了,因為b=1所以迴圈結束 
7 CPU1執行assert(a == 1);因為a本來就在CPU1-cache中,而且值為0,所以斷言為假 
8 CPU1收到read invalidate訊息,將並將包含a的cache-line傳遞給CPU0,然後標記cache-line為invalid。但是已經太晚了

就是說,可能出現這類情況,b已經賦值了,但是a還沒有,所以出現了b = 1, a = 0的情況。對於這類問題,硬體設計者也愛莫能助,因為CPU無法知道變數之間的關聯關係。所以硬體設計者提供了memory barrier指令,讓軟體來告訴CPU這類別關係。解決方案是修改代碼如下:

   1: void foo(void)
   2: {
   3: a = 1;
   4: smp_mb();
   5: b = 1;
   6: }

smp_mb()指令可以迫使CPU在進行後續store操作前重新整理store-buffer。以上面的程式為例,增加memory barrier之後,就可以保證在執行b=1的時候CPU0-store-buffer中的a已經重新整理到cache中了,此時CPU1-cache中的a 必然已經標記為invalid。對於CPU1中執行的代碼,則可以保證當b==0為假時,a已經不在CPU1-cache中,從而必須從CPU0- cache傳遞,得到新值“1”。具體過程見[1]。

上面的例子是使用memory barrier的一種環境,另一種環境涉及到另一個緩衝區,確切的說是一個隊列——“Invalidate Queues”。

store buffer一般很小,所以CPU執行幾個store操作就會填滿。這時候CPU必須等待invalidation ACK訊息,來釋放緩衝區空間——得到invalidation ACK訊息的記錄會同步到cache中,並從store buffer中移除。同樣的情形發生在memory barrier執行以後,這時候所有後續的store操作都必須等待invalidation完成,不論這些操作是否導致cache-miss。解決辦法 很簡單,即使用“Invalidate Queues”將invalidate訊息排隊,然後馬上返回invalidate ACK訊息。不過這種方法有問題。

考慮下面的情況:

   1: void foo(void)
   2: {
   3: a = 1;
   4: smp_mb();
   5: b = 1;
   6: }
   7:  
   8: void bar(void)
   9: {
  10: while (b == 0) continue;
  11: assert(a == 1);
  12: }

a處於shared狀態,b在CPU0-cache內。CPU0執行foo(),CPU1執行函數bar()。執行操作如下:

1 CPU0執行a=1。因為cache-line是shared狀態,所以新值放到store-buffer裡,並傳遞invalidate訊息來通知CPU1
2 CPU1執行 while(b==0) continue;但是b不再CPU1-cache中,所以發送read訊息 
3 CPU1接受到CPU0的invalidate訊息,將其排隊,然後返回ACK訊息 
4 CPU0接收到來自CPU1的ACK訊息,然後執行smp_mb(),將a從store-buffer移到cache-line中 
5 CPU0執行b=1;因為已經包含了該cache-line,所以將b的新值寫入cache-line 
6 CPU0接收到了read訊息,於是傳遞包含b新值的cache-line給CPU1,並標記為shared狀態 
7 CPU1接收到包含b的cache-line 
8 CPU1繼續執行while(b==0) continue;因為為假所以進行下一個語句 
9 CPU1執行assert(a==1),因為a的舊值依然在CPU1-cache中,宣告失敗 
10 儘管宣告失敗了,但是CPU1還是處理了隊列中的invalidate訊息,並真的invalidate了包含a的cache-line,但是為時已晚

可以看出出現問題的原因是,當CPU排隊某個invalidate訊息後,在它還沒有處理這個訊息之前,就再次讀取該訊息對應的資料了,該資料此時本應該已經失效的。

解決方案是在bar()中也增加一個memory barrier:

   1: void bar(void)
   2: {
   3: while (b == 0) continue;
   4: smp_mb();
   5: assert(a == 1);
   6: }

此處smp_mb()的作用是處理“Invalidate Queues”中的訊息,於是在執行assert(a==1)時,CPU1中的包含a的cache-line已經無效了,新的值要重新從CPU0-cache中讀取。

memory bariier還可以細分為“write memory barrier(wmb)”和“read memory barrier(rmb)”。rmb只處理Invalidate Queues,wmb只處理store buffer。

可以使用rmb和wmb重寫上面的例子:

   1: void foo(void)
   2: {
   3: a = 1;
   4: smp_wmb();
   5: b = 1;
   6: }
   7:  
   8: void bar(void)
   9: {
  10: while (b == 0) continue;
  11: smp_rmb();
  12: assert(a == 1);
  13: }

最後提一下x86的mb。x86CPU會自動處理store順序,所以smp_wmb()原語什麼也不做,但是load有可能亂序,smp_rmb()和smp_mb()展開為lock;addl。

       [1] http://www.rdrop.com/users/paulmck/scalability/paper/whymb.2010.06.07c.pdf 
       [2] http://en.wikipedia.org/wiki/Memory_barrier 
       [3] http://www.mjmwired.net/kernel/Documentation/memory-barriers.txt

       [4]http://sstompkins.wordpress.com/2011/04/12/why-memory-barrier%EF%BC%9F/

 

-

Echo Chen:Blog.csdn.net/chen19870707

為什麼需要記憶體屏障【轉】

相關文章

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.