put返回实际保存到缓冲区中的数据长度,get返回的是实际取到的数据长度。在上面代码中,需要注意到在写入、取出时候的两次min运算。关于kfifo的分析,已有很多资料了,也可参考 Linux内核数据结构kfifo详解 。
Linux内核实现的kfifo的有以下特点:
使用内存屏障 Memory Barrier
初始化缓冲区空间时要保证缓冲区的大小为2的次幂
使用无符号整数保存in和out(输入输出的指针),并且在放入取出数据的时候不做模运算,让其自然溢出。
优点:
实现单消费者和单生产者的无锁并发访问。多消费者和多生产者的时候还是需要加锁的。
使用与运算in & (size-1)代替模运算
在更新in或者out的值时不做模运算,而是让其自动溢出。这应该是kfifo实现最牛叉的地方了,利用溢出后的值参与运算,并且能够保证结果的正确。溢出运算保证了以下几点:
in - out为缓冲区中的数据长度
size - in + out 为缓冲区中空闲空间
in == out时缓冲区为空
size == (in - out)时缓冲区满了
3.模仿kfifo实现的循环缓冲主要是模仿其无符号溢出的运算方法,并没有利用内存屏障实现单生产者和单消费者的无锁并发访问。初始化及输入输出的代码如下:
struct kfifo{ uint8_t *buffer; uint32_t in; // 输入指针 uint32_t out; // 输出指针 uint32_t size; // 缓冲区大小,必须为2的次幂 kfifo(uint32_t _size) { if (!is_power_of_2(_size)) _size = roundup_power_of_2(_size); buffer = new uint8_t[_size]; in = 0; out = 0; size = _size; } // 返回实际写入缓冲区中的数据 uint32_t put(const uint8_t *data, uint32_t len) { // 当前缓冲区空闲空间 len = min(len,size - in + out); // 当前in位置到buffer末尾的长度 auto l = min(len, size - (in & (size - 1))); // 首先复制数据到[in,buffer的末尾] memcpy(buffer + (in & (size - 1)), data, l); // 复制剩余的数据(如果有)到[buffer的起始位置,...] memcpy(buffer, data + l, len - l); in += len; // 直接加,不作模运算。当溢出时,从buffer的开始位置重新开始 return len; } // 返回实际读取的数据长度 uint32_t get(uint8_t *data, uint32_t len) { // 缓冲区中的数据长度 len = min(len, in - out); // 首先从[out,buffer end]读取数据 auto l = min(len, size - (out & (size - 1))); memcpy(data, buffer + (out & (size - 1)), l); // 从[buffer start,...]读取数据 memcpy(data + l, buffer, len - l); out += len; // 直接加,不错模运算。溢出后,从buffer的起始位置重新开始 return len; }在初始化缓冲空间的时候要验证size是否为2的次幂,如果不是则向上取整为2的次幂。下面着重分析下在放入取出数据时对指针in和out的处理,以及在溢出后怎么能够保证in - out仍然为缓冲区中的已有的数据长度。
put和get方法详解在向缓冲区中put数据的时候,需要两个参数:要put的数据指针data和期望能够put的数据长度len,返回值是实际存放到缓冲区中的数据长度(当缓冲区中空间不足时该值小于len)。下面详细的解释下put中每个语句的作用。
put函数中的第一句是len = min(len,size - in + out)计算实际向缓冲区中写入数据的大小。如果想要写入的数据len大于缓冲区中的空闲空间size - in + out,则只填充满缓冲空间。
因为是循环缓冲区,所以其空闲空间有两部分:从in到缓冲空间的末尾->[in,buffer end]和缓冲空间的起始位置到out->[buffer start,out]。
auto l = min(len, size - (in & (size - 1))); 这个是判断[in,buffer end]这部分空间是否足够写入数据
memcpy(buffer + (in & (size - 1)), data, l); 向[in,buffer end]这部分空间写入数据
memcpy(buffer, data + l, len - l); 如果数据还没有写完,则向[buffer start,out]这部分空间写入数据。
in += len 更新in,不做模运算,让其自然溢出。