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

想实现个循环缓冲区(Circular Buffer),搜了些资料多数是基于循环队列的实现方式。使用一个变量存放缓冲区中的数据长度或者空出来一个空间来判断缓冲区是否满了。偶然间看到分析Linux内核的循环缓冲队列kfifo的实现,确实极其巧妙。kfifo主要有以下特点:

  • 保证缓冲空间的大小为2的次幂,不是的向上取整为2的次幂。
  • 使用无符号整数保存输入(in)和输出(out)的位置,在输入输出时不对in和out的值进行模运算,而让其自然溢出,并能够保证in-out的结果为缓冲区中已存放的数据长度,这也是最能体现kfifo实现技巧的地方;
  • 使用内存屏障(Memory Barrier)技术,实现单消费者和单生产者对kfifo的无锁并发访问,多个消费者、生产者的并发访问还是需要加锁的。

本文主要以下三个部分:

  • 关于2的次幂问题,判断是不是2的次幂以及向上取整为2的次幂
  • Linux内核中kfifo的实现及简要分析
  • 根据kfifo实现的循环缓冲区,并进行一些测试

关于内存屏障的本文不作过多分析,可以参考WikiMemory Barrier。另外,本文所涉及的整数都默认为无符号整数,不再做一一说明。

1. 2的次幂

  • 判断一个数是不是2的次幂
    kfifo要保证其缓存空间的大小为2的次幂,如果不是则向上取整为2的次幂。其对于2的次幂的判断方式也是很巧妙的。如果一个整数n是2的次幂,则二进制模式必然是1000...,而n-1的二进制模式则是0111...,也就是说n和n-1的每个二进制位都不相同,例如:8(1000)和7(0111);n不是2的次幂,则n和n-1的二进制必然有相同的位都为1的情况,例如:7(0111)和6(0110)。这样就可以根据 n & (n-1)的结果来判断整数n是不是2的次幂,实现如下:
/*判断n是否是2的幂若n为2的次幂,   则 n & (n-1) == 0,也就是n和n-1的各个位都不相同。例如 8(1000)和7(0111)若n不是2的次幂, 则 n & (n-1) != 0,也就是n和n-1的各个位肯定有相同的,例如7(0111)和6(0110)
*/
static inline bool is_power_of_2(uint32_t n)
{return (n != 0 && ((n & (n - 1)) == 0));
}
  • 将数字向上取整为2的次幂
    如果设定的缓冲区大小不是2的次幂,则向上取整为2的次幂,例如:设定为5,则向上取为8。上面提到整数n是2的次幂,则其二进制模式为100...,故如果正数k不是n的次幂,只需找到其最高的有效位1所在的位置(从1开始计数)pos,然后1 << pos即可将k向上取整为2的次幂。实现如下:
static inline uint32_t roundup_power_of_2(uint32_t a)
{if (a == 0)return 0;uint32_t position = 0;for (int i = a; i != 0; i >>= 1)position++;return static_cast<uint32_t>(1 << position);
}

2. Linux实现kfifo及分析

Linux内核中kfifo实现技巧,主要集中在放入数据的put方法和取数据的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;   
}   

put返回实际保存到缓冲区中的数据长度,get返回的是实际取到的数据长度。在上面代码中,需要注意到在写入、取出时候的两次min运算。关于kfifo的分析,已有很多资料了,也可参考眉目传情之匠心独运的kfifo 。

Linux内核实现的kfifo的有以下特点:

  • 使用内存屏障 Memory Barrier
  • 初始化缓冲区空间时要保证缓冲区的大小为2的次幂
  • 使用无符号整数保存in和out(输入输出的指针),并且在放入取出数据的时候不做模运算,让其自然溢出。

优点:

  1. 实现单消费者和单生产者的无锁并发访问。多消费者和多生产者的时候还是需要加锁的。
  2. 使用与运算in & (size-1)代替模运算
  3. 在更新in或者out的值时不做模运算,而是让其自动溢出。这应该是kfifo实现最牛叉的地方了,利用溢出后的值参与运算,并且能够保证结果的正确。溢出运算保证了以下几点:

    • in - out为缓冲区中的数据长度
    • size - in + out 为缓冲区中空闲空间
    • in == out时缓冲区为空
    • size == (in - out)时缓冲区满了

3.模仿kfifo实现的循环缓冲

主要是模仿其无符号溢出的运算方法,并没有利用内存屏障实现单生产者和单消费者的无锁并发访问。初始化及输入输出的代码如下:

struct kfifo{uint8_t *buffer;uint32_t in; // 输入指针uint32_t out; // 输出指针uint32_t size; // 缓冲区大小,必须为2的次幂kfifo(uint32_t _size){if (!is_power_of_2(_size))_size = roundup_power_of_2(_size);buffer = new uint8_t[_size];in = 0;out = 0;size = _size;}// 返回实际写入缓冲区中的数据uint32_t put(const uint8_t *data, uint32_t len){// 当前缓冲区空闲空间len = min(len,size - in + out);// 当前in位置到buffer末尾的长度auto l = min(len, size - (in  & (size - 1)));// 首先复制数据到[in,buffer的末尾]memcpy(buffer + (in & (size - 1)), data, l);// 复制剩余的数据(如果有)到[buffer的起始位置,...]memcpy(buffer, data + l, len - l);in += len; // 直接加,不作模运算。当溢出时,从buffer的开始位置重新开始return len;}// 返回实际读取的数据长度uint32_t get(uint8_t *data, uint32_t len){// 缓冲区中的数据长度len = min(len, in - out);// 首先从[out,buffer end]读取数据auto l = min(len, size - (out & (size - 1)));memcpy(data, buffer + (out & (size - 1)), l);// 从[buffer start,...]读取数据memcpy(data + l, buffer, len - l);out += len; // 直接加,不错模运算。溢出后,从buffer的起始位置重新开始return len;}

在初始化缓冲空间的时候要验证size是否为2的次幂,如果不是则向上取整为2的次幂。下面着重分析下在放入取出数据时对指针inout的处理,以及在溢出后怎么能够保证in - out仍然为缓冲区中的已有的数据长度。

put和get方法详解

在向缓冲区中put数据的时候,需要两个参数:要put的数据指针data和期望能够put的数据长度len,返回值是实际存放到缓冲区中的数据长度(当缓冲区中空间不足时该值小于len)。下面详细的解释下put中每个语句的作用。

  • put函数中的第一句是len = min(len,size - in + out)计算实际向缓冲区中写入数据的大小。如果想要写入的数据len大于缓冲区中的空闲空间size - in + out,则只填充满缓冲空间。

因为是循环缓冲区,所以其空闲空间有两部分:从in到缓冲空间的末尾->[in,buffer end]和缓冲空间的起始位置到out->[buffer start,out]。

  • auto l = min(len, size - (in & (size - 1))); 这个是判断[in,buffer end]这部分空间是否足够写入数据
  • memcpy(buffer + (in & (size - 1)), data, l); 向[in,buffer end]这部分空间写入数据
  • memcpy(buffer, data + l, len - l); 如果数据还没有写完,则向[buffer start,out]这部分空间写入数据。
  • in += len 更新in,不做模运算,让其自然溢出。

get和put很类似,首先判断是否有足够的数据取出;在取数据时首先从out取到buffer的末尾,如果不够则从buffer的开始位置取;最后更新out时也是不做模运算,让其溢出。看参看上面put的语句解释,这里就不再多说。

无符号溢出运算

kfifo之所以如次的简洁,很大一部分要归功于其in和out的溢出运算。这里就解释下在溢出的情况下,如何保证in - out仍然为缓冲区中的数据长度。首先来看图:

  • 缓冲区为空
    439761-20161116164452607-581995032.png

  • put 一堆数据后
    439761-20161116164504342-891309580.png

  • get 一堆数据后
    439761-20161116164513967-639076501.png

  • put的数据长度超过in到buffer末尾的长度,有一部分从put到buffer的起始位置
    439761-20161116164539201-1091633412.png

以上图片引用自linux内核数据结构之kfifo,其对kfifo的分析也很详细。

前三种情况下从图中可以很清晰的看出in - out为缓冲区中的已有的数据长度,但是最后一种发现in跑到了out的前面,这时候in - out不是应该为负的么,怎么能是数据长度?这正是kfifo的高明之处,in和out都是无符号整数,那么在in < out 时in - out就是负数,把这个负数当作无符号来看时,其值仍然是缓冲区中的数据长度。这和in累加到溢出的情况基本一致,这里放在一起说。

这里使用8位无符号整数来保存in和out,方便溢出。这里假设out = 100,in = 255,size = 256,如下图

/*--------------------------------------|             |                  |   |--------------------------------------out = 100           in = 250这时缓冲区中已有的数据为:in - out = 150,空闲空间为:size - (in - out) = 106向缓冲区中put10个数据后--------------------------------------|    |       |                       |--------------------------------------in      out这时候 in + 10 = 260 溢出变为in = 4;这是 in - out = 4 - 100 = -96,仍然溢出-96十六进制为`0xA0`,将其直接转换为有符号数`0xA0 = 160`,在没put之前的数据为150,put10个后,缓冲区中的数据刚好为160,刚好为溢出计算结果。
*/

进行上述运算的前提是,size必须为2的次幂。假如size = 257,则上述的运行就不会成功。

测试实例

上面描述都是基于运算推导的,下面据结合本文中的代码进行下验证。
测试代码如下:设置空间大小为128,in和out为8位无符号整数

int main()
{uint8_t output[512] = { 0 };uint8_t data[256] = { 0 };for (int i = 0; i < 256; i++)data[i] = i;kfifo fifo(128);fifo.put(data, 100);fifo.get(output, 50);fifo.put(data, 30);auto c = fifo.put(data + 10, 92);cout << "Empty:" << fifo.isEmpty() << endl;cout << "Left Space:" << fifo.left() << endl;cout << "Length:" << fifo.length() << endl;uint8_t a = fifo.size - fifo.in + fifo.out;uint8_t b = fifo.in - fifo.out;cout << "=======================================" << endl;fifo.get(output, 128);cout << "Empty:" << fifo.isEmpty() << endl;cout << "Left Space:" << fifo.left() << endl;cout << "Length:" << fifo.length() << endl;cout << "======================================" << endl;fifo.put(output, 100);cout << "Empty:" << fifo.isEmpty() << endl;auto d = static_cast<uint8_t>(fifo.left());auto e = static_cast<uint8_t>(fifo.length());printf("Left Space:%d\n", d); printf("Length:%d\n", e);getchar();return 0;
}

执行结果:
439761-20161116164551701-1104998530.png

  • 第一个输出是将缓冲区填满的状态
  • 第二个输出是将缓冲区取空的状态
  • 第三个是in溢出的情况,具体来看看:
    在第二个输出将缓冲区取空的时候,in = out = 178。接着,向缓冲区put了100个数据,这时候in += 100会溢出,溢出后in = 22。看输出结果:put前缓冲区为空,put100个数据后,缓冲区的空闲空间为28,数据长度为100,是正确的。

本文代码下载地址:http://download.csdn.net/detail/brookicv/9684809

转载于:https://www.cnblogs.com/wangguchangqing/p/6070286.html

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/429846.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

win32mysql数据库回复_windows mysql 数据库备份与还原方法

一.使用 Mysql 自带 mysqldump 命令进行数据库备份mysql 数据库自带备份命令 mysqldump ,可对数据库进行备份操作最简单的备份是将数据库备份至本地&#xff0c;生成 **.sql 文件编写备份脚本文件(创建一个 txt 文件&#xff0c;写入批处理脚本&#xff0c;再将文件的后缀改为…

领域模型(domain model)贫血模型(anaemic domain model)充血模型(rich domain model)

领域模型是领域内的概念类或现实世界中对象的可视化表示&#xff0c;又称为概念模型或分析对象模型&#xff0c;它专注于分析问题领域本身&#xff0c;发掘重要的业务领域概念&#xff0c;并建立业务领域概念之间的关系。 贫血模型是指使用的领域对象中只有setter和getter方法&…

datagrid显示mysql_WPF DataGrid显示MySQL查询信息,且可删除、修改、插入 (原发布 csdn 2018-10-13 20:07:28)...

1、入行好几年了&#xff0c;工作中使用数据库几率很小(传统行业)。借着十一假期回家机会&#xff0c;学习下数据库。2、初次了解数据库相关知识&#xff0c;如果本文有误&#xff0c;还望告知。3、本文主要目的&#xff0c;记录下wpf界面显示数据库信息&#xff0c;且可进行删…

网站SEO优化中内部链接的优化

重要性&#xff1a;内链有效的优化能够间接的提高某页面的权重达到搜索排名靠前的效果。同时有效的带领搜索引擎蜘蛛对整站进行抓取。 网站头部导航&#xff1a; 这个导航称为‘网站主导航’&#xff0c;当用户来到网站需要给他们看到的内容。也就是UE&#xff08;用户体验&…

mysql 集群 qps_MySQL Cluster:如何通过扩展为MySQL带来2亿QPS

本篇文章的目的在于介绍MySQL Cluster——也就是MySQL的一套内存内、实时、可扩展且具备高可用性的版本。在解决标题中所提到的每秒2亿查询处理能力问题之前&#xff0c;我们先对MySQL集群的背景信息及其架构进行一番回顾&#xff0c;这将有助于大家理解上述目标的实现过程。My…

测试题的答案(技术博客)

根据老师的要求&#xff0c;我把上次测试的答案汇总了下&#xff0c;将程序写的得到满分的答案给挑了出来&#xff0c;希望大家不要追究版权问题&#xff0c;若有问题&#xff0c;我们私下武力解决问题。 第一题&#xff1a;从键盘输入一个大写字母&#xff0c;要求改用小写字母…

history模式监听_面试题:VueRouter中的 hash 模式和 history 模式有什么区别

面试题&#xff1a;VueRouter中的 hash 模式和 history 模式有什么区别hash模式hash 模式的路由中带有 # 号hash 模式通过 window.onhashchange 方法监听路由的修改hash 模式在页面刷新的时候&#xff0c;发送的请求 url 是不带 # 后面的内容的hash 模式可以兼容部分低版本的浏…

DC综合流程

Design Compiler and the Design Flow 步骤 将HDL描述的设计输入到Design Compiler中Design Compiler使用technology libraries, synthetic or DesignWare libraries, and symbol libraries执行综合过程&#xff0c;并展示综合结果。将HDL翻译为门级描述之后&#xff0c;Design…

hdu 2222 Keywords Search(ac自动机)

题目链接 http://acm.hdu.edu.cn/showproblem.php?pid2222 题意&#xff1a;给你一系列子串&#xff0c;再给你一个主串问你主串一共有几个匹配子串 原来使用字典树写的但数据有点大TLE了&#xff0c;然后就开始学习ac自动机了&#xff0c;ac自动机就像是多串匹配的kmp原理也是…

对象必须实现 iconvertible。_精雕基础教程:对象的显示颜色

“颜色工具栏”用于修改图形、文字等操作对象的颜色&#xff0c;设置轮廓线或者区域填充颜色&#xff0c;从而获得彩色效果图。这一篇文章我们叙述如何修改对象的显示颜色和填充颜色。如下图所示&#xff0c;颜色工具栏按钮功能的说明参见相关部分。图 - 颜色工具栏修改对象颜色…

抓包mysql乱码_抓包数据乱码是什么情况?

为什么会出现这种情况&#xff1f;细心的童鞋可能发现是我们发送给服务器的请求连接的数据不同&#xff1a;第一张图的信息是{"roomid":98284,"uid":271298361556770}第二张图的信息是{"uid":276194535568357,"protover":2,"room…

WebView

WebView在Android系统中内置了一款高性能webkit内核浏览器&#xff0c;在SDK中封装为一个叫做WebView组件&#xff0c;WebView是基于webkit引擎&#xff0c;展现Web页面的控件。 WebView控件功能强大&#xff0c;除了具有一般View的属性和设置外&#xff0c;还可以对url请求、页…

mysql sql优化器_MySQL SQL优化之‘%’

设计索引的主要目的就是帮助我们快速获取查询结果&#xff0c;而以%开头的like查询则不能够使用B-Tree索引。考虑到innodb的表都是聚簇表(类似于oracle中的索引组织表)&#xff0c;且二级索引叶节点中记录的结构为(索引字段->主键字段)&#xff0c;我们可以通过改写sql(mysq…

[转载]析构函数的虚析构和非虚析构调用的差别

代码示例: 非虚析构的情况下. #include <stdio.h> #include <iostream>using namespace std;class A { public:A(){std::cout << "A is created." << std::endl;}~A(){std::cout << "A is deleted." << std::endl;} }…

外部函数能修改闭包内的变量_Python函数式编程,Python闭包

前置内容为了更容易理解闭包&#xff0c;在说闭包之前&#xff0c;讲一下两个概念&#xff1a;作用域和嵌套函数。作用域作用域是变量能被访问的范围&#xff0c;定义在函数内的变量是局部变量&#xff0c;局部变量的作用范围只能在函数内部&#xff0c;它不能在函数被外引用。…

onmousedown活用之碰撞效果

通过绝对定位,在页面中随意位置设置两个div; 也就是说div 是拖动的框,div1和div2是被触碰的框; 1 <!DOCTYPE html>2 <html>3 4 <head>5 <meta charset"UTF-8">6 <title></title>7 <style type"text/css&quo…

python判断密码强度_python – 检查密码的强度(如何检查条件)

你好最好的方法是使用正则表达式搜索这是我目前使用的功能def password_check(password):"""Verify the strength of passwordReturns a dict indicating the wrong criteriaA password is considered strong if:8 characters length or more1 digit or more1 s…

java 实现微博,QQ联合登录

详见&#xff1a;http://blog.yemou.net/article/query/info/tytfjhfascvhzxcyt313 开发平台 http://connect.qq.com/ http://open.weibo.com/ 可以下载相应的sdk 和 API Java代码 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748…

python启动多个进程_Python程序中的进程操作--—--开启多进程

Python程序中的进程操作--—--开启多进程之前我们已经了解了很多进程相关的理论知识&#xff0c;了解进程是什么应该不再困难了&#xff0c;刚刚我们已经了解了&#xff0c;运行中的程序就是一个进程。所有的进程都是通过它的父进程来创建的。因此&#xff0c;运行起来的python…

Angular JS (2)

通过Angular JS的官方教学文档&#xff0c;了解 routeProvider 的用法&#xff0c; angular.module(aaa).config([$locationProvider,$routeProvider, funtion config($locationProvider, $routeProvider){ app.config.js ---- angular. module(phonecatApp). config([$locat…