背景信息

在软件开发中,尤其是在嵌入式系统、实时数据处理以及通信协议的实现中,环形缓冲区(Circular Buffer) 是一种非常高效的数据结构。它以其循环存储的特性,解决了传统线性队列在固定空间内的浪费问题。最近,在处理高频数据传输时,我遇到了以下挑战:

  1. 数据流的缓冲与传递:需要在生产者与消费者之间缓冲数据,避免数据丢失。
  2. 有限内存资源的高效利用:系统内存有限,必须尽可能减少分配与释放操作的开销。
  3. 性能瓶颈:传统队列在频繁操作时效率较低,环形缓冲区的循环队列机制可以显著提升性能。

为了应对这些问题,我实现了一个简单而高效的环形缓冲区,通过合理地控制读写指针,有效地解决了数据流存储和读取的需求,同时避免了内存浪费和性能问题。接下来,我将结合代码示例,详细介绍环形缓冲区的原理与实现。

环形缓冲区指针位置详解

环形缓冲区是一个逻辑上首尾相连的数据存储结构,其核心在于通过写指针读指针的管理,实现高效的数据读写操作。下面我们具体说明两者的作用。

要点 1:缓冲区写指针位置

写指针(wroffset 的作用:

  1. 指向缓冲区中下一次写入数据的位置
  2. 写入数据后,指针向前移动写入的字节数。
  3. 当写指针到达缓冲区尾部时,会通过取模操作循环回到缓冲区的开头。
要点 2:缓冲区读指针位置

读指针(rdoffser 的作用:

  1. 指向缓冲区中下一次读取数据的位置
  2. 读取数据后,指针向前移动读取的字节数。
  3. 和写指针一样,当读指针到达缓冲区尾部时,会通过取模操作循环回到缓冲区的开头。

举例说明

1. 创建一个 10 字节大小的缓冲区

假设缓冲区的总大小为 buflen = 10,初始状态如下:

  • 写指针位置wroffset = 0(指向起始位置)。
  • 读指针位置rdoffser = 0(指向起始位置)。
  • 缓冲区为空,尚未存储任何数据。

图示

Index:    1   2   3   4   5   6   7   8   9   10
Data:     .   .   .   .   .   .   .   .   .   .
       R
       W

    //<<计算可用字节
	canWriteLen=(cb_buffer->rdoffser)-(cb_buffer->wroffset); 
    //<<cb_buffer->rdoffser = 0     cb_buffer->wroffset = 0

	if(canWriteLen<=0) canWriteLen+=cb_buffer->buflen; 
    //<<canWriteLen+=cb_buffer->buflen ;  10字节 
2. 写入 3 字节数据

写入数据 0xA, 0xB, 0xC 后:

  • 写入从索引 0 开始,占用位置 0, 1, 2
  • 写指针位置wroffset = 3(指向下一次写入的位置)。
  • 读指针位置rdoffser = 0(未读取数据)。

图示

Index:    1   2   3   4   5   6   7   8   9   10
Data:     A   B   C   .   .   .   .   .   .   .
       R
                  W

3. 读取 2 字节数据

读取数据 0xA, 0xB 后:

  • 数据从索引 0, 1 读取,占用位置释放。
  • 写指针位置wroffset = 3(未发生写入)。
  • 读指针位置rdoffser = 2(指向下一次读取的位置)。

图示

Index:    1   2   3   4   5   6   7   8   9   10
Data:     .   .   C   .   .   .   .   .   .   .
              R
                  W

4. 写入 5 字节数据

写入数据 0xD, 0xE, 0xF, 0xG, 0xH 后:

  • 写入从索引 3 开始,占用位置 3, 4, 5, 6, 7
  • 写指针位置wroffset = 8(指向下一次写入的位置)。
  • 读指针位置rdoffser = 2(未发生读取)。
Index:    1   2   3   4   5   6   7   8   9   10
Data:     .   .   C   D   E   F   G   H   .   .
              R
                                      W
5. 写入 3 字节数据(跨越尾部)

写入数据 0xI, 0xJ, 0xK 后:

  • 第一部分写入到索引 8, 9
  • 第二部分写入到索引 0(环形缓冲区循环)。
  • 写指针位置wroffset = 1(循环回到缓冲区开头)。
  • 读指针位置rdoffser = 2(未发生读取)。

图示

Index:    1   2   3   4   5   6   7   8   9   10
Data:     K   .   C   D   E   F   G   H   I   J
              R
          W

 //<<计算可用字节
	canWriteLen=(cb_buffer->rdoffser)-(cb_buffer->wroffset); 
    //<<cb_buffer->rdoffser = 2     cb_buffer->wroffset = 1    canWriteLen = 1

6. 相关代码


//<<环形缓冲区结构体
typedef struct circlebuffer
{
	ZUINT32 size;        //<<缓冲区可用空间  
	ZUINT32 wroffset;    //<<缓冲区写指针位置
	ZUINT32 rdoffser;    //<<缓冲区读指针位置
	ZUINT8 * buf;        //<<缓冲区数据包
	ZUINT32 buflen;      //<<缓冲区数据包大小
}circleBuffer;

//<<环形缓冲区指针
circleBuffer* cbBuffer[8];


/***********************************************************************
*@函数名称:               cb_init
*@功能描述:               初始化环形缓冲区,并为其分配内存;
*@参数:
         index[in]:       缓冲区在全局数组中的索引;
         bufLen[in]:      环形缓冲区的长度;
*@返回:
         circleBuffer*:   成功,返回初始化的环形缓冲区指针;
         NULL:            失败,缓冲区长度为 0 或分配失败;
*@备注:
         - 在使用此函数前,需确保 cbBuffer 已正确定义并分配空间;
         - 调用方需要在不再使用缓冲区时释放分配的内存。
 **********************************************************************/

circleBuffer * cb_init(ZUINT32 index,ZUINT32 bufLen)
{
	if(bufLen==0)
		return NULL;

	//<<结构体指针开空间
	cbBuffer[index] = (circleBuffer*)malloc(sizeof(circleBuffer));
	memset(cbBuffer[index],0,sizeof(circleBuffer));

	//<<环形缓冲区开空间
	cbBuffer[index]->buflen=bufLen;
	cbBuffer[index]->buf=(UCHAR *)malloc(bufLen);
	memset(cbBuffer[index]->buf,0,bufLen);

	//<<返回开好的结构体指针
	return cbBuffer[index];
}



/***********************************************************************
*@函数名称:               cb_getcnt
*@功能描述:               获取指定环形缓冲区中当前有效数据的字节数;
*@参数:
         index[in]:       缓冲区在全局数组 cbBuffer 中的索引;
*@返回:
         ZUINT32:         当前缓冲区中的有效数据字节数;
*@备注:
         - 返回值为缓冲区中已写入但尚未读取的数据长度;
         - 调用方需确保 index 对应的环形缓冲区已正确初始化。
 **********************************************************************/

ZUINT32 cb_getcnt(ZUINT32 index)
{
	ZUINT32 dataCnt = 0;
	dataCnt = cbBuffer[index]->size;
	return dataCnt;
}



/***********************************************************************
*@函数名称:               cb_Reset
*@功能描述:               重置指定的环形缓冲区,将其恢复为初始状态;
*@参数:
         index[in]:       缓冲区在全局数组 cbBuffer 中的索引;
*@返回:                  无;
*@备注:
         - 该函数会清空缓冲区中的所有数据,并重置读写指针;
         - 缓冲区的长度和分配的内存不会改变;
         - 调用方需确保 index 对应的缓冲区已正确初始化。
 **********************************************************************/

void cb_Reset(ZUINT32 index)  
{ 
	//<<清0
	cbBuffer[index]->size = 0;
	cbBuffer[index]->rdoffser = 0;
	cbBuffer[index]->wroffset = 0;
	memset(cbBuffer[index]->buf,0,cbBuffer[index]->buflen);
}  




/***********************************************************************
*@函数名称:               cb_free
*@功能描述:               释放指定索引的环形缓冲区及其相关资源;
*@参数:
         index[in]:       缓冲区在全局数组 cbBuffer 中的索引;
*@返回:                  无;
*@备注:
         - 该函数会释放环形缓冲区结构体及其内部的缓冲区内存;
         - 同时清空与该缓冲区相关的全局数组 RecvData 和 Data 的内存;
         - 调用该函数后,index 对应的缓冲区指针将被置为 NULL,防止悬空指针;
         - 调用方需确保在释放前,缓冲区及相关数组已正确初始化。
 **********************************************************************/
 
void cb_free(ZUINT32 index)  
{  
	if (cbBuffer[index])  
	{  
	if (cbBuffer[index]->buf)  
	{  
		free(cbBuffer[index]->buf);  
		cbBuffer[index]->buf = NULL;  
	}  
	free(cbBuffer[index]);  
	cbBuffer[index] = NULL;  
	}

	//<<清空数组
	if(	Data[index])
	{
	    free(RecvData[index]);
		RecvData[index] = NULL;
	}


	//<<清空数组
	if(Data[index])
	{
	    free(Data[index]);
		Data[index] = NULL;
	}
} 



/***********************************************************************
*@函数名称:               cb_write
*@功能描述:               向环形缓冲区写入指定长度的数据;
*@参数:
         cb_buffer[in]:   指向目标环形缓冲区的指针;
         inbuf[in]:       输入数据的起始地址;
         wrireLen[in]:    请求写入的数据长度;
*@返回:
         ZUINT32:         成功,返回实际写入的数据字节数;
         0:               失败,环形缓冲区已满或输入无效;
         -1:              失败,环形缓冲区剩余空间不足;
*@备注:
         - 写入操作前会检查缓冲区的剩余空间,确保不会覆盖未读数据;
         - 当写入数据长度超出尾部空间时,支持折行写入;
         - 调用方需确保 cb_buffer 已正确初始化。
 **********************************************************************/

ZUINT32 cb_write(circleBuffer * cb_buffer,ZUINT8 * inbuf,ZUINT32 wrireLen)
{
	int canWriteLen=0;

	//<<指针检查
	if(cb_buffer==NULL ||inbuf==NULL ||wrireLen==0 )
	{
		return 0;
	}

	//检查环形buffer是否是满的
	if(cb_buffer->size==cb_buffer->buflen)
	{
		return 0;
	} 


	//检查剩余的空间是否够写入
	//<<rdoffser:读指针   wroffset:写指针   读指针 - 写指针:有多少空间可以写
	canWriteLen=(cb_buffer->rdoffser)-(cb_buffer->wroffset);  
	if(canWriteLen<=0) canWriteLen+=cb_buffer->buflen;        


	//<<没有足够的空间则不写入
	if(wrireLen>canWriteLen) return -1;



	//<<开始写
	if(wrireLen<cb_buffer->buflen-cb_buffer->wroffset)
	{
		//不用折行,直接写入
		memcpy(&cb_buffer->buf[cb_buffer->wroffset],inbuf,wrireLen);
	}else
	{
		//一行写完以后,需要重新折过来,从开头在写入 wrireLen-(cb_buffer->buflen-cb_buffer->wroffset)
		memcpy(&cb_buffer->buf[cb_buffer->wroffset],inbuf,cb_buffer->buflen-cb_buffer->wroffset);
		memcpy(cb_buffer->buf,&inbuf[cb_buffer->buflen-cb_buffer->wroffset],wrireLen-(cb_buffer->buflen-cb_buffer->wroffset));
	}



	//更新写指针
	cb_buffer->wroffset=(wrireLen+cb_buffer->wroffset)%cb_buffer->buflen;

	//更新size
	cb_buffer->size+=wrireLen;

	//<<返回写的数据量
	return wrireLen;
}




/***********************************************************************
*@函数名称:               cb_read
*@功能描述:               从环形缓冲区中读取指定长度的数据;
*@参数:
         cb_buffer[in]:   指向目标环形缓冲区的指针;
         outbuf[out]:     用于存储读取数据的缓冲区;
         readLen[in]:     请求读取的数据长度;
*@返回:
         ZUINT32:         实际读取到的数据字节数;
*@备注:
         - 该函数会检查实际可读取的字节数,确保不会读取超出有效数据的范围;
         - 如果请求读取的长度大于缓冲区中的有效数据量,函数会调整为可读取的最大字节数;
         - 支持跨越缓冲区尾部的折行读取,保证数据的完整性;
         - 调用方需确保 cb_buffer 已正确初始化,outbuf 有足够的空间存储读取的数据。
 **********************************************************************/

ZUINT32 cb_read(circleBuffer * cb_buffer,ZUINT8 * outbuf,ZUINT32 readLen)
{
	int canReadLen=0;

	//<<检查
	if(outbuf==NULL) return 0;
	//<<检查
	if(cb_buffer->size==0) return 0;

	//<<计算出可以读取的字节数
	if (cb_buffer->size == cb_buffer->buflen)
	{
		canReadLen = cb_buffer->size;
	}
	else
	{
		canReadLen=cb_buffer->wroffset-cb_buffer->rdoffser;
		if(canReadLen<0) canReadLen+=cb_buffer->buflen;
	}


	//<<想读取到的>实际有的
	if(canReadLen<readLen)
	{
		readLen=canReadLen;
	}

	//<<读取数据
	if(readLen<cb_buffer->buflen-cb_buffer->rdoffser)
	{
		//<<说明不用不用折回来读
		memcpy(outbuf,&cb_buffer->buf[cb_buffer->rdoffser],readLen);
	}else
	{
		//<<说明存到最大的地方没存完,需要继续折回来读,第一次读先读到数组的结尾
		memcpy(outbuf,&cb_buffer->buf[cb_buffer->rdoffser],cb_buffer->buflen-cb_buffer->rdoffser);
		memcpy(&outbuf[cb_buffer->buflen-cb_buffer->rdoffser],cb_buffer->buf,readLen-(cb_buffer->buflen-cb_buffer->rdoffser));
	}

	//<<更新下实际的数量大小
	cb_buffer->size-=readLen;

	//<<更新读指针
	cb_buffer->rdoffser=(readLen+cb_buffer->rdoffser)%cb_buffer->buflen;

	//<<返回读到的数据量
	return readLen;
}

Logo

openvela 操作系统专为 AIoT 领域量身定制,以轻量化、标准兼容、安全性和高度可扩展性为核心特点。openvela 以其卓越的技术优势,已成为众多物联网设备和 AI 硬件的技术首选,涵盖了智能手表、运动手环、智能音箱、耳机、智能家居设备以及机器人等多个领域。

更多推荐