从源码解析Linux内核中的kfifo:C++实现单读单写无锁队列

2024-05-15 19:04

本文主要是介绍从源码解析Linux内核中的kfifo:C++实现单读单写无锁队列,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

kfifo 简介

kfifo是Linux内核的一个FIFO数据结构,采用环形循环队列的数据结构来实现,提供一个无边界的字节流服务,并且使用并行无锁编程技术,即单生产者单消费者场景下两个线程可以并发操作,不需要任何加锁行为就可以保证kfifo线程安全。

其数据结构如下:

struct kfifo
{unsigned char *buffer;unsigned int size;unsigned int in;unsigned int out;spinlock_t *lock;
};

源码阅读

设计要点

保证buffer size为2的幂:kfifo->size值在调用者传递参数size的基础上向2的幂扩展,目的是使kfifo->size取模运算可以转化为位与运算(提高运行效率)。kfifo->in % kfifo->size转化为 kfifo->in & (kfifo->size – 1)保证size是2的幂可以通过位运算的方式求余,在频繁操作队列的情况下可以大大提高效率。

使用自旋锁实现同步:确保在多线程环境下对缓冲区的操作是线程安全的。spin_lock_irqsave 和 spin_unlock_irqrestore 确保在临界区内的操作不会被中断打断。

线性代码结构:代码中没有任何if-else分支来判断是否有足够的空间存放数据,kfifo每次入队或出队只是简单的 +len 判断剩余空间,并没有对kfifo->size 进行取模运算,所以kfifo->in和kfifo->out总是一直增大,直到unsigned in超过最大值时绕回到0这一起始端,但始终满足:kfifo->in - kfifo->out <= kfifo->size。

使用内存屏障:内存屏障确保编译器和CPU在内存访问上的有序性,避免乱序访问带来的逻辑错误。适用于多处理器和单处理器环境下的不同类型内存屏障,确保同步原语和无锁数据结构的正确性。

kfifo的巧妙之处在于in和out定义为无符号类型,在put和get时,in和out都是增加,当达到最大值时,产生溢出,使得从0开始,进行循环使用。

创建队列

struct kfifo *kfifo_init(unsigned char *buffer, unsigned int size, gfp_t gfp_mask, spinlock_t *lock)
{struct kfifo *fifo;// 判断是否为2的幂BUG_ON(!is_power_of_2(size));fifo = kmalloc(sizeof(struct kfifo), gfp_mask);if (!fifo)return ERR_PTR(-ENOMEM);fifo->buffer = buffer;fifo->size = size;fifo->in = fifo->out = 0;fifo->lock = lock;return fifo;
}

其传参如下,这些是用来初始化kfifo:

  • unsigned char *buffer:缓冲区的数据存储区。
  • unsigned int size:缓冲区的大小。
  • gfp_t gfp_mask:内存分配标志,用于内存分配函数kmalloc。
  • spinlock_t *lock:自旋锁,用于多线程同步。

其会检查size是否是2的幂。如果不是2的幂,BUG_ON宏会导致程序崩溃并打印错误信息。环形缓冲区的大小必须是2的幂,以便使用高效的位运算(如按位与操作)代替取模运算。

对于一个索引 index,取模运算是:index%N,这是因为环形缓冲区是循环的,当索引超过缓冲区大小时,需要返回到缓冲区的起点。如果N是2的幂,那么可以N表示为2^k。此时,取模运算可以通过位运算替代:

index % N = index & (N - 1)

这种方式极大地提高了计算效率,因为按位与运算比取模运算要快得多。

分配空间

struct kfifo *kfifo_alloc(unsigned int size, gfp_t gfp_mask, spinlock_t *lock)
{unsigned char *buffer;struct kfifo *ret;// 判断是否为2的幂if (!is_power_of_2(size)){BUG_ON(size > 0x80000000);// 向上扩展成2的幂size = roundup_pow_of_two(size);}buffer = kmalloc(size, gfp_mask);if (!buffer)return ERR_PTR(-ENOMEM);ret = kfifo_init(buffer, size, gfp_mask, lock);if (IS_ERR(ret))kfree(buffer);return ret;
}

这一步主要就是检查缓冲区大小是否为2的幂。如果不是,则将其调整为不小于原大小的最接近的2的幂,
为缓冲区分配内存。

入队函数

unsigned int __kfifo_put(struct kfifo *fifo, const unsigned char *buffer, unsigned int len)
{unsigned int l; //buffer中空的长度len = min(len, fifo->size - fifo->in + fifo->out);// 内存屏障:smp_mb(),smp_rmb(), smp_wmb()来保证对方观察到的内存操作顺序smp_mb();// 将数据追加到队列尾部l = min(len, fifo->size - (fifo->in & (fifo->size - 1)));memcpy(fifo->buffer + (fifo->in & (fifo->size - 1)), buffer, l);memcpy(fifo->buffer, buffer + l, len - l);smp_wmb();//每次累加,到达最大值后溢出,自动转为0fifo->in += len;return len;
}
static inline unsigned int kfifo_put(struct kfifo *fifo, const unsigned char *buffer, unsigned int len)
{unsigned long flags;unsigned int ret;spin_lock_irqsave(fifo->lock, flags);ret = __kfifo_put(fifo, buffer, len);spin_unlock_irqrestore(fifo->lock, flags);return ret;
}

通过在 kfifo_put 中使用自旋锁(spin_lock_irqsave 和 spin_unlock_irqrestore),确保了在多线程环境下对缓冲区的操作是线程安全的。

进入临界区前,获取自旋锁,并禁用中断。这确保在临界区内不会发生上下文切换或中断。调用实际的写入函数 __kfifo_put,执行数据写入操作。释放自旋锁,并恢复中断状态。

出队操作

unsigned int __kfifo_get(struct kfifo *fifo, unsigned char *buffer, unsigned int len)
{unsigned int l;//有数据的缓冲区的长度len = min(len, fifo->in - fifo->out);smp_rmb();l = min(len, fifo->size - (fifo->out & (fifo->size - 1)));memcpy(buffer, fifo->buffer + (fifo->out & (fifo->size - 1)), l);memcpy(buffer + l, fifo->buffer, len - l);smp_mb();fifo->out += len; //每次累加,到达最大值后溢出,自动转为0return len;
}
static inline unsigned int kfifo_get(struct kfifo *fifo, unsigned char *buffer, unsigned int len)
{unsigned long flags;unsigned int ret;spin_lock_irqsave(fifo->lock, flags);ret = __kfifo_get(fifo, buffer, len);//当fifo->in == fifo->out时,buufer为空if (fifo->in == fifo->out)fifo->in = fifo->out = 0;spin_unlock_irqrestore(fifo->lock, flags);return ret;
}

在数据出队时,有两个复制操作,与入队时很类似,下

memcpy(buffer, fifo->buffer + (fifo->out & (fifo->size - 1)), l);
memcpy(buffer + l, fifo->buffer, len - l);

假设:缓冲区大小 N = 8。当前读指针 out = 6。需要读取的数据长度 len = 5。

第一步:计算可以连续读取的长度 l = min(5, 8 - (6 & 7)) = min(5, 2) = 2。执行 memcpy(buffer, fifo->buffer + 6, 2):从位置 6 开始读取 2 字节数据到目标缓冲区 buffer。

第二步:计算剩余需要读取的数据长度 len - l = 5 - 2 = 3。执行 memcpy(buffer + 2, fifo->buffer, 3):从环形缓冲区起始位置读取 3 字节数据到目标缓冲区 buffer + 2。

计算长度

static inline unsigned int __kfifo_len(struct kfifo *fifo)
{return fifo->in - fifo->out;
}static inline unsigned int kfifo_len(struct kfifo *fifo)
{unsigned long flags;unsigned int ret;spin_lock_irqsave(fifo->lock, flags);ret = __kfifo_len(fifo);spin_unlock_irqrestore(fifo->lock, flags);return ret;
}

重置

static inline void __kfifo_reset(struct kfifo *fifo)
{fifo->in = fifo->out = 0;
}static inline void kfifo_reset(struct kfifo *fifo)
{unsigned long flags;spin_lock_irqsave(fifo->lock, flags);__kfifo_reset(fifo);spin_unlock_irqrestore(fifo->lock, flags);
}

直接重置环形缓冲区的读写指针,使其重新变为空状态。

C++仿写

使用C++的无锁编程实现kfifo,100w次入队出队只需要0.7s。

#include <iostream>
#include <vector>
#include <atomic>
#include <thread>
#include <cassert>
#include <chrono>template<typename T>
class LockFreeQueue {
public:explicit LockFreeQueue(size_t size): buffer_(size), size_(size), head_(0), tail_(0) {assert((size & (size - 1)) == 0 && "Size must be a power of 2");}// Enqueue an element into the queue. Returns false if the queue is full.bool enqueue(const T& item) {size_t head = head_.load(std::memory_order_relaxed);size_t next_head = (head + 1) & (size_ - 1);if (next_head == tail_.load(std::memory_order_acquire)) {// Queue is fullreturn false;}buffer_[head] = item;head_.store(next_head, std::memory_order_release);return true;}// Dequeue an element from the queue. Returns false if the queue is empty.bool dequeue(T& item) {size_t tail = tail_.load(std::memory_order_relaxed);if (tail == head_.load(std::memory_order_acquire)) {// Queue is emptyreturn false;}item = buffer_[tail];tail_.store((tail + 1) & (size_ - 1), std::memory_order_release);return true;}private:std::vector<T> buffer_;const size_t size_;std::atomic<size_t> head_;std::atomic<size_t> tail_;
};void producer(LockFreeQueue<int>& queue, int num_items) {for (int i = 0; i < num_items; ++i) {while (!queue.enqueue(i)) {// Busy-wait until the item is enqueued}}
}void consumer(LockFreeQueue<int>& queue, int num_items) {int item;for (int i = 0; i < num_items; ++i) {while (!queue.dequeue(item)) {// Busy-wait until the item is dequeued}}
}int main() {const int queue_size = 1024;  // Size must be a power of 2const int num_items = 1000000;  // Number of items the producer will produceLockFreeQueue<int> queue(queue_size);auto start_time = std::chrono::high_resolution_clock::now();// Create producer and consumer threadsstd::thread producer_thread(producer, std::ref(queue), num_items);std::thread consumer_thread(consumer, std::ref(queue), num_items);// Join producer and consumer threadsproducer_thread.join();consumer_thread.join();auto end_time = std::chrono::high_resolution_clock::now();std::chrono::duration<double> duration = end_time - start_time;std::cout << "Test completed in " << duration.count() << " seconds." << std::endl;return 0;
}

这篇关于从源码解析Linux内核中的kfifo:C++实现单读单写无锁队列的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/992659

相关文章

windows和Linux使用命令行计算文件的MD5值

《windows和Linux使用命令行计算文件的MD5值》在Windows和Linux系统中,您可以使用命令行(终端或命令提示符)来计算文件的MD5值,文章介绍了在Windows和Linux/macO... 目录在Windows上:在linux或MACOS上:总结在Windows上:可以使用certuti

使用Python实现IP地址和端口状态检测与监控

《使用Python实现IP地址和端口状态检测与监控》在网络运维和服务器管理中,IP地址和端口的可用性监控是保障业务连续性的基础需求,本文将带你用Python从零打造一个高可用IP监控系统,感兴趣的小伙... 目录概述:为什么需要IP监控系统使用步骤说明1. 环境准备2. 系统部署3. 核心功能配置系统效果展

Python实现微信自动锁定工具

《Python实现微信自动锁定工具》在数字化办公时代,微信已成为职场沟通的重要工具,但临时离开时忘记锁屏可能导致敏感信息泄露,下面我们就来看看如何使用Python打造一个微信自动锁定工具吧... 目录引言:当微信隐私遇到自动化守护效果展示核心功能全景图技术亮点深度解析1. 无操作检测引擎2. 微信路径智能获

C#如何调用C++库

《C#如何调用C++库》:本文主要介绍C#如何调用C++库方式,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教... 目录方法一:使用P/Invoke1. 导出C++函数2. 定义P/Invoke签名3. 调用C++函数方法二:使用C++/CLI作为桥接1. 创建C++/CL

Python中pywin32 常用窗口操作的实现

《Python中pywin32常用窗口操作的实现》本文主要介绍了Python中pywin32常用窗口操作的实现,pywin32主要的作用是供Python开发者快速调用WindowsAPI的一个... 目录获取窗口句柄获取最前端窗口句柄获取指定坐标处的窗口根据窗口的完整标题匹配获取句柄根据窗口的类别匹配获取句

Linux之systemV共享内存方式

《Linux之systemV共享内存方式》:本文主要介绍Linux之systemV共享内存方式,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教... 目录一、工作原理二、系统调用接口1、申请共享内存(一)key的获取(二)共享内存的申请2、将共享内存段连接到进程地址空间3、将

在 Spring Boot 中实现异常处理最佳实践

《在SpringBoot中实现异常处理最佳实践》本文介绍如何在SpringBoot中实现异常处理,涵盖核心概念、实现方法、与先前查询的集成、性能分析、常见问题和最佳实践,感兴趣的朋友一起看看吧... 目录一、Spring Boot 异常处理的背景与核心概念1.1 为什么需要异常处理?1.2 Spring B

Python位移操作和位运算的实现示例

《Python位移操作和位运算的实现示例》本文主要介绍了Python位移操作和位运算的实现示例,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一... 目录1. 位移操作1.1 左移操作 (<<)1.2 右移操作 (>>)注意事项:2. 位运算2.1

如何在 Spring Boot 中实现 FreeMarker 模板

《如何在SpringBoot中实现FreeMarker模板》FreeMarker是一种功能强大、轻量级的模板引擎,用于在Java应用中生成动态文本输出(如HTML、XML、邮件内容等),本文... 目录什么是 FreeMarker 模板?在 Spring Boot 中实现 FreeMarker 模板1. 环

Qt实现网络数据解析的方法总结

《Qt实现网络数据解析的方法总结》在Qt中解析网络数据通常涉及接收原始字节流,并将其转换为有意义的应用层数据,这篇文章为大家介绍了详细步骤和示例,感兴趣的小伙伴可以了解下... 目录1. 网络数据接收2. 缓冲区管理(处理粘包/拆包)3. 常见数据格式解析3.1 jsON解析3.2 XML解析3.3 自定义