Linux kernel 里面从来就不缺少简洁,优雅和高效的代码,只是我们缺少发现和品味的眼光。在Linux kernel里面,简洁并不表示代码使用神出鬼没的超然技巧,相反,它使用的不过是大家非常熟悉的基础数据结构,但是kernel开发者能从基础的数据结构中,提炼出优美的特性。
kfifo
就是这样的一类优美代码,它十分简洁,绝无多余的一行代码,却非常高效。
关于kfifo信息如下:
本文分析的原代码版本:2.6.25
kfifo的定义文件:kernel/kfifo.c
kfifo的头文件:include/linux/kfifo.h
kfifo概述
kfifo
是内核里面的一个First In First Out数据结构,它采用环形循环队列的数据结构来实现;它提供一个无边界的字节流服务,最重要的一点是,它使用并行无锁编程技术,即当它用于只有一个入队线程和一个出队线程的场情时,两个线程可以并发操作,而不需要任何加锁行为,就可以保证kfifo
的线程安全。
kfifo
代码既然肩负着这么多特性,那我们先一瞥它的代码:
这是kfifo
的数据结构,kfifo
主要提供了两个操作,__kfifo_put
(入队操作)和__kfifo_get
(出队操作)。 它的各个数据成员如下:buffer
用于存放数据的缓存size
buffer空间的大小,在初化时,将它向上扩展成2的幂lock
如果使用不能保证任何时间最多只有一个读线程和写线程,需要使用该lock实施同步。in
,out
和buffer
一起构成一个循环队列。 in
指向buffer
中队头,而且out
指向buffer
中的队尾,它的结构如示图如下:
当然,内核开发者使用了一种更好的技术处理了in
,out
和buffer
的关系,我们将在下面进行详细的分析。
kfifo 内存分配和初始化工作
kfifo_alloc
这里值得一提的是,kfifo->size
的值总是在调用者传进来的size
参数的基础上向2的幂扩展,这是内核一贯的做法。这样的好处不言而喻 —— 对kfifo->size
取模运算可以转化为与运算,如下:
可以转化为
在kfifo_alloc
函数中,使用size & (size – 1)
来判断size
是否为 2 的幂,如果条件为真,则表示size
不是 2 的幂,然后调用roundup_pow_of_two
将之向上扩展为 2 的幂。 这些都是很常用的技巧,只不过大家没有将它们结合起来使用而已,下面要分析的 __kfifo_put
和__kfifo_get
则是将kfifo->size
的特点发挥到了极致。
kfifo_init
kfifo_free
__kfifo_put
和__kfifo_get
,巧妙的入队和出队操作,无锁并发
__kfifo_put
是入队操作,它先将数据放入buffer
里面,最后才修改in
参数;__kfifo_get
是出队操作,它先将数据从buffer
中移走,最后才修改out
。你会发现in
和out
两者各司其职。计算机科学家已经证明,当只有一个读经程和一个写线程并发操作时,不需要任何额外的锁,就可以确保是线程安全的,也即kfifo
使用了无锁编程技术,以提高kernel的并发。
__kfifo_put
__kfifo_get
kfifo
每次入队或出队,kfifo->in
或kfifo->out
只是简单地kfifo->in
(or)kfifo->out
+= len
,并没有对kfifo->size
进行取模运算。因此kfifo->in
和kfifo->out
总是一直增大,直到unsigned in
最大值时,回绕到 0 这一起始端。但始终满足 kfifo->out < kfifo->in
,除非kfifo->in
回绕到了 0 的那一端,即使如此,代码中计算长度的性质仍然是保持的。
我们先用简单的例子来形象说明这些性质吧:
上图的out
和in
为kfifo->buffer
的出队数据和入队数据的游标,方框为buffer
的内存区域。当有数据入队时,那么in
的值可能超过kfifo->size
的值,那么我们使用另一个虚拟的方框来表示in
变化后,在buffer
内对kfifo->size
取模的值。如下图所示:
当用户调用__kfifo_put
函数,入队的数据使kfifo->in
游标发生如上图所示的变化时,要分两次拷贝内存数据。
因为入队数据,一部存放在kfifo->buffer
的尾部,另一部分存放在kfifo->buffer
的头部。计算公式非常简单:
表示in
下标到buffer
末尾,还有多少空间。
如果len
表示需要拷贝的长度的话,那么len - l
则表示有多少字节需要拷贝到buffer
开始之处。
这样,我们读__kfifo_put
代码就很容易了。
fifo->in – fifo->out
表示队列里面已使用的空间大小,fifo->size - (fifo->in – fifo->out)
表示队列未使用的空间,
因此len = min(…)
,取两者之小,表示实际要拷贝的字节数。
拷贝len
个字符数,fifo->in
到buffer
末尾所剩的空间是多少,这里面计算:
l
表示len
或fifo->in
到buffer
末尾所剩的空间大小的最小值,因为需要拷贝l
字节到 fifo->buffer + (fifo->in & (fifo->size - 1))
的位置上;那么剩下要拷贝到buffer
开始之处的长度为len – l
,当然,此值可能会为0,为 0 时,memcpy函数不进行任何拷贝。
所有的拷贝完成后(可能是一次,也可能是两次 memcpy),fifo->in
直接 += len
,不需要取模运算。
写到这里,细心的读者会发现,如果fifo->in
超过了unsigned int
的最大值时,而回绕到 0 这一端,上述的计算公式还正确吗? 答案是肯定的。
因为fifo->size
的大小是 2 的幂,而unsigned int
空间的大小是 2^32
,后者刚好是前者的倍数。如果从上述两个图的方式来描述,则表示unsigned int
空间的数轴上刚好可以划成一定数量个 kfifo->size
大小方框,没有长度多余。这样,fifo->in
(or) fifo->out
对fifo->size
取模后,刚好落后对应的位置上。
现在假设往kfifo
加入数据后,使用fifo->in < fifo->out
关系,如下:
假设kfifo
中数据的长度为ldata
,那么fifo->in
和fifo->out
有这样的关系:fifo->in = fifo->out + ldata
,并且fifo->in < fifo->out
。这说明fifo->in
回绕到0这一端了,尽管如此,fifo->in
和fifo->out
的差距还是保持的,没有变化。即fifo->in – fifo->out
仍然是ldata
, 那么此时的可用空间是fifo->size – ldata = fifo->size - (fifo->in – fifo->out) = fifo->size – fifo->in + fifo->out
。
因此无论fifo->in
和fifo->out
谁大谁小,计算fifo
剩余空间大小的公式fifo->size – fifo->in + fifo->out
都正确,故可以保证__kfifo_put
函数里面的长度计算均是正确的。
__kfifo_get
函数使用fifo->in – fifo->out
来计算fifo
内数据的空间长度,然后拷贝需要出队的数据。是否需要两次拷贝,其中原理和方法都和__kfifo_put
是一样的。
kfifo_put 和 kfifo_get
kfifo_put
和kfifo_get
在调用__kfifo_put
和__kfifo_get
过程都进行加锁,防止并发。
kfifo_put
kfifo_get
其他函数
kfifo_reset
kfifo_len
参考
巧夺天工的kfifo
linux内核数据结构之kfifo
《Linux内核设计与实现》读书笔记(六)- 内核数据结构
2014.03.31 更新
新的内核版本中,kfifo 接口已经改写,新的 kfifo 接口请查阅最新的内核源码。
相关的接口修改历史可查阅以下链接:
http://lwn.net/Articles/345015/
http://lwn.net/Articles/347619/