Linux 核心資料結構:點陣圖(Bitmap)

喬永琪發表於2016-12-10

本系列:

點陣圖和位運算

除了各種鏈式和樹形資料結構,Linux核心還提供了點陣圖介面。點陣圖在Linux核心中大量使用。下面的原始碼檔案包含這些結構的通用介面:

除了這兩個檔案,還有一個特定的架構標頭檔案,對特定架構的位運算進行優化。對於x86_64架構,使用下面標頭檔案:

正如我前面提到的,點陣圖在Linux核心中大量使用。比如,點陣圖可以用來儲存系統線上/離線處理器,來支援CPU熱插拔;再比如,點陣圖在Linux核心等初始化過程中儲存已分配的中斷請求

因此,本文重點分析點陣圖在Linux核心中的具體實現。

點陣圖宣告

點陣圖介面使用前,應當知曉Linux核心是如何宣告點陣圖的。一種簡單的點陣圖宣告方式,即unsigned long陣列。比如:

第二種方式,採用DECLARE_BITMAP巨集,此巨集位於標頭檔案include/linux/types.h中:

DECLARE_BITMAP巨集有兩個引數:

  • name – 點陣圖名字;
  • bits – 點陣圖中位元總數目

並且擴充套件元素大小為BITS_TO_LONGS(bits)、型別unsigned long的陣列,而BITS_TO_LONGS巨集將位轉換為long型別,或者說計算出bits中包含多少byte元素:

例如:DECLARE_BITMAP(my_bitmap, 64)結果為:

和:

點陣圖宣告後,我們就可以使用它了。

特定架構的位運算

我們已經檢視了操作點陣圖介面的兩個原始碼檔案和一個標頭檔案。點陣圖最重要最廣泛的應用介面是特定架構,它位於標頭檔案arch/x86/include/asm/bitops.h

首先,我們來看兩個重要的函式:

  • set_bit;
  • clear_bit.

我認為沒有必要介紹這些函式是做什麼的,通過函式名就可以知曉。我們來看函式的實現。進入標頭檔案arch/x86/include/asm/bitops.h,你會注意到每個函式分兩種型別:原子型別和非原子型別。在深入這些函式實現前,我們需要先了解一些原子性運算。

一言以蔽之,原子性操作保障,位於同一資料上的兩個甚至多個運算,不能併發執行。x86架構提供一組原子性指令,如指令xchg、指令cmpxchg。除了原子性指令,一些非原子性指令可藉助指令lock進行原子性運算。目前我們瞭解這些原子性運算就足夠了,接下來可以開始考慮set_bit和clear_bit函式。

先從非原子性型別的函式開始,非原子性set_bit和clear_bit函式名始於雙下劃線。正如你所瞭解的,所有的函式定義在標頭檔案arch/x86/include/asm/bitops.h中,第一個函式__set_bit:

它擁有兩個引數:

  • nr –  點陣圖中位元數目
  • addr –  點陣圖中某個位元需要設值的地址

注意引數addr定義為volatile,告訴編譯器此值或許被某個地址改變。而__set_bit容易實現。正如你所見,恰好它包含一行內聯彙編程式碼。本例中,使用指令bts選擇點陣圖中的某個位元值作為首個運算元,將已選擇位元值存入暫存器CF標籤中,並設定此位元。

此處可以看到nr的用法,那addr呢?或許你已猜到其中的奧祕就在ADDR中。而ADDR是定義在標頭檔案中的巨集,擴充套件字串,在該地址前面加入+m約束:

除了+m,我們可以看到__set_bit函式中其它約束。讓我們檢視這些約束,試著理解其中的含義:

  • +m – 表示記憶體運算元,+表示此運算元為輸入和輸出運算元;
  • I – 表示整數常數;
  • r -表示暫存器運算元

除了這些約束,還看到關鍵字memory,它會告知編譯器此程式碼會更改記憶體中的值。接下來,我們來看同樣功能,原子型別函式。它看起來要比非原子型別函式複雜得多:

注意它與函式__set_bit含有相同的引數列表,不同的是函式被標記有屬性__always_inline。__always_inline是定義在include/linux/compiler-gcc.h中的巨集,只是擴充套件了always_inline屬性:

這意味著函式會被內聯以減少Linux核心映象的大小。接著,我們試著去理解函式set_bit實現。函式set_bit伊始,便對位元數目進行檢查。IS_IMMEDIATE是定義在相同標頭檔案中的巨集,用於擴充套件內建函式gcc

內建函式__builtin_constant_p返回1的條件是此引數在編譯期為常數;否則返回0。無需使用指令bts設定位元值,因為編譯期位元數目為一常量。僅對已知位元組地址進行按位或運算,並對位元數目bits進行掩碼,使其高位為1,其它為0. 而位元數目在編譯期若非常量,函式__set_bit中運算亦相同。巨集CONST_MASK_ADDR:

採用偏移量擴充套件某個地址為包含已知位元的位元組。比如地址0x1000,以及位元數目0x9。0x9等於一個位元組,加一個位元,地址為addr+1:

巨集CONST_MASK表示看做位元組的某已知位元數目,高位為1,其它位元為0:

最後,我們使用按位或運算。假設address為0x4097,需要設定ox9位元:

第九個位元將被設定

注意所有的操作均標記有LOCK_PREFIX,即擴充套件為指令lock,確保運算以原子方式執行。

如我們所知,除了set_bit和__set_bit運算,Linux核心還提供了兩個逆向函式以原子或非原子方式清理位元,clear_bit和__clear_bit。這個兩個函式均定義在相同的標頭檔案中,並擁有相同的引數列表。當然不僅是引數相似,函式本身和set_bit以及 __set_bit都很相似。我們先來看非原子性函式__clear_bit

正如我們所看到的,它們擁有相同引數列表,以及相似的內聯彙編函式塊。不同的是__clear_bit採用指令btr代替指令bts。從函式名我們可以看出,函式用來清除某個地址的某個位元值。指令btr與指令bts類似,選擇某個位元值作為首個運算元,將其值存入暫存器CF標籤中,並清除點陣圖中的這個位元值,且將點陣圖作為指令的第二個運算元。

__clear_bit的原子型別為clear_bit:

正如我們所看到的,它和set_bit相似,僅有兩處不同。第一個不同,使用指令btr進行位元清理,而set_bit使用指令bts位元儲存。第二個不同,使用消除掩碼以及指令and清理某個byte中的bit值,而set_bit使用指令or。

到目前為止,我們可以給任何點陣圖設值清除或位掩碼運算。

點陣圖最常用的運算為Linux核心中點陣圖的設值以及位元值的清除。除了這些運算外,為點陣圖新增額外的運算也是有必要的。Linux核心中,另一個廣泛的運算是判定點陣圖是否已設定位元值。可藉助test_bit巨集進行判定,此巨集定義在標頭檔案arch/x86/include/asm/bitops.h中,並依據位元數目,選擇呼叫constant_test_bit 或 variable_test_bit:

若nr在編譯期為常數,呼叫test_bit中函式constant_test_bit,否則呼叫函式variable_test_bit。我們來看這些函式實現,先從函式variable_test_bit開始:

函式variable_test_bit擁有set_bit等函式相似的引數列表。同樣,我們看到內聯彙編程式碼,執行指令btsbb。指令bt或bit test,從點陣圖中選擇某個位元值作為首個運算元,而點陣圖作為第二個運算元,並將選定的位元值存入暫存器CF標籤中。而指令sbb則會將首個運算元從第二個運算元中移除,並移除CF標籤值。將點陣圖某個位元值寫入CF標籤暫存器,執行指令sbb,計算CF為00000000 ,最後將結果寫入oldbit。

函式constant_test_bit與set_bit相似:

它能夠產生一個位元組,其高位時1,其它位元為0,對這個包含位元數目的位元組做按位與運算。

接下來比較廣泛的點陣圖運算是,點陣圖中的位元值的改變運算。為此,Linux核心提供兩個幫助函式:

  • __change_bit;
  • change_bit.

或許你已能猜到,與set_bit和 __set_bit相似,存在兩個型別,原子型別和非原子型別。我們先來看函式__change_bit的實現:

很容易,難道不是嗎?__change_bit與__set_bit擁有相似的實現,不同的是,前者採用的指令btc而非bts。指令選擇點陣圖中的某個位元值,然後將此值放入CF中,然後使用補位運算改變其值。若位元值為1則改變後的值為0,反之亦然:

函式__change_bit的原子版本為函式change_bit:

與函式set_bit相似,但有兩處不同。第一處不同的是xor運算而非or;第二處不同的是btc而非bts。

至此,我們瞭解了最重要的點陣圖架構相關運算,接下來我們來檢視通用點陣圖介面。

通用位元運算

除了來自標頭檔案arch/x86/include/asm/bitops.h的特定架構介面,Linux核心還提供了點陣圖的通用介面。從前文就已瞭解,標頭檔案include/linux/bitmap.h,以及* lib/bitmap.c原始碼檔案。不過在檢視原始碼檔案之前,我們先來看標頭檔案include/linux/bitops.h,它提供了一組有用的巨集。我們來看其中的一些:

先看下面四個巨集:

  • for_each_set_bit
  • for_each_set_bit_from
  • for_each_clear_bit
  • for_each_clear_bit_from

這些巨集提供了點陣圖迭代器,首個巨集迭代集合set,第二個巨集也是,不過從集合指定的位元處開始。後面兩個巨集也是如此,不同的是迭代清空的位元。我們先來看巨集for_each_set_bit的實現:

正如大家所看到的,此巨集擁有三個引數,以及迴圈從set集合第一個位元開始,到最後一個位元結束,迭代位元數目小於最後一個size,迴圈最後返回函式find_first_bit。

除了這四個巨集,arch/x86/include/asm/bitops.h還提供了64位或32位等值的迭代。

同樣,標頭檔案也提供了點陣圖的其它介面。比如下面的這兩個函式:

  • bitmap_zero;
  • bitmap_fill.

清除點陣圖,併為其填值1 。我們來看函式bitmap_zero實現:

同樣,先檢查nbits,函式small_const_nbits定義在相同標頭檔案中的巨集,具體如下:

正如大家所見,檢查nbits在編譯期是否為一常量,nbits值是否超過BITS_PER_LONG或64 。倘若bits的數目沒有超出long型別的總量,將其設定為0 。否則,需計算多少個long型別值填入點陣圖中,當然我們藉助memset填入。

函式bitmap_fill的實現與bitmap_zero相似,不同的是點陣圖的填值為0xff或0b11111111:

除了函式bitmap_fill和bitmap_zero,標頭檔案include/linux/bitmap.h還提供了函式bitmap_copy,它與bitmap_zero相似,不一樣的是使用memcpy而非memset。與此同時,也提供了諸如bitmap_andbitmap_or, bitamp_xor等函式進行按位運算。考慮到這些函式實現容易理解,在此我們就不做說明;對這些函式感興趣的讀者朋友們,請開啟標頭檔案include/linux/bitmap.h進行研究。

就寫到這裡。

連結

打賞支援我翻譯更多好文章,謝謝!

打賞譯者

打賞支援我翻譯更多好文章,謝謝!

任選一種支付方式

Linux 核心資料結構:點陣圖(Bitmap) Linux 核心資料結構:點陣圖(Bitmap)

相關文章