环形缓冲区

47 阅读3分钟

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

核心逻辑:

  1. 写入:向Head处写入,然后Head向前移动一步。
  2. 读取:从Tail处读取,然后Tail向前移动一步。
  3. 回绕:当指针到达数组末尾时,立刻回到数组开头(索引0)。公式:index = (index + 1)% Size

2. 如何判断空和满

这是一个经典的坑。如果Head == Tail,缓冲区是空的还是满的?

其实这时候就无法判断了。

一般情况下使用下面两种方式:

  1. 增加count 变量:记录当前元素数量。(直观,但是多维护一个变量,多线程下需要额外进行保护)。

  2. 牺牲一个存储单元(正常用这个):

    • 空: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_putrb_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);