前天在编程时,出现了这样的提示信息:
[IncreaseDataAcqrOne]: Ring buffer full!
字面上,这条信息的意思是环形缓存区满了
环形缓存区的先进先出数据结构对于在异步进程之间传递数据是有用的工具,下面是使用C语言实现位操作的方法,不使用C++标准模板库。
(1) 什么是环形缓存区?
环形缓存区(ring buffer),也被称为circular buffer, circular queue, 或cyclic buffer。是一种环形软件队列,该队列有先进先出的数据特征,这种缓存区非常常见并且在许多嵌入式系统中都能找到,通常,大多数开发人员根据需要从头开始编写这些结构。
C++拥有标准模板库,它有一套非常易于使用的类模板。该库使得开发人员能够相对容易的创建队列和其他表。
环形缓存区通常有两个指向元素的标志,这个标志之间距离的范围从0到缓存区中全体元素的总数,双标志的使用意味着队列的长度可以从0到满。
数据从头部标志被写入,从尾部标志被读出。归根结底,最新的数据从头部标志“长出来”。最古老的数据从尾部标志被检索出来。下图为环形缓冲区的线性实现:
(2) 环形缓冲区如何使用
单个进程对单个进程:
通常情况下,队列用于从一个进程到另一个进程发送序列化数据。序列化允许在进程之间存在一定程度的弹性。在许多情况下,队列在某些硬件中断服务例程中用作数据缓冲区。这个缓冲区将收集数据,以便在稍后的某个时间另一个进程可以获取数据进行进一步处理。这个用例是单进程到进程的缓冲用例。
这种用例通常被建立用作从一些非常高优先级的硬件服务向一些更低优先级的在后台循环的服务缓存数据的接口:
在很多情况下,需要为单个中断服务设置两个队列。对于RS-232、I2C或USB等串行设备的驱动程序,使用多个队列是非常常见的做法。
多个进程到单个进程:
一个不常见的需求是把许多数据流序列化成一个接收流。这样的案例在多线程的操作系统中非常常见。在这种情况下,有许多客户端线程向某些服务器或中间人线程请求某种序列化操作。这些请求和消息在一个单个队列中序列化并被一个进程接收:
单个进程到多个进程:
最不常见的使用案例是从单个进程到多个进程。这里的难点在于如何实时确定输出的方向。通常,这是通过给数据元素打上标签的方式来实现的,这样经纪人进程就可以以某种有意义的方式引导数据流动。下图显示了单个进程到多个进程的使用案例。由于可以轻松创建队列,因此通常最好创建多个队列来解决此使用案例,而不是使用单个队列。
(3) 管理溢出
当队列已满并且依然有数据进入时,必须应对这种情况。这种情况被认为是溢出情况。这里有两种处理这种情况的方法: 丢弃最近的数据或覆盖最老的数据。
2.参考Perf ring buffer
perf是一款Linux性能分析工具。
ring buffer是数据传输的基本工具,perf使用ring buffer从内核到用户空间传输事件数据。另一种ring buffer被称为辅助ring buffer也在硬件跟踪技术发挥重要作用
所谓硬件跟踪技术我大概理解就是在硬件电路层上跟踪嵌入式软件运转情况,作用大约相当于Debugger
ring buffer的实现也是至关重要但也是一项非常具有挑战性的工作。一方面,内核和用户空间中的perf工具使用ring buffer交换数据或将数据存入文件。因此ring buffer需要以高吞吐量传输数据。另一方面,ring buffer的管理应该避免严重超载从而分散了性能分析结果。
基本算法
典型的ring buffer是由一个头指针和一个尾指针;头部指针是由一个写着操纵,而尾指针由读者更新。
+---------------------------+
| | |***|***|***| | |
+---------------------------+
`-> Tail `-> Head
* : the data is filled by the writer.
Perf使用同样的方式去管理它的ring buffer。在执行中有两种关键的数据结构被放置在连续的页面中: 具有控制结构的页被称为“用户页”。被放置在连续的虚拟地址中降低了定位ring buffer地址的难度,它是在用户页面之后的页面。
控制结构被称为perf_event_mmap_page,它包括一个头指针data_head和一个尾指针data_tail。当内核开始往ring buffer中填充记录时,它更新头指针以保留内存地址。在另一边,当用户页是可写的映射时,perf工具有权限在从ring buffer消费数据后更新尾指针。
user page ring buffer
+---------+---------+ +---------------------------------------+
|data_head|data_tail|...| | |***|***|***|***|***| | | |
+---------+---------+ +---------------------------------------+
` `----------------^ ^
`----------------------------------------------|
* : the data is filled by the writer.
当使用perf record工具时,我们可以通过选项-m或–mmap-pages=指明ring buffer的大小。给出的大小与2的幂绑定起来,是页的大小的倍数。虽然内核一次性分配了所有的内存页,但直到perf工具访问用户空间中的缓存区时,才能将这些内存页映射到VMA区域
VMA是内核管用户空间的数据结构。
换句话说,在使用perf工具第一次访问用户空间的缓存区页时,一个页错误的数据中断异常发生了,内核趁机将页映射到进程的VMA。因此在从异常返回之后,perf tool能够继续访问页。
3.参考A generic ring buffer for the kernel
ABI(Application Binary Interface)为应用程序二进制接口,描述了应用程序和操作系统之间,一个应用和它的库之间,或者应用的组成部分之间的二进制层面的底层接口。
内核的用户空间ABI并不缺乏ring buffers。比如它们已经为子系统定义,比如 BPF, io_uring, perf, 和tracing。正常情况下,每一种ring buffer都是独立的,在它们之间不存在公共接口。当然,对ABI增加的自然反应是增加另一个ring buffer作为通用选项。
ring buffer简单的说就是一个循环缓冲区,构建于内存之中,在用户空间和内核之间共享。数据流的一边将数据写入缓存区中,另一边在消耗数据。只要缓存区数据不是上溢或下溢,数据可以在完全没有系统调用的情况下被传送。因此,ring buffer的增加能够使得当数据速率较高的时候,能够进行高效数据传输。
(1) 用户空间接口
在用户空间这边,通过被提出的接口,一个ring buffer总是能和一个文件描述符关联起来,以至于第一步将是打开将要连接缓冲区的对象(比如一个设备、一个文件或一个管道)。然后,ring buffer伴随着新的系统调用被建立起来:
int ringbuffer(unsigned int fd, int rw, u32 size, void **buffer);
该调用将请求创造ring buffer,并且该ring buffer与要打开的文件描述符fd关联。为了读或写访问,rw应该被设置成0。请求的缓存区长度被传入size。若能成功返回,缓存区的地址将被存储在buffer中.
注意,实现ring buffer的子系统决定了是否通过创造一个新缓存区或通过把现有缓存区映射给调用者地址空间的方式来响应这种调用。后面一种方式的目的是允许一个单独的ring buffer在多个进程之间共享。
返回的指针将指向一个内存区域,该区域从这个结构的一个副本开始:
struct ringbuffer_ptrs
{
__u32 head;
__u32 tail;
__u32 size; /* always a power of two */
__u32 mask; /* size - 1 */
__u32 data_offset;
};
缓存区自身将从返回的指针的data_offset开始。在实际操作中,偏移量可能总是一页。缓存区的实际大小将被存储在size中,如前所述,它总是2的幂。head偏移量是写位置,然而tail是读位置。如果head和tail值相同,则缓存区为空。
当数据被增加或被消耗时,头和尾都无条件递增,没有环绕的测试。相反,这些值必须与掩码值进行并运算,以获得缓冲区中的实际偏移量。