深入理解环形缓冲区

在嵌入式系统和许多其他软件系统的开发中,环形缓冲区(Ring Buffer)是一种非常实用的数据结构,它在处理数据的产生和消费过程中发挥着至关重要的作用。

1、为什么要用环形缓冲区

比如,现在有一个传感器线程和一个数据发送线程。传感器线程负责获取传感器数据,数据发送线程负责发送数据。当传感器获取数据的速度比数据发送的速度要快时,为了避免数据丢失,这时候可以用一个数组把传感器数据临时存起来,数据发送线程要发送数据时从数组里拿就行了。

那数组的大小一开始定10?还是100呢?都可以,这得根据两边线程的效率来决定。同时,为了这块数组充分被利用,于是就有了环形缓冲区的概念。

2、什么是环形缓冲区

环形缓冲区,也被称为循环缓冲区(Circular Buffer),是一种固定大小的缓冲区,它使用一个固定大小的存储区域,当存储的数据达到其最大容量时,新的数据将覆盖最旧的数据,形成一个环形的存储结构。

顾名思义的想象一下,它就是把一个数组掰成圆的,再加上一个读指针,一个写指针(不是c语言里的指针,这里只是指向的意思),读指针和写指针分别用于指示下一个要读取和写入元素的位置。

  1. 下图是一个存储容量为7个单位的空的环形缓冲区,读指针和写指针都指向数组的0下标:
    在这里插入图片描述

  2. 往写指针指向的下标0写入一个数据20后,写指针会往后移动一位:
    在这里插入图片描述

  3. 从读指针指向的下标0读取一个数据后,读指针会往后移动一位:(下标0的数据20并没有清除,因为这是无用数据了,写入新数据时会覆盖)
    在这里插入图片描述

  4. 这里小结一下,可以发现,写指针和读指针最后一定指向的是下一个要写入和读取的位置。

3、环形缓冲区的几个重要概念

3.1、如何判断缓冲区为空?

当写指针和读指针指向同一个下标时,缓冲区即为空。

3.2、如何判断缓冲区已满?

如下图,当往缓冲区继续填充数据5、88、76、21、34、19后(下标0的数据20并没有清除,因为这是无用数据了,写入新数据时会覆盖),写指针的下一个就是读指针,此时就可以判断缓冲区已满。
在这里插入图片描述

4、环形缓冲区的结构

环形缓冲区通常使用结构体来表示,以下是一个典型的 C 语言实现的环形缓冲区结构体:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

// 定义通用的环形缓冲区结构体
typedef struct ring_buffer {
    void* buffer;  			// 存储数据的内存空间
    size_t size;   			// 环形缓冲区可以存储的元素数量
    size_t element_size;  	// 每个元素的大小,这使得缓冲区可以存储不同类型的数据
    size_t read_index;   	// 读指针,指示下一个要读取的元素位置
    size_t write_index;  	// 写指针,指示下一个要写入的元素位置
    size_t count;  			// 当前存储的元素数量
} ring_buffer_t;
  • buffer:是一个指针,指向存储数据的内存区域。
  • size:表示缓冲区能够存储元素的最大数量。
  • element_size:存储元素的大小,这是环形缓冲区的一个重要特性,它允许存储不同类型的数据,如整数、浮点数、结构体等,只要我们指定元素的大小。
  • read_index 和 write_index:分别用于指示下一个要读取和写入元素的位置。
  • count:记录当前缓冲区中存储的元素数量。

5、环形缓冲区的操作

5.1、初始化环形缓冲区

// 初始化环形缓冲区
ring_buffer_t* ring_buffer_init(size_t size, size_t element_size) 
{
    ring_buffer_t* rb = (ring_buffer_t*)malloc(sizeof(ring_buffer_t));
    if (rb == NULL)
        return NULL;
    
    rb->size = size;
    rb->element_size = element_size;
    rb->buffer = malloc(size * element_size);
    if (rb->buffer == NULL) 
    {
        free(rb);
        return NULL;
    }
    rb->read_index = 0;
    rb->write_index = 0;
    rb->count = 0;
    return rb;
}
  • 这个函数分配了 ring_buffer_t 结构体的内存,并为存储数据的 buffer 分配内存。
  • 它将读指针 read_index 和写指针 write_index 初始化为 0,将元素计数 count 也初始化为 0。

5.2、检查环形缓冲区是否已满或为空

说明一下,这里没有通过判断读指针和写指针的方式来判断满和空,而是使用更为简洁明了的方式。

// 检查环形缓冲区是否满
int ring_buffer_is_full(ring_buffer_t* rb) 
{
    return rb->count == rb->size;
}

// 检查环形缓冲区是否空
int ring_buffer_is_empty(ring_buffer_t* rb) 
{
    return rb->count == 0;
}
  • count 等于 size 时,缓冲区已满。
  • count 等于 0 时,缓冲区为空。

5.3、向环形缓冲区写入数据

写入数据时,需要考虑缓冲区是否已满:

// 向环形缓冲区写入数据
int ring_buffer_write(ring_buffer_t* rb, const void* data) 
{
    if (ring_buffer_is_full(rb)) 
        return -1; 				// 缓冲区已满
    
    memcpy((char*)rb->buffer + rb->write_index * rb->element_size, data, rb->element_size);
    rb->write_index = (rb->write_index + 1) % rb->size;
    rb->count++;
    return 0;
}
  • 首先检查缓冲区是否已满,如果已满,返回 -1。
  • 使用 memcpy 函数将数据复制到写指针位置。
  • 通过 (rb->write_index + 1) % rb->size 计算下一个写指针的位置,这里的取模运算实现了环形的特性,当写指针到达缓冲区末尾时,会回到开头继续存储。
  • 增加元素计数 count

5.4、从环形缓冲区读取数据

// 从环形缓冲区读取数据
int ring_buffer_read(ring_buffer_t* rb, void* data) 
{
    if (ring_buffer_is_empty(rb)) 
        return -1; 				// 缓冲区已空
    
    memcpy(data, (char*)rb->buffer + rb->read_index * rb->element_size, rb->element_size);
    rb->read_index = (rb->read_index + 1) % rb->size;
    rb->count--;
    return 0;
}
  • 首先检查缓冲区是否为空,如果为空,返回 -1。
  • 使用 memcpy 函数从读指针位置读取数据。
  • 通过 (rb->read_index + 1) % rb->size 更新读指针位置,实现环形特性。
  • 减少元素计数 count

5.5、销毁缓冲区

// 销毁环形缓冲区
void ring_buffer_destroy(ring_buffer_t* rb) 
{
    free(rb->buffer);
    free(rb);
}
  • 先释放存储数据的 buffer 内存,再释放 ring_buffer_t 结构体的内存。

6、使用示例

以下是一个使用环形缓冲区的简单示例,我们存储 message_t 结构体的数据:

// 示例使用
typedef struct message {
    int sensor_id;
    float value;
} message_t;

int main() {
    ring_buffer_t* rb = ring_buffer_init(10, sizeof(message_t));
    if (rb == NULL) 
    {
        perror("Failed to initialize ring buffer");
        return 1;
    }

    message_t msg1 = {1, 10.0};
    message_t msg2 = {2, 20.0};
    message_t msg3 = {3, 30.0};

    ring_buffer_write(rb, &msg1);
    ring_buffer_write(rb, &msg2);
    ring_buffer_write(rb, &msg3);

    message_t read_msg;
    if (ring_buffer_read(rb, &read_msg) == 0) 
    {
        printf("Read message: sensor_id = %d, value = %f\n", read_msg.sensor_id, read_msg.value);
    }

    ring_buffer_destroy(rb);
    return 0;
}

7、多线程环境下的考虑

在多线程或多进程环境中,环形缓冲区需要考虑并发访问的问题。为了保证数据的一致性和完整性,我们可以使用互斥锁(Mutex)等同步机制。以下是一个添加了互斥锁的环形缓冲区示例:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <pthread.h>

typedef struct ring_buffer {
    void* buffer;
    size_t size;
    size_t element_size;
    size_t read_index;
    size_t write_index;
    size_t count;
    pthread_mutex_t mutex;
} ring_buffer_t;

ring_buffer_t* ring_buffer_init(size_t size, size_t element_size) {
    ring_buffer_t* rb = (ring_buffer_t*)malloc(sizeof(ring_buffer_t));
    if (rb == NULL) {
        return NULL;
    }
    rb->size = size;
    rb->element_size = element_size;
    rb->buffer = malloc(size * element_size);
    if (rb->buffer == NULL) {
        free(rb);
        return NULL;
    }
    rb->read_index = 0;
    rb->write_index = 0;
    rb->count = 0;
    if (pthread_mutex_init(&rb->mutex, NULL)!= 0) {
        free(rb->buffer);
        free(rb);
        return NULL;
    }
    return rb;
}

int ring_buffer_is_full(ring_buffer_t* rb) {
    return rb->count == rb->size;
}

int ring_buffer_is_empty(ring_buffer_t* rb) {
    return rb->count == 0;
}

int ring_buffer_write(ring_buffer_t* rb, const void* data) {
    pthread_mutex_lock(&rb->mutex);
    if (ring_buffer_is_full(rb)) {
        pthread_mutex_unlock(&rb->mutex);
        return -1; // 缓冲区已满
    }
    memcpy((char*)rb->buffer + rb->write_index * rb->element_size, data, rb->element_size);
    rb->write_index = (rb->write_index + 1) % rb->size;
    rb->count++;
    pthread_mutex_unlock(&rb->mutex);
    return 0;
}

int ring_buffer_read(ring_buffer_t* rb, void* data) {
    pthread_mutex_lock(&rb->mutex);
    if (ring_buffer_is_empty(rb)) {
        pthread_mutex_unlock(&rb->mutex);
        return -1; // 缓冲区已空
    }
    memcpy(data, (char*)rb->buffer + rb->read_index * rb->element_size, rb->element_size);
    rb->read_index = (rb->read_index + 1) % rb->size;
    rb->count--;
    pthread_mutex_unlock(&rb->mutex);
    return 0;
}

void ring_buffer_destroy(ring_buffer_t* rb) {
    pthread_mutex_destroy(&rb->mutex);
    free(rb->buffer);
    free(rb);
}

8、总结

参考文章:豆包AI。

串口通信是计算机与外部设备之间进行数据交换的一种常见方式,而环形缓冲区是一种常用的内存管理技术,用于高效地处理串口数据的发送和接收。在编程实现串口环形缓存区发送数据时,通常会涉及到以下几个关键步骤: 1. 初始化串口:配置串口的波特率、数据位、停止位和校验位等参数。 2. 创建环形缓存区:动态分配一块连续的内存空间作为环形缓冲区,并设置读写指针。 3. 数据写入:将要发送的数据写入环形缓存区,更新写指针。 4. 数据发送:通过串口发送环形缓存区中的数据,并在发送后更新读指针。 5. 循环发送:持续检查环形缓存区是否有数据待发送,如果有,则继续发送,实现数据的连续发送。 以下是串口环形缓存区发送数据的一个简单示例代码(假设使用的是C语言,并且是基于Windows平台): ```c #include <windows.h> #include <stdio.h> // 环形缓存区结构体 typedef struct { unsigned char *buf; // 缓存区指针 int head; // 读位置 int tail; // 写位置 int size; // 缓存区大小 } RingBuffer; // 初始化环形缓存区 void RingBuffer_Init(RingBuffer *ring, int size) { ring->buf = (unsigned char *)malloc(size); ring->size = size; ring->head = 0; ring->tail = 0; } // 写入数据到环形缓存区 int RingBuffer_Write(RingBuffer *ring, unsigned char *data, int len) { int written = 0; while (written < len && RingBuffer_AvailableForWrite(ring) > 0) { ring->buf[ring->tail] = data[written]; ring->tail = (ring->tail + 1) % ring->size; written++; } return written; } // 从环形缓存区发送数据 int RingBuffer_Send(RingBuffer *ring, HANDLE hComm) { DWORD bytes_written; if (RingBuffer_AvailableForRead(ring) > 0) { if (WriteFile(hComm, ring->buf + ring->head, RingBuffer_AvailableForRead(ring), &bytes_written, NULL)) { ring->head = (ring->head + bytes_written) % ring->size; return bytes_written; } } return 0; } // 检查环形缓存区是否有空间写入 int RingBuffer_AvailableForWrite(RingBuffer *ring) { return ring->size - (ring->tail - ring->head + ring->size) % ring->size; } // 检查环形缓存区中是否有数据可读 int RingBuffer_AvailableForRead(RingBuffer *ring) { return (ring->tail - ring->head + ring->size) % ring->size; } int main() { RingBuffer ring; HANDLE hComm; // 假设已经成功打开了串口设备并且初始化 hComm = CreateFile("COM1", GENERIC_WRITE, 0, 0, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, 0); if (hComm == INVALID_HANDLE_VALUE) { // 错误处理 return 1; } RingBuffer_Init(&ring, 1024); // 初始化环形缓存区大小为1024字节 // 假设有一些数据要发送 unsigned char data[] = {0x01, 0x02, 0x03}; RingBuffer_Write(&ring, data, sizeof(data)); // 发送数据 while (RingBuffer_AvailableForRead(&ring) > 0) { RingBuffer_Send(&ring, hComm); } // 关闭串口和释放资源 CloseHandle(hComm); free(ring.buf); return 0; } ``` 请注意,这个代码只是一个示例,实际使用时需要根据具体的串口和操作系统API进行相应的调整。同时,错误处理和资源管理(如关闭句柄和释放内存)应该更加严格。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值