文章目录
在嵌入式系统和许多其他软件系统的开发中,环形缓冲区(Ring Buffer)是一种非常实用的数据结构,它在处理数据的产生和消费过程中发挥着至关重要的作用。
1、为什么要用环形缓冲区
比如,现在有一个传感器线程和一个数据发送线程。传感器线程负责获取传感器数据,数据发送线程负责发送数据。当传感器获取数据的速度比数据发送的速度要快时,为了避免数据丢失,这时候可以用一个数组把传感器数据临时存起来,数据发送线程要发送数据时从数组里拿就行了。
那数组的大小一开始定10?还是100呢?都可以,这得根据两边线程的效率来决定。同时,为了这块数组充分被利用,于是就有了环形缓冲区的概念。
2、什么是环形缓冲区
环形缓冲区,也被称为循环缓冲区(Circular Buffer),是一种固定大小的缓冲区,它使用一个固定大小的存储区域,当存储的数据达到其最大容量时,新的数据将覆盖最旧的数据,形成一个环形的存储结构。
顾名思义的想象一下,它就是把一个数组掰成圆的,再加上一个读指针,一个写指针(不是c语言里的指针,这里只是指向的意思),读指针和写指针分别用于指示下一个要读取和写入元素的位置。
-
下图是一个存储容量为7个单位的空的环形缓冲区,读指针和写指针都指向数组的0下标:
-
往写指针指向的下标0写入一个数据20后,写指针会往后移动一位:
-
从读指针指向的下标0读取一个数据后,读指针会往后移动一位:(下标0的数据20并没有清除,因为这是无用数据了,写入新数据时会覆盖)
-
这里小结一下,可以发现,写指针和读指针最后一定指向的是下一个要写入和读取的位置。
3、环形缓冲区的几个重要概念
3.1、如何判断缓冲区为空?
当写指针和读指针指向同一个下标时,缓冲区即为空。
3.2、如何判断缓冲区已满?
如下图,当往缓冲区继续填充数据5、88、76、21、34、19后(下标0的数据20并没有清除,因为这是无用数据了,写入新数据时会覆盖),写指针的下一个就是读指针,此时就可以判断缓冲区已满。
4、环形缓冲区的结构
环形缓冲区通常使用结构体来表示,以下是一个典型的 C 语言实现的环形缓冲区结构体:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
// 定义通用的环形缓冲区结构体
typedef struct ring_buffer {
void* buffer; // 存储数据的内存空间
size_t size; // 环形缓冲区可以存储的元素数量
size_t element_size; // 每个元素的大小,这使得缓冲区可以存储不同类型的数据
size_t read_index; // 读指针,指示下一个要读取的元素位置
size_t write_index; // 写指针,指示下一个要写入的元素位置
size_t count; // 当前存储的元素数量
} ring_buffer_t;
- buffer:是一个指针,指向存储数据的内存区域。
- size:表示缓冲区能够存储元素的最大数量。
- element_size:存储元素的大小,这是环形缓冲区的一个重要特性,它允许存储不同类型的数据,如整数、浮点数、结构体等,只要我们指定元素的大小。
- read_index 和 write_index:分别用于指示下一个要读取和写入元素的位置。
- count:记录当前缓冲区中存储的元素数量。
5、环形缓冲区的操作
5.1、初始化环形缓冲区
// 初始化环形缓冲区
ring_buffer_t* ring_buffer_init(size_t size, size_t element_size)
{
ring_buffer_t* rb = (ring_buffer_t*)malloc(sizeof(ring_buffer_t));
if (rb == NULL)
return NULL;
rb->size = size;
rb->element_size = element_size;
rb->buffer = malloc(size * element_size);
if (rb->buffer == NULL)
{
free(rb);
return NULL;
}
rb->read_index = 0;
rb->write_index = 0;
rb->count = 0;
return rb;
}
- 这个函数分配了
ring_buffer_t
结构体的内存,并为存储数据的buffer
分配内存。 - 它将读指针
read_index
和写指针write_index
初始化为 0,将元素计数count
也初始化为 0。
5.2、检查环形缓冲区是否已满或为空
说明一下,这里没有通过判断读指针和写指针的方式来判断满和空,而是使用更为简洁明了的方式。
// 检查环形缓冲区是否满
int ring_buffer_is_full(ring_buffer_t* rb)
{
return rb->count == rb->size;
}
// 检查环形缓冲区是否空
int ring_buffer_is_empty(ring_buffer_t* rb)
{
return rb->count == 0;
}
- 当
count
等于size
时,缓冲区已满。 - 当
count
等于 0 时,缓冲区为空。
5.3、向环形缓冲区写入数据
写入数据时,需要考虑缓冲区是否已满:
// 向环形缓冲区写入数据
int ring_buffer_write(ring_buffer_t* rb, const void* data)
{
if (ring_buffer_is_full(rb))
return -1; // 缓冲区已满
memcpy((char*)rb->buffer + rb->write_index * rb->element_size, data, rb->element_size);
rb->write_index = (rb->write_index + 1) % rb->size;
rb->count++;
return 0;
}
- 首先检查缓冲区是否已满,如果已满,返回 -1。
- 使用
memcpy
函数将数据复制到写指针位置。 - 通过
(rb->write_index + 1) % rb->size
计算下一个写指针的位置,这里的取模运算实现了环形的特性,当写指针到达缓冲区末尾时,会回到开头继续存储。 - 增加元素计数
count
。
5.4、从环形缓冲区读取数据
// 从环形缓冲区读取数据
int ring_buffer_read(ring_buffer_t* rb, void* data)
{
if (ring_buffer_is_empty(rb))
return -1; // 缓冲区已空
memcpy(data, (char*)rb->buffer + rb->read_index * rb->element_size, rb->element_size);
rb->read_index = (rb->read_index + 1) % rb->size;
rb->count--;
return 0;
}
- 首先检查缓冲区是否为空,如果为空,返回 -1。
- 使用
memcpy
函数从读指针位置读取数据。 - 通过
(rb->read_index + 1) % rb->size
更新读指针位置,实现环形特性。 - 减少元素计数
count
。
5.5、销毁缓冲区
// 销毁环形缓冲区
void ring_buffer_destroy(ring_buffer_t* rb)
{
free(rb->buffer);
free(rb);
}
- 先释放存储数据的
buffer
内存,再释放ring_buffer_t
结构体的内存。
6、使用示例
以下是一个使用环形缓冲区的简单示例,我们存储 message_t
结构体的数据:
// 示例使用
typedef struct message {
int sensor_id;
float value;
} message_t;
int main() {
ring_buffer_t* rb = ring_buffer_init(10, sizeof(message_t));
if (rb == NULL)
{
perror("Failed to initialize ring buffer");
return 1;
}
message_t msg1 = {1, 10.0};
message_t msg2 = {2, 20.0};
message_t msg3 = {3, 30.0};
ring_buffer_write(rb, &msg1);
ring_buffer_write(rb, &msg2);
ring_buffer_write(rb, &msg3);
message_t read_msg;
if (ring_buffer_read(rb, &read_msg) == 0)
{
printf("Read message: sensor_id = %d, value = %f\n", read_msg.sensor_id, read_msg.value);
}
ring_buffer_destroy(rb);
return 0;
}
7、多线程环境下的考虑
在多线程或多进程环境中,环形缓冲区需要考虑并发访问的问题。为了保证数据的一致性和完整性,我们可以使用互斥锁(Mutex)等同步机制。以下是一个添加了互斥锁的环形缓冲区示例:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <pthread.h>
typedef struct ring_buffer {
void* buffer;
size_t size;
size_t element_size;
size_t read_index;
size_t write_index;
size_t count;
pthread_mutex_t mutex;
} ring_buffer_t;
ring_buffer_t* ring_buffer_init(size_t size, size_t element_size) {
ring_buffer_t* rb = (ring_buffer_t*)malloc(sizeof(ring_buffer_t));
if (rb == NULL) {
return NULL;
}
rb->size = size;
rb->element_size = element_size;
rb->buffer = malloc(size * element_size);
if (rb->buffer == NULL) {
free(rb);
return NULL;
}
rb->read_index = 0;
rb->write_index = 0;
rb->count = 0;
if (pthread_mutex_init(&rb->mutex, NULL)!= 0) {
free(rb->buffer);
free(rb);
return NULL;
}
return rb;
}
int ring_buffer_is_full(ring_buffer_t* rb) {
return rb->count == rb->size;
}
int ring_buffer_is_empty(ring_buffer_t* rb) {
return rb->count == 0;
}
int ring_buffer_write(ring_buffer_t* rb, const void* data) {
pthread_mutex_lock(&rb->mutex);
if (ring_buffer_is_full(rb)) {
pthread_mutex_unlock(&rb->mutex);
return -1; // 缓冲区已满
}
memcpy((char*)rb->buffer + rb->write_index * rb->element_size, data, rb->element_size);
rb->write_index = (rb->write_index + 1) % rb->size;
rb->count++;
pthread_mutex_unlock(&rb->mutex);
return 0;
}
int ring_buffer_read(ring_buffer_t* rb, void* data) {
pthread_mutex_lock(&rb->mutex);
if (ring_buffer_is_empty(rb)) {
pthread_mutex_unlock(&rb->mutex);
return -1; // 缓冲区已空
}
memcpy(data, (char*)rb->buffer + rb->read_index * rb->element_size, rb->element_size);
rb->read_index = (rb->read_index + 1) % rb->size;
rb->count--;
pthread_mutex_unlock(&rb->mutex);
return 0;
}
void ring_buffer_destroy(ring_buffer_t* rb) {
pthread_mutex_destroy(&rb->mutex);
free(rb->buffer);
free(rb);
}
8、总结
参考文章:豆包AI。