多年以前写了篇《巧夺天工的kfifo》文章,最近有好位网友发消息讨论kfifo实现细节。发现原文有些细节没有说得很清楚,甚至有点啰嗦,于是趁今晚有空修订一下。

Linux kernel里面从来就不缺少简洁,优雅和高效的代码,只是我们缺少发现和品味的眼光。在Linux kernel里面,简洁并不表示代码使用神出鬼没的超然技巧,相反,它使用的不过是大家非常熟悉的基础数据结构,但是kernel开发者能从基础的数据结构中,提炼出优美的特性。
kfifo就是这样的一类优美代码,它十分简洁,绝无多余的一行代码,却非常高效。
关于kfifo信息如下:

本文分析的原代码版本: 2.6.24.4

kfifo的定义文件: kernel/kfifo.c

kfifo的头文件: include/linux/kfifo.h

kfifo概述

kfifo是内核里面的一个First In First Out数据结构,它采用环形循环队列的数据结构来实现;它提供一个无边界的字节流服务,最重要的一点是,它使用并行无锁编程技术,即当它用于只有一个入队线程和一个出队线程的场情时,两个线程可以并发操作,而不需要任何加锁行为,就可以保证kfifo的线程安全。
kfifo代码既然肩负着这么多特性,那我们先一敝它的代码:

struct kfifo {unsigned char *buffer;    /* the buffer holding the data */unsigned int size;    /* the size of the allocated buffer */unsigned int in;    /* data is added at offset (in % size) */unsigned int out;    /* data is extracted from off. (out % size) */spinlock_t *lock;    /* protects concurrent modifications */
};

这是kfifo的数据结构,kfifo主要提供了两个操作,__kfifo_put(入队操作)和__kfifo_get(出队操作)。 它的各个数据成员如下:

buffer: 用于存放数据的缓存

size: buffer空间的大小,在初化时,将它向上扩展成2的幂

lock: 如果使用不能保证任何时间最多只有一个读线程和写线程,需要使用该lock实施同步。

in, out: 和buffer一起构成一个循环队列。 in指向buffer中队头,而且out指向buffer中的队尾,它的结构如示图如下:

+--------------------------------------------------------------+
|            |<----------data---------->|                      |
+--------------------------------------------------------------+
             ^                          ^                      ^
             |                          |                      |
            out                        in                     size

当然,内核开发者使用了一种更好的技术处理了in, out和buffer的关系,我们将在下面进行详细分析。

kfifo功能描述

kfifo提供如下对外功能规格

  1. 只支持一个读者和一个读者并发操作
  2. 无阻塞的读写操作,如果空间不够,则返回实际访问空间

kfifo_alloc 分配kfifo内存和初始化工作

struct kfifo *kfifo_alloc(unsigned int size, gfp_t gfp_mask, spinlock_t *lock)
{unsigned char *buffer;struct kfifo *ret;/** round up to the next power of 2, since our 'let the indices* wrap' tachnique works only in this case.*/if (size & (size - 1)) {BUG_ON(size > 0x80000000);size = roundup_pow_of_two(size);}buffer = kmalloc(size, gfp_mask);if (!buffer)return ERR_PTR(-ENOMEM);ret = kfifo_init(buffer, size, gfp_mask, lock);if (IS_ERR(ret))kfree(buffer);return ret;
}

这里值得一提的是,kfifo->size的值总是在调用者传进来的size参数的基础上向2的幂扩展,这是内核一贯的做法。这样的好处不言而喻——对kfifo->size取模运算可以转化为与运算,如下:

kfifo->in % kfifo->size 可以转化为 kfifo->in & (kfifo->size – 1)

在kfifo_alloc函数中,使用size & (size – 1)来判断size 是否为2幂,如果条件为真,则表示size不是2的幂,然后调用roundup_pow_of_two将之向上扩展为2的幂。

这都是常用的技巧,只不过大家没有将它们结合起来使用而已,下面要分析的__kfifo_put和__kfifo_get则是将kfifo->size的特点发挥到了极致。

__kfifo_put和__kfifo_get巧妙的入队和出队

__kfifo_put是入队操作,它先将数据放入buffer里面,最后才修改in参数;__kfifo_get是出队操作,它先将数据从buffer中移走,最后才修改out。你会发现in和out两者各司其职。

下面是__kfifo_put和__kfifo_get的代码

unsigned int __kfifo_put(struct kfifo *fifo,unsigned char *buffer, unsigned int len)
{unsigned int l;len = min(len, fifo->size - fifo->in + fifo->out);/** Ensure that we sample the fifo->out index -before- we* start putting bytes into the kfifo.*/smp_mb();/* first put the data starting from fifo->in to buffer end */l = min(len, fifo->size - (fifo->in & (fifo->size - 1)));memcpy(fifo->buffer + (fifo->in & (fifo->size - 1)), buffer, l);/* then put the rest (if any) at the beginning of the buffer */memcpy(fifo->buffer, buffer + l, len - l);/** Ensure that we add the bytes to the kfifo -before-* we update the fifo->in index.*/smp_wmb();fifo->in += len;return len;
}

奇怪吗?代码完全是线性结构,没有任何if-else分支来判断是否有足够的空间存放数据。内核在这里的代码非常简洁,没有一行多余的代码。

l = min(len, fifo->size - (fifo->in & (fifo->size - 1)));

这个表达式计算当前写入的空间,换成人可理解的语言就是:

l = kfifo可写空间和预期写入空间的最小值

使用min宏来代if-else分支

__kfifo_get也应用了同样技巧,代码如下:

unsigned int __kfifo_get(struct kfifo *fifo,unsigned char *buffer, unsigned int len)
{unsigned int l;len = min(len, fifo->in - fifo->out);/** Ensure that we sample the fifo->in index -before- we* start removing bytes from the kfifo.*/smp_rmb();/* first get the data from fifo->out until the end of the buffer */l = min(len, fifo->size - (fifo->out & (fifo->size - 1)));memcpy(buffer, fifo->buffer + (fifo->out & (fifo->size - 1)), l);/* then get the rest (if any) from the beginning of the buffer */memcpy(buffer + l, fifo->buffer, len - l);/** Ensure that we remove the bytes from the kfifo -before-* we update the fifo->out index.*/smp_mb();fifo->out += len;return len;
}

认真读两遍吧,我也读了多次,每次总是有新发现,因为in, out和size的关系太巧妙了,竟然能利用上unsigned int回绕的特性。

原来,kfifo每次入队或出队,kfifo->in或kfifo->out只是简单地kfifo->in/kfifo->out += len,并没有对kfifo->size 进行取模运算。因此kfifo->in和kfifo->out总是一直增大,直到unsigned in最大值时,又会绕回到0这一起始端。但始终满足:

kfifo->in - kfifo->out <= kfifo->size

即使kfifo->in回绕到了0的那一端,这个性质仍然是保持的。

对于给定的kfifo:

数据空间长度为:kfifo->in - kfifo->out

而剩余空间(可写入空间)长度为:kfifo->size - (kfifo->in - kfifo->out)

尽管kfifo->in和kfofo->out一直超过kfifo->size进行增长,但它对应在kfifo->buffer空间的下标却是如下:

kfifo->in % kfifo->size (i.e. kfifo->in & (kfifo->size - 1))

kfifo->out % kfifo->size (i.e. kfifo->out & (kfifo->size - 1))

往kfifo里面写一块数据时,数据空间、写入空间和kfifo->size的关系如果满足:

kfifo->in % size + len > size

那就要做写拆分了,见下图:

                                                    kfifo_put(写)空间开始地址
                                                    |
                                                   \_/
                                                    |XXXXXXXXXX
XXXXXXXX|
+--------------------------------------------------------------+
|                        |<----------data---------->|          |
+--------------------------------------------------------------+
                         ^                          ^          ^
                         |                          |          |
                       out%size                   in%size     size
        ^
        |
      写空间结束地址                      

第一块当然是: [kfifo->in % kfifo->size, kfifo->size]
第二块当然是:[0, len - (kfifo->size - kfifo->in % kfifo->size)]

下面是代码,细细体味吧:

/* first put the data starting from fifo->in to buffer end */
l = min(len, fifo->size - (fifo->in & (fifo->size - 1)));
memcpy(fifo->buffer + (fifo->in & (fifo->size - 1)), buffer, l);   /* then put the rest (if any) at the beginning of the buffer */
memcpy(fifo->buffer, buffer + l, len - l);  

对于kfifo_get过程,也是类似的,请各位自行分析。

kfifo_get和kfifo_put无锁并发操作

计算机科学家已经证明,当只有一个读经程和一个写线程并发操作时,不需要任何额外的锁,就可以确保是线程安全的,也即kfifo使用了无锁编程技术,以提高kernel的并发。

kfifo使用in和out两个指针来描述写入和读取游标,对于写入操作,只更新in指针,而读取操作,只更新out指针,可谓井水不犯河水,示意图如下:

                                               |<--写入-->|
+--------------------------------------------------------------+
|                        |<----------data----->|               |
+--------------------------------------------------------------+
                         |<--读取-->|
                         ^                     ^               ^
                         |                     |               |
                        out                   in              size

为了避免读者看到写者预计写入,但实际没有写入数据的空间,写者必须保证以下的写入顺序:

  1. 往[kfifo->in, kfifo->in + len]空间写入数据
  2. 更新kfifo->in指针为 kfifo->in + len

在操作1完成时,读者是还没有看到写入的信息的,因为kfifo->in没有变化,认为读者还没有开始写操作,只有更新kfifo->in之后,读者才能看到。

那么如何保证1必须在2之前完成,秘密就是使用内存屏障:smp_mb(),smp_rmb(), smp_wmb(),来保证对方观察到的内存操作顺序。

总结

读完kfifo代码,令我想起那首诗“众里寻他千百度,默然回首,那人正在灯火阑珊处”。不知你是否和我一样,总想追求简洁,高质量和可读性的代码,当用尽各种方法,江郞才尽之时,才发现Linux kernel里面的代码就是我们寻找和学习的对象。

巧夺天工的kfifo(修订版)相关推荐

  1. 巧夺天工的kfifo

    本文已有修订版本:<巧夺天工的kfifo(修订版)>,欢迎前往阅读. Linux kernel里面从来就不缺少简洁,优雅和高效的代码,只是我们缺少发现和品味的眼光.在Linux kerne ...

  2. 【高并发】多线程之无锁队列|性能优化

    队列操作模型 (1)单生产者--单消费者 (2)多生产者--单消费者 (3)单生产者--多消费者 (4)多生产者--多消费者 3.队列数据定长与变长 (1)队列数据定长 (2)队列数据变长 并发无锁处 ...

  3. 阅读天龙八部的代码有感----两种逻辑处理模式的比较

    1: 天龙八部的逻辑处理模块: 逻辑处理模块: void run { for(;;){ select();  //epoll wait(); process_inputs(); process_com ...

  4. 《Photoshop Lab修色圣典(修订版)》—第1课1.7节言归正传

    本节书摘来自异步社区<Photoshop Lab修色圣典(修订版)>一书中的第1课1.7节言归正传,作者[美]Dan Margulis,更多章节内容可以访问云栖社区"异步社区&q ...

  5. 《实现模式(修订版)》—第1章1.2节那么,现在……

    本节书摘来自异步社区<实现模式(修订版)>一书中的第1章1.2节那么,现在--,作者[美]Kent Beck,更多章节内容可以访问云栖社区"异步社区"公众号查看. 1. ...

  6. 《JavaScript高效图形编程(修订版)》——6.2 位图、矢量图,或两者兼而有之?...

    本节书摘来自异步社区<JavaScript高效图形编程(修订版)>一书中的第6章,第6.2节,作者:[美]Raffaele Cecco著,更多章节内容可以访问云栖社区"异步社区& ...

  7. 《Python 3程序开发指南(第2版•修订版)》——1.2 Python的关键要素

    本节书摘来自异步社区<Python 3程序开发指南(第2版•修订版)>一书中的第1章,第1.2节,作者[英]Mark Summerfield,王弘博,孙传庆 译,更多章节内容可以访问云栖社 ...

  8. 眉目传情之匠心独运的kfifo【转】

    转自:http://blog.csdn.net/chen19870707/article/details/39899743 权声明:本文为博主原创文章,未经博主允许不得转载. 目录(?)[-] 一kf ...

  9. 《领域驱动设计:软件核心复杂性应对之道(修订版)》—第2章 2.1节模式:Ubiquitous Language...

    本节书摘来自异步社区<领域驱动设计:软件核心复杂性应对之道(修订版)>一书中的第2章,第2.1节模式:Ubiquitous Language,作者[美]埃里克•埃文斯(Eric Evans ...

最新文章

  1. POJ 3258:River Hopscotch (最大化最小值)
  2. 数据库开发个人总结(ADO.NET小结)
  3. linux下的权限问题
  4. Phonegap win7下环境配置流程
  5. 我的编程学习日志(1)--进制转换
  6. 7-1 最大子列和问题 (20 分)
  7. go语言调用python_Golang中调用Python3
  8. MyBatis中的注解
  9. 标量函数,多语句表值函数,内嵌表值函数
  10. jQuery插件开发精品教程,让你的jQuery提升一个台阶
  11. AI头发笔刷_1500款设计字体,海量PS笔刷,icon模板免费送!还为设计发愁吗?
  12. 计算机毕业设计ssm智能停车场管理系统
  13. win7网络怎么添加一台计算机,win7网络共享的设置方法【详细步骤】
  14. 王家林人工智能AI第15课:通过SVM进一步改进在Social Network上销售汽车推荐系统的精准性 老师微信13928463918
  15. 周易六十四卦——火水未济卦
  16. 毕业寄语 | 关于毕业季的温柔文案
  17. JavaScript replace() 方法转换时间数据中的“-”和“/”
  18. 【ArchSummit】社交元宇宙的技术挑战与探索
  19. 程序员哑巴英语修炼指南
  20. 云日历(原软媒mytime)分享给大家

热门文章

  1. s3服务器虚拟屏幕怎么用,aws s3挂载使用教程
  2. 综述:Reading, writing and erasing mRNA methylation
  3. Windows使用笔记_Windows xp之开机启动项设置
  4. 求给定非负整数的阶乘
  5. uthash官方使用说明文档
  6. 有关stm32单片机外部低速晶振LSE32.768k注意事项;RTC_WaitForSynchro()卡死;
  7. C# 关于Lambda表达式
  8. 国内服务器绕过备案详细教程
  9. 如何实现一个直播APP
  10. lij IDEA快速编写代码