1. 原理图解
想象一个首尾相连的圆环,我们需要维护两个指针(或索引)。
- Head(Write Ptr):写入数据的位置。
- Tail(Read Ptr):读取数据的位置。
[ Empty ] <-- 0
[ Data ] [ Data ]
7 1
[ Data ] [ Data ] <-- Tail (读取位置)
6 2
[ Data ] [ Free ]
5 3
[ Free ] [ Free ] <-- Head (写入位置)
4
核心逻辑:
- 写入:向Head处写入,然后Head向前移动一步。
- 读取:从Tail处读取,然后Tail向前移动一步。
- 回绕:当指针到达数组末尾时,立刻回到数组开头(索引0)。公式:
index = (index + 1)% Size。
2. 如何判断空和满
这是一个经典的坑。如果Head == Tail,缓冲区是空的还是满的?
其实这时候就无法判断了。
一般情况下使用下面两种方式:
-
增加
count变量:记录当前元素数量。(直观,但是多维护一个变量,多线程下需要额外进行保护)。 -
牺牲一个存储单元(正常用这个):
- 空:Head == Tail
- 满:
(Head + 1) % Size == Tail(即Head追上了Tail,只差一步);
3. 代码实现
ring_buffer.h
#ifndef __RINGBUFFER_H
#define __RINGBUFFER_H
#include <stdint.h>
#include <stdbool.h>
#include <stddef.h>
// 定义状态码
typedef enum {
RB_SUCCESS = 0,
RB_ERROR_NULL, // 传入了空指针
RB_ERROR_FULL, // 缓冲区满
RB_ERROR_EMPTY // 缓冲区空
} rb_status_t;
// 定义 RingBuffer 结构体
typedef struct {
uint8_t *buffer; // 实际内存块
size_t size; // 缓冲区总容量
size_t head; // 写入索引(write index)
size_t tail; // 读取索引(read index)
} ring_buffer_t;
// 初始化缓冲区
rb_status_t rb_init(ring_buffer_t *rb, uint8_t *buffer_ptr, size_t size);
// 重置缓冲区
rb_status_t rb_reset(ring_buffer_t *rb);
// 写入一个字节
rb_status_t rb_put(ring_buffer_t *rb, uint8_t data);
// 读出一个字节
rb_status_t rb_get(ring_buffer_t *rb, uint8_t *data);
// 检查状态
bool rb_is_full(const ring_buffer_t *rb);
bool rb_is_empty(const ring_buffer_t *rb);
size_t rb_available(const ring_buffer_t *rb); // 可读数量
#endif
ring_buffer.c
#include "ring_buffer.h"
// 内部宏:计算下一个位置
// 健壮性:size 必须大于0,初始化时会检查
#define NEXT_POS(x,size) (((x) + 1) % (size))
rb_status_t rb_init(ring_buffer_t *rb, uint8_t *buffer_ptr, size_t size) {
if (rb == NULL || buffer_ptr == NULL || size < 2) {
// 大小至少为2,因为要牺牲一个槽位进行判断满
return RB_ERROR_NULL;
}
rb->buffer = buffer_ptr;
rb->size = size;
rb->head = 0;
rb->tail = 0;
return RB_SUCCESS;
}
rb_status_t rb_reset(ring_buffer_t *rb) {
if (rb == NULL) return RB_ERROR_NULL;
// 只需要重置指针,数据会被覆盖,无需memset
rb->head = 0;
rb->tail = 0;
return RB_SUCCESS;
}
bool rb_is_full(const ring_buffer_t *rb) {
if (rb == NULL) return false;
return NEXT_POS(rb->head, rb->size) == rb->tail;
}
bool rb_is_empty(const ring_buffer_t *rb) {
if (rb == NULL) return false;
return rb->head == rb->tail;
}
size_t rb_available(const ring_buffer_t *rb) {
if (rb == NULL) return false;
if (rb->head >= rb->tail) {
return rb->head - rb->tail;
} else {
return rb->size + rb->head - rb->tail;
}
}
rb_status_t rb_put(ring_buffer_t *rb, uint8_t data) {
if (rb == NULL) return RB_ERROR_NULL;
if (rb_is_full(rb)) {
return RB_ERROR_FULL;
}
rb->buffer[rb->head] = data;
rb->head = NEXT_POS(rb->head, rb->size);
return RB_SUCCESS;
}
rb_status_t rb_get(ring_buffer_t *rb, uint8_t *data) {
if (rb == NULL || data == NULL) return RB_ERROR_NULL;
if (rb_is_empty(rb)) {
return RB_ERROR_EMPTY;
}
*data = rb->buffer[rb->tail];
rb->tail = NEXT_POS(rb->tail, rb->size);
return RB_SUCCESS;
}
4. 如何让代码更”强壮“
如果在多线程或中断环境中使用(例如单片机UART接收中断),上面的代码需要升级:
A. 线程安全 (Thread Safety)
在rb_put和rb_get中修改head和tail是非原子操作。
- 单生产者/单消费者(SPSC):如果一个线程只写,另一个线程只读,上述“牺牲一个槽位”的代码通常是安全的(只要读写指针的修改是原子的,比如32位系统上的size_t赋值)。不需要加锁。
- 多生产者/多消费者(MPMC):必须加锁!
加锁示例(伪代码):
rb_status_t rb_put_thread_safe(ring_buffer_t *rb, uint8_t data) {
mutex_lock(&rb->lock); // 上锁
rb_status_t status = rb_put(rb, data);
mutex_unlock(&rb->lock); // 解锁
return status;
}
性能优化
如果缓冲区大小 Size 保证是2 的幂 (如 16,32,1024),可以用位运算代替取模运算(%),速度快很多。
// 原代码 (head + 1) % size
// 优化代码:(size必须是 2^n)(head + 1) & (size - 1);