环形缓冲区 ring buffer 概述
环形缓冲区 ring buffer 概述
1. 简介
环形缓冲区(ring buffer),是一种用于表示一个固定尺寸、头尾相连的缓冲区的数据结构,适合缓存数据流。也称作环形缓冲区(circular buffer),环形队列(circular queue),循环缓冲区(cyclic buffer)。
2. 特点及应用场景
环形缓冲区的特点:
- 固定大小
- 线性地址空间
- 先进先出FIFO
- 高效读取(数据消费后不需要移动其他数据)
环形缓冲区的应用场景:
- 网络通信(TCP/IP网络协议栈数据缓存)
- 任务间的通信(生产者消费者缓存、异步通信优化)
- 操作系统内核(串口网口等驱动设备数据缓存、log日志缓存)
- 音频/视频流处理(流媒体传输、实时编解码)
环形缓冲区的典型实现:
- Linux内核kfifo:采用镜像指示位法实现无锁环形队列,缓冲区大小为2的幂
- RT-Thread的ringbuffer:为嵌入式系统优化,支持零拷贝操作和中断安全API
- Boost库circular_buffer:支持动态扩容和元素保留策略,提供STL兼容接口
3. 设计要点
3.1 如何在线性地址空间上实现循环读写?
所谓循环读写是指在环形缓冲区的末尾读写之后,下一次读写需要回到环形缓冲区首地址的过程。也称作指针回绕。
对于环形缓冲区首地址m_buffer和缓冲区大小bufferSize是已知条件,通过增加读指针m_readIndex和写指针m_writeIndex就可以解决循环读写问题。
char* buffer; // 环形缓冲区指针
uint32_t bufferSize; // 环形缓冲区大小
uint32_t readIndex; // 读指针,指向下一个要读取的位置
uint32_t writeIndex; // 写指针,指向下一个要写入的位置
计算公式
读索引 = 读指针 % 缓冲区长度; // 基础: 读写指针取模
写索引 = 写指针 % 缓冲区长度;读索引 = 读指针 & (缓冲区长度 - 1); // 优化: 当bufferSize为2的幂时,取模运算可优化为按位与
写索引 = 写指针 & (缓冲区长度 - 1);
假设 环形缓冲区大小bufferSize为4
- 环形缓冲区的初始状态,大小为4
m_readIndex = 0
m_writeIndex = 0
start end │ │ ▼ ▼
┌─────┬─────┬─────┬─────┐
│ │ │ │ │
└─────┴─────┴─────┴─────┘▲ ▲ │ │ r w
- 环形缓冲区的写入1个数据A
m_readIndex = 0
m_writeIndex = 1 (1 % 4 = 1 或 1 & (4 -1) = 1)
start end │ │ ▼ ▼
┌─────┬─────┬─────┬─────┐
│ A │ │ │ │
└─────┴─────┴─────┴─────┘▲ ▲ │ │ r w
- 环形缓冲区的写入4个数据ABCD(缓冲区满)
m_readIndex = 0
m_writeIndex = 0 (指针回绕 4%4=0 或 4 & (4 -1) = 0)
start end │ │ ▼ ▼
┌─────┬─────┬─────┬─────┐
│ A │ B │ C │ D │
└─────┴─────┴─────┴─────┘▲ ▲ │ │ r w
- 环形缓冲区的读取2个数据AB
m_readIndex = 2 (2 % 4 = 2 或 2& (4 -1) = 2)
m_writeIndex = 0
start end │ │ ▼ ▼
┌─────┬─────┬─────┬─────┐
│ │ │ C │ D │
└─────┴─────┴─────┴─────┘▲ ▲ │ │ w r
3.2 如何判断缓冲区空和满?
读写指针在相同位置时,缓冲区可能为空或者满。
可以用以下几种方法判断:
- 计数法:维护写入个数count,count == 0 为空,count == bufferSize为满。需要通过原子操作保证线程安全
- 预留空位法: 写入时保留最后一个位置。当
(writeIndex + 1) % bufferSize == readIndex
时为满,writeIndex == readIndex
时为空 - 镜像指示位法:通过镜像标志位标记读写指针(0至 2n-1)是否进入镜像区间(n 至 2n-1)。当读写指针相等且镜像标志位相同时为空,当读写指针相等且镜像标志位不同时为满。当缓冲区大小为2的幂时,可省略镜像标志位,使用异或运算
writeIndex == (readIndex ^ bufferSize )
直接判断满状态。RT-Thread、Linux内核(kfifo)采用该方案。
3. 3 数据溢出与数据覆盖策略?
环形缓冲区的写入6个数据ABCDEF(缓冲区溢出,数据区满后写入数据模式为覆盖)
m_readIndex = 2 (覆盖了2个数据AB,需要移动2位)
m_writeIndex = 2( 6 % 4 = 2 或 6 & (4 -1) = 2)
start end │ │ ▼ ▼
┌─────┬─────┬─────┬─────┐
│ E │ F │ C │ D │
└─────┴─────┴─────┴─────┘▲ ▲ │ │ r w
数据溢出时一般有以下几种策略:
- 阻塞写入:抛出异常或阻塞等待缓冲区有足够空间,保证数据完整性(如日志记录)
- 覆盖写入:覆盖旧数据,适用于实时流处理(如音视频传输)
- 动态扩容:缓冲区大小扩容,但会破坏环形缓冲区的固定内存特性
Reference:
- Circular buffer - Wikipedia
- CSerialPort/include/CSerialPort/ibuffer.hpp