除了不一样的基于链式和树的数据结构之外,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