环形缓冲区RingBuff

本文涉及的产品
全局流量管理 GTM,标准版 1个月
公共DNS(含HTTPDNS解析),每月1000万次HTTP解析
云解析 DNS,旗舰版 1个月
简介: 环形缓冲区RingBuff

       今天我们一起来聊一下环形缓冲区RingBuff又叫LoopBuff等等,都是相同的东西,只是一个名字。

       我们在编写代码的时候缓冲区是几乎每个代码都必不可少的东西,比如存放串口接收的数据、做Modbus通讯、和一些模块通讯等等都会用到缓冲区,简单的来写的话往往就是开辟一个buffer,把接收到的数据保存在这个buffer中,然后再去解析这个buffer的数据。

       

       以串口和GPS通讯为例,我们常常使用串口接收中断接收GPS的数据包,把接收到的数据保存在RecBuff中,然后再去申明一个函数去解析这个数据包如:void GPSDataDecode(uint8_t *gps_data);通过解析函数找到并计算出GPS数据包中的UTC时间、经纬度、海拔高度等数据。解析完之后又在去更新RecBuff中的数据,表面上看上去这样的逻辑做好像并没有什么问题,但在实际中总会出现丢包的问题存在,原因很简单,即便串口的工作很稳定能保证接收到每一个字节到RecBuff中去,但因为解析RecBuff也需要时间,通常在解析的时候我们会加锁,在这个时间内串口的数据仍在源源不断的接收,就有可能会出现RecBbuff中的数据更改,导致解析出错。


       以上只是简单的举个例子,也就是接收和解析不能同时进行,无法一边接收数据,一边解析数据,两者之间互不干扰。


       介于以上问题,有的人就说了双缓冲其实就可以了,确实是,但是嘛,逼格不高啊,还有一个问题就是采集的速度和处理的速度不同时,环形缓冲区可能是更好的选择。说白了就是生产者和消费者的问题。


       本文是基于环形缓冲区介绍的,双缓冲就不过多的展开讨论。


此处分割线,以下是正文,以上凑字数,哈哈哈


       

       在计算机中内存是一段连续的地址空间,当我们把开辟的一段地址空间的首尾连接起来的时候就构成了一个环形缓冲区。类似下面的图示:

首尾相连后发挥你的想象,就是下图这样的结构,可能便于我们理解。


       其实说白了,就是从起始地址开始写入,写入一定的长度后再从下一个地址再写入数据,读出也是一样的道理。


       下面我们分别分析下读写过程,便于后面我们代码的实现。


1、写入数据

  我们按照先进先出的原则来操作我们的RingBuff。即在尾部插入数据,头部读出数据,当缓冲区满了之后,再有数据写入就舍弃最先进入缓冲区的数据。

   初始时,尾指针指向环形缓冲区的起始地址0x80000000,当我们向缓冲区内写入两个字节的数据时,此时尾指针由0x80000000指向了0x80000002的位置,下次再插入数据的时候就从这个地址开始插入数据。看图最清楚了。

      “ 我们每次写入数据的时候都需要移动尾指针Tail到下一个待写入的位置”

这里有一个特殊情况需要考虑下,例如我们上面往缓冲区中写入了2个字节了,当我们再次写入7个字节时,我们发现缓冲区剩下的只有6个字节的空间,那么第七个字节就需要循环回到起始地址去保存,这也是为什么叫做环形缓冲的原因。


2、读出数据

   读数据时从缓冲区的缓冲头读出数据,例如我们上面已经写入了两个字节,此时我们再去读出来一个字节,我们看下头指针的位置。


       我们可以很清楚的看到Head指针的移动,当读完一个字节后就移动到下一个开始读取的位置。

       这里和写入一样有一个特殊情况需要考虑进去,例如我现在读出了一个字节,需要再读出8个字节,剩余的环形缓冲区有7个位置,第8个字节就需要重新再回到0x80000000开始读取。


我们分析了环形缓冲区的读写过程,其实不难发现,我们在定义环形缓冲区的时候需要哪些数据结构去描述这个环形缓冲区。一般就定义一个结构体,如下:

typedef struct 
{
    uint8_t   *Buff;  /* 环形缓冲区内存池 */
    uint16_t  Head;   /* 环形缓冲区的头指针--数据读出的起始位置 */
    uint16_t  Tail;   /* 环形缓冲区的尾指针--数据写入的起始位置 */
    uint16_t  Size;   /* 环形缓冲区的大小 */
    uint16_t  Remain; /* 缓冲区中剩余可以写入的字节数 */
}stRingBuff;

   我们在实际项目中使用环形缓冲区中还是要先实现几个基本的接口函数供我们使用,如下:

int RingBuff_Creat(stRingBuff *ringbuff, uint16_t size);
int RingBuff_Write(stRingBuff *ringbuff, uint8_t *wbuff, uint16_t wlen);
int RingBuff_Read(stRingBuff *ringbuff, uint8_t *rbuff, uint16_t rlen);
int RingBuff_Free(stRingBuff *ringbuff);

      上述的四个接口函数分别实现了环形缓冲区的创建及初始化,读写缓冲区以及释放缓冲区。


       下面来点“肝货”的,实现上述四个接口的代码:


1、创建一个环形缓冲区

/*
* 函数名称:RingBuff_Init
* 输入参数:{stRingBuff *} ringbuff:环形缓冲区数据结构体
           {uint16_t    } size:申请的内存大小
* 返 回 值:-1:内存申请失败  
           0:RingBuff初始化完成
* 作    者:Barry--创客小巴
* 功能描述:创建环形缓冲区并申请内存空间
* 修改记录:None
*/
int RingBuff_Creat(stRingBuff *ringbuff, uint16_t size)
{
    /* 给环形缓冲区申请内存空间 */
    ringbuff->Buff = malloc(size);
    /* 内存申请失败 */
    if(ringbuff == NULL)
        return -1;
    /* 初始化环形缓冲区参数 */
    ringbuff -> Head = 0;
    ringbuff -> Tail = 0;
    ringbuff -> Size = size;
    ringbuff -> Remain = size;
    printf("Ringbuff size = %d, remain = %d\n", ringbuff -> Size, ringbuff -> Remain);
    return 0;
}

这里主要给环形缓冲区的的buff申请了一段内存空间,同时初始化环形缓冲区的相关参数。


2、写环形缓冲区

/*
* 函数名称:RingBuff_Write
* 输入参数:{stRingBuff *} ringbuff:环形缓冲区数据结构体
           {uint8_t *   } wbuff:待写入环形缓冲区的数据
           {uint16_t    } wlen:写入环形缓冲区的字节长度
* 返 回 值:-2:环形缓冲区没有内存空间保存    
           -1:非法指针传入    
           wlen:向环形缓冲区中写入wlen个wbuff数据成功
* 作    者:Barry--创客小巴
* 功能描述:向环形缓冲区中写入wlen个wbuff数据
* 修改记录:None
*/
int RingBuff_Write(stRingBuff *ringbuff, uint8_t *wbuff, uint16_t wlen)
{
    if((ringbuff == NULL) || (wbuff == NULL))
    {
        printf("Please check point param\n.");
        return -1;
    }    
    /* 环形缓冲区中没有剩余空间写入,即缓冲区中的数据还未解析完成空出部分内存空间 */
    if(ringbuff -> Remain == 0)
    {
        printf("Insufficient memory space for writing data.\n");
        return -2;
    }    
    /* 从Tail地址开始写入的数据长度超过缓冲区的最大字节数 */
    if(ringbuff -> Tail + wlen > ringbuff -> Size)
    {
        /* 保存环形缓冲区中剩余的内存空间 */
        uint16_t Remain_Size = ringbuff -> Size - ringbuff -> Tail;
        /* 填充尾部的最后的内存空间 */
        memcpy(&ringbuff -> Buff[ringbuff -> Tail], wbuff, Remain_Size);
        /* 剩下的从头开始继续填充数据 */
        memcpy(ringbuff -> Buff, &wbuff[Remain_Size], wlen - Remain_Size);
        /* 移动Tail,指向下一个待写入数据的位置 */
        ringbuff -> Tail = wlen - Remain_Size;
    }
    /* 剩余的缓冲区长度足够写入wlen长度的wbuff数据 */
    else
    {
        /* 复制wlen长度的wbuff数据到环形缓冲区中 */
        memcpy(&ringbuff -> Buff[ringbuff -> Tail], wbuff, wlen);
        /* 移动Tail,指向下一个待写入数据的位置 */
        ringbuff -> Tail += wlen;
    }
    /* 环形缓冲区中的剩余空间自减--减去写入缓冲区的长度 */
    ringbuff -> Remain -= wlen;
    printf("<%s> Ringbuff remain size = %d, Tail_Point = %d\n", __func__, ringbuff -> Remain, ringbuff -> Tail);
    return wlen;
}

3、读环形缓冲区

/*
* 函数名称:RingBuff_Read
* 输入参数:{stRingBuff *} ringbuff:环形缓冲区数据结构体
           {uint8_t *   } rbuff:待读出环形缓冲区的数据
           {uint16_t    } rlen:读出环形缓冲区的字节长度
* 返 回 值:-2:环形缓冲区没有内存空间保存    
           -1:非法指针传入    
           rlen:从环形缓冲区中读出rlen个rbuff数据成功
* 作    者:Barry--创客小巴
* 功能描述:从环形缓冲区中读出rlen个rbuff数据
* 修改记录:None
*/
int RingBuff_Read(stRingBuff *ringbuff, uint8_t *rbuff, uint16_t rlen)
{
    if((ringbuff == NULL) || (rbuff == NULL))
    {
        printf("Please check point param\n.");
        return -1;
    }    
    /* 环形缓冲区中剩余空间 = 缓冲区的总长,即环形缓冲区中无数据,返回 */
    if(ringbuff -> Remain == ringbuff -> Size)
    {
        printf("No data in the ringbuffer.\n");
        return -2;
    }    
    /* 从Head地址开始读出的数据长度超过缓冲区的最大字节数 */
    if(ringbuff -> Head + rlen > ringbuff -> Size)
    {
        /* 保存环形缓冲区中待读出的尾部剩余字节数 */
        uint16_t Remain_Size = ringbuff -> Size - ringbuff -> Head;
        /* 读出尾部的最后的剩余字节 */
        memcpy(rbuff, &ringbuff -> Buff[ringbuff -> Head], Remain_Size);
        /* 剩下的从头开始继续读取数据 */
        memcpy(&rbuff[Remain_Size], ringbuff -> Buff, rlen - Remain_Size);
        /* 移动Head,指向下一个待读出数据的位置 */
        ringbuff -> Head = rlen - Remain_Size;
    }
    /* 剩余的缓冲区长度足够读出rlen长度的rbuff数据 */
    else
    {
        /* 复制wlen长度的wbuff数据到环形缓冲区中 */
        memcpy(rbuff, &ringbuff -> Buff[ringbuff -> Head], rlen);
        /* 移动Tail,指向下一个待写入数据的位置 */
        ringbuff -> Head += rlen;
    }
    /* 环形缓冲区中的剩余空间自加--加上读出缓冲区的长度 */
    ringbuff -> Remain += rlen;
    printf("<%s> Ringbuff remain size = %d, Head_Point = %d\n", __func__, ringbuff -> Remain, ringbuff -> Head);
    return rlen;
}

4、释放环形缓冲区

/*
* 函数名称:RingBuff_Free
* 输入参数:{stRingBuff *} ringbuff:环形缓冲区数据结构体
* 返 回 值:-1:非法指针传入    
           0:释放环形缓冲区成功
* 作    者:Barry--创客小巴
* 功能描述:释放环形缓冲区,并复位环形缓冲区参数
* 修改记录:None
*/
int RingBuff_Free(stRingBuff *ringbuff)
{
    if(ringbuff == NULL)
        return -1;
    /* 释放环形缓冲区的内存空间 */
    free(ringbuff);
    ringbuff -> Head = 0;
    ringbuff -> Tail = 0;
    ringbuff -> Size = 0;
    ringbuff -> Remain = 0;
    printf("Free ringbuff.\n");
    return 0;
}

好嘞,上面主要的四个接口函数的代码实现已经贴出来了,有需要的小伙伴就可以拿走直接到项目上使用啦,不过不要直接照搬照抄哦,具体项目具体对待,修修改改去适配自己的代码就可以啦。


下面我们来测试以下环形缓冲区的运行。

使用下面的代码来简单测试下,通过打印看下头尾指针的位置以及剩余空间是否正确。

int main(void)
{
    /* 写入20个字节的数据 */
    uint8_t Write_Buff[20];
    /* 读出10个字节的数据 */
    uint8_t Read_BUff[10];
    /* 再次写入16个字节的数据 */
    uint8_t Write1_Buff[16];
    /* 创建环形缓冲区,长度为32个字节 */
    RingBuff_Creat(&RingBuff, 32);
    /*向环形缓冲区中写入Write_Buff*/
    RingBuff_Write(&RingBuff, Write_Buff, sizeof(Write_Buff));
    /*从环形缓冲区中读出数据*/
    RingBuff_Read(&RingBuff, Read_BUff, sizeof(Read_BUff));
    /*向环形缓冲区中写入Write1_Buff*/
    RingBuff_Write(&RingBuff, Write1_Buff, sizeof(Write1_Buff));
    /* 程序退出--释放环形缓冲区 */
    RingBuff_Free(&RingBuff);
    return 0;
}

打印输出如下:

Ringbuff size = 32, remain = 32
<RingBuff_Write> Ringbuff remain size = 12, Tail_Point = 20
<RingBuff_Read> Ringbuff remain size = 22, Head_Point = 10
<RingBuff_Write> Ringbuff remain size = 6, Tail_Point = 4
Free ringbuff.

从上述打印中可以看出,符合我们的设计要求,完工。


创作不易,点个赞和在看再走呗~

相关文章
|
7月前
|
缓存
内存学习(三):物理地址空间
内存学习(三):物理地址空间
220 0
|
4月前
|
Ubuntu Linux 数据安全/隐私保护
内核实验(七):使用内核KFIFO环形缓冲区机制
本文通过一个内核模块实验,演示了如何在Linux内核中使用KFIFO环形缓冲区机制,包括定义KFIFO、编写驱动程序以及在Qemu虚拟机中进行编译、部署和测试,展示了KFIFO在无需额外加锁的情况下如何安全地在读者和写者线程间进行数据传输。
171 0
内核实验(七):使用内核KFIFO环形缓冲区机制
|
6月前
|
程序员 编译器 C++
C++内存分区模型(代码区、全局区、栈区、堆区)
C++内存分区模型(代码区、全局区、栈区、堆区)
|
7月前
|
程序员 编译器 C++
内存分区模型(代码区、全局区、栈区、堆区)
内存分区模型(代码区、全局区、栈区、堆区)
|
7月前
|
存储 程序员 编译器
【C/C++ 堆栈以及虚拟内存分段 】C/C++内存分布/管理:代码区、数据区、堆区、栈区和常量区的探索
【C/C++ 堆栈以及虚拟内存分段 】C/C++内存分布/管理:代码区、数据区、堆区、栈区和常量区的探索
296 0
获取不同时区的时间
获取不同时区的时间
60 0
|
存储 缓存 索引
环形缓冲区、链表及二叉树示例
环形缓冲区、链表及二叉树示例
106 0
|
存储
环形缓冲区
环形缓冲区 是一段 先进先出 的循环缓冲区,有一定的大小,我们可以把它抽象理解为一块环形的内存。 我们使用环形缓冲区主要有两个原因; (1)当我们要存储大量数据时,我们的计算机只能处理先写入的数据,处理完毕释放数据后,后面的数据需要前移一位,大量的数据会频繁分配释放内存,从而导致很大的开销。使用环形缓冲区 可以减少内存分配继而减少系统的开销。 (2)如果我们频繁快速的持续向计算机输入数据,计算机可能执行某个进程不能及时的执行输入的数据,导致数据丢失。这时,我们可以将要输入的数据放入环形缓冲区内,计算机就不会造成数据丢失。
111 0
|
存储 安全 C语言
C语言实现环形缓冲区
C语言实现环形缓冲区
204 0
|
存储 程序员 编译器
关于栈区、堆区、全局区、文字常量区、程序代码区
一个由C/C++编译的程序占用的内存分为以下几个部分: 栈区、栈区、堆区、全局区、文字常量区、程序代码区
183 0