环形缓冲区的实现

环形缓冲区的实现

消息队列锁调用太频繁的问题算是解决了,另一个让人有些苦恼的大概是这太多的内存分配和释放操作了。频繁的内存分配不但增加了系统开销,更使得内存碎片不断增多,非常不利于我们的服务器长期稳定运行。也许我们可以使用内存池,比如SGI STL中附带的小内存分配器。但是对于这种按照严格的先进先出顺序处理的,块大小并不算小的,而且块大小也并不统一的内存分配情况来说,更多使用的是一种叫做环形缓冲区的方案,mangos的网络代码中也有这么一个东西,其原理也是比较简单的。

  就好比两个人围着一张圆形的桌子在追逐,跑的人被网络IO线程所控制,当写入数据时,这个人就往前跑;追的人就是逻辑线程,会一直往前追直到追上跑的人。如果追上了怎么办?那就是没有数据可读了,先等会儿呗,等跑的人向前跑几步了再追,总不能让游戏没得玩了吧。那要是追的人跑的太慢,跑的人转了一圈过来反追上追的人了呢?那您也先歇会儿吧。要是一直这么反着追,估计您就只能换一个跑的更快的追逐者了,要不这游戏还真没法玩下去。

  前面我们特别强调了,按照严格的先进先出顺序进行处理,这是环形缓冲区的使用必须遵守的一项要求。也就是,大家都得遵守规定,追的人不能从桌子上跨过去,跑的人当然也不允许反过来跑。至于为什么,不需要多做解释了吧。

  环形缓冲区是一项很好的技术,不用频繁的分配内存,而且在大多数情况下,内存的反复使用也使得我们能用更少的内存块做更多的事。

  在网络IO线程中,我们会为每一个连接都准备一个环形缓冲区,用于临时存放接收到的数据,以应付半包及粘包的情况。在解包及解密完成后,我们会将这个数据包复制到逻辑线程消息队列中,如果我们只使用一个队列,那这里也将会是个环形缓冲区,IO线程往里写,逻辑线程在后面读,互相追逐。可要是我们使用了前面介绍的优化方案后,可能这里便不再需要环形缓冲区了,至少我们并不再需要他们是环形的了。因为我们对同一个队列不再会出现同时读和写的情况,每个队列在写满后交给逻辑线程去读,逻辑线程读完后清空队列再交给IO线程去写,一段固定大小的缓冲区即可。没关系,这么好的技术,在别的地方一定也会用到的。

在通信程序中,经常使用环形缓冲区作为数据结构来存放通信中发送和接收的数据。环形缓冲区是一个先进先出的循环缓冲区,可以向通信程序提供对缓冲区的互斥访问。

1.环形缓冲区的实现原理

环形缓冲区通常有一个读指针和一个写指针。读指针指向环形缓冲区中可读的数据,写指针指向环形缓冲区中可写的缓冲区。通过移动读指针和写指针就可以实现缓冲区的数据读取和写入。在通常情况下,环形缓冲区的读用户仅仅会影响读指针,而写用户仅仅会影响写指针。如果仅仅有一个读用户和一个写用户,那么不需要添加互斥保护机制就可以保证数据的正确性。如果有多个读写用户访问环形缓冲区,那么必须添加互斥保护机制来确保多个用户互斥访问环形缓冲区。

2、实例:环形缓冲区的实现

环形缓冲区是数据通信程序中使用最为广泛的数据结构之一,下面的代码,实现了一个环形缓冲区:

/*ringbuf .c*/

#include<stdio. h>

#include<ctype. h>

#define NMAX 8

int iput = 0; /* 环形缓冲区的当前放入位置 */

int iget = 0; /* 缓冲区的当前取出位置 */

int n = 0; /* 环形缓冲区中的元素总数量 */

double buffer[NMAX];

/* 环形缓冲区的地址编号计算函数,如果到达唤醒缓冲区的尾部,将绕回到头部。

环形缓冲区的有效地址编号为:0到(NMAX-1)

*/

int addring (int i)

{

       return (i+1) == NMAX ? 0 : i+1;

}

/* 从环形缓冲区中取一个元素 */

double get(void)

{

int pos;

if (n>0){

                     Pos = iget;

                     iget = addring(iget);

                     n--;

                     return buffer[pos];

}

else {

printf(“Buffer is empty\n”);

return 0.0;

}

/* 向环形缓冲区中放入一个元素*/

void put(double z)

{

if (n<NMAX){

                     buffer[iput]=z;

                     iput = addring(iput);

                     n++;

}

else

printf(“Buffer is full\n”);

}

int main{void)

{

chat opera[5];

double z;

do {

printf(“Please input p|g|e?”);

scanf(“%s”, &opera);

              switch(tolower(opera[0])){

              case ‘p’: /* put */

                 printf(“Please input a float number?”);

                 scanf(“%lf”, &z);

                 put(z);

                 break;

case ‘g’: /* get */

                 z = get();

printf(“%8.2f from Buffer\n”, z);

break;

case ‘e’:

                 printf(“End\n”);

                 break;

default:

                 printf(“%s - Operation command error! \n”, opera);

}/* end switch */

}while(opera[0] != ’e’);

return 0;

}

在CAN通信卡设备驱动程序中,为了增强CAN通信卡的通信能力、提高通信效率,根据CAN的特点,使用两级缓冲区结构,即直接面向CAN通信卡的收发缓冲区和直接面向系统调用的接收帧缓冲区。 通讯中的收发缓冲区一般采用环形队列(或称为FIFO队列),使用环形的缓冲区可以使得读写并发执行,读进程和写进程可以采用“生产者和消费者”的模型来访问缓冲区,从而方便了缓存的使用和管理。然而,环形缓冲区的执行效率并不高,每读一个字节之前,需要判断缓冲区是否为空,并且移动尾指针时需要进行“折行处理”(即当指针指到缓冲区内存的末尾时,需要新将其定向到缓冲区的首地址);每写一个字节之前,需要判断缓区是否为满,并且移动尾指针时同样需要进行“ 折行处理”。程序大部分的执行过程都是在处理个别极端的情况。只有小部分在进行实际有效的操作。这就是软件工程中所谓的“8比2”关系。结合CAN通讯实际情况,在本设计中对环形队列进行了改进,可以较大地提高数据的收发效率。 由于CAN通信卡上接收和发送缓冲器每次只接收一帧CAN数据,而且根据CAN的通讯协议,CAN控制器的发送数据由1个字节的标识符、一个字节的RTR 和DLC位及8个字节的数据区组成,共10个字节;接收缓冲器与之类似,也有10个字节的寄存器。所以CAN控制器收的数据是短小的定长帧(数据可以不满 8字节)。于是,采用度为10字节的数据块业分配内存比较方便,即每次需要内存缓冲区时,直接分配10个字节,由于这10个字节的地址是线性的,故不需要进行“折行”处理。更重要的是,在向缓冲区中写数据时,只需要判断一次是否有空闲块并获取其块首指针就可以了,从而减少了重复性的条件判断,大大提高了程序的执行效率;同样在从缓冲队列中读取数据时,也是一次读取10字节的数据块,同样减少了重复性的条件判断。 在CAN卡驱动程序中采用如下所示的称为“Block_Ring_t”的数据结构作为收发数据的缓冲区:

typedef struct {

long signature;

unsigned char *head_p;

unsigned char *tail_p;

unsigned char *begin_p;

unsigned char *end_p;

unsigned char buffer[BLOCK_RING_BUFFER_SIZE];

int usedbytes;

}Block_Ring_t;

该数据结构在通用的环形队列上增加了一个数据成员usedbytes,它表示当前缓冲区中有多少字节的空间被占用了。使用usedbytes,可以比较方 便地进行缓冲区满或空的判断。当usedbytes=0时,缓冲区空;当usedbytes=BLOCK_RING_BUFFER_SIZE时,缓冲区满。 本驱动程序除了收发缓冲区外,还有一个接收帧缓冲区,接收帧队列负责管理经Hilon A协议解包后得到的数据帧。由于有可能要同接收多个数据帧,而根据CAN总线遥通信协议,高优先级的报文将抢占总线,则有可能在接收一个低优先级且被分为 好几段发送的数据帧时,被一个优先级高的数据帧打断。这样会出现同时接收到多个数据帧中的数据包,因而需要有个接收队列对同时接收的数据帧进行管理。当有新的数据包到来时,应根据addr(通讯地址),mode(通讯方式),index(数据包的序号)来判断是否是新的数据帧。如果是,则开辟新的 frame_node;否则如果已有相应的帧节点存地,则将数据附加到该帧的末尾;在插入数据的同时,应该检查接收包的序号是否正确,如不正确将丢弃这包数据。 每次建立新的frame_node时,需要向frame_queue申请内存空间;当frame_queue已满时,释放掉队首的节点(最早接收的但未完成的帧)并返回该节点的指针。 当系统调用读取了接收帧后,释放该节点空间,使设备驱动程序可以重新使用该节点。

环形缓冲队列


项目中需要线程之间共享一个缓冲FIFO队列,一个线程往队列中添数据,另一个线程取数据(经典的生产者-消费者问题)。开始考虑用STL的vector 容器, 但不需要随机访问,频繁的删除最前的元素引起内存移动,降低了效率。使用LinkList做队列的话,也需要频繁分配和释放结点内存。于是自己实现一个有 限大小的FIFO队列,直接采用数组进行环形读取。

队列的读写需要在外部进程线程同步(另外写了一个RWGuard类, 见另一文)

到项目的针对性简单性,实现了一个简单的环形缓冲队列,比STL的vector简单

PS: 第一次使用模板,原来类模板的定义要放在.h 文件中, 不然会出现连接错误。

template <class _Type>

class CShareQueue 

{

public:

CShareQueue();

CShareQueue(unsigned int bufsize);

virtual ~CShareQueue();

_Type pop_front();

bool push_back( _Type item);

//返回容量

unsigned int capacity() { //warning:需要外部数据一致性

return m_capacity;

}

//返回当前个数

unsigned int size() { //warning:需要外部数据一致性

return m_size;

}

//是否满//warning: 需要外部控制数据一致性

bool IsFull() {

return (m_size >= m_capacity);

}

bool IsEmpty() {

return (m_size == 0);

}

 

protected:

UINT m_head;

UINT m_tail;

UINT m_size;

UINT m_capacity;

_Type *pBuf;

 

};

template <class _Type>

CShareQueue<_Type>::CShareQueue() :m_head(0), m_tail(0), m_size(0)

{

pBuf = new _Type[512];//默认512

m_capacity = 512;

}

template <class _Type>

CShareQueue<_Type>::CShareQueue(unsignedint bufsize) : m_head(0), m_tail(0)

{

if( bufsize > 512 || bufsize < 1)

{

pBuf = new _Type[512];

m_capacity = 512;

}

else

{

pBuf = new _Type[bufsize];

m_capacity = bufsize;

}

}

template <class _Type>

CShareQueue<_Type>::~CShareQueue()

{

delete[] pBuf;

pBuf = NULL;

m_head = m_tail = m_size = m_capacity = 0;

}

//前面弹出一个元素

template <class _Type>

_Type CShareQueue<_Type>::pop_front()

{

if( IsEmpty() )

{

return NULL;

}

_Type itemtmp;

itemtmp = pBuf[m_head];

m_head = (m_head + 1) % m_capacity;

--m_size;

return itemtmp;

}

//从尾部加入队列

template <class _Type>

bool CShareQueue<_Type>::push_back(_Type item)

{

if ( IsFull() )

{

return FALSE;

}

pBuf[m_tail] = item;

m_tail = (m_tail + 1) % m_capacity;

++m_size;

return TRUE;

}

 

#endif

http://hi.baidu.com/zkheartboy/blog/item/f162b20fdbf250eeab6457be.html

实现环形缓冲区的通用类: http://hi.baidu.com/broland/blog/item/6a6ddf813f3425c69123d956.html

http://hi.baidu.com/uc100200/blog/item/c6d670543df4544fd00906ac.html

http://hi.baidu.com/282280072/blog/item/9927685090cbb9928d543075.html

★环形缓冲区介绍 

环形缓冲区是生产者和消费者模型中常用的数据结构。生产者将数据放入数组的尾端,而消费者从数组的另一端移走数据,当达到数组的尾部时,生产者绕回到数组的头部。

  如果只有一个生产者和一个消费者,那么就可以做到免锁访问环形缓冲区(Ring Buffer)。写入索引只允许生产者访问并修改,只要写入者在更新索引之前将新的值保存到缓冲区中,则读者将始终看到一致的数据结构。同理,读取索引也只允许消费者访问并修改。

环形缓冲区实现原理图

 

如图所示,当读者和写者指针相等时,表明缓冲区是空的,而只要写入指针在读取指针后面时,表明缓冲区已满。

 

★环形缓冲区内部结构

  ◇外部接口相似

  在介绍环形缓冲区之前,咱们先来回顾一下普通的队列。普通的队列有一个写入端和一个读出端。队列为空的时候,读出端无法读取数据;当队列满(达到最大尺寸)时,写入端无法写入数据。

  对于使用者来讲,环形缓冲区和队列缓冲区是一样的。它也有一个写入端(用于push)和一个读出端(用于pop),也有缓冲区“满”和“空”的状态。所以,从队列缓冲区切换到环形缓冲区,对于使用者来说能比较平滑地过渡。

  ◇内部结构迥异

  虽然两者的对外接口差不多,但是内部结构和运作机制有很大差别。队列的内部结构此处就不多啰嗦了。重点介绍一下环形缓冲区的内部结构。

  大伙儿可以把环形缓冲区的读出端(以下简称R)和写入端(以下简称W)想象成是两个人在体育场跑道上追逐(R追W)。当R追上W的时候,就是缓冲区为空;当W追上R的时候(W比R多跑一圈),就是缓冲区满。

  为了形象起见,去找来一张图并略作修改,如下:

 

  从上图可以看出,环形缓冲区所有的push和pop操作都是在一个固定 的存储空间内进行。而队列缓冲区在push的时候,可能会分配存储空间用于存储新元素;在pop时,可能会释放废弃元素的存储空间。所以环形方式相比队列方式,少掉了对于缓冲区元素所用存储空间的分配、释放。这是环形缓冲区的一个主要优势。

 

 ★环形缓冲区的实现 

  如果你手头已经有现成的环形缓冲区可供使用,并且你对环形缓冲区的内部实现不感兴趣,可以跳过这段。

  ◇数组方式 vs 链表方式

  环形缓冲区的内部实现,即可基于数组(此处的数组,泛指连续存储空间)实现,也可基于链表实现。

  数组在物理存储上是一维的连续线性结构,可以在初始化时,把存储空间一次性 分配好,这是数组方式的优点。但是要使用数组来模拟环,你必须在逻辑上把数组的头和尾相连。在顺序遍历数组时,对尾部元素(最后一个元素)要作一下特殊处理。访问尾部元素的下一个元素时,要重新回到头部元素(第0个元素)。如下图所示:

 

  使用链表的方式,正好和数组相反。链表省去了头尾相连的特殊处理。但是链表在初始化的时候比较繁琐,而且在有些场合(比如后面提到的跨进程的IPC)不太方便使用。

  ◇读写操作

  环形缓冲区要维护两个索引,分别对应写入端(W)和读取端(R)。写入(push)的时候,先确保环没满,然后把数据复制到W所对应的元素,最后W指向下一个元素;读取(pop)的时候,先确保环没空,然后返回R对应的元素,最后R指向下一个元素。

  ◇判断“空”和“满”

  上述的操作并不复杂,不过有一个小小的麻烦:空环和满环的时候,R和W都指向同一个位置!这样就无法判断到底是“空”还是“满”。大体上有两种方法可以解决该问题。

  办法1:始终保持一个元素不用

  当空环的时候,R和W重叠。当W比R跑得快,追到距离R还有一个元素间隔的时候,就认为环已经满。当环内元素占用的存储空间较大的时候,这种办法显得很土(浪费空间)。

  办法2:维护额外变量

  如果不喜欢上述办法,还可以采用额外的变量来解决。比如可以用一个整数记录当前环中已经保存的元素个数(该整数>=0)。当R和W重叠的时候,通过该变量就可以知道是“空”还是“满”。

  ◇元素的存储

  由于环形缓冲区本身就是要降低存储空间分配的开销,因此缓冲区中元素的类型要选好。尽量存储值 类型的数据,而不要存储指针(引用) 类型的数据。因为指针类型的数据又会引起存储空间(比如堆内存)的分配和释放,使得环形缓冲区的效果打折扣。

 

  ★应用场合 

  刚才介绍了环形缓冲区内部的实现机制。按照前一个帖子 的惯例,我们来介绍一下在线程和进程方式下的使用。

  如果你所使用的编程语言和开发库中带有现成的、成熟的 环形缓冲区,强烈建议使用现成的库,不要重新制造轮子;确实找不到现成的,才考虑自己实现。如果你纯粹是业余时间练练手,那另当别论。

  ◇用于并发线程

  和线程中的队列缓冲区类似,线程中的环形缓冲区也要考虑线程安全的问题。除非你使用的环形缓冲区的库已经帮你实现了线程安全,否则你还是得自己动手搞定。线程方式下的环形缓冲区用得比较多,相关的网上资料也多,下面就大致介绍几个。

  对于C++的程序员,强烈推荐使用boost 提供的circular_buffer 模板,该模板最开始是在boost 1.35版本中引入的。鉴于boost在C++社区中的地位,大伙儿应该可以放心使用该模板。

  对于C程序员,可以去看看开源项目circbuf ,不过该项目是GPL协议的,不太爽;而且活跃度不太高;而且只有一个开发人员。大伙儿慎用!建议只拿它当参考。

  对于C#程序员,可以参考CodeProject上的一个示例 。

  ◇用于并发进程

  进程间的环形缓冲区,似乎少有现成的库可用。大伙儿只好自己动手、丰衣足食了。

  适用于进程间环形缓冲的IPC类型,常见的有共享内存 和文件。在这两种方式上进行环形缓冲,通常都采用数组的方式实现。程序事先分配好一个固定长度的存储空间,然后具体的读写操作、判断“空”和“满”、元素存储等细节就可参照前面所说的来进行。

  共享内存方式的性能很好,适用于数据流量很大的场景。但是有些语言(比如Java)对于共享内存不支持。因此,该方式在多语言协同开发的系统中,会有一定的局限性。

  而文件方式在编程语言方面支持很好,几乎所有编程语言都支持操作文件。但它可能会受限于磁盘读写(Disk I/O)的性能。所以文件方式不太适合于快速数据传输;但是对于某些“数据单元 ”很大的场合,文件方式是值得考虑的。

  对于进程间的环形缓冲区,同样要考虑好进程间的同步、互斥等问题。

<think>我们被要求提供环形缓冲区实现的完整例程。环形缓冲区(Circular Buffer)是种高效的数据结构,特别适用于数据流处理,如串口通信等场景。它通过两个指针(读指针和写指针)来管理缓冲区的读写操作,当指针到达缓冲区末尾时会自动绕回到开头,形成个环形结构。 根据引用[2]中的示例,我们可以编写个更完整的环形缓冲区实现。以下是个使用C语言实现环形缓冲区例程,包含初始化、写入、读取和释放等基本操作。 ### 环形缓冲区实现例程 ```c #include <stdio.h> #include <stdlib.h> #include <string.h> // 定义环形缓冲区结构体 typedef struct { unsigned char *buffer; // 缓冲区指针 int size; // 缓冲区大小 int read_pos; // 读位置 int write_pos; // 写位置 int count; // 当前缓冲区中数据数量 } ring_buffer_t; // 初始化环形缓冲区 ring_buffer_t* init_ringbuffer(int size) { ring_buffer_t *rb = (ring_buffer_t*)malloc(sizeof(ring_buffer_t)); if (rb == NULL) { return NULL; } rb->buffer = (unsigned char*)malloc(size); if (rb->buffer == NULL) { free(rb); return NULL; } rb->size = size; rb->read_pos = 0; rb->write_pos = 0; rb->count = 0; return rb; } // 释放环形缓冲区 void release_ringbuffer(ring_buffer_t *rb) { if (rb != NULL) { free(rb->buffer); free(rb); } } // 向环形缓冲区写入数据 int write_ringbuffer(ring_buffer_t *rb, const unsigned char *data, int len) { if (rb == NULL || data == NULL || len <= 0) { return 0; } // 检查是否有足够的空间 int free_space = rb->size - rb->count; if (len > free_space) { len = free_space; // 只写入可用的空间 } // 分两段写入:从写指针到缓冲区末尾,然后从缓冲区开头开始(如果需要) int first_part = rb->size - rb->write_pos; if (len <= first_part) { memcpy(rb->buffer + rb->write_pos, data, len); rb->write_pos = (rb->write_pos + len) % rb->size; } else { memcpy(rb->buffer + rb->write_pos, data, first_part); int second_part = len - first_part; memcpy(rb->buffer, data + first_part, second_part); rb->write_pos = second_part; // 因为从0开始,所以写指针移动到second_part } rb->count += len; return len; } // 从环形缓冲区读取数据 int read_ringbuffer(ring_buffer_t *rb, unsigned char *data, int len) { if (rb == NULL || data == NULL || len <= 0) { return 0; } // 检查可读数据量 if (len > rb->count) { len = rb->count; } // 分两段读取:从读指针到缓冲区末尾,然后从缓冲区开头开始(如果需要) int first_part = rb->size - rb->read_pos; if (len <= first_part) { memcpy(data, rb->buffer + rb->read_pos, len); rb->read_pos = (rb->read_pos + len) % rb->size; } else { memcpy(data, rb->buffer + rb->read_pos, first_part); int second_part = len - first_part; memcpy(data + first_part, rb->buffer, second_part); rb->read_pos = second_part; // 读指针移动到second_part位置(从0开始) } rb->count -= len; return len; } // 测试主函数 int main() { // 初始化环形缓冲区,大小为10 ring_buffer_t *rb = init_ringbuffer(10); if (rb == NULL) { printf("Failed to initialize ring buffer.\n"); return -1; } // 写入数据 char test_data[] = "Hello, Ring Buffer!"; int write_len = write_ringbuffer(rb, (unsigned char*)test_data, strlen(test_data)); printf("Write %d bytes: %s\n", write_len, test_data); // 读取数据 unsigned char read_data[20] = {0}; int read_len = read_ringbuffer(rb, read_data, sizeof(read_data)-1); printf("Read %d bytes: %s\n", read_len, read_data); // 再次写入和读取(测试环形) char test_data2[] = "12345"; write_ringbuffer(rb, (unsigned char*)test_data2, strlen(test_data2)); read_len = read_ringbuffer(rb, read_data, sizeof(read_data)-1); printf("Read again: %s\n", read_data); // 释放环形缓冲区 release_ringbuffer(rb); return 0; } ``` ### 代码说明 1. **结构体定义**:`ring_buffer_t` 结构体包含缓冲区的指针、大小、读位置、写位置和当前数据数量。 2. **初始化**:`init_ringbuffer` 函数分配内存并初始化环形缓冲区。 3. **释放**:`release_ringbuffer` 函数释放分配的内存。 4. **写入操作**:`write_ringbuffer` 函数将数据写入缓冲区,如果缓冲区剩余空间不足,则只写入部分数据。返回实际写入的字节数。 5. **读取操作**:`read_ringbuffer` 函数从缓冲区读取数据,如果请求的字节数大于当前缓冲区中的数据量,则只读取有效数据。返回实际读取的字节数。 6. **测试主函数**:演示了环形缓冲区的写入和读取操作,包括次写入超过缓冲区大小的数据(会被截断),以及多次写入和读取。 ### 注意事项 - 该实现是线程不安全的,如果在多线程环境中使用,需要添加互斥锁。 - 写入和读取操作都考虑了数据跨越缓冲区末尾的情况,分两段处理。 - 环形缓冲区的大小固定,旦初始化就不能改变。 ### 应用场景 环形缓冲区常用于以下场景: 1. 串口通信的数据缓冲。 2. 生产者-消费者问题中作为共享缓冲区。 3. 音频、视频流处理。 §§相关问题§§ 1. 如何实现环形缓冲区的线程安全版本? 2. 当环形缓冲区满时,有哪些策略可以处理新数据? 3. 环形缓冲区与普通队列相比有什么优势?</think>### 环形缓冲区完整例程(C语言实现) 以下是个完整的环形缓冲区实现,包含初始化、读写操作和资源释放功能,适用于嵌入式系统和串口通信场景: ```c #include <stdio.h> #include <stdlib.h> #include <string.h> #define BUFFER_SIZE 128 // 缓冲区大小 // 环形缓冲区结构体 typedef struct { unsigned char *buffer; // 缓冲区指针 int size; // 缓冲区总大小 int head; // 写指针位置 int tail; // 读指针位置 int count; // 当前数据量 } RingBuffer; // 初始化环形缓冲区 RingBuffer* initRingBuffer(int size) { RingBuffer *rb = (RingBuffer*)malloc(sizeof(RingBuffer)); if (!rb) return NULL; rb->buffer = (unsigned char*)malloc(size); if (!rb->buffer) { free(rb); return NULL; } rb->size = size; rb->head = 0; rb->tail = 0; rb->count = 0; return rb; } // 释放环形缓冲区 void releaseRingBuffer(RingBuffer *rb) { if (rb) { free(rb->buffer); free(rb); } } // 写入数据到缓冲区 int writeRingBuffer(RingBuffer *rb, const unsigned char *data, int len) { if (!rb || !data || len <= 0) return 0; int free_space = rb->size - rb->count; if (len > free_space) { len = free_space; // 只写入可用空间 } // 分两段写入(处理跨越缓冲区末尾的情况) int first_chunk = rb->size - rb->head; if (len <= first_chunk) { memcpy(rb->buffer + rb->head, data, len); rb->head = (rb->head + len) % rb->size; } else { memcpy(rb->buffer + rb->head, data, first_chunk); memcpy(rb->buffer, data + first_chunk, len - first_chunk); rb->head = len - first_chunk; // 头部移动到第二段末尾 } rb->count += len; return len; } // 从缓冲区读取数据 int readRingBuffer(RingBuffer *rb, unsigned char *data, int len) { if (!rb || !data || len <= 0) return 0; if (len > rb->count) { len = rb->count; // 只读取有效数据 } // 分两段读取(处理跨越缓冲区末尾的情况) int first_chunk = rb->size - rb->tail; if (len <= first_chunk) { memcpy(data, rb->buffer + rb->tail, len); rb->tail = (rb->tail + len) % rb->size; } else { memcpy(data, rb->buffer + rb->tail, first_chunk); memcpy(data + first_chunk, rb->buffer, len - first_chunk); rb->tail = len - first_chunk; // 尾部移动到第二段末尾 } rb->count -= len; return len; } // 获取当前数据量 int getRingBufferCount(RingBuffer *rb) { return rb ? rb->count : 0; } // 测试主函数 int main() { RingBuffer *rb = initRingBuffer(BUFFER_SIZE); if (!rb) { printf("Failed to initialize ring buffer\n"); return -1; } // 测试数据 char test_data[] = "Hello, this is a ring buffer test!"; char read_buffer[BUFFER_SIZE] = {0}; // 写入测试 int written = writeRingBuffer(rb, (unsigned char*)test_data, strlen(test_data)); printf("Written %d bytes: %s\n", written, test_data); // 读取测试 int read = readRingBuffer(rb, (unsigned char*)read_buffer, sizeof(read_buffer)); printf("Read %d bytes: %s\n", read, read_buffer); // 循环写入测试 for (int i = 0; i < 5; i++) { char chunk[10]; snprintf(chunk, sizeof(chunk), "Chunk%d ", i); writeRingBuffer(rb, (unsigned char*)chunk, strlen(chunk)); } // 分段读取 memset(read_buffer, 0, sizeof(read_buffer)); while (getRingBufferCount(rb) > 0) { int read_size = readRingBuffer(rb, (unsigned char*)read_buffer, 5); printf("Partial read (%d bytes): %.*s\n", read_size, read_size, read_buffer); } releaseRingBuffer(rb); return 0; } ``` ### 关键功能说明: 1. **数据结构**: - `head`:写指针位置 - `tail`:读指针位置 - `count`:当前存储数据量 - `size`:缓冲区总容量 2. **核心操作**: - **写入**:处理缓冲区末尾回绕,自动计算可用空间 - **读取**:处理跨越缓冲区末尾的分段读取 - **空间管理**: - 写入时检查剩余空间 - 读取时检查有效数据量 3. **性能特点**: - $O(1)$ 时间复杂度操作 - 零拷贝内存管理 - 固定内存占用 ### 应用场景: 1. 串口通信数据缓冲(解决接收溢出问题) 2. 实时数据流处理(如传感器数据采集) 3. 生产者-消费者模式中的共享缓冲区 4. 音频/视频流处理管道 ### 测试输出示例: ``` Written 34 bytes: Hello, this is a ring buffer test! Read 34 bytes: Hello, this is a ring buffer test! Partial read (5 bytes): Chunk Partial read (5 bytes): 0 Chu Partial read (5 bytes): nk1 C ... ```
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值