C语言创建循环缓冲区(环形缓冲区)-- Circular Buffer(Ring Buffer)

由于嵌入式系统的资源有限性,循环缓冲区数据结构体(Circular Buffer Data Structures)被大量的使用。

循环缓冲区(也称为环形缓冲区)是固定大小的缓冲区,工作原理就像内存是连续的且可循环的一样。在生成和使用内存时,不需将原来的数据全部重新清理掉,只要调整head/tail 指针即可。当添加数据时,head 指针前进。当使用数据时,tail 指针向前移动。当到达缓冲区的尾部时,指针又回到缓冲区的起始位置。

目录:

  1. 为什么使用循环缓冲区
  2. C 实例
  • 使用封装
  • API设计
  • 确认缓冲区是否已满
  • 循环缓冲区容器类型
  • 实例
  • 使用

为什么使用循环缓冲区?

循环缓冲区通常用作固定大小的队列。固定大小的队列对于嵌入式系统的开发非常友好,因为开发人员通常会尝试使用静态数据存储的方法而不是动态分配。

循环缓冲区对于数据写入和读出以不同速率发生的情况也是非常有用的结构:最新数据始终可用。如果读取数据的速度跟不上写入数据的速度,旧的数据将被新写入的数据覆盖。通过使用循环缓冲区,能够保证我们始终使用最新的数据。

有关其他的用例,请查看Embedded.com上的Ring Buffer Basics

C实例

我们将使用C语言来开始实现,我们将会碰到一些设计上的挑战。

使用封装

我们将创建一个Circular Buffer库,来避免直接操作结构体。

在我们的库文件头部,前置声明结构体:

// Opaque circular buffer structure
typedef struct CIRCULAR_BUFFER_T circular_buf_t;

我们不希望用户直接操作 circular_buf_t 结构体,因为他们可能会觉得可以取消对值的引用。取而代之我们创建一个句柄类型来给用户使用。

最简单的方法是将cbuf_handle_t定义为一个指向circular buffer的指针。这会避免我们在函数中进行强制转换指针。

// Handle type, the way users interact with the API
typedef circular_buf_t* cbuf_handle_t;

另一种方法是使句柄为uintptr_t或void *值。在程序内,我们将句柄转换为适当的指针类型。保证circular buffer类型对用户隐藏,与数据交互的唯一方法是通过句柄。

我们坚持简单的句柄实现,来使代码简单明了。

API Design

首先,我们应该思考用户如何与循环缓冲区交互:

  • 用户需要使用一个 buffer 和 size 来初始化循环缓冲区容器
  • 用户需要销毁循环缓冲区容器
  • 用户需要 reset 循环缓冲区容器
  • 用户需要能够从缓冲区取出下一个值
  • 用户需要知道缓冲区是满还是空
  • 用户需要知道当前缓冲区元素的数量
  • 用户需要知道缓冲区的最大容量

使用这个列表,我们能够合并一个API到库中。用户将使用我们在初始化期间创建的不透明句柄类型和缓冲区库进行交互。

在此实例中,我们选择使用 uint8_t 作为基础数据类型。你可以使用任意你喜欢的特定类型 - 但要注意适当地处理底层缓冲区和字节数。

/// Pass in a storage buffer and size
/// Returns a circular buffer handle
cbuf_handle_t circular_buf_init(uint8_t* buffer, size_t size);

/// Free a circular buffer structure.
/// Does not free data buffer; owner is responsible for that
void circular_buf_free(cbuf_handle_t cbuf);

/// Reset the circular buffer to empty, head == tail
void circular_buf_reset(cbuf_handle_t cbuf);

/// Put version 1 continues to add data if the buffer is full
/// Old data is overwritten
void circular_buf_put(cbuf_handle_t cbuf, uint8_t data);

/// Put Version 2 rejects new data if the buffer is full
/// Returns 0 on success, -1 if buffer is full
int circular_buf_put2(cbuf_handle_t cbuf, uint8_t data);

/// Retrieve a value from the buffer
/// Returns 0 on success, -1 if the buffer is empty
int circular_buf_get(cbuf_handle_t cbuf, uint8_t * data);

/// Returns true if the buffer is empty
bool circular_buf_empty(cbuf_handle_t cbuf);

/// Returns true if the buffer is full
bool circular_buf_full(cbuf_handle_t cbuf);

/// Returns the maximum capacity of the buffer
size_t circular_buf_capacity(cbuf_handle_t cbuf);

/// Returns the current number of elements in the buffer
size_t circular_buf_size(cbuf_handle_t cbuf);

确认缓冲区是否已满

在继续之前,我们应该花费一点时间去讨论一个方法去确认缓冲的空满。

循环缓冲区的 “full” 和 “empty” 看起来是相同的:head 和 tail 指针是相等的。有两种方法区分 full 和 empty:

浪费缓冲区中的一个数据槽:

  • Full:tail + 1 == head
  • Empty:head == tail

使用一个bool标志位和其他逻辑来区分:

  • Full:full
  • Empty:(head == tail) && (!full)

与其浪费一个数据槽,下面方法使用了bool标志位。使用标志位的方法要求在 get 和 put 函数中使用其他逻辑来更新标志。

缓冲区容器类型

现在我们已经确定了需要支持的操作,可以开始设计循环缓冲区容器了。

我们使用容器结构体来管理缓冲区状态。为了保留封装,容器结构体定义在library.c文件中,而不是头文件中。

我们需要跟踪以下信息:

  • 基础数据缓冲区
  • 缓冲区的最大范围
  • “head”指针的当前位置(添加元素时增加)
  • “tail”指针的当前位置(读取元素后增加)
  • 一个标志位来指示缓冲区是否已满
// The hidden definition of our circular buffer structure
struct circular_buf_t {
    uint8_t * buffer;
    size_t head;
    size_t tail;
    size_t max; //of the buffer
    bool full;
};

现在,容器已经设计完成,接下来完成库函数。

实例

需要注意的是,每一个API都需要一个初始化缓冲区的句柄。我们不使用条件语句来填充我们的代码,而是使用断言以“Design by Contract”样式来强制执行我们的API要求。

这样如果程序处理不当,将直接终止程序。

初始化和复位

init 函数:初始化循环缓冲区。我们的API是用户提供底层 buffer 和 buffer size,API返回一个 circular buffer 句柄。

我们需要在库端创建一个循环缓冲区容器。为了简单起见,我使用了 malloc 函数。不能使用动态内存的系统只需修改 init 函数来使用其他方法实现创建目的。例如从循环缓冲区的静态池中分配。

另一种方法是破坏封装,允许用户静态声明循环缓冲区容器结构。在这种情况下,circular_buf_init 需要更新来采用结构指针,或者初始化能够在堆栈上创建一个容器结构体并返回它。但是,由于封装被破坏,用户将无需使用例程就能修改结构体。

所以我们使用第一种方法。

// User provides struct
void circular_buf_init(circular_buf_t* cbuf, uint8_t* buffer,
    size_t size);

// Return a struct
circular_buf_t circular_buf_init(uint8_t* buffer, size_t size)

创建容器之后,我们需要填充数据并在其上调用 reset 函数。在 init 返回之前,我们要确保缓冲区容器是在空状态下创建的。

cbuf_handle_t circular_buf_init(uint8_t* buffer, size_t size)
{
    assert(buffer && size);

    cbuf_handle_t cbuf = malloc(sizeof(circular_buf_t));
    assert(cbuf);

    cbuf->buffer = buffer;
    cbuf->max = size;
    circular_buf_reset(cbuf);

    assert(circular_buf_empty(cbuf));

    return cbuf;
}

reset 函数:目的是将缓冲区置为 “空” 状态,需要更新 head,tail 和 full 。

void circular_buf_reset(cbuf_handle_t cbuf)
{
    assert(cbuf);

    cbuf->head = 0;
    cbuf->tail = 0;
    cbuf->full = false;
}

当我们有了一个创建循环缓冲区容器的方法,同样的我们也需要一个能够销毁容器的等效方法。我们可以调用 free 函数来释放容器。但不要尝试释放底层缓冲区,释放容器指针就好,因为根据我们的初始化方法,我们不需要也不能理会底层缓冲区。

void circular_buf_free(cbuf_handle_t cbuf)
{
    assert(cbuf);
    free(cbuf);
}

状态检查

接下来,我们将实现与缓冲区容器状态相关的函数部分。

full 函数:很容易实现,因为我们已经有一个标志位来表示满状态了:

bool circular_buf_full(cbuf_handle_t cbuf)
{
    assert(cbuf);

    return cbuf->full;
}

empty 函数:因为我们已经有 full 标志位来区分空满状态了,我们只需将 full 标志位和“head == tail”的检查结果合并处理。

bool circular_buf_empty(cbuf_handle_t cbuf)
{
    assert(cbuf);

    return (!cbuf->full && (cbuf->head == cbuf->tail));
}

capacity 函数:由于在初始化阶段就已经设定了缓冲区的容量大小,所以只需要返回这个值即可:

size_t circular_buf_capacity(cbuf_handle_t cbuf)
{
    assert(cbuf);

    return cbuf->max;
}

预期计算缓冲区元素的数量是一个棘手的问题,许多人建议使用除法来计算,但在测试的时候遇到了许多奇怪的情况。所以我选择了条件语句进行简化运算。

关于缓冲区的元素数量有以下三种情况:

① 缓冲区状态是 full ,我们就知道当前的容量已经达到了最大;

② head >= tail,只需将两个值相减就可以得出大小;

③ tail > head,我们需要用最大值来抵消差值,才能得到正确的大小;

size_t circular_buf_size(cbuf_handle_t cbuf)
{
    assert(cbuf);

    size_t size = cbuf->max;

    if(!cbuf->full)
    {
        if(cbuf->head >= cbuf->tail)
        {
            size = (cbuf->head - cbuf->tail);
        }
        else
        {
            size = (cbuf->max + cbuf->head - cbuf->tail);
        }
    }

    return size;
}

添加和删除数据

有了这些功能之后,是时候开始深入研究了:从队列中添加和删除数据。

从循环缓冲区添加和删除数据需要操纵 head 和 tail 指针。当向缓冲区添加数据时,我们将新的数据插入当前 head 指针所在的位置,然后将 head 指针向前移一位。当从缓冲区删除数据时,我们从当前 tail 指针的位置取出数据,然后将 tail 指针向前移一位。

但是,向缓冲区添加数据时需要更多考虑。如果缓冲区是满状态,我们需要同时移动 tail 和 head 指针。我们还需要检查插入数据是否会出发 full 条件。

我们将实现两个版本的 put 函数,因此,让我们将指针前进函数提起到一个辅助函数中。如果缓冲区已满,移动 tail 指针。我们每次都向前移动一位 head 指针。当指针移动之后,我们通过检查 “head == tail” 的结果来判断是否填充 full 标志位。

注意下面使用的除法(%)运算符。当填充的数据达到最大值时,除法运算将导致 head 和 tail 指针重置为0。这样确保head 和 tail 指针始终是底层数据缓冲区的有效索引。

static void advance_pointer(cbuf_handle_t cbuf)
{
    assert(cbuf);

    if(cbuf->full)
       {
        cbuf->tail = (cbuf->tail + 1) % cbuf->max;
    }

    cbuf->head = (cbuf->head + 1) % cbuf->max;
    cbuf->full = (cbuf->head == cbuf->tail);
}

我们可以写一个类似的辅助函数当从缓冲区删除数据时调用。当删除数据时,full 标志置为 flase ,tail 指针向前移一位。

static void retreat_pointer(cbuf_handle_t cbuf)
{
    assert(cbuf);

    cbuf->full = false;
    cbuf->tail = (cbuf->tail + 1) % cbuf->max;
}

我们将创建两个版本的 put 函数。第一个版本向缓冲区插入数据并向前移动指针。如果缓冲区已满,旧数据将会被覆盖。这是循环缓冲区的标准使用案例。

void circular_buf_put(cbuf_handle_t cbuf, uint8_t data)
{
    assert(cbuf && cbuf->buffer);

    cbuf->buffer[cbuf->head] = data;

    advance_pointer(cbuf);
}

第二个版本如果缓冲区已满 put 函数将返回 error。这里只提供一个示范样例,在我们的系统中并没有使用这个变体。

int circular_buf_put2(cbuf_handle_t cbuf, uint8_t data)
{
    int r = -1;

    assert(cbuf && cbuf->buffer);

    if(!circular_buf_full(cbuf))
    {
        cbuf->buffer[cbuf->head] = data;
        advance_pointer(cbuf);
        r = 0;
    }

    return r;
}

从缓冲区删除数据,我们取出 tail 指针位置的值并更新 tail 指针。如果缓冲区是空的,我们不返回数据或者修改指针值。相反,我们返回 error 给用户。

int circular_buf_get(cbuf_handle_t cbuf, uint8_t * data)
{
    assert(cbuf && data && cbuf->buffer);

    int r = -1;

    if(!circular_buf_empty(cbuf))
    {
        *data = cbuf->buffer[cbuf->tail];
        retreat_pointer(cbuf);

        r = 0;
    }

    return r;
}

这样就完成了循环缓冲区库的实现。

使用

在使用这个库时,用户负责创建 circular_buf_init 的底层数据缓冲区,将会返回 cbuf_handle_t :

uint8_t * buffer  = malloc(EXAMPLE_BUFFER_SIZE * sizeof(uint8_t));
cbuf_handle_t cbuf = circular_buf_init(buffer,
    EXAMPLE_BUFFER_SIZE);

改处理用于和其他剩余的所有库函数交互:

bool full = circular_buf_full(cbuf);
bool empty = circular_buf_empty(cbuf);
printf("Current buffer size: %zu\n", circular_buf_size(cbuf);

当处理完之后不要忘记 free 底层数据缓冲区和容器:

free(buffer);
circular_buf_free(cbuf);

源资源帖:https://embeddedartistry.com/blog/2017/05/17/creating-a-circular-buffer-in-c-and-c/

原文地址:https://www.cnblogs.com/youngjum/p/12188780.html

时间: 2024-10-12 00:43:51

C语言创建循环缓冲区(环形缓冲区)-- Circular Buffer(Ring Buffer)的相关文章

环形缓冲区

作者:曾志优 出处:http://www.cnblogs.com/zengzy 1.环形缓冲区 缓冲区的好处,就是空间换时间和协调快慢线程.缓冲区可以用很多设计法,这里说一下环形缓冲区的几种设计方案,可以看成是几种环形缓冲区的模式.设 计环形缓冲区涉及到几个点,一是超出缓冲区大小的的索引如何处理,二是如何表示缓冲区满和缓冲区空,三是如何入队.出队,四是缓冲区中数据长度如何计算. ps.规定以下所有方案,在缓冲区满时不可再写入数据,缓冲区空时不能读数据 1.1.常规数组环形缓冲区 设缓冲区大小为N

环形缓冲区的设计及其在生产者消费者模式下的使用(并发有锁环形队列)

1.环形缓冲区 缓冲区的好处,就是空间换时间和协调快慢线程.缓冲区可以用很多设计法,这里说一下环形缓冲区的几种设计方案,可以看成是几种环形缓冲区的模式.设计环形缓冲区涉及到几个点,一是超出缓冲区大小的的索引如何处理,二是如何表示缓冲区满和缓冲区空,三是如何入队.出队,四是缓冲区中数据长度如何计算. ps.规定以下所有方案,在缓冲区满时不可再写入数据,缓冲区空时不能读数据 1.1.常规数组环形缓冲区 设缓冲区大小为N,队头out,队尾in,out.in均是下标表示: 初始时,in=out=0 队头

C#使用Fixed创建固定大小的缓冲区

在 C# 中,可以使用 fixed 语句在数据结构中创建带有固定大小数组的缓冲区. 使用现有代码(如使用其他语言.预先存在的 DLL 或 COM 项目编写的代码)时,这种方法非常有用. 固定数组可采用允许普通结构成员使用的任何特性或修饰符. 唯一的限制是,数组类型必须是 bool.byte. char. short.int.long.sbyte.ushort.uint.ulong.float 或 double. private fixed char name[30]; 在早期版本的 C# 中,声

用于拼包和解码的环形缓冲区类

using System;using System.Collections.Generic;using System.Linq;using System.Text; namespace Test{    /// <summary>    /// 环形缓冲区    /// </summary>    public class CRawBuffer    {        byte[] m_BufArr;        int m_i4BufLen;   //数据缓冲区大小      

SQL Server 环形缓冲区(Ring Buffer) -- RING_BUFFER_SECURITY_ERROR 诊断安全相

SQL Server 环形缓冲区(Ring Buffer) -- RING_BUFFER_SECURITY_ERROR 诊断安全相关错误 环形缓冲存储了大量的在过去一段时间段内的安全错误信息,有助于分析SQL Server安全问题. 例如,当你尝试创建一个SQL登录账号,并启用密码策略,但是提供的密码不匹配密码策略.然后,你将会收到一个错误消息说明密码不匹配.这个错误将会存储在环形缓冲区.当你执行下面的查询,你将会导致错误的SPID以及导致失败的API名称.如上面示例描述的,你会找到NetVal

线程安全的环形缓冲区实现

来源:http://blog.csdn.net/lezhiyong    应用背景:线程1将每次数量不一的音频采样点(PCM音频数据)写入环形缓冲区,线程2每次取固定数量采样点送音频编码器,线程1线程2在平均时间内的读写数据量相等.(倒入桶中的水量有时大有时小,但每次取一瓢喝:)   该环形缓冲区借鉴CoolPlayer音频播放器中的环形缓冲区代码实现,在读写操作函数中加了锁,允许多线程同时操作.CPs_CircleBuffer基于内存段的读写,比用模板实现的环形缓冲队列适用的数据类型更广些,

[转]环形缓冲区

在通信程序中,经常使用环形缓冲区作为数据结构来存放通信中发送和接收的数据.环形缓冲区是一个先进先出的循环缓冲区,可以向通信程序提供对缓冲区的互斥访问. 1.环形缓冲区的实现原理 环形缓冲区通常有一个读指针和一个写指针.读指针指向环形缓冲区中可读的数据,写指针指向环形缓冲区中可写的缓冲区.通过移动读指针和写指针就可以实现缓冲区的数据读取和写入.在通常情况下,环形缓冲区的读用户仅仅会影响读指针,而写用户仅仅会影响写指针.如果仅仅有一个读用户和一个写用户,那么不需要添加互斥保护机制就可以保证数据的正确

SQL Server 环形缓冲区(Ring Buffer) -- 环形缓冲在AlwaysOn的应用

SQL Server 环形缓冲区(Ring Buffer) -- 环形缓冲在AlwaysOn的应用 可以从SQL Server环形缓冲区得到一些诊断AlwaysOn的信息,或从sys.dm_os_ring_buffers动态管理视图.环形缓冲在SQL Server启动的时候创建,在SQL Server系统内记录告警用于内部诊断.它们不被支持,但你仍能从中获取有用的信息.下面的查询能从AlwaysON环形缓冲获取所有的事件记录. SELECT * FROM sys.dm_os_ring_buffe

【转】环形缓冲区

原文地址:http://blog.csdn.net/linlinlinxi007/article/details/5086806 在通信程序中,经常使用环形缓冲区作为数据结构来存放通信中发送和接收的数据.环形缓冲区是一个先进先出的循环缓冲区,可以向通信程序提供对缓冲区的互斥访问. 1.环形缓冲区的实现原理 环形缓冲区通常有一个读指针和一个写指针.读指针指向环形缓冲区中可读的数据,写指针指向环形缓冲区中可写的缓冲区.通过移动读指针和写指针就可以实现缓冲区的数据读取和写人.在通常情况下,环形缓冲区的