Linux Kernel:内核数据结构之位图(Bitmap)

756 阅读41分钟

本文采用 Linux 内核 v3.10 版本 x86_64架构

位图(Bitmap)在 Linux 内核中使用非常广泛,比如用来标识中断是否已安装处理程序(used_vectors)、处理器是否在线(cpumask)等等。内核中,位图相关的接口及实现主要在以下几个文件中:

  • include/linux/bitmap.h

  • lib/bitmap.c

  • lib/find_next_bit.c

  • include/linux/bitops.h

  • arch/x86/include/asm/bitops.h

其中头文件 arch/x86/include/asm/bitops.h 中,保存的是特定于 x86-64 架构的位图操作。

一、声明位图

使用宏 DECLARE_BITMAP 来声明位图,该宏定义如下:

// file: include/linux/types.h
#define DECLARE_BITMAP(name,bits) \
	unsigned long name[BITS_TO_LONGS(bits)]

该宏接收 2 个参数:

  • name - 位图名称
  • bits - 位图中比特位的数量

由于我们使用 unsigned long 数组来表示位图,所以需要将比特位数量转换成数组的元素数量。宏 BITS_TO_LONGS 用来实现对应的转换,其定义如下:

 // file: include/linux/bitops.h
 #define BITS_TO_LONGS(nr)   DIV_ROUND_UP(nr, BITS_PER_BYTE * sizeof(long))

BITS_TO_LONGS 中又引用了宏 DIV_ROUND_UPBITS_PER_BYTE

BITS_PER_BYTE扩展为 8,表示每个字节包含 8 个比特位:

 // file: include/linux/bitops.h
 #define BITS_PER_BYTE       8

BITS_PER_BYTE * sizeof(long) 计算 long 类型数据占用的比特位数量。

DIV_ROUND_UP 用来把整数 n 向上圆整到 d 的倍数,其实现方式就是把 n 加上 d-1 后再除以 d,该宏定义如下:

 // file: include/linux/kernel.h
 #define DIV_ROUND_UP(n,d) (((n) + (d) - 1) / (d))

在我们的案例中,就是将比特位数量向上圆整到unsigned long类型的整数倍,保证数组有足够容量来保存指定数量的比特位。

二、相关汇编指令

2.1 bt 指令

bt (Bit Test)指令选择位图中指定的位,并将该位的值保存到 EFLAGS 寄存器的 CF 标志位。具体用法可参考 Intel 64 and IA-32 Architectures Software Developer Manuals Volume 2A, Chapter 3 中的 BT 指令。

2.2 bts 指令

bts (Bit Test and Set)指令会将位图中指定偏移处的比特位设置为 1,并把原比特位的值保存到 EFLAGS 寄存器的 CF 标志位中。具体用法可参考 Intel 64 and IA-32 Architectures Software Developer Manuals Volume 2A, Chapter 3 中的 BTS 指令。

2.3 btr 指令

btr (Bit Test and Reset)指令会将位图中指定偏移处的比特位设置为 0,并把原比特位的值保存到 EFLAGS 寄存器的 CF 标志位中。具体用法可参考 Intel 64 and IA-32 Architectures Software Developer Manuals Volume 2A, Chapter 3 中的 BTR 指令。

2.4 btc 指令

btc (Bit Test and Complement)指令会将位图中指定偏移处的比特位取反(求补),并把原比特位的值保存到 EFLAGS 寄存器的 CF 标志位中。具体用法可参考 Intel 64 and IA-32 Architectures Software Developer Manuals Volume 2A, Chapter 3 中的 BTC 指令。

2.5 bsf 指令

bsf (Bit Scan Forward)指令有 2 个操作数。会从源操作数中搜索第一个为 1 的位。如果找到,会把该位的索引保存到目标操作数中。如果未找到,那么目的操作数的内容是未定义的。源操作数可以是寄存器或内存操作数;目标操作数必须是寄存器。具体用法可参考 Intel 64 and IA-32 Architectures Software Developer Manuals Volume 2A, Chapter 3 中的 BSF 指令。

2.6 lock 前缀

Intel 文档对 lock 前缀指令说明如下:

Causes the processor’s LOCK# signal to be asserted during execution of the accompanying instruction (turns the instruction into an atomic instruction). In a multiprocessor environment, the LOCK# signal ensures that the processor has exclusive use of any shared memory while the signalis asserted.

The LOCK prefix can be prepended only to the following instructions and only to those forms of the instructions where the destination operand is a memory operand: ADD, ADC, AND, BTC, BTR, BTS, CMPXCHG, CMPXCH8B, CMPXCHG16B, DEC, INC, NEG, NOT, OR, SBB, SUB, XOR, XADD, and XCHG.

The XCHG instruction always asserts the LOCK# signal regardless of the presence or absence of the LOCK prefix.

The LOCK prefix is typically used with the BTS instruction to perform a read-modify-write operation on a memory location in shared memory environment.

简单翻译下:

在多处理器系统中,lock 前缀可以用来对共享内存进行独占访问。

lock 前缀只能放在 ADDADCANDBTCBTRBTSCMPXCHGCMPXCH8BCMPXCHG16BDECINCNEGNOTORSBBSUBXORXADDXCHG 指令前面。

对于 XCHG 指令来说,不论 lock 前缀是否存在,都会触发 lock 信号。

lock 前缀的典型使用,就是用在像 BTS 这种对共享内存进行 “读-修改-写” 操作的指令上。

2.7 sbb 指令

sbb(Integer Subtraction With Borrow)指令,即借位减法指令。该指令会把源操作数和 CF 标志位相加,然后从目标操作数中减去相加后的值,将减法计算的结果保存到目标寄存器。

三、相关接口

3.1 接口列表

下表中列出本文中涉及到接口,完整接口列表请参考 include/linux/bitmap.h 文件。

序号函数说明
1__set_bit(bit, addr)位设置,非原子,`*addrbit`
2set_bit(bit, addr)位设置,原子,`*addrbit`
3__clear_bit(bit, addr)位清除,非原子,*addr &= ~bit
4clear_bit(bit, addr)位清除,原子,*addr &= ~bit
5__change_bit(bit, addr)位反转,非原子,*addr ^= bit
6change_bit(bit, addr)位反转,原子,*addr ^= bit
7test_bit(bit, addr)检测指定的位是否为 1
8__test_and_set_bit(bit, addr)设置位并返回旧值,非原子
9test_and_set_bit(bit, addr)设置位并返回旧值,原子
10__test_and_clear_bit(bit, addr)清除位并返回旧值,非原子
11test_and_clear_bit(bit, addr)清除位并返回旧值,原子
12__test_and_change_bit(bit, addr)位反转并返回旧值,非原子
13test_and_change_bit(bit, addr)位反转并返回旧值,原子
14find_first_bit(addr, nbits)查找位图中的第一个被置位(为 1)的位
15find_first_zero_bit(addr, nbits)查找位图中的第一个被清除(为 0)的位
16find_next_bit(addr, nbits, bit)从指定位置开始,查找下一个为 1 的位
17find_next_zero_bit(addr, nbits, bit)从指定位置开始,查找下一个为 0 的位
18for_each_set_bit(bit, addr, size)遍历位图中为 1 的位
19for_each_set_bit_from(bit, addr, size)从指定位置开始,遍历位图中为 1 的位
20for_each_clear_bit(bit, addr, size)遍历位图中为 0 的位
21for_each_clear_bit_from(bit, addr, size)从指定位置开始,遍历位图中为 0 的位
22bitmap_zero(dst, nbits)将位图设置为 0
23bitmap_fill(dst, nbits)将位图设置为 1

3.2 位设置

位设置函数有两个不同的实现版本:原子的和非原子的。

3.2.1 位设置函数(非原子) -- __set_bit

我们先来看下非原子操作 __set_bit

// file: arch/x86/include/asm/bitops.h
/**
 * __set_bit - Set a bit in memory
 * @nr: the bit to set
 * @addr: the address to start counting from
 *
 * Unlike set_bit(), this function is non-atomic and may be reordered.
 * If it's called on the same region of memory simultaneously, the effect
 * may be that only one operation succeeds.
 */
static inline void __set_bit(int nr, volatile unsigned long *addr)
{
	asm volatile("bts %1,%0" : ADDR : "Ir" (nr) : "memory");
}

__set_bit 函数接收 2 个参数:

  • nr - 待设置的比特位位置
  • addr - 位图的起始地址

可以看到,参数 addr 使用了关键字 volatilevolatile 告诉编译器此变量可能被本程序外的其它程序(比如中断处理程序或者其它线程)改变,指示编译器不要对其进行优化。__set_bit 函数内部使用一段内联汇编代码来完成功能,汇编代码使用了原生的 bts(Bit Test and Set)指令。

bts 指令接收 2 个参数:位偏移以及位图基地址。该指令会将位图中指定偏移处的比特位设置为 1,并把原比特位的值保存到 EFLAGS 寄存器的 CF 标志位中。

ADDR 定义如下:

// file: arch/x86/include/asm/bitops.h
#define ADDR				BITOP_ADDR(addr)

其中,宏 BITOP_ADDR 定义如下:

// file: arch/x86/include/asm/bitops.h
/*
 * These have to be done with inline assembly: that way the bit-setting
 * is guaranteed to be atomic. All bit operations return 0 if the bit
 * was cleared before the operation and != 0 if it was not.
 *
 * bit 0 is the LSB of addr; bit 32 is the LSB of (addr+1).
 */

#if __GNUC__ < 4 || (__GNUC__ == 4 && __GNUC_MINOR__ < 1)
/* Technically wrong, but this avoids compilation errors on some gcc
   versions. */
#define BITOP_ADDR(x) "=m" (*(volatile long *) (x))
#else
#define BITOP_ADDR(x) "+m" (*(volatile long *) (x))
#endif

__GNUC____GNUC_MINOR__是 gcc 的预定义宏,用来表示 gcc 版本,详见 Common Predefined Macros

可以看到,如果 gcc 版本小于 4.1,宏 BITOP_ADDR(x) 扩展后的输出操作数类型修饰符为 "=m","=" 表示该操作数为只写的,"m" 表示操作数位于内存中;否则,扩展为 "+m","+" 表示该操作数为可读写的。另外,我们从注释也能看到,使用修饰符 "=m" 只是为了避免编译器错误,正常应该使用 "+m" 修饰符。为啥输出操作数要是可读写的呢?这是因为 bts 指令需要执行 “读-修改-写” 操作,即先从内存读出数据,修改后再写回内存,所以要求该内存地址是可读写的。

另一个关注点就是,BITOP_ADDR 宏将入参转型为 volatile 类型,禁止编译器对该变量的操作进行优化。

破坏列表中的 memory 关键字,告诉编译器,除了输入和输出中的内存操作数,还有其它内存数据会被修改。实际上,在破坏列表中使用 memory 关键字,会形成编译器 读/写屏障,禁止编译器重排序。详见 Clobbers and Scratch Registers

3.2.2 位设置函数(原子) -- set_bit

上文介绍了非原子的位设置函数,接下来,介绍原子的位设置函数 -- set_bit

__set_bit 函数一样,该函数接收 2 个参数:

  • nr - 待设置的比特位位置
  • addr - 位图的起始地址
// file: arch/x86/include/asm/bitops.h
/**
 * set_bit - Atomically set a bit in memory
 * @nr: the bit to set
 * @addr: the address to start counting from
 *
 * This function is atomic and may not be reordered.  See __set_bit()
 * if you do not require the atomic guarantees.
 *
 * Note: there are no guarantees that this function will not be reordered
 * on non x86 architectures, so if you are writing portable code,
 * make sure not to rely on its reordering guarantees.
 *
 * Note that @nr may be almost arbitrarily large; this function is not
 * restricted to acting on a single-word quantity.
 */
static __always_inline void
set_bit(unsigned int 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 函数根据入参 nr 是否为编译时常量,会进入不同的分支。

IS_IMMEDIATE 会检查参数 nr 是否为编译时常量,该宏定义如下:

 // file: arch/x86/include/asm/bitops.h
 #define IS_IMMEDIATE(nr)        (__builtin_constant_p(nr))

该宏内部调用了__builtin_constant_p 函数。__builtin_constant_p 是 gcc 的内建函数,用来检测一个值是否为编译时常量。如果是编译时常量,返回 1;否则,返回 0。__builtin_constant_p 具体用法可参考 gcc 文档

如果__builtin_constant_p 函数返回 1,会进入 if 分支。

该分支使用的是 or指令,指令后缀 b 指示操作数是字节大小。宏 CONST_MASK_ADDRCONST_MASK 定义如下:

 // file: arch/x86/include/asm/bitops.h
 #define CONST_MASK_ADDR(nr, addr)   BITOP_ADDR((void *)(addr) + ((nr)>>3))
 #define CONST_MASK(nr)          (1 << ((nr) & 7))

addr 是位图的起始地址;(nr)>>3nr 右移 3 位,计算出该比特位处于位图的第几个字节;(void *)(addr) + ((nr)>>3) 获取到比特位对应的字节基地址。然后通过 BITOP_ADDR 宏将该地址转换为 volatile long 类型。

最终,宏 CONST_MASK_ADDR 获取比特位所在字节的基地址,并且是 volatile 类型的。

再来看下宏 CONST_MASK,该宏的作用就是将字节内对应的比特位置位(设置为 1)。

(nr) & 7 获取比特位在字节内的偏移,1 << ((nr) & 7)将字节内对应的比特位设置为 1。

最后,通过 or 指令,把宏 CONST_MASK_ADDR 与宏 CONST_MASK_ADDR 进行按位或操作,将位图中对应的比特位置位。

如果 __builtin_constant_p 函数返回 0,会进入 else 分支。

该分支实现比较简单,直接使用 bts 指令来设置比特位。

至于为什么当 nr 为编译时常量时,要使用 or 指令而不是 bts 指令。那是因为,当 nr 为编译时常量时,使用 or 指令只需要一个写内存的操作;而使用 bts 指令,则需要执行 “读-修改-写” 操作,效率上肯定不如 or 指令。

3.2.3 锁前缀 -- LOCK_PREFIX

不管是 if 分支还是 else 分支,都使用了宏 LOCK_PREFIX,该宏的实现依赖内核配置参数 CONFIG_SMP。当CONFIG_SMP 为真时,宏 LOCK_PREFIX 扩展为 lock 前缀;否则,就是一个空定义。从 set_bit 函数的注释中也能看到,该函数保证位设置是一个原子操作,所以在多处理器(MP)系统下,使用了 lock 锁前缀。

 // file: arch/x86/include/asm/alternative.h
 /*
  * Alternative inline assembly for SMP.
  *
  * The LOCK_PREFIX macro defined here replaces the LOCK and
  * LOCK_PREFIX macros used everywhere in the source tree.
  *
  * SMP alternatives use the same data structures as the other
  * alternatives and the X86_FEATURE_UP flag to indicate the case of a
  * UP system running a SMP kernel.  The existing apply_alternatives()
  * works fine for patching a SMP kernel for UP.
  *
  * The SMP alternative tables can be kept after boot and contain both
  * UP and SMP versions of the instructions to allow switching back to
  * SMP at runtime, when hotplugging in a new CPU, which is especially
  * useful in virtualized environments.
  *
  * The very common lock prefix is handled as special case in a
  * separate table which is a pure address list without replacement ptr
  * and size information.  That keeps the table sizes small.
  */
 
 #ifdef CONFIG_SMP
 #define LOCK_PREFIX_HERE \
         ".pushsection .smp_locks,\"a\"\n"   \
         ".balign 4\n"               \
         ".long 671f - .\n" /* offset */     \
         ".popsection\n"             \
         "671:"
 
 #define LOCK_PREFIX LOCK_PREFIX_HERE "\n\tlock; "
 
 #else /* ! CONFIG_SMP */
 #define LOCK_PREFIX_HERE ""
 #define LOCK_PREFIX ""
 #endif

3.2.4 示意图

位设置函数示意如下:

set_bit.png

3.3 位清除

跟位设置函数类似,位清除函数也有原子非原子两个版本。

3.3.1 位清除函数(非原子)-- __clear_bit

__clear_bit 函数定义如下:

// file: arch/x86/include/asm/bitops.h
static inline void __clear_bit(int nr, volatile unsigned long *addr)
{
	asm volatile("btr %1,%0" : ADDR : "Ir" (nr));
}

函数内部,使用了汇编指令 btr (Bit Test and Reset)来实现。btr 指令接收 2 个参数:位偏移以及位图基地址。该指令会将位图中指定偏移处的比特位设置为 0,并把原比特位的值保存到 EFLAGS 寄存器的 CF 标志位中。

3.3.2 位清除函数(原子)- clear_bit

原子版的位清除函数 clear_bit 定义如下:

// file: arch/x86/include/asm/bitops.h
/**
 * clear_bit - Clears a bit in memory
 * @nr: Bit to clear
 * @addr: Address to start counting from
 *
 * clear_bit() is atomic and may not be reordered.  However, it does
 * not contain a memory barrier, so if it is used for locking purposes,
 * you should call smp_mb__before_clear_bit() and/or smp_mb__after_clear_bit()
 * in order to ensure changes are visible on other processors.
 */
static __always_inline void
clear_bit(int 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));
	}
}

IS_IMMEDIATE用来检查入参 nr 是否编译时常量。如果是,进入 if 分支,使用 andb 指令来操作;否则,使用 btr 指令来操作。该函数内部引用的宏与 set_bit 函数一样,我们就不详细分析了。至于为什么不统一使用 btr 指令,其原因与 set_bit 函数类似,即 and 指令的内存操作要少于 btr 指令,能够提高性能。

3.3.3 示意图

位清除函数示意如下:

clear_bit.png

3.4 位取反

位取反操作也有原子和非原子两个函数版本 -- 非原子的 __change_bit 函数,以及原子的 change_bit 函数。

3.4.1 位取反函数(非原子)-- __change_bit

// file: arch/x86/include/asm/bitops.h
/**
 * __change_bit - Toggle a bit in memory
 * @nr: the bit to change
 * @addr: the address to start counting from
 *
 * Unlike change_bit(), this function is non-atomic and may be reordered.
 * If it's called on the same region of memory simultaneously, the effect
 * may be that only one operation succeeds.
 */
static inline void __change_bit(int nr, volatile unsigned long *addr)
{
	asm volatile("btc %1,%0" : ADDR : "Ir" (nr));
}

函数内部,使用了汇编指令 btc (Bit Test and Complement)来实现功能。

btc 指令接收 2 个参数:位偏移以及位图基地址。该指令会将位图中指定偏移处的比特位取反,并把原比特位的值保存到 EFLAGS 寄存器的 CF 标志位中。

3.4.2 位取反函数(原子)-- change_bit

// file: arch/x86/include/asm/bitops.h
/**
 * change_bit - Toggle a bit in memory
 * @nr: Bit to change
 * @addr: Address to start counting from
 *
 * change_bit() is atomic and may not be reordered.
 * Note that @nr may be almost arbitrarily large; this function is not
 * restricted to acting on a single-word quantity.
 */
static inline void change_bit(int 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));
	}
}

IS_IMMEDIATE用来检查入参 nr 是否编译时常量,如果是,进入 if 分支,使用 xorb 指令来操作;否则,使用 btc 指令来操作。内部引用的宏与 set_bit 函数一样,我们就不详细分析了。

3.4.3 示意图

位取反函数示意如下:

change_bit.png

3.5 位测试 -- test_bit

test_bit 检查位图中指定的位是否为 1。如果是,返回非 0 值,否则,返回 0。其定义如下:

// file: arch/x86/include/asm/bitops.h
#define test_bit(nr, addr)			\
	(__builtin_constant_p((nr))		\
	 ? constant_test_bit((nr), (addr))	\
	 : variable_test_bit((nr), (addr)))

test_bit 宏的实现,依赖其入参 nr 的值。如果 nr 是编译时常量,那么该宏扩展为 constant_test_bit 函数;否则,扩展为 variable_test_bit 函数。

3.5.1 constant_test_bit 函数

constant_test_bit 函数接收 2 个参数:

  • nr -- 待测试的比特位
  • addr -- 位图起始地址

函数定义如下:

// file: arch/x86/include/asm/bitops.h
static __always_inline int constant_test_bit(unsigned int nr, const volatile unsigned long *addr)
{
	return ((1UL << (nr % BITS_PER_LONG)) &
		(addr[nr / BITS_PER_LONG])) != 0;
}

BITS_PER_LONG 表示 long 类型占用的比特位数量,在 64 位模式下扩展为 64,其定义如下:

// file: include/asm-generic/bitsperlong.h
#define BITS_PER_LONG 64

该函数将位图看做一个 unsigned long 类型的数组,其计算过程如下:

  • addr[nr / BITS_PER_LONG] 从内存中取出比特位所属的数组成员的值

  • 1UL << (nr % BITS_PER_LONG) 计算出指定比特位在 long 类型数据中偏移,并将对应的比特位设置为 1

  • 将上述两步的结果进行按位与操作,如果最终结果不为 0,说明对应的位为 1,返回 true(数值为 1);否则,说明对应的位为 0,返回 false(数值为 0)。

3.5.2 variable_test_bit 函数

variable_test_bit 函数定义如下:

// file: arch/x86/include/asm/bitops.h
static inline int variable_test_bit(int 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;
}

该函数接收的参数与其它位操作函数类似,即位图起始地址和比特位的偏移。在函数内,通过内联汇编代码,执行了 bt(Bit Test) 指令和 sbb (Integer Subtraction With Borrow)指令。

bt 指令选择位图中指定的位,并将该位的值保存到 EFLAGS 寄存器的 CF 标志位。

sbb 指令把源操作数和 CF 标志位相加,然后从目标操作数中减去上一步相加的值,并将减法计算的结果保存到目标操作数中。

在上述代码中,先执行 bt 指令,把指定比特位的值保存到 CF 标志位;然后执行 sbb 指令。由于 sbb 指令的源操作数和目的操作数相同,所以最终计算结果等于 0x0 - CF。当指定位为 1 时,计算结果为 -1;当指定位为 0 时,计算结果为 0。

除了上文提到的四种基本操作之外,内核中还提供了几种组合操作,每种操作又分为原子和非原子两个版本。

3.6 位测试及设置操作

3.6.1 非原子操作 -- __test_and_set_bit

该函数会将位图中指定的位置位,并返回旧值。其内部使用了 bts 和 sbb 指令。

// file: arch/x86/include/asm/bitops.h
/**
 * __test_and_set_bit - Set a bit and return its old value
 * @nr: Bit to set
 * @addr: Address to count from
 *
 * This operation is non-atomic and can be reordered.
 * If two examples of this operation race, one can appear to succeed
 * but actually fail.  You must protect multiple accesses with a lock.
 */
static inline int __test_and_set_bit(int nr, volatile unsigned long *addr)
{
	int oldbit;

	asm("bts %2,%1\n\t"
	    "sbb %0,%0"
	    : "=r" (oldbit), ADDR
	    : "Ir" (nr));
	return oldbit;
}

3.6.2 原子操作 -- test_and_set_bit

// file: arch/x86/include/asm/bitops.h
/**
 * test_and_set_bit - Set a bit and return its old value
 * @nr: Bit to set
 * @addr: Address to count from
 *
 * This operation is atomic and cannot be reordered.
 * It also implies a memory barrier.
 */
static inline int test_and_set_bit(int nr, volatile unsigned long *addr)
{
	int oldbit;

	asm volatile(LOCK_PREFIX "bts %2,%1\n\t"
		     "sbb %0,%0" : "=r" (oldbit), ADDR : "Ir" (nr) : "memory");

	return oldbit;
}

该函数功能与__test_and_set_bit一致,只不过通过锁前缀保证了操作原子性。

3.7 位测试及清除操作

3.7.1 非原子操作 -- __test_and_clear_bit

清除位图中指定的位,并返回旧值。函数内部使用了 btrsbb 指令。

// file: arch/x86/include/asm/bitops.h
/**
 * __test_and_clear_bit - Clear a bit and return its old value
 * @nr: Bit to clear
 * @addr: Address to count from
 *
 * This operation is non-atomic and can be reordered.
 * If two examples of this operation race, one can appear to succeed
 * but actually fail.  You must protect multiple accesses with a lock.
 *
 * Note: the operation is performed atomically with respect to
 * the local CPU, but not other CPUs. Portable code should not
 * rely on this behaviour.
 * KVM relies on this behaviour on x86 for modifying memory that is also
 * accessed from a hypervisor on the same CPU if running in a VM: don't change
 * this without also updating arch/x86/kernel/kvm.c
 */
static inline int __test_and_clear_bit(int nr, volatile unsigned long *addr)
{
	int oldbit;

	asm volatile("btr %2,%1\n\t"
		     "sbb %0,%0"
		     : "=r" (oldbit), ADDR
		     : "Ir" (nr));
	return oldbit;
}

3.7.2 原子操作 -- test_and_clear_bit

// file: arch/x86/include/asm/bitops.h
/**
 * test_and_clear_bit - Clear a bit and return its old value
 * @nr: Bit to clear
 * @addr: Address to count from
 *
 * This operation is atomic and cannot be reordered.
 * It also implies a memory barrier.
 */
static inline int test_and_clear_bit(int nr, volatile unsigned long *addr)
{
	int oldbit;

	asm volatile(LOCK_PREFIX "btr %2,%1\n\t"
		     "sbb %0,%0"
		     : "=r" (oldbit), ADDR : "Ir" (nr) : "memory");

	return oldbit;
}

功能与 __test_and_clear_bit 函数一致,只不过通过锁前缀保证了操作原子性。

3.8 位测试及取反操作

3.8.1 非原子操作 -- __test_and_change_bit

反转位图中指定的位,并返回旧值。函数内部使用了 btcsbb 指令。

// file: arch/x86/include/asm/bitops.h
/* WARNING: non atomic and it can be reordered! */
static inline int __test_and_change_bit(int nr, volatile unsigned long *addr)
{
	int oldbit;

	asm volatile("btc %2,%1\n\t"
		     "sbb %0,%0"
		     : "=r" (oldbit), ADDR
		     : "Ir" (nr) : "memory");

	return oldbit;
}

3.8.2 原子操作 -- test_and_change_bit

// file: arch/x86/include/asm/bitops.h
/**
 * test_and_change_bit - Change a bit and return its old value
 * @nr: Bit to change
 * @addr: Address to count from
 *
 * This operation is atomic and cannot be reordered.
 * It also implies a memory barrier.
 */
static inline int test_and_change_bit(int nr, volatile unsigned long *addr)
{
	int oldbit;

	asm volatile(LOCK_PREFIX "btc %2,%1\n\t"
		     "sbb %0,%0"
		     : "=r" (oldbit), ADDR : "Ir" (nr) : "memory");

	return oldbit;
}

功能与 __test_and_change_bit 函数一致,只不过通过锁前缀保证了操作原子性。

3.9 位图零化 -- bitmap_zero

bitmap_zero 函数将位图中所有比特位设置为 0。该函数接收两个参数:

  • dst - 位图起始地址
  • nbits - 位图的比特位数量
// file: include/linux/bitmap.h
static inline void bitmap_zero(unsigned long *dst, int nbits)
{
	if (small_const_nbits(nbits))
		*dst = 0UL;
	else {
		int len = BITS_TO_LONGS(nbits) * sizeof(unsigned long);
		memset(dst, 0, len);
	}
}

函数内部,首先通过宏 small_const_nbits 判断要操作的位数是否属于比较小的常量。

small_const_nbits 定义如下:

// file: include/linux/bitmap.h
#define small_const_nbits(nbits) \
	(__builtin_constant_p(nbits) && (nbits) <= BITS_PER_LONG)

上文已经讲过,__builtin_constant_p 是 gcc 内建函数,用来判断入参是否是编译时常量,如果是返回 1,否则返回 0。另外,宏 BITS_PER_LONG 扩展为 64。

如果比特位数量是编译时常量,而且不超过一个 long 类型的容量(64位),那么直接将指针 dst 指向的数据设置为 0 。否则,就先通过宏 BITS_TO_LONGS 将位数向上圆整到 long 类型的整数倍,计算出实际需要处理的位数,然后通过 memset 函数,将这些位全部设置为 0。

BITS_TO_LONGS 的功能我们声明位图一节介绍过,此处不再赘述。

示意图:

bitmap-zero.png

3.10 位图填充 -- bitmap_fill

bitmap_fill 函数将位图的所有比特位设置为 1。该函数接收两个参数:

  • dst - 位图起始地址
  • nbits - 位图的比特位数量
static inline void bitmap_fill(unsigned long *dst, int nbits)
{
	size_t nlongs = BITS_TO_LONGS(nbits);
	if (!small_const_nbits(nbits)) {
		int len = (nlongs - 1) * sizeof(unsigned long);
		memset(dst, 0xff,  len);
	}
	dst[nlongs - 1] = BITMAP_LAST_WORD_MASK(nbits);
}

由于内核使用 unsigned long 类型的数组来表示位图,所以位相关的操作需要转换成数组操作。

在函数开头,通过 BITS_TO_LONGS 宏计将位的数量转换成数组的成员数量。注意,宏 BITS_TO_LONGS 会向上圆整。

由于不确定待设置的比特位数量是否填满整个数组,所以数组中最后一个成员有可能只需置位部分比特位,所以该成员需要单独处理;而其它成员只需将所有比特位置位就可以了。

small_const_nbits 函数用来检测入参 nbits 是否小的编译时常量(参见 bitmap_zero 函数中的说明)。

如果 small_const_nbits 函数返回真,说明数组成员数量 nlongs 至少为 2。那么前 nlongs - 1 个成员和最后一个成员需要分开处理。对于前 nlongs - 1 个成员,计算出成员的总位数之后,直接调用 memset 函数将这些位全部设置为 1 就可以了。这就是下面两行代码所做的工作:

		int len = (nlongs - 1) * sizeof(unsigned long);
		memset(dst, 0xff,  len);

对于最后一个数组成员,使用宏 BITMAP_LAST_WORD_MASK 进行处理,该宏定义如下:

// file: include/linux/bitmap.h
#define BITMAP_LAST_WORD_MASK(nbits)					\
(									\
	((nbits) % BITS_PER_LONG) ?					\
		(1UL<<((nbits) % BITS_PER_LONG))-1 : ~0UL		\
)

在宏的内部,首先通过 (nbits) % BITS_PER_LONG 将待处理位数对 BITS_PER_LONG(扩展为 64)求余。如果余数为 0,说明正好是long类型的整数倍,把最后 一个成员的比特位全部设置为 1;否则,显示通过 (nbits) % BITS_PER_LONG 计算出需要处理的比特位数量,然后通过 (1UL<<((nbits) % BITS_PER_LONG))-1 把对应的比特位设置为 1。

如果 small_const_nbits 函数返回假,说明只需一个数组成员就能容纳,直接按最后一个成员来处理。即直接执行

dst[nlongs - 1] = BITMAP_LAST_WORD_MASK(nbits);

示意图:

bitmap-fill.png

3.11 查找第一个为 1 的比特位 -- find_first_bit

find_first_bit 函数查找指位图内第一个被设置的比特位。该函数接收两个参数:

  • addr - 位图起始地址
  • size - 位图的位容量

如果在 size 范围内有设置的位,那么返回该比特位的索引(0 ~ size-1);否则,返回 size 的值。

find_first_bit 函数实现如下:

// file: lib/find_next_bit.c
/*
 * Find the first set bit in a memory region.
 */
unsigned long find_first_bit(const unsigned long *addr, unsigned long size)
{
	const unsigned long *p = addr;
	unsigned long result = 0;
	unsigned long tmp;

	while (size & ~(BITS_PER_LONG-1)) {
		if ((tmp = *(p++)))
			goto found;
		result += BITS_PER_LONG;
		size -= BITS_PER_LONG;
	}
	if (!size)
		return result;

	tmp = (*p) & (~0UL >> (BITS_PER_LONG - size));
	if (tmp == 0UL)		/* Are any bits set? */
		return result + size;	/* Nope. */
found:
	return result + __ffs(tmp);
}

Linux 内核中,位图的容器是 unsigned long 类型的数组。查找时,先确定位所属的数组成员;再通过位操作,计算出该位在成员中的位置;最后,把两者相加即可。由于不确定 size 是否对齐到 long 类型位数(64),数组中最后一个成员可能只有部分比特位有效,所以该成员需要单独处理。

比如,要查找的最大位数为 138,那么前 128 位在两个 unsigned long成员里;但是,最后一个成员里的 64 位里,只有低 10 位是有效位,就只能查找前 10 位,因为后面的 54 位的值是随机的,不可信的。如果在前 10 位里能找到说明确实存在,否则说明不存在。

我们来来看具体实现。

在程序开头,定义了几个变量。其中,指针 p 被初始化为位图起始地址,该变量会随着迭代指向不同的数组成员。变量 result 用于保存已经搜索的位数。变量 tmp 用来临时保存数组成员。

	const unsigned long *p = addr;
	unsigned long result = 0;
	unsigned long tmp;

接下来,进入 while 循环,我们来先看下循环条件:

size & ~(BITS_PER_LONG-1)

BITS_PER_LONG 表示每个 long 类型包含的位数,扩展为 64。~(BITS_PER_LONG-1) 作为掩码,其低 6 位为 0,其余位为 1,作为掩码使用。size & ~(BITS_PER_LONG-1)size 的低 6 位置 0。

当循环条件为 0 时,说明未处理的位数已经不足 64 位,即处理到了最后一个数组成员,该成员需要特殊处理,此时 while 循环结束。

当循环条件为真时,说明是在处理普通成员,先是从地址 p 处取出数组成员的值并赋值给临时变量 tmp ,然后指针 p 自增,指向下一个数组成员。如果 tmp 为真,说明该成员中至少有一个位是 1,目标找到,跳转到 found 标签处。否则,把 result 增加 BITS_PER_LONG ,并将 size 减小 BITS_PER_LONG ,进入下一轮循环。

while 循环执行完毕时,除了最后一个数组成员还没搜索,其它成员都找过了且没找到。现在需要判断最后一个数组成员中是否有为 1 的位。

接下来执行

	if (!size)
		return result;

如果 while 循环结束后 size 为 0,即 !size 为真,说明要查找的位数是 BITS_PER_LONG 的整数倍,这种情况下,所有数组成员在 while 循环中都被查找过了,并且没有找到,所以直接返回了 result,此时 result 的值就等于原始的 size 值。

如果 while 循环结束后,size 不为 0,说明要查找的位数不是 BITS_PER_LONG 的整数倍,还需要继续在最后一个成员中查找。由于最后一个成员不是所有位都有效,我们只能在有效位查找,所以要屏蔽掉无效的位。 ~0UL >> (BITS_PER_LONG - size) 就是用于屏蔽掉无效位的掩码,该掩码的低 size 位全为 1,其它位为 0。接下来,计算屏蔽后的值:

tmp = (*p) & (~0UL >> (BITS_PER_LONG - size));

此时,指针 p 指向数组最后一个成员。

如果屏蔽后的结果为 0,说明最后一个成员中也没有为 1 的位,返回 result + size,即位图的总量。

	if (tmp == 0UL)		/* Are any bits set? */
		return result + size;	/* Nope. */

再来看 found 标签处的代码。程序运行到这里,说明找到为 1 的位了。现在我们只知道该位存在于 tmp 成员里,但不知道是 tmp 中的第几位。通过调用 __ffs 函数,来计算该位在 tmp 中的偏移。

__ffs 函数定义如下:

// file: arch/x86/include/asm/bitops.h
/**
 * __ffs - find first set bit in word
 * @word: The word to search
 *
 * Undefined if no bit exists, so code should check against 0 first.
 */
static inline unsigned long __ffs(unsigned long word)
{
	asm("rep; bsf %1,%0"
		: "=r" (word)
		: "rm" (word));
	return word;
}

可以看到,__ffs函数内部使用了 bsf (Bit Scan Forward)指令。该指令会从源操作数中搜索最低的设置为 1 的位。如果找到,该位的索引会保存在目标操作数中。

最后,将 result + __ffs(tmp) 返回值。其中,result 是已经扫描过的位数,__ffs(tmp)tmp 中首个被置位的比特位的索引,两者相加就得到实际的索引值。

3.12 查找第一个为 0 的比特位 -- find_first_zero_bit

find_first_zero_bit 函数用于查找位图内第一个为 0 的比特位。

该函数接收两个参数:

  • addr - 位图的起始地址
  • size - 位图的位容量

如果找到,返回该位的索引值(0 ~ size-1);否则,返回位图容量 size 的值。

该函数实现如下:

/*
 * Find the first cleared bit in a memory region.
 */
unsigned long find_first_zero_bit(const unsigned long *addr, unsigned long size)
{
	const unsigned long *p = addr;
	unsigned long result = 0;
	unsigned long tmp;

	while (size & ~(BITS_PER_LONG-1)) {
		if (~(tmp = *(p++)))
			goto found;
		result += BITS_PER_LONG;
		size -= BITS_PER_LONG;
	}
	if (!size)
		return result;

	tmp = (*p) | (~0UL << size);
	if (tmp == ~0UL)	/* Are any bits zero? */
		return result + size;	/* Nope. */
found:
	return result + ffz(tmp);
}

从实现过程看,该函数与 find_first_bit 非常类似,只不过判断是否找到的标准不同。在 find_first_bit 中,使用 tmp = *(p++) 来判断数组成员中是否有为 1 的比特位。如果为真,说明有的比特位为 1。在此函数中,我们要找的是为 0 的位,所以判断条件改成了~(tmp = *(p++)),如果对数组成员按位取反后其值为真,说明未取反前至少有一个位是 0。

另外,在判断最后一个数组成员中是否有为 0 的位时,使用的是

tmp = (*p) | (~0UL << size)

此时,size 表示在 while 循环后,尚未检测的位数。~0UL << size 的低 size 位为 0,与(*p)进行按位或运算之后,如果计算结果的所有位全部为 1,说明这 size 个位中没有 0,也说明整个位图中都没找到为 0 的位,所以返回 result + size,即原始的 size 值。

	if (tmp == ~0UL)	/* Are any bits zero? */
		return result + size;	/* Nope. */

再来看 found 标签处的代码。

found:
	return result + ffz(tmp);

程序运行到这里,说明找到为 0 的位了。现在我们只知道该位存在于 tmp 成员里,但不知道是 tmp 中的第几位。通过调用 ffz 函数,来计算该位在 tmp 中的偏移。

// file: arch/x86/include/asm/bitops.h
/**
 * ffz - find first zero bit in word
 * @word: The word to search
 *
 * Undefined if no zero exists, so code should check against ~0UL first.
 */
static inline unsigned long ffz(unsigned long word)
{
	asm("rep; bsf %1,%0"
		: "=r" (word)
		: "r" (~word));
	return word;
}

该函数内部同样使用了 bsf(Bit Scan Forward)指令,该指令会从源操作数中搜索最低位的设置为 1 的位。如果找到,该位的索引会保存在目标操作数中。由于在本函数中需要查找的是为 0 的位,所以对输入操作数 word 先按位取反后,再执行查找工作。

最后,将 result + ffz(tmp) 的值返回。其中,result 是已经扫描过的位数,ffz(tmp)tmp 中首个为 0 的比特位的索引,两者相加就得到实际的索引值。

3.13 从指定偏移处查找下一个为 1 的比特位 -- find_next_bit

find_next_bit 函数用于从指定偏移处查找下一个为 1 的比特位。该函数接收 3 个参数:

  • addr - 位图起始地址
  • size - 位图的位容量
  • offset - 位偏移,指示从哪一位开始查找

如果找到,返回该位的索引值(0 ~ size-1);否则,返回位容量 size 的值。

该函数定义如下:

/*
 * Find the next set bit in a memory region.
 */
unsigned long find_next_bit(const unsigned long *addr, unsigned long size,
			    unsigned long offset)
{
	const unsigned long *p = addr + BITOP_WORD(offset);
	unsigned long result = offset & ~(BITS_PER_LONG-1);
	unsigned long tmp;

	if (offset >= size)
		return size;
	size -= result;
	offset %= BITS_PER_LONG;
	if (offset) {
		tmp = *(p++);
		tmp &= (~0UL << offset);
		if (size < BITS_PER_LONG)
			goto found_first;
		if (tmp)
			goto found_middle;
		size -= BITS_PER_LONG;
		result += BITS_PER_LONG;
	}
	while (size & ~(BITS_PER_LONG-1)) {
		if ((tmp = *(p++)))
			goto found_middle;
		result += BITS_PER_LONG;
		size -= BITS_PER_LONG;
	}
	if (!size)
		return result;
	tmp = *p;

found_first:
	tmp &= (~0UL >> (BITS_PER_LONG - size));
	if (tmp == 0UL)		/* Are any bits set? */
		return result + size;	/* Nope. */
found_middle:
	return result + __ffs(tmp);
}

在分析函数执行过程前,我们还是再强调一下。内核使用 unsigned long 数组作为位图的容器,所以我们是先对数组进行操作,确定位所属的数组成员;然后使用位操作指令从成员中获取位偏移;最后,将两者相加就得到最终的索引值。

另外,由于最大位数可能不是 BITS_PER_LONG 的整数倍,所以最后一个数组成员需要特殊处理。比如,要查找的最大位数为 138,那么前 128 位在两个 unsigned long成员里;但是,最后一个成员里的 64 位里,只有 10 位是有效位,就只能查找前 10 位,因为后面的 54 位的值是随机的,不可信的。如果在前 10 位里能找到说明确实存在,否则说明不存在。

在函数一开始,定义了三个变量

	const unsigned long *p = addr + BITOP_WORD(offset);
	unsigned long result = offset & ~(BITS_PER_LONG-1);
	unsigned long tmp;

其中,unsigned long 类型的指针 p,指向数组中的成员 。

该成员地址计算如下:

  • 先是通过宏 BITOP_WORD计算出偏移 offset 位后所在的数组成员索引
  • 通过addr + BITOP_WORD(offset) 计算出对应的数组成员地址

BITOP_WORD 定义如下:

// file: lib/find_next_bit.c
#define BITOP_WORD(nr)		((nr) / BITS_PER_LONG)

offset & ~(BITS_PER_LONG-1)offset 向下对齐到 BITS_PER_LONG (64)的倍数,并赋值给变量 result,作为计算的起始位。

	if (offset >= size)
		return size;

此时,各变量的值如下所示:

find_next_bit.png

如果偏移量比最大位数都大,说明参数错误,直接返回 size,表示没找到。

	size -= result;
	offset %= BITS_PER_LONG;

上述代码中,第一行计算出还需要搜索多少位,并保存到变量 size 中。第二行,计算 offsetBITS_PER_LONG 求模后的余数,并保存在 offset 中,该值表示在搜索的第一个数组成员中,有多少位是无效位,不需要搜索,这些位需要屏蔽掉。

	if (offset) {
		tmp = *(p++);
		tmp &= (~0UL << offset);
		if (size < BITS_PER_LONG)
			goto found_first;
		if (tmp)
			goto found_middle;
		size -= BITS_PER_LONG;
		result += BITS_PER_LONG;
	}

如果余数不为 0,说明 offset 没有对齐到 long 类型边界,第一个数组成员需要特殊处理。该成员中的低 offset 位不在搜索范围,所以通过掩码 ~0UL << offset 将成员中的低 offset 位全部设置为 0。

在执行完 if 语句的前两行代码之后,临时变量 tmp 中保存的是屏蔽后的第一个成员的值。

如果 size < BITS_PER_LONG 为真,说明要搜索的位数不超过 long 类型的容量,即只需要搜索第一个成员就可以了,跳转到 found_first 标签处执行。

如果 tmp 为真,说明第一个成员中就有为 1 的位,跳转到标签 found_middle 处执行。

然后,将 size 自减 BITS_PER_LONG,result 自加 BITS_PER_LONG,从下一个成员处继续执行。

处理完第一个特殊的成员后,后续的执行过程就跟 find_first_bit 函数基本相同了,可参考find_first_bit 函数的实现,此处不在赘述。

3.14 从指定偏移处查找下一个为 0 的位 -- find_next_zero_bit

find_next_bit 函数用于从指定偏移处查找下一个为 0 的比特位。该函数接收 3 个参数:

  • addr - 位图起始地址
  • size - 最大查找位数
  • offset - 位偏移,指示从哪一位开始查找

如果找到,返回该位的索引值(0 ~ size-1);否则,返回最大查找位数 size 的值。

该函数定义如下:

// file: lib/find_next_bit.c
/*
 * This implementation of find_{first,next}_zero_bit was stolen from
 * Linus' asm-alpha/bitops.h.
 */
unsigned long find_next_zero_bit(const unsigned long *addr, unsigned long size,
				 unsigned long offset)
{
	const unsigned long *p = addr + BITOP_WORD(offset);
	unsigned long result = offset & ~(BITS_PER_LONG-1);
	unsigned long tmp;

	if (offset >= size)
		return size;
	size -= result;
	offset %= BITS_PER_LONG;
	if (offset) {
		tmp = *(p++);
		tmp |= ~0UL >> (BITS_PER_LONG - offset);
		if (size < BITS_PER_LONG)
			goto found_first;
		if (~tmp)
			goto found_middle;
		size -= BITS_PER_LONG;
		result += BITS_PER_LONG;
	}
	while (size & ~(BITS_PER_LONG-1)) {
		if (~(tmp = *(p++)))
			goto found_middle;
		result += BITS_PER_LONG;
		size -= BITS_PER_LONG;
	}
	if (!size)
		return result;
	tmp = *p;

found_first:
	tmp |= ~0UL << size;
	if (tmp == ~0UL)	/* Are any bits zero? */
		return result + size;	/* Nope. */
found_middle:
	return result + ffz(tmp);
}

该函数的实现请参考 find_next_bit 以及 find_first_zero_bit 函数,此处不再赘述。

3.15 位迭代

include/linux/bitops.h 文件中,提供了 4 个宏用进行位图的迭代:

  • for_each_set_bit
  • for_each_set_bit_from
  • for_each_clear_bit
  • for_each_clear_bit_from

这些宏内部,引用了我们上文介绍过的 4 个查找函数。

3.15.1 for_each_set_bit

for_each_set_bit 宏会从位图的起始地址开始,迭代查找为 1 的位,其定义如下:

// file: include/linux/bitops.h
#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))

3.15.2 for_each_set_bit_from

for_each_set_bit_from 宏与for_each_set_bit 类似,但是会从位图的指定位置开始,迭代查找为 1 的位,其定义如下:

/* same as for_each_set_bit() but use bit as value to start with */
#define for_each_set_bit_from(bit, addr, size) \
	for ((bit) = find_next_bit((addr), (size), (bit));	\
	     (bit) < (size);					\
	     (bit) = find_next_bit((addr), (size), (bit) + 1))

3.15.3 for_each_clear_bit

for_each_clear_bit 宏会从位图的起始地址开始,迭代查找其中为 0 的位,其定义如下:

// file: include/linux/bitops.h
#define for_each_clear_bit(bit, addr, size) \
	for ((bit) = find_first_zero_bit((addr), (size));	\
	     (bit) < (size);					\
	     (bit) = find_next_zero_bit((addr), (size), (bit) + 1))

3.15.4 for_each_clear_bit_from

for_each_clear_bit_from 宏与for_each_clear_bit 类似,但是会从位图的指定位置开始,迭代查找其中为 0 的位,其定义如下:

// file: include/linux/bitops.h
/* same as for_each_clear_bit() but use bit as value to start with */
#define for_each_clear_bit_from(bit, addr, size) \
	for ((bit) = find_next_zero_bit((addr), (size), (bit));	\
	     (bit) < (size);					\
	     (bit) = find_next_zero_bit((addr), (size), (bit) + 1))

3.16 bitmap_weight

bitmap_weight 函数用于统计位图中为 1 的比特位数量。该函数接收 2 个参数:

  • src -- 位图的起始地址
  • nbits -- 位图容量,有效比特位数量

函数定义如下:

// file: include/linux/bitmap.h
static inline int bitmap_weight(const unsigned long *src, int nbits)
{
	if (small_const_nbits(nbits))
		return hweight_long(*src & BITMAP_LAST_WORD_MASK(nbits));
	return __bitmap_weight(src, nbits);
}

函数 small_const_nbits用来检查参数 nbits 是否为编译时常量,且小于 long 类型的容量。如果是,说明该位图只有一个数组成员,则调用 hweight_long函数计算;否则,调用 __bitmap_weight 函数计算。

3.16.1 hweight_long

hweight_long 函数用来计算单个 long 类型数据中被置位的比特位数量,其定义如下:

// file: include/linux/bitops.h
static inline unsigned long hweight_long(unsigned long w)
{
	return sizeof(w) == 4 ? hweight32(w) : hweight64(w);
}

在 64 位模式下,unsigned long类型为 8 字节,所以实际使用的是宏 hweight64,该宏定义如下:

// file: include/asm-generic/bitops/const_hweight.h
#define hweight64(w) (__builtin_constant_p(w) ? __const_hweight64(w) : __arch_hweight64(w))

__builtin_constant_p 函数是 GCC 的内建函数,用于检查一个值是否编译时常量。如果 w 是编译时常量,那么调用宏 __const_hweight64 来计算;否则,调用宏 __arch_hweight64 来计算。

__const_hweight64

__const_hweight64 实现如下:

// file: include/asm-generic/bitops/const_hweight.h
#define __const_hweight64(w) (__const_hweight32(w) + __const_hweight32((w) >> 32))
#define __const_hweight32(w) (__const_hweight16(w) + __const_hweight16((w) >> 16))
#define __const_hweight16(w) (__const_hweight8(w)  + __const_hweight8((w)  >> 8 ))
/*
 * Compile time versions of __arch_hweightN()
 */
#define __const_hweight8(w)		\
      (	(!!((w) & (1ULL << 0))) +	\
	(!!((w) & (1ULL << 1))) +	\
	(!!((w) & (1ULL << 2))) +	\
	(!!((w) & (1ULL << 3))) +	\
	(!!((w) & (1ULL << 4))) +	\
	(!!((w) & (1ULL << 5))) +	\
	(!!((w) & (1ULL << 6))) +	\
	(!!((w) & (1ULL << 7)))	)

可以看到,64 位(四字)的值被分解为 2 个 32 位(双字)的值,继续被分解为 4 个 16 位(字)的值,最后被分解为 8 个 8 位(字节)的值来计算。计算出每个字节中被置位的比特位数量后,再将这些值相加,就得到了整个 64 位数值中为 1 的比特位数量。

在分解的最底层,是使用 __const_hweight8 宏来计算的。该宏将数字 1 分别左移 0 ~ 7 位后,与 w 中对应的字节做按位与操作,然后双重取反得到 0 或 1,最后将这 8 个值相加得到最终的置位数量。

__arch_hweight64

接下来看下的 __arch_hweight64 函数的实现:

// file: arch/x86/include/asm/arch_hweight.h
static inline unsigned long __arch_hweight64(__u64 w)
{
	unsigned long res = 0;

#ifdef CONFIG_X86_32
	return  __arch_hweight32((u32)w) +
		__arch_hweight32((u32)(w >> 32));
#else
	asm (ALTERNATIVE("call __sw_hweight64", POPCNT64, X86_FEATURE_POPCNT)
		     : "="REG_OUT (res)
		     : REG_IN (w));
#endif /* CONFIG_X86_32 */

	return res;
}

函数内关于 32 位系统的部分就直接略过了,毕竟我们是 64 位系统。在 64 位系统下,是通过内联汇编来实现的。

ALTERNATIVE 宏接收 3 个参数:默认指令,替换指令,CPU 特征。如果处理器支持参数 3 指定的特征,那么就会执行替换指令,否则执行默认指令。

X86_FEATURE_POPCNT 是指 x86 架构的 POPCNT 指令。

// file: arch/x86/include/asm/cpufeature.h
#define X86_FEATURE_POPCNT      (4*32+23) /* POPCNT instruction */

POPCNT64 扩展为 popcnt %rdi, %rax 指令的机器码。

// file: arch/x86/include/asm/arch_hweight.h
/* popcnt %rdi, %rax */
#define POPCNT64 ".byte 0xf3,0x48,0x0f,0xb8,0xc7"

popcnt 指令会计算源操作数(%rdi)中为 1 的比特位数量,并保存到目标寄存器(%rax)中。

在本例中,如果处理支持 POPCNT 指令,那么就会通过该指令来计算;否则,调用 __sw_hweight64 函数来计算。 __sw_hweight64 函数我们就不具体介绍了,总之,实现的功能是一致的。

3.16.2 __bitmap_weight

如果位图的容量超过一个 long 类型的大小,就需要通过__bitmap_weight 函数来计算,该函数定义如下:

// file: lib/bitmap.c
int __bitmap_weight(const unsigned long *bitmap, int bits)
{
	int k, w = 0, lim = bits/BITS_PER_LONG;

	for (k = 0; k < lim; k++)
		w += hweight_long(bitmap[k]);

	if (bits % BITS_PER_LONG)
		w += hweight_long(bitmap[k] & BITMAP_LAST_WORD_MASK(bits));

	return w;
}

该函数接收 2 个参数:

  • bitmap -- 位图起始地址
  • bits -- 位图容量,有效位数

在函数内部,先是通过 bits/BITS_PER_LONG 计算出位图包含的完整的数组成员数量,并保存到变量 lim 中。

接下来,遍历这些数组成员,调用函数 hweight_long 计算出每个成员中为 1 的比特位数量,并对数量求和。

再接着,通过 bits % BITS_PER_LONG 检查是否有不完整的成员,该成员因为对齐到 long 类型边界而填充了无效比特位。如果有,那么最后一个成员需要调用宏 BITMAP_LAST_WORD_MASK 特殊处理。然后,把最后一个成员中为 1 的比特位数量与上一步得到的数量相加,得到总数量。

最后,把计算得到的数量返回。

3.17 其它函数

其它函数简要说明:

// file: include/linux/bitmap.h
/*
 * The available bitmap operations and their rough meaning in the
 * case that the bitmap is a single unsigned long are thus:
 *
 * Note that nbits should be always a compile time evaluable constant.
 * Otherwise many inlines will generate horrible code.
 *
 * bitmap_zero(dst, nbits)			*dst = 0UL
 * bitmap_fill(dst, nbits)			*dst = ~0UL
 * bitmap_copy(dst, src, nbits)			*dst = *src
 * bitmap_and(dst, src1, src2, nbits)		*dst = *src1 & *src2
 * bitmap_or(dst, src1, src2, nbits)		*dst = *src1 | *src2
 * bitmap_xor(dst, src1, src2, nbits)		*dst = *src1 ^ *src2
 * bitmap_andnot(dst, src1, src2, nbits)	*dst = *src1 & ~(*src2)
 * bitmap_complement(dst, src, nbits)		*dst = ~(*src)
 * bitmap_equal(src1, src2, nbits)		Are *src1 and *src2 equal?
 * bitmap_intersects(src1, src2, nbits) 	Do *src1 and *src2 overlap?
 * bitmap_subset(src1, src2, nbits)		Is *src1 a subset of *src2?
 * bitmap_empty(src, nbits)			Are all bits zero in *src?
 * bitmap_full(src, nbits)			Are all bits set in *src?
 * bitmap_weight(src, nbits)			Hamming Weight: number set bits
 * bitmap_set(dst, pos, nbits)			Set specified bit area
 * bitmap_clear(dst, pos, nbits)		Clear specified bit area
 * bitmap_find_next_zero_area(buf, len, pos, n, mask)	Find bit free area
 * bitmap_shift_right(dst, src, n, nbits)	*dst = *src >> n
 * bitmap_shift_left(dst, src, n, nbits)	*dst = *src << n
 * bitmap_remap(dst, src, old, new, nbits)	*dst = map(old, new)(src)
 * bitmap_bitremap(oldbit, old, new, nbits)	newbit = map(old, new)(oldbit)
 * bitmap_onto(dst, orig, relmap, nbits)	*dst = orig relative to relmap
 * bitmap_fold(dst, orig, sz, nbits)		dst bits = orig bits mod sz
 * bitmap_scnprintf(buf, len, src, nbits)	Print bitmap src to buf
 * bitmap_parse(buf, buflen, dst, nbits)	Parse bitmap dst from kernel buf
 * bitmap_parse_user(ubuf, ulen, dst, nbits)	Parse bitmap dst from user buf
 * bitmap_scnlistprintf(buf, len, src, nbits)	Print bitmap src as list to buf
 * bitmap_parselist(buf, dst, nbits)		Parse bitmap dst from kernel buf
 * bitmap_parselist_user(buf, dst, nbits)	Parse bitmap dst from user buf
 * bitmap_find_free_region(bitmap, bits, order)	Find and allocate bit region
 * bitmap_release_region(bitmap, pos, order)	Free specified bit region
 * bitmap_allocate_region(bitmap, pos, order)	Allocate specified bit region
 */

四、参考资料

1、Intel 64 and IA-32 Architectures Software Developer Manuals Volume 2A, Chapter 3 中的 BT、BTS、BTR、BTC、BSF、LOCK 以及 SBB 指令。

2、 GCC 在线文档:Common Predefined MacrosClobbers and Scratch Registers__builtin_constant_p

3、Linux Kernel 源码学习必备知识之:GCC 内联汇编