天天看點

Linux核心:memory barrier(上)

一、前言

我記得以前上學的時候大家經常說的一個詞彙叫做所見即所得,有些程式設計工具是所見即所得的,給程式員帶來極大的友善。對于一個c程式員,我們的編寫的代碼能所見即所得嗎?我們看到的c程式的邏輯是否就是最後CPU運作的結果呢?很遺憾,不是,我們的“所見”和最後的執行結果隔着:

1、編譯器

2、CPU取指執行

編譯器将符合人類思考的邏輯(c代碼)翻譯成了符合CPU運算規則的彙編指令,編譯器了解底層CPU的思維模式,是以,它可以在将c翻譯成彙編的時候進行優化(例如記憶體通路指令的重新排序),讓産出的彙編指令在CPU上運作的時候更快。然而,這種優化産出的結果未必符合程式員原始的邏輯,是以,作為程式員,作為c程式員,必須有能力了解編譯器的行為,并在通過内嵌在c代碼中的memory barrier來指導編譯器的優化行為(這種memory barrier又叫做優化屏障,Optimization barrier),讓編譯器産出即高效,又邏輯正确的代碼。

CPU的核心思想就是取指執行,對于in-order的單核CPU,并且沒有cache(這種CPU在現實世界中還存在嗎?),彙編指令的取指和執行是嚴格按照順序進行的,也就是說,彙編指令就是所見即所得的,彙編指令的邏輯被嚴格的被CPU執行。然而,随着計算機系統越來越複雜(多核、cache、superscalar、out-of-order),使用彙編指令這樣貼近處理器的語言也無法保證其被CPU執行的結果的一緻性,進而需要程式員(看,人還是最不可以替代的)告知CPU如何保證邏輯正确。

綜上所述,memory barrier是一種保證記憶體通路順序的一種方法,讓系統中的HW block(各個cpu、DMA controler、device等)對記憶體有一緻性的視角。

二、不使用memory barrier會導緻問題的場景

1、編譯器的優化

我們先看下面的一個例子:

preempt_disable()

臨界區

preempt_enable

有些共享資源可以通過禁止任務搶占來進行保護,是以臨界區代碼被preempt_disable和preempt_enable給保護起來。其實,我們知道所謂的preempt enable和disable其實就是對目前程序的struct thread_info中的preempt_count進行加一和減一的操作。具體的代碼如下:

#define preempt_disable() \

do { \

    preempt_count_inc(); \

    barrier(); \

} while (0)

linux

kernel中的定義和我們的想像一樣,除了barrier這個優化屏障。barrier就象是c代碼中的一個栅欄,将代碼邏輯分成兩段,barrier之前的代碼和barrier之後的代碼在經過編譯器編譯後順序不能亂掉。也就是說,barrier之後的c代碼對應的彙編,不能跑到barrier之前去,反之亦然。之是以這麼做是因為在我們這個場景中,如果編譯為了榨取CPU的performace而對彙編指令進行重排,那麼臨界區的代碼就有可能位于preempt_count_inc之外,進而起不到保護作用。

現在,我們知道了增加barrier的作用,問題來了,barrier是否夠呢?對于multi-core的系統,隻有當該task被排程到該CPU上執行的時候,該CPU才會通路該task的preempt count,是以對于preempt enable和disable而言,不存在多個CPU同時通路的場景。但是,即便這樣,如果CPU是亂序執行(out-of-order excution)的呢?其實,我們也不用擔心,正如前面叙述的,preempt count這個memory實際上是不存在多個cpu同時通路的情況,是以,它實際上會本cpu的程序上下文和中斷上下文通路。能終止目前thread執行preempt_disable的隻有中斷。為了友善描述,我們給代碼編址,如下:

Linux核心:memory barrier(上)

當發生中斷的時候,硬體會擷取目前PC值,并精确的得到了發生指令的位址。有兩種情況:

(1)在位址a發生中斷。對于out-of-order的CPU,臨界區指令1已經執行完畢,preempt_disable正在pipeline中等待執行。由于是在a位址發生中斷,也就是preempt_disable位址上發生中斷,對于硬體而言,它會保證a位址之前(包括a位址)的指令都被執行完畢,并且a位址之後的指令都沒有執行。是以,在這種情況下,臨界區指令1的執行結果被抛棄掉,是以,實際臨界區指令不會先于preempt_disable執行

(2)在位址a+4發生中斷。這時候,雖然發生中斷的那一刻的位址上的指令(臨界區指令1)已經執行完畢了,但是硬體會保證位址a+4之前的所有的指令都執行完畢,是以,實際上CPU會執行完preempt_disable,然後跳轉的中斷異常向量執行。

上面描述的是優化屏障在記憶體中的變量的應用,下面我們看看硬體寄存器的場景。一般而言,序列槽的驅動都會包括控制台部分的代碼,例如:

static struct console xx_serial_console = {undefined

……

    .write        = xx_serial_console_write,

};

如果系統enable了序列槽控制台,那麼當你的驅動調用printk的時候,實際上最終是通過console的write函數輸出到了序列槽控制台。而這個console write的函數可能會包含下面的代碼:

do {undefined

    擷取TX FIFO狀态寄存器

    barrier();

} while (TX FIFO沒有ready);

寫TX FIFO寄存器;

對于某些CPU archtecture而言(至少ARM是這樣的),外設硬體的IO位址也被映射到了一段記憶體位址空間,對編譯器而言,它并不知道這些位址空間是屬于外設的。是以,對于上面的代碼,如果沒有barrier的話,擷取TX FIFO狀态寄存器的指令可能和寫TX FIFO寄存器指令進行重新排序,在這種情況下,程式邏輯就不對了,因為我們必須要保證TX FIFO ready的情況下才能寫TX FIFO寄存器。

對于multi core的情況,上面的代碼邏輯也是OK的,因為在調用console write函數的時候,要擷取一個console semaphore,確定了隻有一個thread進入,是以,console write的代碼不會在多個CPU上并發。和preempt count的例子一樣,我們可以問同樣的問題,如果CPU是亂序執行(out-of-order excution)的呢?barrier隻是保證compiler輸出的彙編指令的順序是OK的,不能確定CPU執行時候的亂序。 對這個問題的回答來自ARM architecture的記憶體通路模型:對于program order是A1-->A2的情況(A1和A2都是對Device或是Strongly-ordered的memory進行通路的指令),ARM保證A1也是先于A2執行的。是以,在這樣的場景下,使用barrier足夠了。 對于X86也是類似的,雖然它沒有對IO space采樣memory mapping的方式,但是,X86的所有操作IO端口的指令都是被順執行的,不需要考慮memory access order。

2、cpu architecture和cache的組織

注:本章節的内容來自對Paul E. McKenney的Why memory barriers文檔了解,更細緻的内容可以參考該文檔。這個章節有些晦澀,需要一些耐心。作為一個c程式員,你可能會抱怨,為何設計CPU的硬體工程師不能屏蔽掉memory barrier的内容,讓c程式員關注在自己需要關注的程式邏輯上呢?本章可以展開叙述,或許能解決一些疑問。

(1)基本概念

The Memory Hierarchy

文檔中,我們已經了解了關于cache一些基礎的知識,一些基礎的内容,這裡就不再重複了。我們假設一個多核系統中的cache如下:

Linux核心:memory barrier(上)

我們先了解一下各個cpu cache line狀态的遷移過程:

(a)我們假設在有一個memory中的變量為多個CPU共享,那麼剛開始的時候,所有的CPU的本地cache中都沒有該變量的副本,所有的cacheline都是invalid狀态。

(b)是以當cpu 0 讀取該變量的時候發生cache miss(更具體的說叫做cold miss或者warmup miss)。當該值從memory中加載到chache 0中的cache line之後,該cache line的狀态被設定為shared,而其他的cache都是Invalid。

(c)當cpu 1 讀取該變量的時候,chache 1中的對應的cache line也變成shared狀态。其實shared狀态就是表示共享變量在一個或者多個cpu的cache中有副本存在。既然是被多個cache所共享,那麼其中一個CPU就不能武斷修改自己的cache而不通知其他CPU的cache,否則會有一緻性問題。

(d)總是read多沒勁,我們讓CPU n對共享變量來一個load and store的操作。這時候,CPU n發送一個read invalidate指令,加載了Cache n的cache line,并将狀态設定為exclusive,同時将所有其他CPU的cache對應的該共享變量的cacheline設定為invalid狀态。正因為如此,CPU n實際上是獨占了變量對應的cacheline(其他CPU的cacheline都是invalid了,系統中就這麼一個副本),就算是寫該變量,也不需要通知其他的CPU。CPU随後的寫操作将cacheline設定為modified狀态,表示cache中的資料已經dirty,和memory中的不一緻了。modified狀态和exclusive狀态都是獨占該cacheline,但是modified狀态下,cacheline的資料是dirty的,而exclusive狀态下,cacheline中的資料和memory中的資料是一緻的。當該cacheline被替換出cache的時候,modified狀态的cacheline需要write back到memory中,而exclusive狀态不需要。

(e)在cacheline沒有被替換出CPU n的cache之前,CPU 0再次讀該共享變量,這時候會怎麼樣呢?當然是cache miss了(因為之前由于CPU n寫的動作而導緻其他cpu的cache line變成了invalid,這種cache miss叫做communiction miss)。此外,由于CPU n的cache line是modified狀态,它必須響應這個讀得操作(memory中是dirty的)。是以,CPU 0的cacheline變成share狀态(在此之前,CPU n的cache line應該會發生write back動作,進而導緻其cacheline也是shared狀态)。當然,也可能是CPU n的cache line不發生write back動作而是變成invalid狀态,CPU 0的cacheline變成modified狀态,這和具體的硬體設計相關。

繼續閱讀