面向数据流编程:理解环形缓冲区——Buffer Ring
所谓环形缓冲区是我们预留的一片用来保存接收数据的区域(可以是外部的也可以是内部的),存储数据的区域是有限的,而不断接收的数据长度是未知的,甚至是无限的(理论上不断电不破坏,传感器就能一直有数据传回),鉴于硬件内存的限制我们无法创建一个无限大的存储空间,只能在有限的数据存储区域内,边存边写,来减小数据的丢失。
环形缓冲区实际上就是一个边存边处理数据的模式,新存储的数据在存储区达到上限后将重回起点将已经处理完的数据重新覆盖,达到一个重复利用的目的(这里可以理解为铅笔,橡皮檫与纸,你在打草稿时,一页的计算量计算出结果后,你只需保存答案,而草稿纸上的计算过程则可以擦除,用来进行下一次计算,环形缓冲区也是如此,处理过后的数据边可以擦除,腾出来空间用以存储新的数据,当然这里没有擦除的必要,重新覆盖即可)。
举一个简单的例子,现在我创建一个大小为50的数组用以接收数据a[50]
,在向存储数据的同时,开始读取数据进行处理,如果读取数据的速度与存储的速度相同,那么存满50个数据时,我同时读取了50个数据,这个数据里面存储的数据对我已经没有了作用,存储数据的指针将会重新指向数组第一个,读取数据的指针同样如此如此便达到了一片小区域无限存储的目的。将数组想象成一个首尾相连的环,这就是所谓的环形缓冲区。
储空间。
环形Buffer的特点:
通常包含一个读指针(read_index)和一个写指针(write_index)。读指针指向环形 Buffer中第一个可读的数据,写指针指向环形Buffer中第一个可写的缓冲区。通过移动读指针和写指针就可以实现Buffer的数据读取和写入。在 通常情况下,环形Buffer的读用户仅仅会影响读指针,而写用户也仅仅会影响写指针。
环形Buffer的原理:
首先在内存里开辟一片区域(大小为 buffer_size),对于写用户,顺次往Buffer里写入东西,一直写到最后那个内存(buffer_size)时再将写指针指向内存区域的首地 址,即接下来的数据转个环放到最开始处,只有遇到Buffer里的有效存储空间为0时,才丢掉数据;对于读用户,顺次从Buffer里读出东西,一直写到 最后那个内存(buffer_size)时再将读指针指向内存区域的首地址,即接下来转个环从最开始处取数据。
有效存储空间与buffer_size的区别:
有效存储空间是指那些没有存放数据,或者以前存放过但已经处理过的数据,就是可用的空间大小;而buffer_size指的是总大小。
通过上面介绍可知,环形Buffer仍然是一长条区域,只不过其空间会被循环使用而已。示意图如下,根据读写指针的位置可分为两种情况,其中阴影填充部分为数据/已用空间,空白的为可用空间,即有效存储空间。
struct rb
{
rt_uint16_t read_index, write_index; //读指针和写指针
rt_uint8_t *buffer_ptr; //环形buffer指针
rt_uint16_t buffer_size; //环形buffer大小
};
然后就是实现读/写操作了,对于写操作:
/* 向环形buffer中写入数据 */
static rt_bool_t rb_put(struct rb* rb, const rt uint8_t *ptr, rt_uint16_t length)
{
rt_size_t size;
/* 判断是否有足够的剩余空间 */
//图示的第一种情况,相减即为有效存储空间,就是中间那段空白区域了
if (rb->read_index > rb->write_index)
size = rb->read_index - rb->write_index;
else // 图示的第二种情况,两段效存储空间之和
size = rb->buffer_size - rb->write_index + rb->read_index;
/* 没有多余的空间,即空间不足无法完成写入,直接返回错误 */
if (size < length) return RT_FALSE;
/* 以下均是指有效存储空间满足,可以完成写操作 */
//图示的第一种情况,所以直接将数据从write_index处写入到buffer_ptr中即可
if (rb->read_index > rb->write_index)
{
/* read_index - write_index 即为总的空余空间
* 关于memcpy, 指的是将prt中长度为length的数据拷贝到第一个参数所指的地址
* 其实每次写入都必须是从write_index(写指针)开始,
* 因为write_index是指向环形Buffer中的第一个可写缓冲区
*/
memcpy(&rb->buffer_ptr[rb->write_index], ptr, length);
rb->write_index += length; //写操作完成后需要将写指针后移相应的长度
}
else //图示的第二种情况,有两段可用空间
{
//如果后半段空闲空间足够容纳,则直接拷贝即可
if (rb->buffer_size - rb->write_index > length)
{
memcpy(&rb->buffer_ptr[rb->write_index], ptr, length);
rb->write_index += length; //写指针后移相应的长度
}
else //如果后半段空间不足,则先将后半段装满,然后再将剩余的装到前半段之中
{
/*
* write_index 后面剩余的空间不存在足够的长度,
* 需要把部分数据复制到前面的剩余空间中
*/
//先塞满后半段,注意大小不是length,而是后半段的长度
memcpy(&rb->buffer_ptr[rb->write index], ptr, rb->buffer_size - rb->write_index);
//此时只能从buffer_ptr的开始处进行写入了,长度为总长减去上次已经写入的
memcpy(&rb->buffer_ptr[0], &ptr[rb->buffer_size - rb->write_index], length - (rb->buffer_size - rb->write_index));
//写指针后移相应的长度,其实就是第二次写入的长度
rb->write_index = length - (rb->buffer_size - rb->write_index);
}
}
return RT_TRUE;
}
对于读操作:
/* 从环形buffer中读出数据 */
static rt_bool_t rb_get(struct rb* rb, rt_uint8 t *ptr, rt_uint16_t length)
{
rt_size_t size;
/* 判断是否有足够的数据 */
if (rb->read_index > rb->write_index) //图示的第一种情况,有效数据为前后两段的和
size = rb->buffer_size - rb->read_index + rb->write_index;
else // 图示的第二种情况, 中间一段为数据,直接相减即可
size = rb->write_index - rb->read_index;
/* 没有足够的数据,如果剩余数据不足,这直接返回错误 */
if (size < length) return RT_FALSE;
/* 以下均是指剩余数据量满足,可以完成读操作 */
if (rb->read_index > rb->write_index) //图示的第一种情况,有两段数据
{
//如果后半段数据足够,则直接取用
if (rb->buffer_size - rb->read_index > length)
{
//这里是将buffer_ptr里面取出长度为length的数据放到ptr之中;
//切记总是从read_index(读指针)开始取
memcpy(ptr, &rb->buffer_ptr[rb->read_index], length);
rb->read_index += length; //读指针后移相应的长度
}
else //如果后半段不够用,则先取完后半段,然后从前半段取剩余的部分
{
/* read index的数据不够,需要分段复制 */
//注意数据长度
memcpy(ptr, &rb->buffer_ptr[rb->read index], rb->buffer_size - rb->read_index);
//注意数据长度和存储的起始位置
memcpy(&ptr[rb->buffer_size - rb->read_index], &rb->buffer_ptr[0], length - rb->buffer_size + rb->read_index);
rb->read_index = length - rb->buffer_size + rb->read_index;
}
}
else //图示的第二种情况,仅中间一段有数据,直接取用
{
memcpy(ptr, &rb->buffer_ptr[rb->read_index], length);
rb->read_index += length; //读指针后移相应的长度
}
return RT_TRUE;
}c
本文在另一篇文章的基础上加了点自己的见解,现附上原文链接理解RT-Thead中使用的环形缓冲区 — Buffer Ring