奥格尔巧妙kfifo

  • Author:Echo Chen(陈斌)

  • Email:chenb19870707@gmail.com

  • Blog:Blog.csdn.net/chen19870707

  • Date:October 8th, 2014

    学不考儒,务掇精华。文不按古,匠心独运。Linux kernal 鬼斧神工,博大精深。让人叹为观止。拍手叫绝。然匠心独运的设计并不是扑朔迷离、盘根错节。真正的匠心独运乃辞简理博、化繁为简,在简洁中昭显优雅和智慧。kfifo就是这样一种数据结构,它就是这样简约高效,匠心独运,妙不可言。以下就跟大家一起探讨学习。

    一、kfifo概述

    本文分析的原代码版本号 2.6.32.63
    kfifo的头文件 include/linux/kfifo.h
    kfifo的源文件

    h=linux-2.6.38.y">kernel/kfifo.c

    kfifo是一种"First In First Out “数据结构。它採用了前面提到的环形缓冲区来实现,提供一个无边界的字节流服务。

    採用环形缓冲区的优点为,当一个数据元素被用掉后。其余数据元素不须要移动其存储位置,从而降低拷贝提高效率。

    更重要的是,kfifo採用了并行无锁技术。kfifo实现的单生产/单消费模式的共享队列是不须要加锁同步的。

       1: struct kfifo {
       2:     unsigned char *buffer;    /* the buffer holding the data */
       3:     unsigned int size;    /* the size of the allocated buffer */
       4:     unsigned int in;    /* data is added at offset (in % size) */
       5:     unsigned int out;    /* data is extracted from off. (out % size) */
       6:     spinlock_t *lock;    /* protects concurrent modifications */
       7: };
    buffer 用于存放数据的缓存
    size 缓冲区空间的大小。在初化时,将它向上圆整成2的幂
    in 指向buffer中队头
    out 指向buffer中的队尾
    lock 假设使用不能保证不论什么时间最多仅仅有一个读线程和写线程,必须使用该lock实施同步。

    它的结构如图:

    这看起来与普通的环形缓冲区没有什么区别,可是让人叹为观止的地方就是它巧妙的用 in 和 out 的关系和特性,处理各种操作,以下我们来具体分析。

    二、kfifo内存分配和初始化

    首先,看一个非常有趣的函数。推断一个数是否为2的次幂,依照一般的思路。求一个数n是否为2的次幂的方法为看 n % 2 是否等于0, 我们知道“取模运算”的效率并没有 “位运算” 的效率高,有兴趣的同学能够自己做下实验。

    以下再验证一下这样取2的模的正确性,若n为2的次幂,则n和n-1的二进制各个位肯定不同 (如8(1000)和7(0111))。&出来的结果肯定是0。假设n不为2的次幂,则各个位肯定有同样的
    (如7(0111) 和6(0110)),&出来结果肯定为0。是不是非常巧妙?

       1: bool is_power_of_2(unsigned long n)
       2: {
       3:     return (n != 0 && ((n & (n - 1)) == 0));
       4: }

    再看下kfifo内存分配和初始化的代码。前面提到kfifo总是对size进行2次幂的圆整,这种优点不言而喻,能够将kfifo->size取模运算能够转化为与运算。例如以下:

               kfifo->in % kfifo->size 能够转化为 kfifo->in & (kfifo->size – 1)

    “取模运算”的效率并没有 “位运算” 的效率高还记得不,不放过不论什么一点能够提高效率的地方。

       1: struct kfifo *kfifo_alloc(unsigned int size, gfp_t gfp_mask, spinlock_t *lock)
       2: {
       3:     unsigned char *buffer;
       4:     struct kfifo *ret;
       5:  
       6:     /*
       7:      * round up to the next power of 2, since our 'let the indices
       8:      * wrap' technique works only in this case.
       9:      */
      10:     if (!is_power_of_2(size)) {
      11:         BUG_ON(size > 0x80000000);
      12:         size = roundup_pow_of_two(size);
      13:     }
      14:  
      15:     buffer = kmalloc(size, gfp_mask);
      16:     if (!buffer)
      17:         return ERR_PTR(-ENOMEM);
      18:  
      19:     ret = kfifo_init(buffer, size, gfp_mask, lock);
      20:  
      21:     if (IS_ERR(ret))
      22:         kfree(buffer);
      23:  
      24:     return ret;
      25: }

    三、kfifo并发无锁奥秘---内存屏障

     

       为什么kfifo实现的单生产/单消费模式的共享队列是不须要加锁同步的呢?天底下没有免费的午餐的道理人人都懂,以下我们就来看看kfifo实现并发无锁的奥秘。

    我们知道 编译器编译源码时。会将源码进行优化,将源码的指令进行重排序。以适合于CPU的并行运行。然而,内核同步必须避免指令又一次排序,优化屏障(Optimization barrier)避免编译器的重排序优化操作。保证编译程序时在优化屏障之前的指令不会在优化屏障之后运行

    举个样例,假设多核CPU运行下面程序:

       1: a = 1;
       2: b = a + 1;
       3: assert(b == 2);

    如果初始时a和b的值都是0。a处于CPU1-cache中,b处于CPU0-cache中。如果依照以下流程运行这段代码:

    1 CPU0运行a=1;

    2 由于a在CPU1-cache中,所以CPU0发送一个read invalidate消息来占有数据

    3 CPU0将a存入store buffer

    4 CPU1接收到read invalidate消息,于是它传递cache-line,并从自己的cache中移出该cache-line

    5 CPU0開始运行b=a+1;

    6 CPU0接收到了CPU1传递来的cache-line,即“a=0”

    7 CPU0从cache中读取a的值,即“0”

    8 CPU0更新cache-line,将store buffer中的数据写入,即“a=1”

    9 CPU0使用读取到的a的值“0”。运行加1操作,并将结果“1”写入b(b在CPU0-cache中,所以直接进行)

    10 CPU0运行assert(b == 2); 失败

    软件可通过读写屏障强制内存訪问次序。读写屏障像一堵墙,全部在设置读写屏障之前发起的内存訪问,必须先于在设置屏障之后发起的内存訪问之前完毕,确保内存訪问按程序的顺序完毕。Linux内核提供的内存屏障API函数说明例如以下表。内存屏障可用于多处理器和单处理器系统,假设仅用于多处理器系统。就使用smp_xxx函数。在单处理器系统上,它们什么都不要。

    smp_rmb
    适用于多处理器的读内存屏障。

    smp_wmb
    适用于多处理器的写内存屏障。
    smp_mb
    适用于多处理器的内存屏障。

    假设对上述代码加上内存屏障,就能保证在CPU0取a时。一定已经设置好了a = 1:

       1: void foo(void)
       2: {
       3:  a = 1;
       4:  smp_wmb();
       5:  b = a + 1;
       6: }

    这里仅仅是简介了内存屏障的概念。假设想对内存屏障有进一步理解,请參考我的译文《为什么须要内存屏障》。

    四、kfifo的入队__kfifo_put和出队__kfifo_get操作

    __kfifo_put是入队操作,它先将数据放入buffer中,然后移动in的位置,其源码例如以下:

       1: unsigned int __kfifo_put(struct kfifo *fifo,
       2:             const unsigned char *buffer, unsigned int len)
       3: {
       4:     unsigned int l;
       5:  
       6:     len = min(len, fifo->size - fifo->in + fifo->out);
       7:  
       8:     /*
       9:      * Ensure that we sample the fifo->out index -before- we
      10:      * start putting bytes into the kfifo.
      11:      */
      12:  
      13:     smp_mb();
      14:  
      15:     /* first put the data starting from fifo->in to buffer end */
      16:     l = min(len, fifo->size - (fifo->in & (fifo->size - 1)));
      17:     memcpy(fifo->buffer + (fifo->in & (fifo->size - 1)), buffer, l);
      18:  
      19:     /* then put the rest (if any) at the beginning of the buffer */
      20:     memcpy(fifo->buffer, buffer + l, len - l);
      21:  
      22:     /*
      23:      * Ensure that we add the bytes to the kfifo -before-
      24:      * we update the fifo->in index.
      25:      */
      26:  
      27:     smp_wmb();
      28:  
      29:     fifo->in += len;
      30:  
      31:     return len;
      32: }
     
    6行,环形缓冲区的剩余容量为fifo->size - fifo->in + fifo->out,让写入的长度取len和剩余容量中较小的,避免写越界;
    13行。加内存屏障,保证在開始放入数据之前。fifo->out取到正确的值(还有一个CPU可能正在改写out值)
    16行,前面讲到fifo->size已经2的次幂圆整。并且kfifo->in % kfifo->size 能够转化为 kfifo->in & (kfifo->size – 1),所以fifo->size - (fifo->in & (fifo->size - 1)) 即位 fifo->in 到 buffer末尾所剩余的长度,l取len和剩余长度的最小值。即为须要拷贝l 字节到fifo->buffer + fifo->in的位置上。
    17行。拷贝l 字节到fifo->buffer + fifo->in的位置上,假设l = len。则已拷贝完毕。第20行len – l 为0。将不运行,假设l = fifo->size - (fifo->in & (fifo->size - 1)) 。则第20行还须要把剩下的 len – l 长度复制到buffer的头部。
    27行,加写内存屏障,保证in 加之前,memcpy的字节已经所有写入buffer,假设不加内存屏障。可能数据还没写完,还有一个CPU就来读数据。读到的缓冲区内的数据不全然,由于读数据是通过 in – out 来推断的。

    29行,注意这里 仅仅是用了 fifo->in +=  len而未取模。这就是kfifo的设计精妙之处。这里用到了unsigned int的溢出性质。当in 持续添加到溢出时又会被置为0。这样就节省了每次in向前添加都要取模的性能,锱铢必较。精益求精,让人不得不佩服。
    __kfifo_get是出队操作,它从buffer中取出数据。然后移动out的位置,其源码例如以下:
       1: unsigned int __kfifo_get(struct kfifo *fifo,
       2:              unsigned char *buffer, unsigned int len)
       3: {
       4:     unsigned int l;
       5:  
       6:     len = min(len, fifo->in - fifo->out);
       7:  
       8:     /*
       9:      * Ensure that we sample the fifo->in index -before- we
      10:      * start removing bytes from the kfifo.
      11:      */
      12:  
      13:     smp_rmb();
      14:  
      15:     /* first get the data from fifo->out until the end of the buffer */
      16:     l = min(len, fifo->size - (fifo->out & (fifo->size - 1)));
      17:     memcpy(buffer, fifo->buffer + (fifo->out & (fifo->size - 1)), l);
      18:  
      19:     /* then get the rest (if any) from the beginning of the buffer */
      20:     memcpy(buffer + l, fifo->buffer, len - l);
      21:  
      22:     /*
      23:      * Ensure that we remove the bytes from the kfifo -before-
      24:      * we update the fifo->out index.
      25:      */
      26:  
      27:     smp_mb();
      28:  
      29:     fifo->out += len;
      30:  
      31:     return len;
      32: }

    6行,可去读的长度为fifo->in – fifo->out,让读的长度取len和剩余容量中较小的,避免读越界;

    13行,加读内存屏障。保证在開始取数据之前。fifo->in取到正确的值(还有一个CPU可能正在改写in值)

    16行。前面讲到fifo->size已经2的次幂圆整,并且kfifo->out % kfifo->size 能够转化为 kfifo->out & (kfifo->size – 1)。所以fifo->size - (fifo->out & (fifo->size - 1)) 即位 fifo->out 到 buffer末尾所剩余的长度,l取len和剩余长度的最小值。即为从fifo->buffer + fifo->in到末尾所要去读的长度。

    17行。从fifo->buffer + fifo->out的位置開始读取l长度,假设l = len,则已读取完毕,第20行len – l 为0,将不运行,假设l =fifo->size - (fifo->out & (fifo->size - 1)) ,则第20行还需从buffer头部读取 len – l 长。
    27行,加内存屏障。保证在改动out前,已经从buffer中取走了数据,假设不加屏障,可能先运行了添加out的操作,数据还没取完,令一个CPU可能已经往buffer写数据,将数据破坏。由于写数据是通过fifo->size - (fifo->in & (fifo->size - 1))来推断的 。

    29行,注意这里 仅仅是用了 fifo->out +=  len 也未取模。相同unsigned int的溢出性质,当out 持续添加到溢出时又会被置为0,假设in先溢出。出现 in  < out 的情况,那么 in – out 为负数(又将溢出)。in – out 的值还是为buffer中数据的长度。
     
    这里图解一下 in 先溢出的情况,size = 64, 写入前 in = 4294967291, out = 4294967279 ,数据 in – out = 12;
        写入 数据16个字节,则 in + 16 = 4294967307。溢出为 11,此时 in – out = –4294967268,溢出为28。数据长度仍然正确,由此可见。在这样的特殊情况下,这样的计算仍然正确,是不是让人叹为观止,妙不可言?
     

    五、扩展

    kfifo设计静止,妙不可言,但主要为内核提供服务,内存屏障函数也主要为内核提供服务,并未开放出来,可是我们学习到了这样的设计巧妙之处。就能够依葫芦画瓢,写出自己的并发无锁环形缓冲区,这将在下篇文章中给出,至于内存屏障函数的问题,好在gcc 4.2以上的版本号都内置提供__sync_synchronize()这类的函数,效果相差点儿相同。

    眉目传情之并发无锁环形队列的实现》给出自己的并发无锁的实现,有兴趣的朋友能够參考一下。

    Reference

    1.http://blog.csdn.net/xujianqun/article/details/7800813

    2.http://zh.wikipedia.org/wiki/%E7%92%B0%E5%BD%A2%E7%B7%A9%E8%A1%9D%E5%8D%80#.E7.94.A8.E6.B3.95

    3.http://blog.csdn.net/linyt/article/details/5764312

    -

    Echo Chen:Blog.csdn.net/chen19870707

    -

  • 版权声明:本文博主原创文章,博客,未经同意不得转载。

    奥格尔巧妙kfifo的更多相关文章

    1. C# “贝格尔”编排法

      采用“贝格尔”编排法,编排时如果参赛队为双数时,把参赛队数分一半(参赛队为单数时,最后以“0”表示形成双数),前一半由1号开始,自上而下写在左边:后一半的数自下而上写在右边,然后用横线把相对的号数连接 ...

    2. English trip EM2-LP-1B Favorite Things Teacher:William Full name: Willian Richard Ogzrd 威廉理查德·奥格兹德

      课上内容(Lesson) # music pop  流行音乐 light music 轻音乐 rep 说唱音乐 rack 摇滚乐 classic  古典乐 hip-hop n. 街舞,即兴音乐:嘻哈 ...

    3. 广告制胜无它,顺应人性尔——leo鉴书63

      近期看了几本怎样写文案的书.对广告有了些兴趣.查了下相关销量排行,位置比較高的是本叫<科学的广告+我的广告生涯>的书,是同一作者(Claude C. Hopkins)两本书的合集.前者是他 ...

    4. Intel 英特尔

      英特尔 英特尔 基本资料   公司名称:英特尔(集成电路公司)    外文名称:Intel Corporation(Integrated Electronics Corporation)    总部地 ...

    5. iOS的非常全的三方库,插件,大牛博客

      转自: http://www.cnblogs.com/zyjzyj/p/6015625.html github排名:https://github.com/trending, github搜索:http ...

    6. 为什么你有10年经验,但成不了专家?(重复性刻意训练+反馈修正,练习的精髓是要持续地做自己做不好的,太精彩了)真正的高手都有很强的自学能力,老师和教练的最重要作用是提供即时的反馈(莫非我从小到大学习不好的原因在这里?没有单独刻意训练?) good

      也许简单看书就是没有刻意训练.更没有反馈,所以没有效果 我倒是想起自己,研究VCL源码的时候,都是自己给自己提问,然后苦思冥想.自己解决问题,然后Windows编程水平果然上了一个台阶.对什么叫做“框 ...

    7. 天气预报API(二):全球城市、景点代码列表(“旧编码”)

      说明 2016-12-10 补充 (后来)偶然发现中国天气网已经有城市ID列表的网页...还发现城市编码有两种,暂且称中国天气网这些编码为旧标准"旧编码"的特征是 9个字符长度; ...

    8. 世界城市 XML

      下载地址:http://www.qlcoder.com/uploads/dd01140921/147988679320159.xml <Location> <CountryRegio ...

    9. nba技能表

      球员名 主动技能名 主动技能效果 拆解技能名 拆解技能效果 巅峰-纳什 跑投三分 全队三分出手概率提高X%,但体力消耗增加Y% 稳固进攻 全队进攻增加X%,持续整场比赛 巅峰-科比 肉搏 对方全体行动 ...

    随机推荐

    1. 算法 Tricks(五)—— 将一个序列量化为何值时平方误差最小

      设数列为 A[],区间在 [a, b],设使量化后误差平方最小的数值为 m,则误差平方关于 m 的函数可以写作如下形式: ∑i=ab(A[i]−m)2=(b−a+1)⋅m2−2m∑i=abA[i]+∑ ...

    2. 【44.10%】【codeforces 723B】Text Document Analysis

      time limit per test1 second memory limit per test256 megabytes inputstandard input outputstandard ou ...

    3. Instruments性能优化-Core Animation

      简书地址:http://www.jianshu.com/users/6cb2622d5eac/latest_articles 当App发展到一定的规模.性能优化就成为不可缺少的一点.可是非常多人,又对 ...

    4. Node.js日志框架选型比較:Bunyan

      前一篇Node.js日志框架选型比較:Winston Bunyan Bunyan(by Trent Mick)是另外一个值得考虑的日志框架,以稍微不同的方式处理结构化,机器可读性被重点对待. 其结果是 ...

    5. C++ 工具类 —— 词条类(Entry)

      Entry 以键值对(key-value pair)的形式定义. template <typename K, typename V> struct Entry{ K key; V valu ...

    6. ajax 发送请求无法重定向问题

      原因: ajax请求默认就是不支持重定向的,因为它是局部刷新,不重新加载页面. 解决方案: 开发中需要多处使用重定向的情况下,大多都是在Spring mvc 的拦截器中,或过滤器中使用,此方法是在sp ...

    7. activiti自己定义流程之整合(五):启动流程时获取自己定义表单

      流程定义部署之后,自然就是流程定义列表了,但和前一节一样的是,这里也是和之前单独的activiti没什么差别.因此也不多说.我们先看看列表页面以及相应的代码,然后在一步步说明点击启动button时怎样 ...

    8. 前端切图:调用百度地图API

      原型图 图片发自简书App <!DOCTYPE html> <html> <head> <meta charset="UTF-8"> ...

    9. 【hdu 1536】S-Nim

      Time Limit: 5000/1000 MS (Java/Others) Memory Limit: 65536/32768 K (Java/Others) Total Submission(s) ...

    10. IDEA 多线程Debug

      一.问题描述 在idea中的进行调试时,代码中有多线程,想对线程中的代码进行跟踪,代码如下: for (int i = 0; i < 5; i++) { final int index = i; ...