news 2026/6/12 14:29:47

环形缓冲区(Ring Buffer)完全指南:从原理到实用实现

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
环形缓冲区(Ring Buffer)完全指南:从原理到实用实现

环形缓冲区(Ring Buffer)完全指南:从原理到实用实现

在嵌入式系统、串口通信、音频处理等高频数据场景中,数据生产者与消费者的速度往往不匹配。环形缓冲区(Ring Buffer)是一种经典的无锁数据结构,能够高效地解决这种速率适配问题。本文将从零开始,手把手实现一个高性能、可配置的环形缓冲区,并深入其设计原理。


1. 最简单的线性缓冲区

在处理串口通信读取的数据时,通常会遇到解析协议包、CRC校验、打印调试信息等相关操作,初学时习惯性的将这些函数放到串口中断里进行处理,这会出现当串口发完一帧数据时,中断并没有处理完成数据时,下一帧数据就已经提前到来了,导致数据丢失。因此,可以采用缓冲区来写入接收到的数据,使用时,通过读取缓冲区来处理接收到的数据,在串口中断中,只把接收到的数据**"压入"缓冲区,然后退出中断。在主循环**(或者task)里轮询缓冲区进行数据的解析处理。

首先,根据定义创建一个数据缓冲区uint8_t *buf;并初始化uint8_t buffer[16];

在进行数据的读写时采用读取数组下标的形式进行读写操作,因此这里需要设置写索引(wr_idx)和读索引(rd_idx),通过buffer[wr_idx++] = data;data = buffer[rd_idx++];的形式进行数据的读写

当写入一个数据的时候例如写入0xAA则变化为

写入一个字节buffer[wr_idx++] = data;
读取一个字节data = buffer[rd_idx++];

但是运行一段时间后会出现问题,即wr_idxrd_idx会不断增大,当它们达到BUFFER_SIZE时,就无法再写入或读取了。数组的内存空间在程序后面将不会再次被使用导致内存浪费,即不能重复使用。

2. 改进:让数组首尾相连

可以通过取模运算让指针在到达末尾后自动绕回到开头,这样数组在逻辑上变成了一个圆环。

索引 idx取模结果 (idx % BUFFER_SIZE)
00
11
22
33
44
55
66
1414
1515
160
171

假设BUFFER_SIZE = 16

根据上述表格可知,当wr_idxrd_idx不断增大时,即使越界仍然可以正常的指向buffer的下标区间即[0, BUFFER_SIZE-1]区间内,这就将线性缓冲区变成了环形缓冲区

wr_idx % BUFFER_SIZE == rd_idx% BUFFER_SIZE;根据取模运算结果表可以发现缓冲区存在两种可能,即wr_idx == 0 rd_idx == 0wr_idx == 16 rd_idx == 16,这表示缓冲区可能是,也可能是(写指针追上了读指针)因此要想办法区分

这里可以通过留一个空位的方式来进行区分,即始终保留一个元素不使用,当(wr_idx+1)%BUFFER_SIZE == rd_idx%BUFFER_SIZE时为满,这种设置方式最为简单,但是会浪费一个存储单元。

3. 取模性能优化:使用位与运算

取模运算%在 CPU 上开销较大(几十个时钟周期)。但是如果限制缓冲区大小为2 的幂(如 64、128、1024),就可以用位与运算&代替取模,后者仅需 1 个时钟周期。

idxidx % 16idx & 15是否相等
000
111
222
333
444
555
666
777
888
999
101010
111111
121212
131313
141414
151515
1600
1711
1822
1933
2044
2155
2266
2377
2488
2599
261010
271111
281212
291313
301414
311515

表1:BUFFER_SIZE = 16(2 的幂,BUFFER_SIZE-1 = 15= 0b1111)

BUFFER_SIZE为 2 的幂时,取模运算等价于位与运算,结果完全一致。


idxidx % 15idx & 14是否相等
000
110
222
332
444
554
666
776
888
998
101010
111110
121212
131312
141414
15014
1610
1720
1832
1942
2054
2164
2276
2386
2498
25108
261110
271210
281312
291412
30014
31114

表2:BUFFER_SIZE = 15(非 2 的幂,BUFFER_SIZE-1 = 14= 0b1110)

BUFFER_SIZE不是 2 的幂时,位与运算只保留二进制低位(且最低位恒为 0),结果通常与取模不同,不可互换。仅在少数偶数索引上偶然相等(如 idx 为 0,2,4,6,8,10,12,14 时两者相等,但 idx=16 时取模 1 而位与 0 已不相等)。

通过取模运算位与运算比较表可以得知,当缓冲区大小为2的幂时,完全可以通过位与运算替代取模运算

这里建议设置大小时,设置为2的幂。


4. 写入策略

环形缓冲区写满后,后续写入行为有两种处理策略,需根据数据重要性与实时性选择。

拒绝模式(REJECT)

  • 行为:缓冲区已满时,新写入操作失败,数据被丢弃,原有数据保持不变。
  • 适用场景:命令传输、配置下发、事务日志等必须保证每条数据完整不丢失的场景。
  • 原因:数据丢失可能导致状态错误或流程中断,拒绝写入能触发上层重试或流控,确保数据可达。
  • 选择结论:数据完整性要求高于实时性时,采用REJECT

覆盖模式(OVERWRITE)

  • 行为:缓冲区已满时,新数据覆盖最旧的未读数据,始终保留最近写入的数据。
  • 适用场景:传感器采样、音视频流、实时监控等数据量大、旧数据价值低的场景。
  • 原因:最新数据比历史数据更有意义,允许丢弃旧数据以保证系统持续运行,避免阻塞生产者。
  • 选择结论:数据新鲜度与持续写入能力优先时,采用OVERWRITE

要完整性用拒绝,要实时性用覆盖。3. 数据结构定义

5. 环形缓冲区实现

5.1 定义结构体以及相关宏

创建ringbuf.h头文件,定义结构体及相关宏。

#ifndefRINGBUF_H#defineRINGBUF_H#include<stdint.h>#include<stdbool.h>// 写入模式配置#defineRINGBUF_MODE_REJECT0// 满时拒绝写入#defineRINGBUF_MODE_OVERWRITE1// 满时覆盖旧数据typedefstructringbuf{volatileuint32_twr_idx;// 写索引,生产者修改volatileuint32_trd_idx;// 读索引,消费者修改uint8_t*data;// 数据缓冲区(外部提供)uint32_tsize;// 缓冲区大小(建议为2的幂)uint32_tmask;// size - 1(当size为2的幂时使用)uint8_tmode;// 写入模式bool power_of_two;// 标记size是否为2的幂}ringbuf_t;/** * @brief 初始化环形缓冲区 * * @param rb 环形缓冲区对象指针 * @param buf 外部提供的字节数组,用作数据存储区 * @param size 缓冲区大小(字节数) * @param mode 写入模式:RINGBUF_MODE_REJECT 或 RINGBUF_MODE_OVERWRITE */voidringbuf_init(ringbuf_t*rb,uint8_t*buf,uint32_tsize,uint8_tmode);/** * @brief 向环形缓冲区写入一个字节 * * @param rb 环形缓冲区对象指针 * @param c 待写入的字节数据 * @return true 写入成功(拒绝模式下非满,或覆盖模式下总是成功) * @return false 写入失败(仅拒绝模式下缓冲区已满时返回false) */boolringbuf_write(ringbuf_t*rb,uint8_tc);/** * @brief 从环形缓冲区读取一个字节 * * @param rb 环形缓冲区对象指针 * @param c 输出指针,用于存储读取到的字节 * @return true 读取成功(缓冲区非空) * @return false 读取失败(缓冲区为空) */boolringbuf_read(ringbuf_t*rb,uint8_t*c);/** * @brief 获取环形缓冲区中当前有效数据的字节数 * * @param rb 环形缓冲区对象指针 * @return uint32_t 当前数据长度(0 ~ size) */uint32_tringbuf_count(constringbuf_t*rb);/** * @brief 判断环形缓冲区是否为空 * * @param rb 环形缓冲区对象指针 * @return true 缓冲区为空 * @return false 缓冲区非空 */boolringbuf_empty(constringbuf_t*rb);/** * @brief 判断环形缓冲区是否为满(仅对拒绝模式有意义) * * @param rb 环形缓冲区对象指针 * @return true 缓冲区已满(拒绝模式下无法再写入;覆盖模式下始终返回false,因为始终可写) * @return false 缓冲区未满 */boolringbuf_full(constringbuf_t*rb);/** * @brief 复位环形缓冲区(清空数据,重置读写索引) * * @param rb 环形缓冲区对象指针 */voidringbuf_reset(ringbuf_t*rb);#endif// RINGBUF_H

关键点说明

  • volatile关键字:防止编译器优化,确保多线程/中断中索引的可见性。
  • data由外部提供:允许用户自定义内存分配策略(静态数组、malloc 等)。
  • power_of_two:在初始化时自动检测,运行时用于选择最优取模方法。

5.2 初始化函数实现

ringbuf.c中实现初始化逻辑。

#include"ringbuf.h"#include<stddef.h>#include<assert.h>// 内部取模函数:根据是否为2的幂选择位运算或取模staticinlineuint32_tringbuf_mod(constringbuf_t*rb,uint32_tval){if(rb->power_of_two)returnval&rb->mask;//使用按位与elsereturnval%rb->size;//使用取模符号}voidringbuf_init(ringbuf_t*rb,uint8_t*buf,uint32_tsize,uint8_tmode){assert(rb!=NULL&&buf!=NULL&&size>0);rb->wr_idx=0;rb->rd_idx=0;rb->data=buf;rb->size=size;rb->mode=mode;// 判断 size 是否为 2 的幂:size & (size-1) == 0rb->power_of_two=((size&(size-1))==0);rb->mask=rb->power_of_two?(size-1):0;}

取模优化原理
size = 2^n时,size - 1的低 n 位全为 1。例如size = 8(二进制 1000),mask = 7(0111)。index & mask等价于index % 8,但位运算仅需 1 个 CPU 周期,而除法需要数十个周期。


5.3 写入字节:拒绝模式与覆盖模式

这是环形缓冲区的核心操作。根据配置的mode处理缓冲区满的情况。

boolringbuf_write(ringbuf_t*rb,uint8_tc){assert(rb!=NULL&&rb->data!=NULL);uint32_tnext_wr=rb->wr_idx+1;uint32_tcur_rd=rb->rd_idx;if(rb->mode==RINGBUF_MODE_REJECT){// 检查是否满(留一个空位)if(ringbuf_mod(rb,next_wr)==ringbuf_mod(rb,cur_rd))returnfalse;// 满,拒绝写入}else{// OVERWRITE 模式// 如果满,则移动读索引(丢弃最旧数据)if(ringbuf_mod(rb,next_wr)==ringbuf_mod(rb,cur_rd))rb->rd_idx++;// 单调递增,仅在访问数组时取模}// 数据写入当前写指针位置rb->data[ringbuf_mod(rb,rb->wr_idx)]=c;rb->wr_idx=next_wr;returntrue;}

(wr_idx + 1) % size == rd_idx % size时视为满


5.4 读取字节

读取操作简单:检查是否为空,非空则取出数据并移动读指针。

boolringbuf_read(ringbuf_t*rb,uint8_t*c){assert(rb!=NULL&&c!=NULL);if(rb->rd_idx==rb->wr_idx)returnfalse;// 空*c=rb->data[ringbuf_mod(rb,rb->rd_idx)];rb->rd_idx++;returntrue;}

注意rd_idx同样是单调递增的,允许溢出。由于我们使用无符号整数,溢出后的减法仍能得到正确差值(环形特性)。


5.5 辅助函数:计数、判空、判满

uint32_tringbuf_count(constringbuf_t*rb){assert(rb!=NULL);uint32_tdiff=rb->wr_idx-rb->rd_idx;if(rb->power_of_two)returndiff&rb->mask;// 位运算优化elsereturndiff%rb->size;}boolringbuf_empty(constringbuf_t*rb){assert(rb!=NULL);returnrb->rd_idx==rb->wr_idx;}boolringbuf_full(constringbuf_t*rb){assert(rb!=NULL);if(rb->mode==RINGBUF_MODE_OVERWRITE)returnfalse;// 覆盖模式下永远不会“满”// REJECT 模式:判断是否留一个空位returnringbuf_mod(rb,rb->wr_idx+1)==ringbuf_mod(rb,rb->rd_idx);}voidringbuf_reset(ringbuf_t*rb){assert(rb!=NULL);rb->wr_idx=0;rb->rd_idx=0;// 注意:不清空 data 缓冲区内容(通常不需要)}

计数公式原理
(wr_idx - rd_idx) % size能够正确处理索引回绕。例如:size = 8wr_idx = 10(实际下标 2),rd_idx = 7(实际下标 7)。差值10-7=33%8=3,正确。当wr_idx溢出为 0 而rd_idx很大时,无符号减法自动得到补码差值,再取模依然正确。

5.6 使用示例(串口接收 + 主循环处理)

#include"ringbuf.h"#defineUART_BUF_SIZE64staticuint8_tuart_buffer[UART_BUF_SIZE];staticringbuf_tuart_rb;// 串口中断服务函数voidUART_IRQHandler(void){uint8_tdata=USART_ReceiveData();ringbuf_write(&uart_rb,data);// 根据需求选择覆盖或拒绝模式}// 主循环intmain(void){// 使用覆盖模式,防止中断阻塞ringbuf_init(&uart_rb,uart_buffer,UART_BUF_SIZE,RINGBUF_MODE_OVERWRITE);while(1){uint8_tbyte;while(ringbuf_read(&uart_rb,&byte)){process_byte(byte);// 协议解析、CRC等耗时操作}// 执行其他任务}}

6. 总结

环形缓冲区是嵌入式系统中解决数据流速率不匹配问题的常见数据结构。本文从最简单的线性数组出发,逐步揭示了其无法复用内存的缺陷,进而引出索引回绕、取模运算、空/满状态区分(留空位法)以及性能优化(2 的幂 + 位与运算)。同时,根据业务需求给出了两种写入策略:拒绝模式保证数据完整性,覆盖模式确保数据新鲜度。

实现要点回顾

  • 使用单调递增的读写索引(允许溢出),仅在访问数组时取模。
  • 通过“留一个空位”区分缓冲区空和满。
  • 缓冲区大小建议设为 2 的幂,以便用位与运算替代取模,提升性能。
  • volatile关键字保证索引在中断/多线程中的可见性。
  • 代码适用于单生产者、单消费者(SPSC)场景,如“中断写入 + 主循环读取”。

注意事项

  • 若有多个中断源同时写入,或生产者在多任务环境下运行,需要额外增加临界区保护(如关中断、互斥锁)。
  • 在弱内存模型架构(如 ARM、RISC-V)上,若生产者和消费者运行在不同优先级,可视情况插入内存屏障。
  • 本文提供的代码已包含基本的参数断言(assert),可在调试阶段帮助发现错误,正式发布时可定义NDEBUG关闭断言。

掌握环形缓冲区的基本原理和实现细节,能够帮助开发者高效应对串口通信、音频流、数据采集等各种嵌入式场景。希望本文成为您构建可靠嵌入式系统的一份实用参考。

6.1 完整代码分享

百度网盘

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/6/12 14:28:56

3分钟找回遗忘的Navicat数据库密码:开源解密工具完全指南

3分钟找回遗忘的Navicat数据库密码&#xff1a;开源解密工具完全指南 【免费下载链接】navicat_password_decrypt 忘记navicat密码时,此工具可以帮您查看密码 项目地址: https://gitcode.com/gh_mirrors/na/navicat_password_decrypt 你是否曾经因为忘记Navicat中的数据…

作者头像 李华
网站建设 2026/6/12 14:25:53

魔兽争霸3终极优化指南:WarcraftHelper完整配置与性能调校方案

魔兽争霸3终极优化指南&#xff1a;WarcraftHelper完整配置与性能调校方案 【免费下载链接】WarcraftHelper Warcraft III Helper , support 1.20e, 1.24e, 1.26a, 1.27a, 1.27b 项目地址: https://gitcode.com/gh_mirrors/wa/WarcraftHelper 魔兽争霸3作为经典RTS游戏&…

作者头像 李华
网站建设 2026/6/12 14:21:02

2026 Lazada流量转化实战专家测评榜单选型指南(中立客观版)

一、测评引言&#xff1a;Lazada流量转化行业现状与测评说明1.1 行业市场现状与发展趋势依据Lazada官方发布《2026东南亚跨境商家增长白皮书》及亿邦动力东南亚电商季度报告&#xff0c;当前Lazada全域流量已完成从平台算法增量红利向存量精细化转化转型&#xff1a;2026年Q1平…

作者头像 李华
网站建设 2026/6/12 14:21:01

2026 Google流量转化导师/机构中立测评榜单:出海全域转化选型指南

一、引言&#xff1a;行业现状与测评说明1.1 行业公开现状与趋势依据Google官方《2026中国企业出海数字营销报告》、艾瑞咨询跨境流量白皮书公开数据&#xff1a;2025年国内通过Google渠道获客的出海企业规模突破12.7万家&#xff0c;同比增长23.4%&#xff1b;行业痛点集中表现…

作者头像 李华