环形缓冲区(ring buffer)
推荐参考 环形缓冲区(ring buffer)原理与实现详解
环形缓冲区(ring buffer)的概念
环形缓冲区(又称循环缓冲区、圆形队列或圆形缓冲区)是一种具有先进先出(FIFO)特性的高效缓冲区结构。尽管名称中包含"环形"概念,但实际实现上它仍是一段线性内存空间,只是通过巧妙的逻辑处理使其具有首尾相连的循环特性。
这种设计具有以下几个关键特点:
内存效率:使用固定大小的数组实现,避免了动态内存分配的开销
读写指针:通过维护读指针(head)和写指针(tail)两个索引来跟踪缓冲区状态
循环机制:当指针到达数组末尾时会自动绕回到数组开头,形成逻辑上的环形结构
线程安全:通过互斥锁或原子操作确保多线程/多进程环境下的安全访问

环形缓冲区(ring buffer)的常见应用场景
数据流处理: 环形缓冲区在实时数据流处理中非常有用,如音频/视频流处理、传感器数据采集。生产者持续写入数据,消费者按顺序读取,避免数据丢失或阻塞。
生产者-消费者模型: 在多线程编程中,环形缓冲区作为共享数据结构,协调生产者和消费者的速度差异。生产者写入数据到缓冲区,消费者从中读取,无需频繁同步。
网络数据包处理 :网络设备(如路由器、交换机)使用环形缓冲区存储临时数据包。高速网络接口卡(NIC)通常采用环形缓冲区管理接收和发送的数据包队列。
嵌入式系统 :资源受限的嵌入式系统中,环形缓冲区因其固定大小和高效的内存利用率被广泛使用。例如串口通信中存储接收到的字节流。
日志系统 :高性能日志系统利用环形缓冲区存储临时日志条目。当缓冲区满时,旧日志被新日志覆盖,确保最新日志始终可用。
图形渲染 :GPU渲染管线中,环形缓冲区用于存储顶点数据或命令队列。图形API(如Vulkan)常使用环形缓冲区管理帧数据
环形缓冲区(ring buffer)实现原理
环形缓冲区(ring buffer)数据结构
- 保存环形缓冲区的首地址信息的指针buffer
- 保存读取起始位置信息的变量head
- 保存写入起始位置信息的变量tail
- 保存当前可读的数据量信息size
- 保存缓冲区容量大小信息的capacity
typedef struct {char *buffer; // 数据存储区int head; // 读取位置int tail; // 写入位置int size; // 当前数据量int capacity; // 缓冲区容量
} loopbuf_t;
环形缓冲区(ring buffer)算法实现
- init,分配并初始化环形缓冲区
- free,销毁环形缓冲区
- write,写n个数据进环形缓冲区
- read,从环形缓冲区中读出n个数据
extern loopbuf_t* loopbuf_init(int capacity);
extern void loopbuf_free(loopbuf_t *lb);
extern int loopbuf_write(loopbuf_t *lb, const char *data, int n);
extern int loopbuf_read(loopbuf_t *lb, char *data, int n);
分配并初始化环形缓冲区
给结构体分配内存使用malloc分配的是堆内存只要不释放就会全局存在
使用malloc通过缓冲区容量大小来分配缓冲区长度,全局存在
初始化缓冲区变量信息
// (1) 初始化环形缓冲区
loopbuf_t* loopbuf_init(int capacity) {if (capacity <= 0) return NULL;loopbuf_t *lb = (loopbuf_t*)malloc(sizeof(loopbuf_t));//给结构体对象分配内存if (!lb) return NULL;//如果分配失败就返回NULLlb->buffer = (char*)malloc(capacity * sizeof(char));//给缓冲区分配容量if (!lb->buffer) { //如果分配失败就释放返回NULLfree(lb);return NULL;}lb->head = 0;lb->tail = 0;lb->size = 0;lb->capacity = capacity;return lb;
}
销毁环形缓冲区
不再使用缓冲区需要释放内存,防止内存泄漏
// (2) 销毁环形缓冲区
void loopbuf_free(loopbuf_t *lb) {if (lb) {if (lb->buffer) {free(lb->buffer);}free(lb);}
}
写n个数据进环形缓冲区
1、写入缓冲区需要判断是否覆盖旧数据
通过判断当前需要写入的数据是否大于剩余容量,如果大于则进入覆盖逻辑,小于则进入直接拷贝逻辑
2、覆盖逻辑
更新保存读取起始位置信息的变量head,保证其保存的信息是存在时间最长的数据
更新当前数据量。
分段写入:先写入剩余容量,再写入覆盖部分
// (3) 写入n个数据到缓冲区
int loopbuf_write(loopbuf_t *lb, const char *data, int n) {if (!lb || !data || n <= 0) return 0;// 如果需要覆盖旧数据if (n > lb->capacity - lb->size) {//判断缓冲区剩余容量是否可以完全写入,不完全则覆盖旧数据//需要覆盖的旧数据数量。如果 n大于可用空间,多出的部分会覆盖缓冲区中最旧的的数据。int overwrite_count = n - (lb->capacity - lb->size);//lb->head指向被覆盖部分的后一位(保证读取的第一个数据存在时间最长),取余确保指针在缓冲区范围内循环,维持环形结构lb->head = (lb->head + overwrite_count) % lb->capacity;lb->size = lb->capacity;//当前数据量} else {lb->size += n;//当前数据量}// 分段写入数据int write_pos = lb->tail;//写入数据起始位if (write_pos + n <= lb->capacity) {//判断缓冲区容量是否足够保存旧数据和新数据memcpy(lb->buffer + write_pos, data, n);//缓冲区足够则直接写入} else {int first_part = lb->capacity - write_pos;//剩余容量memcpy(lb->buffer + write_pos, data, first_part);//写入剩余容量memcpy(lb->buffer, data + first_part, n - first_part);//覆盖旧数据}lb->tail = (lb->tail + n) % lb->capacity;//更新写入位置return n;
}
从环形缓冲区中读出n个数据
1、判断实际读取数量
如果需要读取数据大于当前数据量则实际读取数据为当前数据量,如果小于当前数据量,则实际读取数据为需要读取的数据量
2、判断是否需要存在越界读取的数据
如果存在越界读取的数据则进入,越界读取数据逻辑,通过判断起始位置序号加上实际读取数据量,是否大于缓冲区容量
3、更新数据量和读取起始位
head读取起始位一定保存着最旧的数据位置序号
当前数据量减去实际读取数据量,保证以读取的数据在逻辑上失效
// (4) 从缓冲区读取n个数据,从最老的数据开始读取
int loopbuf_read(loopbuf_t *lb, char *data, int n) {if (!lb || !data || n <= 0 || lb->size == 0) return 0;int actual_read = (n > lb->size) ? lb->size : n;//判断读取长度是否大于当前数据量int read_pos = lb->head;//读取起始位if (read_pos + actual_read <= lb->capacity) {//判断读取数据是否越界memcpy(data, lb->buffer + read_pos, actual_read);//如果读取读取数据里不存在越界则正常读取} else {int first_part = lb->capacity - read_pos; //没有越界的部分长度memcpy(data, lb->buffer + read_pos, first_part);memcpy(data + first_part, lb->buffer, actual_read - first_part);//读取越界部分}lb->head = (lb->head + actual_read) % lb->capacity;//更新读取位置信息,序号指向存在时间最长的数据lb->size -= actual_read;//更新当前数据return actual_read;
}
环形缓冲区(ring buffer)测试
void loopbuf_test(void)
{// 测试基本功能loopbuf_t *buf = loopbuf_init(5);printf("=== 基本功能测试 ===\r\n");// 写入数据loopbuf_write(buf, "12345", 5);printf("写入: 12345\r\n");// 读取数据char read_data[10] = {0};loopbuf_read(buf, read_data, 5);printf("读取: %s\r\n", read_data);// 测试覆盖写入printf("=== 覆盖写入测试 ===\r\n");loopbuf_write(buf, "ABCDE", 5);loopbuf_write(buf, "FGHI", 4);memset(read_data, 0, sizeof(read_data));loopbuf_read(buf, read_data, 5);//第一个数据指向最旧的数据就是E 然后是FGHIprintf("覆盖后读取: %s\r\n", read_data);// 测试部分读写printf("=== 部分读写测试 ===\r\n");loopbuf_write(buf, "123", 3);printf("写入3字节后,读取2字节: ");memset(read_data, 0, sizeof(read_data));loopbuf_read(buf, read_data, 2);printf("%s\r\n", read_data);loopbuf_free(buf);}
