0. 目录
1. 起因
做嵌入式的过程中,很容易用到环形队列来做一个buffer缓存的管理。个人感觉这么几种场景用的比较多。
- 多线程场景
比如A模块给B模块发数据,B模块通过中断实时收取,但是不能实时处理,就需要进行缓存,在另外一个线程中处理。 - 输入速率不稳定,但是要求输出稳定的场景。
比如我们算法计算结果不准时,但是客户要求can输出是准确的周期,就要先放到环形队列里,然后再进行发送处理。
优势在于:多线程中操作也可以无锁操作(单入单出)
2. 概念
队列 (Queue):是一种先进先出(First In First Out ,简称 FIFO)的线性表,只允许在一端插入(入队),在另一端进行删除(出队)。
一般队列分为:
- 普通队列
- 环形队列
用的数据结构也有两种
- 线性数组
- 链表
3. 实现
3.1 结构体
一般环形队列结构体都如下:
typedef struct RingQueue
{
int nRdIdx;
int nWrIdx;
(int nLength;)(可选:数组长度)
(int nFull;) (可选:是否满)
BufMem tbuf[BufLen];
}
其中,RdIdx,WrIdx这种读写标志 也会用Head、Tail 这种“头尾”概念来进行描述。
3.2 存取机制
我司内部存取数据一直有这样的称呼:
- D-buf
- Q-buf
具体来源哪里我还没搞清楚,好像是一个视频流的架构里的称呼。
或者
- enqueue
- dequeue
3.2.1 存
存的时候,先将数据放入,再修改WrIdx参数。
有空闲buf的时候很好处理,当没有空闲的时候,有两种处理:
- 报错,不存新的值进去
- 记录,覆盖存储
存数据的步骤:
- 判断当前队列是否满((nWrIdx+1)%BufLen = nRdIdx),是否可以写入数据
- 将新的数据放入buf,同时nWrIdx加1。
3.2.2 取
当buf不空的时候((nWrIdx+1)%BufLen != nRdIdx),可以先从内存中取走nReadIdx所指的buf,然后改写nReadIdx指向下一块buf。
4. Kfifo
Kfifo是linux内核中用的比较广泛的一种无锁环形队列。
简介高效。
其实多看看别人实现的经典代码,往往能收获很多。
相关资料很多很多,有很多详细解析。
这也是最提升的地方。
struct kfifo {
unsigned char *buffer; /* the buffer holding the data */
unsigned int size; /* the size of the allocated buffer */
unsigned int in; /* data is added at offset (in % size) */
unsigned int out; /* data is extracted from off. (out % size) */
spinlock_t *lock; /* protects concurrent modifications */
};
kfifo有几个特别的地方,很有技巧:
- buffer长度都用2的N次幂,这样的话,每次取余计算就不用%符号,而是可以做位运算&,即:
- 当x为2的幂的时候,取模运算%x都可以用与运算&(x-1)代替
- 读写的index,in和out,每次如果读取或者写入一定的长度,直接加就行,不用取模运算,即 in = in + len; out = out + len; 加超过unsigned int范围之后会自动环回。
数据空间(已写入空间)长度为:kfifo->in - kfifo->out
剩余空间(可写入空间)长度为:kfifo->size - (kfifo->in - kfifo->out)- 多次利用min宏来运算,减少了条件分支。
- 内存屏障(这块我也不太懂)
不过说实话,在内核里这么写,确实效率很高又巧妙,但是在上层我还是愿意老老实实的写,因为现在感觉自己记忆力越来越差了,我担心有一天我看不懂自己写的代码。。还是可读性最重要。蓝瘦。