天天看點

Linux核心:memory barrier(中)

(2)Store buffer

我們考慮另外一個場景:在上一節中step e中的操作變成CPU 0對共享變量進行寫的操作。這時候,寫的性能變得非常的差,因為CPU 0必須要等到CPU n上的cacheline 資料傳遞到其cacheline之後,才能進行寫的操作(CPU n上的cacheline 變成invalid狀态,CPU 0則切換成exclusive狀态,為後續的寫動作做準備)。而從一個CPU的cacheline傳遞資料到另外一個CPU的cacheline是非常消耗時間的,而這時候,CPU 0的寫的動作隻是hold住,直到cacheline的資料完成傳遞。而實際上,這樣的等待是沒有意義的,是以,這時候cacheline的資料仍然會被覆寫掉。為了解決這個問題,多核系統中的cache修改如下:

Linux核心:memory barrier(中)

這樣,問題解決了,寫操作不必等到cacheline被加載,而是直接寫到store buffer中然後歡快的去幹其他的活。在CPU n的cacheline把資料傳遞到其cache 0的cacheline之後,硬體将store buffer中的内容寫入cacheline。

雖然性能問題解決了,但是邏輯錯誤也随之引入,我們可以看下面的例子:

我們假設a和b是共享變量,初始值都是0,可以被cpu0和cpu1通路。cpu 0的cache中儲存了b的值(exclusive狀态),沒有a的值,而cpu 1的cache中儲存了a的值,沒有b的值,cpu 0執行的彙編代碼是(用的是ARM彙編,沒有辦法,其他的都不是那麼熟悉):

ldr     r2, [pc, #28]   -------------------------- 取變量a的位址

ldr     r4, [pc, #20]   -------------------------- 取變量b的位址

mov     r3, #1

str     r3, [r2]           --------------------------a=1

str     r3, [r4]           --------------------------b=1

CPU 1執行的代碼是:

       ldr     r2, [pc, #28]   -------------------------- 取變量a的位址

             ldr     r3, [pc, #20]  -------------------------- 取變量b的位址

start:     ldr     r3, [r3]          -------------------------- 取變量b的值

            cmp     r3, #0          ------------------------ b的值是否等于0?

            beq     start            ------------------------ 等于0的話跳轉到start

            ldr     r2, [r2]          -------------------------- 取變量a的值

當cpu 1執行到--取變量a的值--這條指令的時候,b已經是被cpu0修改為1了,這也就是說a=1這個代碼已經執行了,是以,從彙編代碼的邏輯來看,這時候a值應該是确定的1。然而并非如此,cpu 0和cpu 1執行的指令和動作描述如下:

Linux核心:memory barrier(中)

  對于硬體,CPU不清楚具體的代碼邏輯,它不可能直接幫助軟體工程師,隻是提供一些memory barrier的指令,讓軟體工程師告訴CPU他想要的記憶體通路邏輯順序。這時候,cpu 0的代碼修改如下:

確定清空store buffer的memory barrier instruction

這種情況下,cpu 0和cpu 1執行的指令和動作描述如下:

Linux核心:memory barrier(中)

由于增加了memory barrier,保證了a、b這兩個變量的通路順序,進而保證了程式邏輯。

(3)Invalidate Queue

我們先回憶一下為何出現了stroe buffer:為了加快cache miss狀态下寫的性能,硬體提供了store buffer,以便讓CPU先寫入,進而不必等待invalidate ack(這些互動是為了保證各個cpu的cache的一緻性)。然而,store buffer的size比較小,不需要特别多的store指令(假設每次都是cache miss)就可以将store buffer填滿,這時候,沒有空間寫了,是以CPU也隻能是等待invalidate ack了,這個狀态和memory barrier指令的效果是一樣的。

怎麼解決這個問題?CPU設計的硬體工程師對性能的追求是不會停歇的。我們首先看看invalidate ack為何如此之慢呢?這主要是因為cpu在收到invalidate指令後,要對cacheline執行invalidate指令,確定該cacheline的确是invalid狀态後,才會發送ack。如果cache正忙于其他工作,當然不能立刻執行invalidate指令,也就無法會ack。

怎麼破?CPU設計的硬體工程師提供了下面的方法:

Linux核心:memory barrier(中)

Invalidate Queue這個HW block從名字就可以看出來是儲存invalidate請求的隊列。其他CPU發送到本CPU的invalidate指令會儲存于此,這時候,并不需要等到實際對cacheline的invalidate操作完成,CPU就可以回invalidate ack了。

同store buffer一樣,雖然性能問題解決了,但是對memory的通路順序導緻的邏輯錯誤也随之引入,我們可以看下面的例子(和store buffer中的例子類似):

我們假設a和b是共享變量,初始值都是0,可以被cpu0和cpu1通路。cpu 0的cache中儲存了b的值(exclusive狀态),而CPU 1和CPU 0的cache中都儲存了a的值,狀态是shared。cpu 0執行的彙編代碼是:

    ldr     r2, [pc, #28]   -------------------------- 取變量a的位址

Linux核心:memory barrier(中)

可怕的memory misorder問題又來了,都是由于引入了invalidate queue引起,看來我們還需要一個memory barrier的指令,我們将程式修改如下:

 ldr     r2, [pc, #28]   -------------------------- 取變量a的位址

確定清空invalidate queue的memory barrier instruction

繼續閱讀