榨干性能:无锁环形缓冲区 (Ring Buffer) 的设计与实现

Evek Golden Lv4

环形缓冲区 (Ring Buffer / Circular Buffer) 是嵌入式通信中最基础的数据结构。
看似简单(一个数组,两个指针),但要写好一个高性能、线程安全、无锁的 Ring Buffer,却大有门道。

1. 使用场景分析

  • UART 接收:中断(ISR)不断往里写数据,主循环不断从里面读数据。
  • 音频播放:应用层填入 PCM 数据,DMA 中断取走数据送给 I2S。

核心需求

  • 速度要快:ISR 里不能拖泥带水。
  • 并发安全:写者(Producer)和读者(Consumer)往往处于不同的执行流(中断 vs 线程)。
  1. 基础实现:Head 与 Tail
1
2
3
4
5
6
typedef struct {
uint8_t *buffer;
uint32_t size;
volatile uint32_t head; // 写指针 (Write Index)
volatile uint32_t tail; // 读指针 (Read Index)
} RingBuffer;
  • 空状态head == tail
  • 满状态(head + 1) % size == tail (需浪费一个字节空间用于区分满和空)

3. 极致优化技巧

3.1 2 的幂次 (Power of 2) 优化

在嵌入式 CPU(特别是没有硬件除法器的 Cortex-M0)上,取模运算 % 是非常慢的。
如果我们将缓冲区大小限制为 $ 2^N $(如 64, 128, 1024),取模运算就可以等价于**位与运算 (&)**。

$$ x \mod 2^N \iff x \ & \ (2^N - 1) $$

1
2
// 假设 size = 1024 (0x400),mask = 1023 (0x3FF)
next_head = (rb->head + 1) & (rb->size - 1);

这条指令单周期完成,比取模快几十倍。

3.2 镜像指示位 (Mirror Bit) —— 解决浪费的一字节

为了不浪费那一个字节,可以使用最高位作为“镜像位”。
当指针溢出回绕时,最高位翻转。

  • head == tail:空。
  • head_index == tail_indexhead_mirror != tail_mirror:满。

4. 无锁设计 (Lock-Free)

在“**单生产者 + 单消费者 (SPSC)**”模型下,Ring Buffer 是天然可以无锁的。

  • 生产者只修改 head
  • 消费者只修改 tail
    两者虽然共享数据,但互不干扰对方的控制变量。不需要 MutexCritical Section

内存屏障 (Memory Barrier)

现代编译器(和某些乱序执行的 CPU)可能会重排指令。
错误写法

1
2
rb->head++;          // 1. 更新指针
rb->buffer[idx] = val; // 2. 写入数据

如果 CPU 重排指令,先更新了指针,此时消费者可能会读到尚未写入的脏数据!

**正确写法 (C11 / C++11)**:

1
2
3
4
5
6
7
8
// 1. 写入数据
rb->buffer[current_head] = val;

// 2. 内存屏障,确保数据先落地,再更新指针
atomic_thread_fence(memory_order_release);

// 3. 更新指针
atomic_store(&rb->head, next_head);

5. 优缺点总结

特性优点缺点
静态内存确定性强,无内存碎片需预估最大容量,浪费 RAM
无锁设计极快,适合 ISR仅限一写一读,多写多读仍需锁
2幂次掩码运算极快缓冲区大小不灵活
  • Title: 榨干性能:无锁环形缓冲区 (Ring Buffer) 的设计与实现
  • Author: Evek Golden
  • Created at : 2023-11-05 23:11:00
  • Updated at : 2026-06-12 08:57:02
  • Link: https://blog.cocodemo.uno/posts/ring7b2/
  • License: This work is licensed under CC BY-NC-SA 4.0.
Comments