本文已有修订版本:《巧夺天工的kfifo(修订版)》,欢迎前往阅读。

Linux kernel里面从来就不缺少简洁,优雅和高效的代码,只是我们缺少发现和品味的眼光。在Linux kernel里面,简洁并不表示代码使用神出鬼没的超然技巧,相反,它使用的不过是大家非常熟悉的基础数据结构,但是kernel开发者能从基础的数据结构中,提炼出优美的特性。

kfifo就是这样的一类优美代码,它十分简洁,绝无多余的一行代码,却非常高效。关于kfifo信息如下:

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

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

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

1. 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---------->|                                    |
+--------------------------------------------------------------+
            ^                                       ^ 
             |                                        |
            in                                       out

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

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的特点发挥到了极致。

3. __kfifo_put和__kfifo_get,巧妙的入队和出队操作,无锁并发

__kfifo_put是入队操作,它先将数据放入buffer里面,最后才修改in参数;__kfifo_get是出队操作,它先将数据从buffer中移走,最后才修改out。你会发现in和out两者各司其职。计算机科学家已经证明,当只有一个读经程和一个写线程并发操作时,不需要任何额外的锁,就可以确保是线程安全的,也即kfifo使用了无锁编程技术,以提高kernel的并发。

下面是__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; }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->out < kfifo->in,除非kfifo->in回绕到了0的那一端,即使如此,代码中计算长度的性质仍然是保持的。

我们先用简单的例子来形象说明这些性质吧:

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

上图的out和in为kfifo->buffer的出队数据和入队数据的一下,方框为buffer的内存区域。当有数据入队时,那么in的值可能超过kfifo->size的值,那么我们使用另一个虚拟的方框来表示in变化后,在buffer内对kfifo->size取模的值。如下图如标:

真实的buffer内存                                     虚拟的buffer内存,方便查看in对kfifo->size取模后在buffer的下标
+----------------------------------------+ +------------------------------------+
|                                   |<—data-------|  |--------->|                                    |
+----------------------------------------+ +------------------------------------+
                                    ^                                       ^
                                    |                                        |
                                    out                                    in

当用户调用__kfifo_put函数,入队的数据使kfifo的内存关系,引起上述两图的变化时,要拷贝两次内存。

因为入队数据,一部存放在kfifo->buffer的尾部,另一部分存放在kfifo->buffer的头部,计算公式非常简单。

l = kfifo->size – kfifo->in & (kfifo->size – 1) 表示in下标到buffer末尾,还有多少空间。

如果len表示需要拷贝的长度的话,那么len - l则表示有多少字节需要拷贝到buffer开始之处。

这样,我们读__kfifo_put代码就很容易了。

len = min(len, fifo->size - fifo->in + fifo->out);

fifo->in – fifo->out表示队列里面已使用的空间大小,fifo->size - (fifo->in – fifo->out)表示队列未使用的空间,

因此en = min(…),取两者之小,表示实际要拷贝的字节数。

拷贝len个字符数,fifo->in到buffer末尾所剩的空间是多少,这里面计算:

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);

l表示len或fifo->in到buffer末尾所剩的空进行间大小的最小值,因为需要拷l字节到fifo->buffer + fifo->in的位置上;那么剩下要拷贝到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/fifo->out对fifo->size取模后,刚好落后对应的位置上。

现在假设往kfifo加入数据后,使用fifo->in < fifo->out关系,如下:

+----------------------------------------+                                   +------------------------------------+
|—data—>|                                          |          ……                     |                               |<--data------|
+----------------------------------------+                                   +------------------------------------+
|-------------------------------------------------------------------------------------------------------->|
0                                                                                                                                         0xffffffff
              ^                                                                                                                ^
              |                                                                                                                  |
              in                                                                                                                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是一样的。

4. 总结

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

巧夺天工的kfifo相关推荐

  1. 巧夺天工的kfifo(修订版)

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

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

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

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

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

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

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

  5. 简洁高效的linux kfifo环形缓冲区

    代码来自于:https://blog.csdn.net/vertor11/article/details/53741681,侵删. struct kfifo{uint8_t *buffer;uint3 ...

  6. linux内核kfifo(一)

    Linux kernel里面从来就不缺少简洁,优雅和高效的代码,只是我们缺少发现和品味的眼光.在Linux kernel里面,简洁并不表示代码使用神出鬼没的超然技巧,相反,它使用的不过是大家非常熟悉的 ...

  7. 模仿Linux内核kfifo实现的循环缓存

    想实现个循环缓冲区(Circular Buffer),搜了些资料多数是基于循环队列的实现方式.使用一个变量存放缓冲区中的数据长度或者空出来一个空间来判断缓冲区是否满了.偶然间看到分析Linux内核的循 ...

  8. 传世的关系模型,巧夺天工的分布式数据库设计

    Hadoop生态的分布式数据库 1.什么是分布式数据库? 从狭义的理解就是分布式关系型数据库,主要特指目前热门的NewSQL. 从广义的理解,分库分表的传统关系型数据库,传统关系型数据库集群,关系型数 ...

  9. linux内核之kfifo队列

    1.前言 最近项目中用到一个环形缓冲区(ring buffer),代码是由linux内核的kfifo改过来的.缓冲区在文件系统中经常用到,通过缓冲区缓解cpu读写内存和读写磁盘的速度.例如一个进程A产 ...

最新文章

  1. C# webform上传图片并生成缩略图
  2. android中设置Animation 动画效果
  3. 20220202--CTF刷题MISC方向--第7题--编码
  4. 控制台添加log4net
  5. CentOS四种方法自建yum仓库
  6. 计算机视觉应用开发功能图,《计算机视觉算法:基于OpenCV的计算机应用开发》 —1.3 理解计算机图像...
  7. 数值计算方法(五)——迭代法求方程根
  8. 检索每个组中的最后一条记录-MySQL
  9. 【问题记录】阿里云轻量云服务器Ubuntu安装图形化桌面+远程连接
  10. 关于 Java 泛型的一些有趣的例子
  11. 命令行构建Unity项目
  12. 如何看待职场猝死?燕麦企业云盘教你9大绝招提升职场幸福感
  13. 超市不同时段人流量统计分析
  14. 【接口测试实战(三)】接口测试用例的编写
  15. ACCESS实例2 资料管理1——表、表关系与查询
  16. 10 个Web3 设计灵感网站
  17. Mysql许久不用后服务无法打开,暴力解决法(报错:服务没有报任何错误)
  18. 解决垃圾小米文件管理器删除文件后存储空间不变,内存不变大,不释放空间。解决方法:点图中那个结束运行,再重新打开软件(直接滑动退出后台无效果)
  19. Microsoft Windows Server 2003版本对比
  20. 全球报告表明,中文是现如今增速最快的语言学习赛道,全球有超过600万人在学中文 | 美通社头条...

热门文章

  1. 美国超人气漫画科普书!85%以上中学物理考点,5岁孩子秒懂物理
  2. 数据库嵌套循环查询联系——1.查询选修了课程号为“3”的学生姓名和系别、4.查询其它系中‘2‘号课程比信息系所有学生分数高的学生学号和姓名、6.查询每门课程中低于该课程平均成绩的学生学号和姓名
  3. 基于Umi搭建的个人Dva脚手架(五) - 可配置的搜索、弹窗组件封装
  4. 2017-2018 ACM-ICPC Asia East Continent League Final L. SOS(博弈,思维)
  5. 小米 MIX 4:屏下摄像头的表现
  6. upupw mysql安装在哪_怎样在window中利用UPUPW安装PHP+MYSQL的web站
  7. 实现Springboot整合uflo2
  8. 声调之调类、调值、调型、调形、调层、调域
  9. 在canvas中绘制箭头
  10. linux shell转换成时间,如何在Bash中将时间戳转换为日期?