除了不一樣的基於鏈式和樹的數據結構之外,Linux 內核也爲位數組或位圖
提供了 API。位數組在 Linux 內核裏被普遍使用,而且在如下的源代碼文件中包含了與這樣的結構搭配使用的通用 API
:python
除了這兩個文件以外,還有體系結構特定的頭文件,它們爲特定的體系結構提供優化的位操做。咱們將探討 x86_64 體系結構,所以在咱們的例子裏,它會是linux
頭文件。正如我上面所寫的,位圖
在 Linux 內核中被普遍地使用。例如,位數組
經常用於保存一組在線/離線處理器,以便系統支持熱插拔的 CPU(你能夠在 cpumasks 部分閱讀更多相關知識 ),一個位數組
能夠在 Linux 內核初始化等期間保存一組已分配的中斷處理。git
所以,本部分的主要目的是瞭解位數組是如何在 Linux 內核中實現的。讓咱們如今開始吧。github
在咱們開始查看位圖操做的 API
以前,咱們必須知道如何在 Linux 內核中聲明它。有兩中通用的方法聲明位數組。第一種簡單的聲明一個位數組的方法是,定義一個 unsigned long 的數組,例如:數組
unsigned long my_bitmap[8]
第二種方法,是使用 DECLARE_BITMAP
宏,它定義於 include/linux/types.h 頭文件:數據結構
#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
宏將一個給定的位數轉換爲 longs
的個數,換言之,就是計算 bits
中有多少個 8
字節元素:ide
#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)
將產生:函數
>>> (((64) + (64) - 1) / (64)) 1
與:
unsigned long my_bitmap[1];
在可以聲明一個位數組以後,咱們即可以使用它了。
咱們已經看了以上一對源文件和頭文件,它們提供了位數組操做的 API。其中重要且普遍使用的位數組 API 是體系結構特定的且位於已說起的頭文件中 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
:
static inline void __set_bit(long nr, volatile unsigned long *addr) { asm volatile("bts %1,%0" : ADDR : "Ir" (nr) : "memory"); }
正如咱們所看到的,它使用了兩個參數:
nr
- 位數組中的位號(從0開始,譯者注)addr
- 咱們須要置位的位數組地址注意,addr
參數使用 volatile
關鍵字定義,以告訴編譯器給定地址指向的變量可能會被修改。 __set_bit
的實現至關簡單。正如咱們所看到的,它僅包含一行內聯彙編代碼。在咱們的例子中,咱們使用 bts 指令,從位數組中選出一個第一操做數(咱們的例子中的 nr
),存儲選出的位的值到 CF 標誌寄存器並設置該位(即 nr
指定的位置爲1,譯者注)。
注意,咱們瞭解了 nr
的用法,但這裏還有一個參數 addr
呢!你或許已經猜到祕密就在 ADDR
。 ADDR
是一個定義在同一頭文件的宏,它展開爲一個包含給定地址和 +m
約束的字符串:
#define ADDR BITOP_ADDR(addr) #define BITOP_ADDR(x) "+m" (*(volatile long *) (x))
除了 +m
以外,在 __set_bit
函數中咱們能夠看到其餘約束。讓咱們查看並試圖理解它們所表示的意義:
+m
- 表示內存操做數,這裏的 +
代表給定的操做數爲輸入輸出操做數;I
- 表示整型常量;r
- 表示寄存器操做數除了這些約束以外,咱們也能看到 memory
關鍵字,其告訴編譯器這段代碼會修改內存中的變量。到此爲止,如今咱們看看相同的原子性變體函數。它看起來比非原子性變體更加複雜:
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"); } }
(BITOP_ADDR 的定義爲:#define BITOP_ADDR(x) "=m" (*(volatile long *) (x))
,ORB 爲字節按位或,譯者注)
首先注意,這個函數使用了與 __set_bit
相同的參數集合,但額外地使用了 __always_inline
屬性標記。 __always_inline
是一個定義於 include/linux/compiler-gcc.h 的宏,而且只是展開爲 always_inline
屬性:
#define __always_inline inline __attribute__((always_inline))
其意味着這個函數老是內聯的,以減小 Linux 內核映像的大小。如今咱們試着瞭解 set_bit
函數的實現。首先咱們在 set_bit
函數的開頭檢查給定的位數量。IS_IMMEDIATE
宏定義於相同頭文件,並展開爲 gcc 內置函數的調用:
#define IS_IMMEDIATE(nr) (__builtin_constant_p(nr))
若是給定的參數是編譯期已知的常量,__builtin_constant_p
內置函數則返回 1
,其餘狀況返回 0
。倘若給定的位數是編譯期已知的常量,咱們便無須使用效率低下的 bts
指令去設置位。咱們能夠只需在給定地址指向的字節和和掩碼上執行 按位或 操做,其字節包含給定的位,而掩碼爲位號高位 1
,其餘位爲 0。在其餘狀況下,若是給定的位號不是編譯期已知常量,咱們便作和 __set_bit
函數同樣的事。CONST_MASK_ADDR
宏:
#define CONST_MASK_ADDR(nr, addr) BITOP_ADDR((void *)(addr) + ((nr)>>3))
展開爲帶有到包含給定位的字節偏移的給定地址,例如,咱們擁有地址 0x1000
和 位號是 0x9
。由於 0x9
是 一個字節 + 一位
,因此咱們的地址是 addr + 1
:
>>> hex(0x1000 + (0x9 >> 3)) '0x1001'
CONST_MASK
宏將咱們給定的位號表示爲字節,位號對應位爲高位 1
,其餘位爲 0
:
#define CONST_MASK(nr) (1 << ((nr) & 7))
>>> bin(1 << (0x9 & 7)) '0b10'
最後,咱們應用 按位或
運算到這些變量上面,所以,假如咱們的地址是 0x4097
,而且咱們須要置位號爲 9
的位 爲 1:
>>> bin(0x4097) '0b100000010010111' >>> bin((0x4097 >> 0x9) | (1 << (0x9 & 7))) '0b100010'
第 9 位
將會被置位。(這裏的 9 是從 0 開始計數的,好比0010,按照做者的意思,其中的 1 是第 1 位,譯者注)
注意,全部這些操做使用 LOCK_PREFIX
標記,其展開爲 lock 指令,保證該操做的原子性。
正如咱們所知,除了 set_bit
和 __set_bit
操做以外,Linux 內核還提供了兩個功能相反的函數,在原子性和非原子性的上下文中清位。它們爲 clear_bit
和 __clear_bit
。這兩個函數都定義於同一個頭文件 而且使用相同的參數集合。不只參數類似,通常而言,這些函數與 set_bit
和 __set_bit
也很是類似。讓咱們查看非原子性 __clear_bit
的實現吧:
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
(原文這裏爲 btr,可能爲筆誤,修正爲 bts,譯者注)。該指令選出第一操做數指定的位,存儲它的值到 CF
標誌寄存器,而且清楚第二操做數指定的位數組中的對應位。
__clear_bit
的原子性變體爲 clear_bit
:
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
很是類似,同時只包含了兩處差別。第一處差別爲 clear_bit
使用 btr
指令來清位,而 set_bit
使用 bts
指令來置位。第二處差別爲 clear_bit
使用否認的位掩碼和 按位與
在給定的字節上置位,而 set_bit
使用 按位或
指令。
到此爲止,咱們能夠在任何位數組置位和清位了,而且可以轉到位掩碼上的其餘操做。
在 Linux 內核位數組上最普遍使用的操做是設置和清除位,可是除了這兩個操做外,位數組上其餘操做也是很是有用的。Linux 內核裏另外一種普遍使用的操做是知曉位數組中一個給定的位是否被置位。咱們可以經過 test_bit
宏的幫助實現這一功能。這個宏定義於 arch/x86/include/asm/bitops.h 頭文件,並展開爲 constant_test_bit
或 variable_test_bit
的調用,這要取決於位號。
#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
開始看起:
static inline int variable_test_bit(long nr, volatile const unsigned long *addr) { int oldbit; asm volatile("bt %2,%1\n\t" "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
指令計算: 00000000 - CF
,並將結果寫進 oldbit
變量。
constant_test_bit
函數作了和咱們在 set_bit
所看到的同樣的事:
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
的字節(正如咱們在 CONST_MASK
所看到的),並將 按位與 應用於包含給定位號的字節。
下一普遍使用的位數組相關操做是改變一個位數組中的位。爲此,Linux 內核提供了兩個輔助函數:
__change_bit
;change_bit
.你可能已經猜想到,就拿 set_bit
和 __set_bit
例子說,這兩個變體分別是原子和非原子版本。首先,讓咱們看看 __change_bit
函數的實現:
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
,反之亦然:
>>> int(not 1) 0 >>> int(not 0) 1
__change_bit
的原子版本爲 change_bit
函數:
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
,爲做者筆誤,譯者注) 而不是 bts
。
目前,咱們瞭解了最重要的體系特定的位數組操做,是時候看看通常的位圖 API 了。
除了 arch/x86/include/asm/bitops.h 中體系特定的 API 外,Linux 內核提供了操做位數組的通用 API。正如咱們本部分開頭所瞭解的同樣,咱們能夠在 include/linux/bitmap.h 頭文件和* lib/bitmap.c 源文件中找到它。但在查看這些源文件以前,咱們先看看 include/linux/bitops.h 頭文件,其提供了一系列有用的宏,讓咱們看看它們當中一部分。
首先咱們看看如下 4 個 宏:
for_each_set_bit
for_each_set_bit_from
for_each_clear_bit
for_each_clear_bit_from
全部這些宏都提供了遍歷位數組中某些位集合的迭代器。第一個宏迭代那些被置位的位。第二個宏也是同樣,但它是從某一肯定位開始。最後兩個宏作的同樣,可是迭代那些被清位的位。讓咱們看看 for_each_set_bit
宏:
#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))
正如咱們所看到的,它使用了三個參數,並展開爲一個循環,該循環從做爲 find_first_bit
函數返回結果的第一個置位開始到最後一個置位且小於給定大小爲止。
除了這四個宏, arch/x86/include/asm/bitops.h 也提供了 64-bit
或 32-bit
變量循環的 API 等等。
下一個 頭文件 提供了操做位數組的 API。例如,它提供瞭如下兩個函數:
bitmap_zero
;bitmap_fill
.它們分別能夠清除一個位數組和用 1
填充位數組。讓咱們看看 bitmap_zero
函數的實現:
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
是一個定義在同一頭文件 的宏:
#define small_const_nbits(nbits) \ (__builtin_constant_p(nbits) && (nbits) <= BITS_PER_LONG)
正如咱們能夠看到的,它檢查 nbits
是否爲編譯期已知常量,而且其值不超過 BITS_PER_LONG
或 64
。若是位數目沒有超過一個 long
變量的位數,咱們能夠僅僅設置爲 0。在其餘狀況,咱們須要計算有多少個須要填充位數組的 long
變量而且使用 memset 進行填充。
bitmap_fill
函數的實現和 biramp_zero
函數很類似,除了咱們須要在給定的位數組中填寫 0xff
或 0b11111111
:
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_zero
很類似的 bitmap_copy
,只是僅僅使用 memcpy 而不是 memset 這點差別而已。它也提供了位數組的按位操做,像 bitmap_and
, bitmap_or
, bitamp_xor
等等。咱們不會探討這些函數的實現了,由於若是你理解了本部分的全部內容,這些函數的實現是很容易理解的。不管如何,若是你對這些函數是如何實現的感興趣,你能夠打開並研究 include/linux/bitmap.h 頭文件。
本部分到此爲止。
via: https://github.com/0xAX/linux-insides/blob/master/DataStructures/bitmap.md