优秀的内存规划方法——环形缓冲区(ring buffer)

目录

  • 什么是环形缓冲区
  • 使用环形buffer的好处
  • 环形buffer的使用场景
    • 进程间通信
    • 网络IO
  • 区分缓冲区是满或者是空
    • 计数
    • 保持一个存储单元为空
    • 镜像指示位
  • buffer满了之后的操作
    • 实时流
    • 存储流

什么是环形缓冲区

线性缓冲区
初学者一般使用的buffer是线性的,数据依次排列依次读取,就像流水线。
造成的问题就是,处理大量数据时,需要大段内存,并且需要考虑对内存的管理。频繁的内存分配不但增加系统的开销,更使得内存碎片不断增多,非常不利于程序长期运行。

环形缓冲区
使用一段固定长度的内存,在内存用尽后,剩余未存的数据从这段内存的起始位置开始存放。
就像两个人围着一张原型的桌子追逐,一个写,一个读。
这样反复使用内存,能使得我们能使用更少的内存块做更多的事情,并且对内存的管理更加方便更加安全。

使用环形buffer的好处

使用线性buffer——
频繁的内存分配不但增加了系统开销,更使得内存碎片不断增多,非常不利于我们的程序长期稳定运行。

使用环形buffer——
圆形缓冲区(circular buffer),也称作圆形队列(circular queue),循环缓冲区(cyclic buffer),环形缓冲区(ring buffer),是一种用于表示一个固定尺寸、头尾相连的缓冲区的数据结构,适合缓存数据流。
 环形缓冲区是一项很好的技术,不用频繁的分配内存,而且在大多数情况下,内存的反复使用也使得我们能用更少的内存块做更多的事并且对内存的管理更加方便更加安全。

环形buffer的使用场景

进程间通信

一般使用的是共享内存。只需要一定长度的共享内存就可以循环传输大量数据进行通信。
如果使用链表,就需要涉及到内存释放问题。

网络IO

某安霸工程师告诉我,他们的方案是使用socket,原因是共享内存安全性较低。
方案是,为每一个链接都准备一个环形缓冲区,用于临时存放接收到的数据,以应付半包及粘包的情况。解包解密后,将数据包再复制到逻辑线程消息队列中。

区分缓冲区是满或者是空

使用环形buffer的注意点就是:需要区分缓冲区是满或者是空
在这里插入图片描述
满或空,读写指针会出现在同一位置

下面介绍三种方法

计数

最简单的方法,读写分别计数。

保持一个存储单元为空

缓冲区中总是有一个存储单元保持未使用状态。缓冲区最多存入(size - 1) 个数据。

  • 如果读写指针指向同一位置,则缓冲区为空。
  • 如果写指针位于读指针的相邻后一个位置,则缓冲区为满。

这种策略的优点是简单、鲁棒(稳定健壮);缺点是语义上实际可存数据量与缓冲区容量不一致,测试缓冲区是否满需要做取余数计算。

镜像指示位

缓冲区的长度如果是n,逻辑地址空间则为0至n-1;那么,规定n至2n-1为镜像逻辑地址空间。
本策略规定读写指针的地址空间为0至2n-1,其中低半部分对应于常规的逻辑地址空间,高半部分对应于镜像逻辑地址空间。

当指针值大于等于2n时,使其折返(wrapped)到ptr-2n。使用一位表示写指针或读指针是否进入了虚拟的镜像存储区:置位表示进入,不置位表示没进入还在基本存储区。
在这里插入图片描述
这种方法的逻辑是,如果写指针超出了读指针n位,在普通情况下,读写指针重合,但在这种情况下,写指针在镜像空间的读指针+n位,不和读指针重合

如果缓冲区长度是2的幂,则本方法可以省略镜像指示位。如果读写指针的值相等,则缓冲区为空;如果读写指针相差n,则缓冲区为满,这可以用条件表达式(写指针 == (读指针 异或 缓冲区长度))来判断。

buffer满了之后的操作

介绍两种在音视频方向的操作

实时流

这种方案优先保证实时性。如果buffer满了,优先丢弃老的数据,让新数据存进来

存储流

这种方案优先保证稳定性。如果buffer满了,舍弃最新的数据,等待老的流处理完再进新的流进行处理

Published by

风君子

独自遨游何稽首 揭天掀地慰生平