缓冲区一般用于解决设备接收数据的速度和设备处理速度不匹配的情况下,防止丢包,通俗的来说就是:收到数据先存进缓冲区,等到CPU来处理的时候一次性取出处理。
缓冲区有两种形式,一种是数组,一种就是本文所介绍的环形缓冲区ringbuff。
相较于数组,环形缓冲区对整段内存的利用达到最大,并且使用非常方便,如下:
① 写入的时候不用手动维护下标,直接写入即可(由缓冲区的实现维护);
② 读取的时候不用判断从哪里读,直接读取即可(有缓冲区的实现维护)
本文设计的一个简单的不定长串口协议如下:
数据类型:比如0x3F表示这是通道1的数据,0x4E表示通道2的数据;
数据长度:表示后面跟着有效数据的长度;
有效数据:有效字节数;
校验数据:省略;
接下来演示如何用环形缓冲区做到不丢包解析。
3.2. 计算缓冲区大小
假定数据每200ms处理一次,而数据10ms接收一次,每次接收的数据包长度为7个字节。
要想做到不丢包,就需要将200ms内接收到的所有数据包都存进缓冲区,所以缓冲区大小至少为:200/10*7 = 140 个字节。
保险起见,可以将缓冲区适当的扩大一下,设置为150个字节。
环形缓冲区(ring buffer),环形队列(ring queue) 多用于2个线程之间传递数据,是标准的先入先出(FIFO)模型。一般来说,对于多线程共享数据,需要使用mutex来同步,这样共享数据才不至于发生不可预测的修改/读取,然而,mutex的使用也带来了额外的系统开销,ring buffer/queue 的引入,就是为了有效地解决这个问题,因其特殊的结构及算法,可以用于2个线程中共享数据的同步,而且必须遵循A线程push in,B线程pull out的原则。采用这个机制另外一个用处是A push完数据后可以马上接收另外一个数据,当输入数据快速时不容易造成数据阻塞而丢包.
线程 A 媒介 线程 B
data in --> ring buffer/queue --> data out
之所以ringbuffer采用这种数据结构,是因为它在可靠消息传递方面有很好的性能。这就够了,不过它还有一些其他的优点。
首先,因为它是数组,所以要比链表快,而且有一个容易预测的访问模式。(译者注:数组内元素的内存地址的连续性存储的)。这是对CPU缓存友好的—也就是说,在硬件级别,数组中的元素是会被预加载的,因此在ringbuffer当中,cpu无需时不时去主存加载数组中的下一个元素。(校对注:因为只要一个元素被加载到缓存行,其他相邻的几个元素也会被加载进同一个缓存行)
其次,你可以为数组预先分配内存,使得数组对象一直存在(除非程序终止)。这就意味着不需要花大量的时间用于垃圾回收。此外,不像链表那样,需要为每一个添加到其上面的对象创造节点对象—对应的,当删除节点时,需要执行相应的内存清理操作。
2. 实现
2.1. C语言实现
#ifndef __FIFO_H__
#define __FIFO_H__
typedef struct FIFO_s{
volatile unsigned int r;
volatile unsigned int w;
unsigned int depth;
unsigned int count;
unsigned int elem_size;
// call backfunction
void* buf;
}fifo_t;
/**
* @brief Fifo init
* @param pfifo Fifo struct head
* @param pbuf Fifo data buf
* @param elem_size Fifo elem size
* @param depth Fifo depth
*/
void fifo_init(fifo_t* pfifo, void* pbuf, unsigned int elem_size, unsigned int depth);
/**
* @brif
* @param pfifo
* @param pelem
*/
int fifo_write(fifo_t* pfifo, void* pelem);
/**
* @param pfifo
* @param pelem
* @return 1-true, 0-false
*/
int fifo_read(fifo_t* pfifo, void* pelem);
/**
* @param pfifo
*/
void fifo_clear(fifo_t* pfifo);
/**
* @param pfifo
*/
int fifo_is_full(fifo_t* pfifo);
/**
* @param pfifo
*/
int fifo_is_empty(fifo_t* pfifo);
#endif
#include <string.h>
#include "fifo.h"
void fifo_init(fifo_t* pfifo, void* pbuf, unsigned int elem_size, unsigned int depth)
{
pfifo->buf = pbuf;
pfifo->count = 0;
pfifo->depth = depth;
pfifo->elem_size = elem_size;
pfifo->r = 0;
pfifo->w = 0;
return;
}
int fifo_write(fifo_t* pfifo, void* pelem)
{
if(fifo_is_full(pfifo))
return 0;
memcpy((unsigned char*)pfifo->buf + pfifo->w*pfifo->elem_size, (unsigned char*)pelem, pfifo->elem_size);
pfifo->w++;
if(pfifo->w >= pfifo->depth)
pfifo->w = 0;
pfifo->count++;
return 1;
}
int fifo_read(fifo_t* pfifo, void* pelem)
{
if(fifo_is_empty(pfifo))
return 0;
memcpy(pelem, (unsigned char*)pfifo->buf+pfifo->r*pfifo->elem_size, pfifo->elem_size);
pfifo->r++;
if(pfifo->r >= pfifo->depth)
pfifo->r = 0;
pfifo->count--;
return 1;
}
int fifo_is_full(fifo_t* pfifo)
{
if(pfifo->count = pfifo->depth);
return 1;
return 0;
}
int fifo_is_empty(fifo_t* pfifo)
{
if(pfifo->count = 0)
return 1;
return 0;
}
void fifo_clear(fifo_t* pfifo)
{
pfifo->r = 0;
pfifo->w = 0;
pfifo->count = 0;
return;
}
#include <stdio.h>
#include "fifo.h"
using namespace std;
int main(int argc, char** argv)
{
fifo_t fifo;
float fifo_array[10];
printf("Test float data\n");
fifo_init(&fifo, fifo_array, sizeof(float), 10);
for(int i = 0; i < 10; i++ )
{
float fval = (100.0f+1*i);
fifo_write(&fifo, &fval);
}
printf("Current fifo count %d.\n", fifo.count);
float f_getval = 0;
fifo_read(&fifo, &f_getval);
printf("Read fifo member data %.2f.\n", f_getval);
return 0;
}
ing buffer主要用于存储一段连续的数据块,例如网络接收的数据。
r_cursor 读指针,只在线程B中才能被修改,对于线程A,它是readonly的
tr_cursor 辅助读指针,只在线程B中才能被引用,用于计算当前有多少可读数据
w_cursor 写指针,只在线程A中才能被修改,对于线程B,它是readonly的
tw_cursor 辅助写指针,只在线程A中才能被引用,用于计算当前有多少空闲位置可以写入数据
length 缓冲区长度
data 缓冲区实体
需要注意的是,在data中,需要保留1byte的位置,因为:
a...
push数据时,理论上来说,我们应该是从 thiz->tw_cursor = (thiz->w_cursor + 1) % thiz->length;
开始写的,如果此时 thiz->tw_cursor == thiz->r_cursor, 我们认为此时缓冲区已满
b...
pull数据时,也是相同的道理,thiz->r_cursor == thiz->w_cursor,我们认为此时缓冲区无数据可读
也就是说,r_cursor == w_cursor 是一个特殊的位置,所以我们需要在data里面保留1byte。
具体原理可参考 IDirectSoundBuffer8