[翻譯] Linux 內核中的位數組和位操做

Linux 內核裏的數據結構

原文連接與說明

  1. https://github.com/0xAX/linux-insides/blob/master/DataStructures/bitmap.md
  2. 本翻譯文檔原文選題自 Linux中國 ,翻譯文檔版權歸屬 Linux中國 全部

Linux 內核中的位數組和位操做

除了不一樣的基於鏈式的數據結構之外,Linux 內核也爲位數組位圖提供了 API。位數組在 Linux 內核裏被普遍使用,而且在如下的源代碼文件中包含了與這樣的結構搭配使用的通用 APIpython

除了這兩個文件以外,還有體系結構特定的頭文件,它們爲特定的體系結構提供優化的位操做。咱們將探討 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 體系結構提供了一系列原子指令,例如, xchgcmpxchg 等指令。除了原子指令,一些非原子指令能夠在 lock 指令的幫助下具備原子性。目前已經對原子操做有了充分的理解,咱們能夠接着探討 set_bitclear_bit 函數的實現。

咱們先考慮函數的非原子性變體。非原子性的 set_bitclear_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 呢!你或許已經猜到祕密就在 ADDRADDR 是一個定義在同一頭文件的宏,它展開爲一個包含給定地址和 +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_bitvariable_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 及其餘函數使用的類似的參數集合。咱們也能夠看到執行 btsbb 指令的內聯彙編代碼。btbit 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-bit32-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_LONG64。若是位數目沒有超過一個 long 變量的位數,咱們能夠僅僅設置爲 0。在其餘狀況,咱們須要計算有多少個須要填充位數組的 long 變量而且使用 memset 進行填充。

bitmap_fill 函數的實現和 biramp_zero 函數很類似,除了咱們須要在給定的位數組中填寫 0xff0b11111111

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_fillbitmap_zeroinclude/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

相關文章
相關標籤/搜索