写在前面
宗旨:把话说清楚,把道理讲透彻。
约定:所有代码均来自Linux内核2.6.24版。
建议:本文介绍得十分详细,但也略显繁琐,读者可以先看“Ⅴ.总结”部分带注释的源码,如果哪里不清楚,再回头看详细解释。
正文
预备知识
位图:
在Linux下,从数据结构上看,位图本质上是一个数组,数组的每个元素都是long型的(即32bit或64bit)。
假设在32位系统下,某long型数组有128个元素,那么,从逻辑上看,这个数组就是一个128行×32列的bit阵列,就是所谓的位图,见下面的示意图。
上图中的数字就是各个bit位的标号,即索引。
对于位图的操作,也就是对位图中bit位的操作。
从作用上说,位图通常与其它数据相关联,用位图中的bit位对该数据进行统计或管理。
例如,在文件系统中,每个进程都有一个元素为file指针的数组(为表述方便,后面称之为数组A),同时,也有一个位图,位图中有效bit位(何为有效bit位,后面会详述)的个数与数组A中元素的个数相同。当进程打开一个文件时,要先在位图中找一个为0的bit位,然后将该位置1,返回该bit位的索引fd。当内核创建了与要打开文件对应的file实例后,会使数组A中索引为fd的元素(注意,前边说过,该元素是一个file类型的指针)指向该file实例。这里的索引fd,就是我们平常所说的文件描述符。
正题
Ⅰ.源码
先给出find_next_bit的源码(源码来自内核2.6.24,位置:/lib/find_next_bit.c)
1 unsigned long find_next_bit(const unsigned long *addr, unsigned long size, 2 unsigned long offset) 3 { 4 const unsigned long *p = addr + BITOP_WORD(offset); 5 unsigned long result = offset & ~(BITS_PER_LONG-1); 6 unsigned long tmp; 7 8 if (offset >= size) 9 return size; 10 size -= result; 11 offset %= BITS_PER_LONG; 12 if (offset) { 13 tmp = *(p++); 14 tmp &= (~0UL << offset); 15 if (size < BITS_PER_LONG) 16 goto found_first; 17 if (tmp) 18 goto found_middle; 19 size -= BITS_PER_LONG; 20 result += BITS_PER_LONG; 21 } 22 while (size & ~(BITS_PER_LONG-1)) { 23 if ((tmp = *(p++))) 24 goto found_middle; 25 result += BITS_PER_LONG; 26 size -= BITS_PER_LONG; 27 } 28 if (!size) 29 return result; 30 tmp = *p; 31 32 found_first: 33 tmp &= (~0UL >> (BITS_PER_LONG - size)); 34 if (tmp == 0UL) 35 return result + size; 36 found_middle: 37 return result + __ffs(tmp); 38 }
Ⅱ.功能——参数——返回值
功能:在addr指向的位图中,从索引为offset的bit位(包括该位)开始,找到第一个为1的bit位,返回该位的索引。
参数:
@addr:位图(数组)的起始地址。
@size:位图的大小,即位图中有效bit位的个数。注意,Linux内核实际调用该函数时,该参数的值不一定是32的整数倍(32位系统下)。假设构成位图的数组大小为3,即一共有96个bit,但函数调用时,参数size可 能是90,那么,从逻辑上说,数组最后一个元素的最后6位是不参与构成位图的,即它们不是位图的组成部分,是“无效”的;而前边的90个bit共同构成了位图,它们是“有效”的。注意,后面解释中经常会用 到“有效位”和“无效位”的概念,对此,读者一定要理解清楚。
@offset:查找起点。即从位图中索引为offset的位(包括该位)开始,查找第一个为1的bit位,offset之前的bit位不在搜索范围之内。“查找起点”这个概念在后面的叙述中经常会用到,希望读者能理解清楚。
返回值:找到的bit位的索引。
Ⅲ.BITS_PER_LONG和BITOP_WORD
3.1 BITS_PER_LONG
顾名思义,BITS_PER_LONG是指一个long型数据中bit位的个数,看源码
/*include/asm-x86/types.h*/ #ifdef CONFIG_X86_32 # define BITS_PER_LONG 32 #else # define BITS_PER_LONG 64 #endif
可见,32位系统下,它是32;64位系统下,它是64。
3.2 BITOP_WORD
不多说,直接看源码
/*lib/find_next_bit.c*/ #define BITOP_WORD(nr) ((nr) / BITS_PER_LONG)
就是参数nr除以32或64。
Ⅳ.一句一句解释
注:在后面的所有解释中,我们按32位系统讲解,即BITS_PER_LONG取32。
a.
const unsigned long *p = addr + BITOP_WORD(offset);
该句的作用是使p指向数组中索引为offset的bit位(即查找起点)所在的元素。
我们知道,位图的0~31位在数组第0个元素中, 32~63位在第1个元素中,等等(如图1)。假设offset=33,则BITOP_WORD(offset)就是33/32,结果为1,那addr + BITOP_WORD(offset)就是addr+1,即指向数组第1个元素,就是索引为33的bit位所在的元素。
b.
unsigned long result = offset & ~(BITS_PER_LONG-1);
BITS_PER_LONG-1就是31,二进制形式就是0001 1111(这里为了表述方便,采用8位二进制),再取反就是1110 0000,即低5位全0,剩下的都是1。假设offset=70D=0100 0110B,那么,
offset & ~(BITS_PER_LONG-1)就是0100 0110 & 1110 0000 =0100 0000=64。其实,就是将offset的低5位置0,高位保持不变。这个结果有什么意义呢?其实就是:索引为offset的bit位所在的元素前的元素中bit位的个数,如下图所示。很显然,这些bit位都位于查找起点的前面,即它们都不在搜索范围之内,我们可以将它们理解为“已处理”的bit位。请读者牢记,在find_next_bit函数中,result总是表示“已处理”的bit位总数,并且,很显然,它一定是32的整数倍。
c.
if (offset >= size) return size;
如果查找的起始位置offset大于等于位图的总大小,直接返回位图大小。这里解释一下问什么要有等于,假设offset=size=64,但实际上,这种情况下,合法的索引是0~63,所以,值为64的offset不合法。问题的根源在于offset是从0算起的,而size是从1算起的。另外,个人认为这两句应该放在函数体的最前面,这样一来,只要if条件成立,就直接返回,后面的工作就都不用做了。而按照源码的写法,是无论如何都要执行前两句的,但如果此时if条件成立,前边的工作就白做了,这不是一个高效的安排。
d.
size -= result;
offset %= BITS_PER_LONG;
注意,从这两句开始,size和offset的含义和传参时的含义就不同了。
size-=result就是size=size-result,前边说过,此时的result表示查找起点所在数组元素前的元素中bit位的个数,就是“已处理”的bit位个数,我们要找的bit位一定在这后边;而size表示位图总位数。两者相减的结果,就是“未处理的、待查找的”bit位个数。请读者牢记,此后的代码中,result表示“已处理”的bit位总数,而size表示“待处理”的bit位总数,两者的加和,一定等于传参时size的值。
offset %= BITS_PER_LONG执行之后的offset也不再表示查找起点的索引,而表示查找起点在它所在的元素中是第几个。假设原来offset=32,则执行该句之后,offset的值变成了0,而从前面的示意图中我们可以看到,索引为32的bit位正是在数组的第1个元素(从0算起)的第0位(从0算起)。所以,我们可以这样说,代码执行前,offset是查找起点在整个位图中的索引;代码执行后,offset是查找起点在它所在的数组元素中的索引。
e.
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; }
进入这个if的条件是offset不为0,通过前面的分析,就是查找起点不在所在数组元素的首位(我们前边举的例子就是在首位的情况)。好了,开始分析里面的代码,看看这个if做了什么。
e_1
tmp = *(p++);
这句得到的是查找起点所在的数组元素的值,也就是那32个bit。tmp是unsigned long型的,在文章开头给出的源码的第6行定义。注意:
1.表达式(p++)得到的是p的值(后置++),之后指针p再自增1,指向下一个数组元素。
2.tmp只是构成位图的数组元素的拷贝而不是数组元素本身,即,对tmp所进行的任何修改,都不会影响到原位图。
e_2
tmp &= (~0UL << offset);
首先,0UL就是unsigned long型的数字0,从二进制的角度看,就是32位全0;然后,对它取反,就是32位全1;接着,再左移offset位,假设offset=3,移位后就是1111 1000(为表述方便,这里只写8位);最后,再和tmp(即查找起点所在的数组元素的值)相与,结果就是tmp的低3位置0,其余位保持不变。为什么要这么做呢?因为offset=3,它前面的第2位、第1位、第0位其实都不在搜索范围之内,所以,我们将它们置为0(当然,e_1中就说过,这只是在拷贝上操作,不会影响到原位图),这主要是为后面的工作做准备(读者看到e_4小节就会自然明了)。所以,这句代码的用意是:在查找起点所在的元素中,将查找起点前的bit位置0。代码之所以要将查找起点是否在数组元素的第0位区别对待,就是因为若查找起点不在数组元素首位,需要将查找起点前的bit位置0。
e_3
if (size < BITS_PER_LONG) goto found_first;
通过前面的分析,我们知道,此时的size表示的是“待处理”的bit位总数,而现在,这个数字小于32,这说明:
1.函数调用时传入的参数不是32的整数倍,构成位图的数组的最后一个元素中含有“无效位”,这一点,在讲解函数参数时就详细解释过;
2.查找起点就在数组的最后一个元素中!
上面的图2就展示了这种情况,假设函数调用时传入的参数size是86,通过d中的分析,我们知道,此时size的值是86-64=22,满足if中的条件,而显然,这种情况下,查找起点(索引为70的bit位)正是在最后一个数组元素中。
在这种情况下,代码要转到found_first处。我们继续跟踪,看看found_first干了些什么。
e_4
found_first: tmp &= (~0UL >> (BITS_PER_LONG - size)); if (tmp == 0UL) return result + size;
首先,我们要知道,代码能走到这里,存在两个前提:
1.查找起点在数组的最后一个元素中
2.最后一个元素中存在“无效位”
通过e_3中的分析,我们知道,此时的size表示的是“待处理”的bit位的个数,同时,它也表示最后一个元素中“有效位”的个数。那么,很显然,BITS_PER_LONG-size就是“无效位”的个数,为了表述方便,我们假设该值位3,那么~0UL >> (BITS_PER_LONG - size)得到的就是这样的32个bit位:最左3位为0,其余位为1。然后,这32个bit位再与tmp(即最后一个数组元素的拷贝)相与,结果就是:将该元素中的“无效位”都置为0了(注意,对32位的long型数据来说,低位、索引小的位在右,高位、索引大的位在左,所以,“无效位“一定是在最左边的)!
回顾一下e_2,在e_2中,将元素中查找起点之前的位都置成了0,而现在,又将“无效位”都置成了0,如下图
那么,剩下的是什么呢?剩下的就是查找范围,即我们要在上图中的白色部分找第一个位1的bit位。
现在,让我们思考这样一个问题:如果此时最后一个数组元素,即tmp的值为0(即if (tmp == 0UL)),说明了什么呢?细思,极恐,这说明查找范围内全是0!因为红色部分和黄色部分早就置为0了,而现在整个元素的值为0,那结论只有一个——白色部分全是0!
而我们可能会马上想到这样一个不幸事实:这已经是位图中的最后一个数组元素了!
于是,我们就会得到这样一个令人绝望的结论:以函数参数offset为查找起点,在当前位图中找到一个值为1的bit位,已经不可能了!
在这种情况下,函数只好无奈地return result + size了,d中说过,result + size的值,一定等于传参时size的值,即数组的总大小。
这,就是e_4中代码的含义。
那么,如果我们的运气没那么差,if (tmp == 0UL)没有被执行,也就是说,白色部分有1存在!进而我们就会欣喜地想到:这样,就一定能找到满足条件的bit位!那么,具体怎么找呢?看了最初的源码我们会发现,代码走到了found_middle标签下。好吧,让我们来看看found_middle下有什么。
e_5
found_middle: return result + __ffs(tmp);
这里出现了一个新的函数__ffs,它定义在include/asm-generic/bitops/__fss.h中,我们先来看一下这个函数
1 /** 2 * __ffs - find first bit in word. 3 * @word: The word to search 4 * 5 * Undefined if no bit exists, so code should check against 0 first. 6 */ 7 static inline unsigned long __ffs(unsigned long word) 8 { 9 int num = 0; 10 11 /*如果BITS_PER_LONG等于64,要先对低32位进行检查;否则(即BITS_PER_LONG等于32),直接对低16位进行检查*/ 12 #if BITS_PER_LONG == 64 13 if ((word & 0xffffffff) == 0) { //如果与0xffffffff相与得0,说明word的低32位全为0 14 num += 32; //索引加32 15 word >>= 32; //word右移32位,把低32位的0移走 16 } 17 #endif 18 if ((word & 0xffff) == 0) { 19 num += 16; 20 word >>= 16; 21 } 22 if ((word & 0xff) == 0) { 23 num += 8; 24 word >>= 8; 25 } 26 if ((word & 0xf) == 0) { 27 num += 4; 28 word >>= 4; 29 } 30 if ((word & 0x3) == 0) { 31 num += 2; 32 word >>= 2; 33 } 34 if ((word & 0x1) == 0) 35 num += 1; 36 return num; 37 }
该函数的功能是在参数word中找到第一个值为1的bit位,返回该位的索引。可以看到,该函数没有对参数word=0的情况(这意味着不可能在word中找到值为1的bit位)进行检查,所以我们应该在调用该函数前对传入的参数是否为0进行检查,这就是上面代码中第5行的英文注释的意思,而该函数的调用者find_next_bit在调用该函数前已经进行过检查了,正如我们在e_4中分析的那样。
该函数的算法还是很巧妙的,它类似于“折半查找”。假设参数word是32位的,先执行word & 0xffff,析取低16位,这时可能出现两种结果:
1.如果结果为0,说明低16位(0~15位)全为0,那么,可能为1的bit位最小是第16位,所以num += 16,并且,执行word >>= 16,将全0的低16位移走,这样,可能有1存在的高16位变成了低16位;接着,如法炮制,word与0xff相与,对剩下的16位进行“折半查找”。
2.如果结果不为0,说明低16位中有1存在,由于我们是要找第一个为1的bit位,所以,高16位就不用看了,直接在低16位中寻找,所以,下一步就是执行word & 0xff,对低16位进行“折半查找”。
可见,无论word & 0xffff的结果是否为0,word & 0xff都是要执行的,只不过,如果word & 0xffff结果为0,需要进行增加计数和右移的工作。
按照上面的步骤,逐步进行“折半查找”,最终就能得到word中第一个为1的bit位的索引。读者可以自己举个例子,手动执行一下__ffs函数,就更加清楚了,这里不再赘述。
有一点大家要意识到,find_next_bit在调用__ffs时传入的参数是tmp,而在e_4中我们看到,tmp中查找起点前的bit位和“无效位”都被置为0了,并且,也已经判断出tmp不为0,所以,__ffs一定能找到为1的bit位且保证该bit位在合法的搜索空间(图3的白色区域)内。
现在我们再来看found_middle下的那句return result + __ffs(tmp)。__ffs(tmp)得到的是tmp中自查找起点起第一个为1的bit位的索引,而result表示的是位图中tmp之前的所有元素中bit位的总和,所以,两者相加,就是我们要找的bit位在位图中的索引,即find_next_bit的最终结果,于是,将这个结果return。
至此,find_next_bit的查找工作就结束了,但是,我们对find_next_bit的分析还没结束。不知读者是否还记得,我们是从e_3中if (size < BITS_PER_LONG)成立这个“路口”进入,一路追踪,才走到这里来的。所以我们还要分析if (size < BITS_PER_LONG)不成立的情况,于是,让我们再次回到最初的源码……
e_6
if (tmp) goto found_middle;
如果if (size < BITS_PER_LONG)不成立,就会执行上面的代码。if (size < BITS_PER_LONG)不成立,说明了什么呢?这说明tmp不是数组的最后一个元素,因此就不可能有“无效位”,也就不存在将“无效位”置为0的问题(即不用goto found_first了),又因为,在e_4中,已经将查找起点前的bit位都置0了,所以,这里直接判断tmp是否为0,如不为0,说明在这个tmp中一定能找到为1的bit位,所以,转到found_middle,找到第一个为1的bit位在位图中的索引,然后返回结果。
那么,如果这里的tmp为0呢,该执行什么样的代码,我们往下看。
e_7
size -= BITS_PER_LONG;
result += BITS_PER_LONG;
如果tmp为0,说明在当前数组元素中不可能找到为1的bit位,于是需要在下一个数组元素中寻找,在此之前,将“未处理”bit位总数减去32,将“已处理”bit位总数增加32。有读者可能会问:怎么没见指针后移呢?不要忘了,在e_1中,这个工作已经做过了。
至此,整个e中的代码就都分析完了 。但是,革命尚未成功,喘口气,我们还得往下看。
f
while (size & ~(BITS_PER_LONG-1)) { if ((tmp = *(p++))) goto found_middle; result += BITS_PER_LONG; size -= BITS_PER_LONG;
}
首先要明白,有两种情况代码会走到这里:
1.这个while循环上面的if(offset)(就是e中的代码)条件不成立(即查找起点位于数组元素的第0位),if中的语句体没有被执行。
2.if(offset)的语句体被执行了,但该语句体内部的两个if条件都不成立,这又分为两种情况:
1.查找起点所在的数组元素不是数组的最后一个元素,并且,该元素中全是0,没有1。
2.查找起点是数组最后一个元素,但该元素中没有“无效位”,并且,该元素中全是0,没有1。
在上面的两种情况下,我们就要执行上面的while循环,在后面的数组元素中依次查找。
我们先来看一下循环进入条件
while (size & ~(BITS_PER_LONG-1))
我们将BITS_PER_LONG换成32,并转换为二进制形式,上面的语句就变成了while(size & 1110 0000),这是什么呢?其实就是while(size>=32)!因为小于32(0010 0000B)的无符号数(注意size是unsigned long型,请看最初的源码)与1110 0000相与的结果都是0000 0000,而大于等于32的无符号数与1110 0000相与的结果都将大于0010 0000。好了,现在我们知道了,上面那句故弄玄虚的while语句其实就是while(size>=32)。
好了,现在我们来看循环体。先看第一句
if ((tmp = *(p++)))
这句代码依次做了三件事:
1.将p指向的数组元素(当然也是我们要进行查找的数组元素)复制到tmp
2.p指针自增,指向下一元素
3.判断tmp是否为0
这里需要解释一下:
1.如果查找起点在一个数组元素的第0位,那么,e中的if语句体就不会被执行,也就不会执行到if语句体中的tmp = *(p++),所以当第一次进入while循环并执行if ((tmp = *(p++)))时,tmp就是查找起点所在的数组元素的拷贝。
2.如果查找起点不在一个数组元素的第0位,e中的if语句体就会被执行,当第一次进入while循环并执行if ((tmp = *(p++)))时,tmp就是if语句体中那个tmp对应的元素的下一个元素的拷贝。
现在让我们在整体看一下这个循环体
if ((tmp = *(p++))) goto found_middle; result += BITS_PER_LONG; size -= BITS_PER_LONG;
如果tmp不为0,说明我们要找的为1的bit位一定在这个tmp中,于是转到found_middle,进行查找并返回结果,find_next_bit函数结束;如果tmp等于0,说明该元素中不含1,将“已处理”位数增加32,“待处理”位数减少32,然后判断循环条件(“待查找”位数是否大于32),若条件成立,进入循环体,对下一个数组元素进行查找,否则,退出循环。
如果上面的while循环是正常退出(即由于size小于32而退出)的,那就说明整个整个while循环都没找到为1的bit位(否则,代码将从循环体转到found_middle,while循环将不会正常退出)。如果是这种情况,接下来该怎么办呢?我们还是继续看源码吧。
g
if (!size) return result; tmp = *p; found_first: tmp &= (~0UL >> (BITS_PER_LONG - size)); if (tmp == 0UL) return result + size; found_middle: return result + __ffs(tmp);
首先要明白,代码走到这里,有四种情况:
1.查找起点位于数组的最后一个元素中,且位于该元素的第0位,同时,该元素含有“无效位”,即传入的参数size不是32的整数倍,并且,执行完d中的代码后,size小于32(这种情况容易被忽略)。在这种情况下,e中的if语句和f中的while循环都不会被执行,代码直接从d处走到g处。
2.代码执行完e中的if后不再满足while循环的条件,直接走到g中代码处。这种情况是:查找起点在倒数第二个元素中且不位于第0位且该元素32位全是0,同时,最后一个数组元素中有“无效位”(传入参数size不是32的整数倍)。
3.while循环由于size=0而退出(传入参数恰好是32的整数倍),这种情况下,整个位图都查找过了且并没有找到为1的bit位,换句话说,不可能再找到了。
4.while循环由于size介于1~31之间而退出,即数组的最后一个元素中有“无效位”(传入参数size不是32的整数倍)。
下面让我们看看,在这四种情况下,代码是怎么做的:
首先,代码先判断size是否等于0,若等于0,直接返回result,这正对应了上面的情况3。注意,我们前面就说过,size+result的值总是等于传参时size的值(位图总大小),而此时size=0,所以,返回result,就是返回位图总大小。
如果size不等于0,那就对应1、2、4三种情况,这三种情况的共同点是:都需要在数组的最后一个元素中查找,并且该元素中含有“无效位”。在这种情形下,find_next_bit函数的处理是:
1.将最后一个数组元素拷贝到tmp
2.在found_first中,将“无效位”置0,检验tmp是否为0,若是,返回位图总大小,否则,进入found_middle
3.在found_middle中,找到tmp中第一个为1的bit位的索引,返回该位在位图中的索引
至此,find_next_bit函数结束。
Ⅴ.总结
至此,我们对find_next_bit函数的分析就全部完成了。我们可以看到:
1.若函数查找成功,返回自查找起点起第一个为1的bit位的索引;若查找失败,返回位图总大小。
2.find_next_bit函数只是在位图中进行查找,自始至终都没有对位图进行任何修改。
最后,我们给出find_next_bit函数源码的简要注释版,作为最后的总结梳理
/* *@addr:位图首地址 *@size:位图总大小 *@offset:查找起点 */ unsigned long find_next_bit(const unsigned long *addr, unsigned long size, unsigned long offset) { /*找到查找起点所在数组元素的地址*/ const unsigned long *p = addr + BITOP_WORD(offset); /*计算查找起点所在数组元素前的数组元素中bit位的总个数,即“已处理”bit位个数*/ unsigned long result = offset & ~(BITS_PER_LONG-1); unsigned long tmp; /*如果查找起点大于等于位图大小,返回位图大小*/ if (offset >= size) return size; /*size:“未处理”bit位个数*/ size -= result; /*offset:查找起点在所在数组元素中的索引*/ offset %= BITS_PER_LONG; /*如果查找起点不在数组元素中的第0位*/ if (offset) { /*拷贝元素*/ tmp = *(p++); /*将查找起点前的bit位都置0*/ tmp &= (~0UL << offset); /*如果查找起点位于最后一个数组元素且该元素含有“无效位”*/ if (size < BITS_PER_LONG) goto found_first; /*如果tmp不为0,一定能找到*/ if (tmp) goto found_middle; /*“待查找”bit位总数减少32,“已查找”bit位总数增加32*/ size -= BITS_PER_LONG; result += BITS_PER_LONG; } /*while(size>=32)*/ while (size & ~(BITS_PER_LONG-1)) { /*拷贝元素,若不为0,一定能找到*/ if ((tmp = *(p++))) goto found_middle; /*“待查找”bit位总数减少32,“已查找”bit位总数增加32*/ result += BITS_PER_LONG; size -= BITS_PER_LONG; } /*若size=0,返回位图总大小*/ if (!size) return result; /*否则,拷贝最后一个数组元素*/ tmp = *p; found_first: /*将元素中“无效位”都置为0*/ tmp &= (~0UL >> (BITS_PER_LONG - size)); /*若tmp为,不可能再找到,直接返回位图总大小*/ if (tmp == 0UL) return result + size; found_middle: /*走到这里,就一定能找到,查找,返回结果*/ return result + __ffs(tmp); }
写在后面
自认为写得很详细了,但在下才疏学浅,错误疏漏之处在所难免,恳请广大读者批评指正,您的批评指正是在下前进的不竭动力!