本系列:
點陣圖和位運算
除了各種鏈式和樹形資料結構,Linux核心還提供了點陣圖介面。點陣圖在Linux核心中大量使用。下面的原始碼檔案包含這些結構的通用介面:
除了這兩個檔案,還有一個特定的架構標頭檔案,對特定架構的位運算進行優化。對於x86_64架構,使用下面標頭檔案:
正如我前面提到的,點陣圖在Linux核心中大量使用。比如,點陣圖可以用來儲存系統線上/離線處理器,來支援CPU熱插拔;再比如,點陣圖在Linux核心等初始化過程中儲存已分配的中斷請求。
因此,本文重點分析點陣圖在Linux核心中的具體實現。
點陣圖宣告
點陣圖介面使用前,應當知曉Linux核心是如何宣告點陣圖的。一種簡單的點陣圖宣告方式,即unsigned long陣列。比如:
1 |
unsigned long my_bitmap[8] |
第二種方式,採用DECLARE_BITMAP巨集,此巨集位於標頭檔案include/linux/types.h中:
1 2 |
#define DECLARE_BITMAP(name,bits) unsigned long name[BITS_TO_LONGS(bits)] |
DECLARE_BITMAP巨集有兩個引數:
- name – 點陣圖名字;
- bits – 點陣圖中位元總數目
並且擴充套件元素大小為BITS_TO_LONGS(bits)、型別unsigned long的陣列,而BITS_TO_LONGS巨集將位轉換為long型別,或者說計算出bits中包含多少byte元素:
1 2 3 |
#define BITS_PER_BYTE 8 #define DIV_ROUND_UP(n,d) (((n) + (d) - 1) / (d)) #define BITS_TO_LONGS(nr) DIV_ROUND_UP(nr, BITS_PER_BYTE * sizeof(long)) |
例如:DECLARE_BITMAP(my_bitmap, 64)結果為:
1 2 |
>>> (((64) + (64) - 1) / (64)) 1 |
和:
1 |
unsigned long my_bitmap[1]; |
點陣圖宣告後,我們就可以使用它了。
特定架構的位運算
我們已經檢視了操作點陣圖介面的兩個原始碼檔案和一個標頭檔案。點陣圖最重要最廣泛的應用介面是特定架構,它位於標頭檔案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:
1 2 3 4 |
static inline void __set_bit(long nr, volatile unsigned long *addr) { asm volatile("bts %1,%0" : ADDR : "Ir" (nr) : "memory"); } |
它擁有兩個引數:
- nr – 點陣圖中位元數目
- addr – 點陣圖中某個位元需要設值的地址
注意引數addr定義為volatile,告訴編譯器此值或許被某個地址改變。而__set_bit容易實現。正如你所見,恰好它包含一行內聯彙編程式碼。本例中,使用指令bts選擇點陣圖中的某個位元值作為首個運算元,將已選擇位元值存入暫存器CF標籤中,並設定此位元。
此處可以看到nr的用法,那addr呢?或許你已猜到其中的奧祕就在ADDR中。而ADDR是定義在標頭檔案中的巨集,擴充套件字串,在該地址前面加入+m約束:
1 2 |
#define ADDR BITOP_ADDR(addr) #define BITOP_ADDR(x) "+m" (*(volatile long *) (x)) |
除了+m,我們可以看到__set_bit函式中其它約束。讓我們檢視這些約束,試著理解其中的含義:
+m
– 表示記憶體運算元,+表示此運算元為輸入和輸出運算元;- I – 表示整數常數;
- r -表示暫存器運算元
除了這些約束,還看到關鍵字memory,它會告知編譯器此程式碼會更改記憶體中的值。接下來,我們來看同樣功能,原子型別函式。它看起來要比非原子型別函式複雜得多:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
static __always_inline void set_bit(long nr, volatile unsigned long *addr) { if (IS_IMMEDIATE(nr)) { asm volatile(LOCK_PREFIX "orb %1,%0" : CONST_MASK_ADDR(nr, addr) : "iq" ((u8)CONST_MASK(nr)) : "memory"); } else { asm volatile(LOCK_PREFIX "bts %1,%0" : BITOP_ADDR(addr) : "Ir" (nr) : "memory"); } } |
注意它與函式__set_bit含有相同的引數列表,不同的是函式被標記有屬性__always_inline。__always_inline是定義在include/linux/compiler-gcc.h中的巨集,只是擴充套件了always_inline屬性:
1 |
#define __always_inline inline __attribute__((always_inline)) |
這意味著函式會被內聯以減少Linux核心映象的大小。接著,我們試著去理解函式set_bit實現。函式set_bit伊始,便對位元數目進行檢查。IS_IMMEDIATE是定義在相同標頭檔案中的巨集,用於擴充套件內建函式gcc:
1 |
#define IS_IMMEDIATE(nr) (__builtin_constant_p(nr)) |
內建函式__builtin_constant_p返回1的條件是此引數在編譯期為常數;否則返回0。無需使用指令bts設定位元值,因為編譯期位元數目為一常量。僅對已知位元組地址進行按位或運算,並對位元數目bits進行掩碼,使其高位為1,其它為0. 而位元數目在編譯期若非常量,函式__set_bit中運算亦相同。巨集CONST_MASK_ADDR:
1 |
#define CONST_MASK_ADDR(nr, addr) BITOP_ADDR((void *)(addr) + ((nr)>>3)) |
採用偏移量擴充套件某個地址為包含已知位元的位元組。比如地址0x1000,以及位元數目0x9。0x9等於一個位元組,加一個位元,地址為addr+1:
1 2 |
>>> hex(0x1000 + (0x9 >> 3)) '0x1001' |
巨集CONST_MASK表示看做位元組的某已知位元數目,高位為1,其它位元為0:
1 |
#define CONST_MASK(nr) (1 << ((nr) & 7)) |
1 2 |
>>> bin(1 << (0x9 & 7)) '0b10 |
最後,我們使用按位或運算。假設address為0x4097,需要設定ox9位元:
1 2 3 4 |
>>> bin(0x4097) '0b100000010010111' >>> bin((0x4097 >> 0x9) | (1 << (0x9 & 7))) '0b100010' |
第九個位元將被設定
注意所有的操作均標記有LOCK_PREFIX,即擴充套件為指令lock,確保運算以原子方式執行。
如我們所知,除了set_bit和__set_bit運算,Linux核心還提供了兩個逆向函式以原子或非原子方式清理位元,clear_bit和__clear_bit。這個兩個函式均定義在相同的標頭檔案中,並擁有相同的引數列表。當然不僅是引數相似,函式本身和set_bit以及 __set_bit都很相似。我們先來看非原子性函式__clear_bit
1 2 3 4 |
static inline void __clear_bit(long nr, volatile unsigned long *addr) { asm volatile("btr %1,%0" : ADDR : "Ir" (nr)); } |
正如我們所看到的,它們擁有相同引數列表,以及相似的內聯彙編函式塊。不同的是__clear_bit採用指令btr代替指令bts。從函式名我們可以看出,函式用來清除某個地址的某個位元值。指令btr與指令bts類似,選擇某個位元值作為首個運算元,將其值存入暫存器CF標籤中,並清除點陣圖中的這個位元值,且將點陣圖作為指令的第二個運算元。
__clear_bit的原子型別為clear_bit:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
static __always_inline void clear_bit(long nr, volatile unsigned long *addr) { if (IS_IMMEDIATE(nr)) { asm volatile(LOCK_PREFIX "andb %1,%0" : CONST_MASK_ADDR(nr, addr) : "iq" ((u8)~CONST_MASK(nr))); } else { asm volatile(LOCK_PREFIX "btr %1,%0" : BITOP_ADDR(addr) : "Ir" (nr)); } } |
正如我們所看到的,它和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:
1 2 3 4 |
#define test_bit(nr, addr) (__builtin_constant_p((nr)) ? constant_test_bit((nr), (addr)) : variable_test_bit((nr), (addr))) |
若nr在編譯期為常數,呼叫test_bit中函式constant_test_bit,否則呼叫函式variable_test_bit。我們來看這些函式實現,先從函式variable_test_bit開始:
1 2 3 4 5 6 7 8 9 10 11 |
static inline int variable_test_bit(long nr, volatile const unsigned long *addr) { int oldbit; asm volatile("bt %2,%1nt" "sbb %0,%0" : "=r" (oldbit) : "m" (*(unsigned long *)addr), "Ir" (nr)); return oldbit; } |
函式variable_test_bit擁有set_bit等函式相似的引數列表。同樣,我們看到內聯彙編程式碼,執行指令bt、sbb。指令bt或bit test,從點陣圖中選擇某個位元值作為首個運算元,而點陣圖作為第二個運算元,並將選定的位元值存入暫存器CF標籤中。而指令sbb則會將首個運算元從第二個運算元中移除,並移除CF標籤值。將點陣圖某個位元值寫入CF標籤暫存器,執行指令sbb,計算CF為00000000 ,最後將結果寫入oldbit。
函式constant_test_bit與set_bit相似:
1 2 3 4 5 |
static __always_inline int constant_test_bit(long nr, const volatile unsigned long *addr) { return ((1UL << (nr & (BITS_PER_LONG-1))) & (addr[nr >> _BITOPS_LONG_SHIFT])) != 0; } |
它能夠產生一個位元組,其高位時1,其它位元為0,對這個包含位元數目的位元組做按位與運算。
接下來比較廣泛的點陣圖運算是,點陣圖中的位元值的改變運算。為此,Linux核心提供兩個幫助函式:
__change_bit
;change_bit
.
或許你已能猜到,與set_bit和 __set_bit相似,存在兩個型別,原子型別和非原子型別。我們先來看函式__change_bit的實現:
1 2 3 4 |
static inline void __change_bit(long nr, volatile unsigned long *addr) { asm volatile("btc %1,%0" : ADDR : "Ir" (nr)); } |
很容易,難道不是嗎?__change_bit與__set_bit擁有相似的實現,不同的是,前者採用的指令btc而非bts。指令選擇點陣圖中的某個位元值,然後將此值放入CF中,然後使用補位運算改變其值。若位元值為1則改變後的值為0,反之亦然:
1 2 3 4 |
>>> int(not 1) 0 >>> int(not 0) 1 |
函式__change_bit的原子版本為函式change_bit:
1 2 3 4 5 6 7 8 9 10 11 12 |
static inline void change_bit(long nr, volatile unsigned long *addr) { if (IS_IMMEDIATE(nr)) { asm volatile(LOCK_PREFIX "xorb %1,%0" : CONST_MASK_ADDR(nr, addr) : "iq" ((u8)CONST_MASK(nr))); } else { asm volatile(LOCK_PREFIX "btc %1,%0" : BITOP_ADDR(addr) : "Ir" (nr)); } } |
與函式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的實現:
1 2 3 4 |
#define for_each_set_bit(bit, addr, size) for ((bit) = find_first_bit((addr), (size)); (bit) < (size); (bit) = find_next_bit((addr), (size), (bit) + 1)) |
正如大家所看到的,此巨集擁有三個引數,以及迴圈從set集合第一個位元開始,到最後一個位元結束,迭代位元數目小於最後一個size,迴圈最後返回函式find_first_bit。
除了這四個巨集,arch/x86/include/asm/bitops.h還提供了64位或32位等值的迭代。
同樣,標頭檔案也提供了點陣圖的其它介面。比如下面的這兩個函式:
bitmap_zero
;bitmap_fill
.
清除點陣圖,併為其填值1 。我們來看函式bitmap_zero實現:
1 2 3 4 5 6 7 8 9 |
static inline void bitmap_zero(unsigned long *dst, unsigned int nbits) { if (small_const_nbits(nbits)) *dst = 0UL; else { unsigned int len = BITS_TO_LONGS(nbits) * sizeof(unsigned long); memset(dst, 0, len); } } |
同樣,先檢查nbits,函式small_const_nbits定義在相同標頭檔案中的巨集,具體如下:
1 2 |
#define small_const_nbits(nbits) (__builtin_constant_p(nbits) && (nbits) <= BITS_PER_LONG) |
正如大家所見,檢查nbits在編譯期是否為一常量,nbits值是否超過BITS_PER_LONG或64 。倘若bits的數目沒有超出long型別的總量,將其設定為0 。否則,需計算多少個long型別值填入點陣圖中,當然我們藉助memset填入。
函式bitmap_fill的實現與bitmap_zero相似,不同的是點陣圖的填值為0xff或0b11111111:
1 2 3 4 5 6 7 8 9 |
static inline void bitmap_fill(unsigned long *dst, unsigned int nbits) { unsigned int nlongs = BITS_TO_LONGS(nbits); if (!small_const_nbits(nbits)) { unsigned int len = (nlongs - 1) * sizeof(unsigned long); memset(dst, 0xff, len); } dst[nlongs - 1] = BITMAP_LAST_WORD_MASK(nbits); } |
除了函式bitmap_fill和bitmap_zero,標頭檔案include/linux/bitmap.h還提供了函式bitmap_copy,它與bitmap_zero相似,不一樣的是使用memcpy而非memset。與此同時,也提供了諸如bitmap_and、bitmap_or, bitamp_xor等函式進行按位運算。考慮到這些函式實現容易理解,在此我們就不做說明;對這些函式感興趣的讀者朋友們,請開啟標頭檔案include/linux/bitmap.h進行研究。
就寫到這裡。
連結
- bitmap
- linked data structures
- tree data structures
- hot-plug
- cpumasks
- IRQs
- API
- atomic operations
- xchg instruction
- cmpxchg instruction
- lock instruction
- bts instruction
- btr instruction
- bt instruction
- sbb instruction
- btc instruction
- man memcpy
- man memset
- CF
- inline assembler
- gcc
打賞支援我翻譯更多好文章,謝謝!
打賞譯者
打賞支援我翻譯更多好文章,謝謝!
任選一種支付方式