Linux内核里的数据结构——位数组

Linux 内核中的位数组和位操作

除了不同的基于链式和树的数据结构以外,Linux 内核也为位数组(或称为位图(bitmap))提供了 API。位数组在 Linux 内核里被广泛使用,并且在以下的源代码文件中包含了与这样的结构搭配使用的通用 API:

  • lib/bitmap.c
  • include/linux/bitmap.h

除了这两个文件之外,还有体系结构特定的头文件,它们为特定的体系结构提供优化的位操作。我们将探讨 x86_64 体系结构,因此在我们的例子里,它会是

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

头文件。正如我上面所写的,位图在 Linux 内核中被广泛地使用。例如,位数组常常用于保存一组在线/离线处理器,以便系统支持热插拔的 CPU(你可以在 cpumasks 部分阅读更多相关知识 ),一个位数组(bit array)可以在 Linux 内核初始化等期间保存一组已分配的中断处理。

因此,本部分的主要目的是了解位数组(bit array)是如何在 Linux 内核中实现的。让我们现在开始吧。

位数组声明

在我们开始查看位图操作的 API 之前,我们必须知道如何在 Linux 内核中声明它。有两种声明位数组的通用方法。第一种简单的声明一个位数组的方法是,定义一个 unsigned long 的数组,例如:


  1. unsigned long my_bitmap[8] 

第二种方法,是使用 DECLARE_BITMAP 宏,它定义于 include/linux/types.h 头文件:


  1. #define DECLARE_BITMAP(name,bits) \ 
  2.     unsigned long name[BITS_TO_LONGS(bits)] 

我们可以看到 DECLARE_BITMAP 宏使用两个参数:

  • name - 位图名称;
  • bits - 位图中位数;

并且只是使用 BITS_TO_LONGS(bits) 元素展开 unsigned long 数组的定义。 BITS_TO_LONGS 宏将一个给定的位数转换为 long 的个数,换言之,就是计算 bits 中有多少个 8 字节元素:


  1. #define BITS_PER_BYTE           8 
  2. #define DIV_ROUND_UP(n,d) (((n) + (d) - 1) / (d)) 
  3. #define BITS_TO_LONGS(nr)       DIV_ROUND_UP(nr, BITS_PER_BYTE * sizeof(long)) 

因此,例如 DECLARE_BITMAP(my_bitmap, 64) 将产生:


  1. >>> (((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 头文件,你将会注意到这些函数中的每一个都有原子性和非原子性两种变体。在我们开始深入这些函数的实现之前,首先,我们必须了解一些有关原子(atomic)操作的知识。

简而言之,原子操作保证两个或以上的操作不会并发地执行同一数据。x86 体系结构提供了一系列原子指令,例如, xchg、cmpxchg 等指令。除了原子指令,一些非原子指令可以在 lock 指令的帮助下具有原子性。现在你已经对原子操作有了足够的了解,我们可以接着探讨 set_bit 和 clear_bit 函数的实现。

我们先考虑函数的非原子性(non-atomic)变体。非原子性的 set_bit 和 clear_bit 的名字以双下划线开始。正如我们所知道的,所有这些函数都定义于 arch/x86/include/asm/bitops.h 头文件,并且第一个函数就是 __set_bit:


  1. static inline void __set_bit(long nr, volatile unsigned long *addr) 
  2.     asm volatile("bts %1,%0" : ADDR : "Ir" (nr) : "memory"); 

正如我们所看到的,它使用了两个参数:

  • nr - 位数组中的位号(LCTT 译注:从 0 开始)
  • addr - 我们需要置位的位数组地址

注意,addr 参数使用 volatile 关键字定义,以告诉编译器给定地址指向的变量可能会被修改。 __set_bit 的实现相当简单。正如我们所看到的,它仅包含一行内联汇编代码。在我们的例子中,我们使用 bts 指令,从位数组中选出一个第一操作数(我们的例子中的 nr)所指定的位,存储选出的位的值到 CF 标志寄存器并设置该位(LCTT 译注:即 nr 指定的位置为 1)。

注意,我们了解了 nr 的用法,但这里还有一个参数 addr 呢!你或许已经猜到秘密就在 ADDR。 ADDR 是一个定义在同一个头文件中的宏,它展开为一个包含给定地址和 +m 约束的字符串:


  1. #define ADDR                BITOP_ADDR(addr) 
  2. #define BITOP_ADDR(x) "+m" (*(volatile long *) (x)) 

除了 +m 之外,在 __set_bit 函数中我们可以看到其他约束。让我们查看并试着理解它们所表示的意义:

  • +m - 表示内存操作数,这里的 + 表明给定的操作数为输入输出操作数;
  • I - 表示整型常量;
  • r - 表示寄存器操作数

除了这些约束之外,我们也能看到 memory 关键字,其告诉编译器这段代码会修改内存中的变量。到此为止,现在我们看看相同的原子性(atomic)变体函数。它看起来比非原子性(non-atomic)变体更加复杂:


  1. static __always_inline void 
  2. set_bit(long nr, volatile unsigned long *addr) 
  3.     if (IS_IMMEDIATE(nr)) { 
  4.         asm volatile(LOCK_PREFIX "orb %1,%0" 
  5.             : CONST_MASK_ADDR(nr, addr) 
  6.             : "iq" ((u8)CONST_MASK(nr)) 
  7.             : "memory"); 
  8.     } else { 
  9.         asm volatile(LOCK_PREFIX "bts %1,%0" 
  10.             : BITOP_ADDR(addr) : "Ir" (nr) : "memory"); 
  11.     } 

(LCTT 译注:BITOP_ADDR 的定义为:#define BITOP_ADDR(x) "=m" (*(volatile long *) (x)),ORB 为字节按位或。)

首先注意,这个函数使用了与 __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 指令去设置位。我们可以只需在给定地址指向的字节上执行 按位或 操作,其字节包含给定的位,掩码位数表示高位为 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. >>> hex(0x1000 + (0x9 >> 3)) 
  2. '0x1001' 

CONST_MASK 宏将我们给定的位号表示为字节,位号对应位为高位 1,其他位为 0:


  1. #define CONST_MASK(nr)          (1 << ((nr) & 7)) 

  1. >>> bin(1 << (0x9 & 7)) 
  2. '0b10' 

最后,我们应用 按位或 运算到这些变量上面,因此,假如我们的地址是 0x4097 ,并且我们需要置位号为 9 的位为 1:


  1. >>> bin(0x4097) 
  2. '0b100000010010111' 
  3. >>> bin((0x4097 >> 0x9) | (1 << (0x9 & 7))) 
  4. '0b100010' 

第 9 位 将会被置位。(LCTT 译注:这里的 9 是从 0 开始计数的,比如0010,按照作者的意思,其中的 1 是第 1 位)

注意,所有这些操作使用 LOCK_PREFIX 标记,其展开为 lock 指令,保证该操作的原子性。

正如我们所知,除了 set_bit 和 __set_bit 操作之外,Linux 内核还提供了两个功能相反的函数,在原子性和非原子性的上下文中清位。它们是 clear_bit 和 __clear_bit。这两个函数都定义于同一个头文件 并且使用相同的参数集合。不仅参数相似,一般而言,这些函数与 set_bit 和 __set_bit 也非常相似。让我们查看非原子性 __clear_bit 的实现吧:


  1. static inline void __clear_bit(long nr, volatile unsigned long *addr) 
  2.     asm volatile("btr %1,%0" : ADDR : "Ir" (nr)); 

没错,正如我们所见,__clear_bit 使用相同的参数集合,并包含极其相似的内联汇编代码块。它只是使用 btr 指令替换了 bts。正如我们从函数名所理解的一样,通过给定地址,它清除了给定的位。btr 指令表现得像 bts(LCTT 译注:原文这里为 btr,可能为笔误,修正为 bts)。该指令选出第一操作数所指定的位,存储它的值到 CF 标志寄存器,并且清除第二操作数指定的位数组中的对应位。

__clear_bit 的原子性变体为 clear_bit:


  1. static __always_inline void 
  2. clear_bit(long nr, volatile unsigned long *addr) 
  3.     if (IS_IMMEDIATE(nr)) { 
  4.         asm volatile(LOCK_PREFIX "andb %1,%0" 
  5.             : CONST_MASK_ADDR(nr, addr) 
  6.             : "iq" ((u8)~CONST_MASK(nr))); 
  7.     } else { 
  8.         asm volatile(LOCK_PREFIX "btr %1,%0" 
  9.             : BITOP_ADDR(addr) 
  10.             : "Ir" (nr)); 
  11.     } 

并且正如我们所看到的,它与 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 调用。


  1. #define test_bit(nr, addr)          \ 
  2.     (__builtin_constant_p((nr))                 \ 
  3.      ? constant_test_bit((nr), (addr))          \ 
  4.      : variable_test_bit((nr), (addr))) 

因此,如果 nr 是编译期已知常量,test_bit 将展开为 constant_test_bit 函数的调用,而其他情况则为 variable_test_bit。现在让我们看看这些函数的实现,让我们从 variable_test_bit 开始看起:


  1. static inline int variable_test_bit(long nr, volatile const unsigned long *addr) 
  2.     int oldbit; 
  3.     asm volatile("bt %2,%1\n\t" 
  4.              "sbb %0,%0" 
  5.              : "=r" (oldbit) 
  6.              : "m" (*(unsigned long *)addr), "Ir" (nr)); 
  7.     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 所看到的一样的事:


  1. static __always_inline int constant_test_bit(long nr, const volatile unsigned long *addr) 
  2.     return ((1UL << (nr & (BITS_PER_LONG-1))) & 
  3.         (addr[nr >> _BITOPS_LONG_SHIFT])) != 0; 

它生成了一个位号对应位为高位 1,而其他位为 0 的字节(正如我们在 CONST_MASK 所看到的),并将 按位与 应用于包含给定位号的字节。

下一个被广泛使用的位数组相关操作是改变一个位数组中的位。为此,Linux 内核提供了两个辅助函数:

  • __change_bit;
  • change_bit.

你可能已经猜测到,就拿 set_bit 和 __set_bit 例子说,这两个变体分别是原子和非原子版本。首先,让我们看看 __change_bit 函数的实现:


  1. static inline void __change_bit(long nr, volatile unsigned long *addr) 
  2.     asm volatile("btc %1,%0" : ADDR : "Ir" (nr)); 

相当简单,不是吗? __change_bit 的实现和 __set_bit 一样,只是我们使用 btc 替换 bts 指令而已。 该指令从一个给定位数组中选出一个给定位,将该为位的值存进 CF 并使用求反操作改变它的值,因此值为 1 的位将变为 0,反之亦然:


  1. >>> int(not 1) 
  2. >>> int(not 0) 

__change_bit 的原子版本为 change_bit 函数:


  1. static inline void change_bit(long nr, volatile unsigned long *addr) 
  2.     if (IS_IMMEDIATE(nr)) { 
  3.         asm volatile(LOCK_PREFIX "xorb %1,%0" 
  4.             : CONST_MASK_ADDR(nr, addr) 
  5.             : "iq" ((u8)CONST_MASK(nr))); 
  6.     } else { 
  7.         asm volatile(LOCK_PREFIX "btc %1,%0" 
  8.             : BITOP_ADDR(addr) 
  9.             : "Ir" (nr)); 
  10.     } 

它和 set_bit 函数很相似,但也存在两点不同。第一处差异为 xor 操作而不是 or。第二处差异为 btc( LCTT 译注:原文为 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 宏:


  1. #define for_each_set_bit(bit, addr, size) \ 
  2.     for ((bit) = find_first_bit((addr), (size));        \ 
  3.          (bit) < (size);                    \ 
  4.          (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 函数的实现:


  1. static inline void bitmap_zero(unsigned long *dst, unsigned int nbits) 
  2.     if (small_const_nbits(nbits)) 
  3.         *dst = 0UL; 
  4.     else { 
  5.         unsigned int len = BITS_TO_LONGS(nbits) * sizeof(unsigned long); 
  6.         memset(dst, 0, len); 
  7.     } 

首先我们可以看到对 nbits 的检查。 small_const_nbits 是一个定义在同一个头文件 的宏:


  1. #define small_const_nbits(nbits) \ 
  2.     (__builtin_constant_p(nbits) && (nbits) <= BITS_PER_LONG) 

正如我们可以看到的,它检查 nbits 是否为编译期已知常量,并且其值不超过 BITS_PER_LONG 或 64。如果位数目没有超过一个 long 变量的位数,我们可以仅仅设置为 0。在其他情况,我们需要计算有多少个需要填充位数组的 long 变量并且使用 memset 进行填充。

bitmap_fill 函数的实现和 biramp_zero 函数很相似,除了我们需要在给定的位数组中填写 0xff 或 0b11111111:


  1. static inline void bitmap_fill(unsigned long *dst, unsigned int nbits) 
  2.     unsigned int nlongs = BITS_TO_LONGS(nbits); 
  3.     if (!small_const_nbits(nbits)) { 
  4.         unsigned int len = (nlongs - 1) * sizeof(unsigned long); 
  5.         memset(dst, 0xff,  len); 
  6.     } 
  7.     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 头文件。

本部分到此为止。

作者:0xAX

来源:51CTO

时间: 2024-09-23 17:38:44

Linux内核里的数据结构——位数组的相关文章

Linux 内核里的数据结构——位数组

Linux 内核中的位数组和位操作 除了不同的基于链式和树的数据结构以外,Linux 内核也为位数组(或称为位图bitmap)提供了 API.位数组在 Linux 内核里被广泛使用,并且在以下的源代码文件中包含了与这样的结构搭配使用的通用 API: lib/bitmap.c include/linux/bitmap.h 除了这两个文件之外,还有体系结构特定的头文件,它们为特定的体系结构提供优化的位操作.我们将探讨x86_64 体系结构,因此在我们的例子里,它会是 arch/x86/include

Linux 内核里的数据结构——基数树

Linux 内核里的数据结构--基数树 正如你所知道的,Linux内核提供了许多不同的库和函数,它们实现了不同的数据结构和算法.在这部分,我们将研究其中一种数据结构--基数树Radix tree.在 Linux 内核中,有两个文件与基数树的实现和API相关: include/linux/radix-tree.h lib/radix-tree.c 让我们先说说什么是 基数树 吧.基数树是一种 压缩的字典树compressed trie ,而字典树是实现了关联数组接口并允许以键值对 方式存储值的一种

Linux 内核里的数据结构——双向链表

Linux 内核里的数据结构--双向链表 Linux 内核中自己实现了双向链表,可以在 include/linux/list.h 找到定义.我们将会首先从双向链表数据结构开始介绍内核里的数据结构.为什么?因为它在内核里使用的很广泛,你只需要在 free-electrons.com 检索一下就知道了. 首先让我们看一下在 include/linux/types.h 里的主结构体: struct list_head { struct list_head *next, *prev; }; 你可能注意到

Linux 内核里的“智能指针”【转】

转自:http://blog.jobbole.com/88279/ 众所周知,C/C++语言本身并不支持垃圾回收机制,虽然语言本身具有极高的灵活性,但是当遇到大型的项目时,繁琐的内存管理往往让人痛苦异常.现代的C/C++类库一般会提供智能指针来作为内存管理的折中方案,比如STL的auto_ptr,Boost的Smart_ptr库,QT的QPointer家族,甚至是基于C语言构建的GTK+也通过引用计数来实现类似的功能.Linux内核是如何解决这个问题呢?同样作为C语言的解决方案,Linux内核采

Linux内核里的DebugFS

DebugFS,顾名思义,是一种用于内核调试的虚拟文件系统,内核开发者通过debugfs和用户空间交换数据.类似的虚拟文件系统还有procfs和sysfs等,这几种虚拟文件系统都并不实际存储在硬盘上,而是Linux内核运行起来后才建立起来. 通常情况下,最常用的内核调试手段是printk.但printk并不是所有情况都好用,比如打印的数据可能过多,我们真正关心的数据在大量的输出里不是那么一目了然;或者我们在调试时可能需要修改某些内核变量,这种情况下printk就无能为力,而如果为了修改某个值重新

Offset2lib 攻击绕过 64 位 Linux 内核防护

安全研究人员Hector Marco发布公布了针对64位Linux内核的ASLR防护的绕过,slide和论文提 供下载,这种攻击被称为offset2lib,ASLR的实现是在Linux内核里,而在GNU/Linux发行版上加载一个应用程序到内存中会先找一个 随机的地址加载第一个共享库,之后加载的共享库紧跟之前加载对象的地址,一个内存地址的泄漏就足够让地址随机化变得无用,甚至GOT(Global Offset Table)的地址泄漏都不是必须的,由于大部分的GNU/Linux发行版(除了像Gent

Linux内核剖析 之 内核同步

主要内容     1.内核请求何时以交错(interleave)的方式执行以及交错程度如何.     2.内核所实现的基本同步机制.     3.通常情况下如何使用内核提供的同步机制. 内核如何为不同的请求服务     哪些服务?     ====>>>     为了更好地理解内核是如何执行的,我们把内核看做必须满足两种请求的侍者:一种请求来自顾客,另一种请求来自数量有限的几个不同的老板.对于不同的请求,侍者采用如下的策略:     1.老板提出请求时,如果侍者空闲,则侍者开始为老板服务

深度剖析linux内核万能--双向链表,Hash链表模版

我们都知道,链表是数据结构中用得最广泛的一种数据结构,对于数据结构,有顺序存储,数组就是一种.有链式存储,链表算一种.当然还有索引式的,散列式的,各种风格的说法,叫法层出不穷,但是万变不离其中,只要知道什么场合用什么样的数据结构,那就行了. 那么,标题说的内核万能链表,其实就是内核链表,它到底和我们平常大学学的数据结构的链表有什么不同呢??内核链表,是在linux内核里的一种普遍存在的数据结构,比如内核调度算法中有这样的结构,摄像头驱动程序,wifi模块,G_sensor等等,用到链表的东西实在

解析Linux内核的基本的模块管理与时间管理操作---超时处理【转】

转自:http://www.jb51.net/article/79960.htm 这篇文章主要介绍了Linux内核的基本的模块管理与时间管理操作,包括模块加载卸载函数的使用和定时器的用法等知识,需要的朋友可以参考下   内核模块管理Linux设备驱动会以内核模块的形式出现,因此学会编写Linux内核模块编程是学习linux设备驱动的先决条件. Linux内核的整体结构非常庞大,其包含的组件非常多.我们把需要的功能都编译到linux内核,以模块方式扩展内核功能. 先来看下最简单的内核模块 ? 1