Linux内核中的队列 kfifo【转】
转自:http://airekans.github.io/c/2015/10/12/linux-kernel-data-structure-kfifo#api
在内核中经常会有需要用到队列来传递数据的时候,而在Linux内核中就有一个轻量而且实现非常巧妙的队列实现——kfifo。 简单来说kfifo是一个有限定大小的环形buffer,借用网络上的一个图片来说明一下是最清楚的:
kfifo
本身并没有队列元素的概念,其内部只是一个buffer。在使用的时候需要用户知道其内部存储的内容,所以最好是用来存储定长对象。
kfifo
有一个重要的特性,就是当使用场景是单生产者单消费者(1 Producer 1 Consumer,以下简称1P1C)的情况下,不需要加锁,所以在这种情况下的性能较高。
本文中的所有代码均来自linux kernel 2.6.32,所以License也是GPLv2的。
定义及API
kfifo主要定义在include/linux/kfifo.h
里面:
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 */
};
extern struct kfifo *kfifo_init(
unsigned char *buffer, unsigned int size,
gfp_t gfp_mask, spinlock_t *lock);
extern struct kfifo *kfifo_alloc(
unsigned int size, gfp_t gfp_mask,
spinlock_t *lock);
extern void kfifo_free(struct kfifo *fifo);
extern unsigned int __kfifo_put(struct kfifo *fifo,
const unsigned char *buffer, unsigned int len);
extern unsigned int __kfifo_get(struct kfifo *fifo,
unsigned char *buffer, unsigned int len);
可以看到在kfifo本身的定义里面,有一个spinlock_t
,这是用来在多线程同时修改队列的时候加锁的。而其余的成员就很明显了,是用来表示队列的当前状态的。队列本身的内容存储在buffer
里面。
需要注意的是,kfifo要求队列的size是2的幂(2^n),这样在后面操作的时候求余操作可以通过与运算来完成,从而更高效。
初始化通过kfifo_init
和kfifo_alloc
完成。而对于队列操作的主要函数的是kfifo_put
和kfifo_get
。这两个函数会先加锁,然后调用__kfifo_put
或者__kfifo_get
。也就是说真正的逻辑是实现在这两个函数里。 之前也说过kfifo
在1P1C的情况下是不需要加锁的,所以这里我们会着重看看这两个函数。
入队
__kfifo_put
的定义很短:
unsigned int __kfifo_put(struct kfifo *fifo,
const unsigned char *buffer, unsigned int len)
{
unsigned int l;
len = min(len, fifo->size - fifo->in + fifo->out);
/*
* Ensure that we sample the fifo->out index -before- we
* start putting bytes into the kfifo.
*/
smp_mb();
/* first put the data starting from fifo->in to buffer end */
l = min(len, fifo->size - (fifo->in & (fifo->size - 1)));
memcpy(fifo->buffer + (fifo->in & (fifo->size - 1)), buffer, l);
/* then put the rest (if any) at the beginning of the buffer */
memcpy(fifo->buffer, buffer + l, len - l);
/*
* Ensure that we add the bytes to the kfifo -before-
* we update the fifo->in index.
*/
smp_wmb();
fifo->in += len;
return len;
}
可以看到里面加了一些memory barrier来确保1P1C场景的正确,这里我们可以暂时忽略。
主要的步骤如下:
- 计算len和队列余下容量的较小值,如果队列容量不足,则只会拷贝剩余容量的大小。
- 先拷贝一部分内容到队列的尾部。
- 如果队列尾部并不能容下所有的内容,则再在队列的头部空闲空间继续拷贝。
- 把队列内容长度加上len
- 返回新增内容的长度len
这里注意到in只有在__kfifo_put
里面才会修改,而这个函数里面只会对in增加,所以in的值只会增加,不会减少。而in本身是unsigned int
类型的,所以当in超出了2^32的时候,会自动从0开始继续。
同时前面也说过,kfifo
的size是2^n。所以当in > 2^n
的时候,(in & 2^n - 1) == (in % 2^n)
,所以这里可以用与操作替代求余来获取in在队列中实际的位置。
出队
__kfifo_get
的定义和__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);
/*
* Ensure that we sample the fifo->in index -before- we
* start removing bytes from the kfifo.
*/
smp_rmb();
/* first get the data from fifo->out until the end of the buffer */
l = min(len, fifo->size - (fifo->out & (fifo->size - 1)));
memcpy(buffer, fifo->buffer + (fifo->out & (fifo->size - 1)), l);
/* then get the rest (if any) from the beginning of the buffer */
memcpy(buffer + l, fifo->buffer, len - l);
/*
* Ensure that we remove the bytes from the kfifo -before-
* we update the fifo->out index.
*/
smp_mb();
fifo->out += len;
return len;
}
忽略掉memory barrier之后,主要步骤如下:
- 计算len和队列长度的较小值,如果队列内容不够,则只拷贝较小值的大小。
- 拷贝队列尾部的内容到输出buffer里面。
- 如果仍然有部分内容没有拷贝的话,则从队列头部拷贝余下的内容。
- 队列内容长度减少len(也就是
out += len
)。 - 返回拷贝内容的长度。
其实基本就是__kfifo_put
的逆过程。
那这里就有一个问题了,其实队列的长度并不一定要用in
和out
两个变量来表示啊,也可以用一个len
变量来表示啊。那这里就涉及到了多线程的互斥问题了。
多线程互斥
这里我们只考虑最简单的多线程场景——1P1C。如果我们只用一个len
来表示队列长度的话,那么看看__kfifo_put
和__kfifo_get
里面对这个变量都需要做修改,而且一个是+=
操作,一个是-=
。如果在不加锁的情况下,这两个操作并不是原子操作,所以如果只用一个len
,我们必须用锁来保护,无论是多么简单的多线程场景。
如果我们用in
和out
来表示队列的读边界和写边界的话,那么队列的长度可以用in - out
来表示。而且就像我们看到的那样,in
只会在__kfifo_put
里面修改,而out
也只会在__kfifo_get
里面修改,所以无论是in
或out
都只会有一个线程修改,所以不会有互斥的问题。
那是不是这样就线程安全了呢?并不是。
还记得之前忽略掉的那些memory barrier吗?如果没有了那些barrier的话,代码仍然是不安全的。因为在多线程里面,我们不单只需要确保原子性,还需要保证不会有乱序(可见性)。而在没有锁或者memory barrier的情况下,没有办法保证在所有CPU上都不会出现乱序。而上面代码里面的memory barrier就是为了确保不出现乱序而加入的。
简单介绍一下这几个memory barrier的作用:
smp_rmb
保证读操作之间不会出现乱序smp_wmb
保证写操作之间不会出现乱序smp_mb
保证读写操作都不会出现乱序
接着我们可以把kfifo里面对in
、out
和buffer
的读写操作归类一下,那么__kfifo_put
的是下面这样:
- R(in), R(out)
- R(in), W(buffer)
- W(in)
而__kfifo_get
则是下面这样:
- R(in), R(out)
- R(out), R(buffer)
- W(out)
我们先来看__kfifo_put
,有几个内存操作是不可以出现乱序的: 1. R(out)和W(buffer):因为我们需要知道out
的最新值,否则可能出现明明有队列有空间,但是我们仍写不进去数据的情况。这里因为是要保证读写操作之间的顺序,所以需要用smp_mb
。实际上在x86/64平台,连这个barrier也可以忽略,因为在x86上面,读后写是保证不会乱序的,不过Linux内核由于需要保证各个平台都能work,所以仍然需要这里加上。 2. W(buffer)和W(in):这个顺序是必须要保证的,否则可能我们更新了in
之后,这个时候buffer的内容其实并没有copy进去,但是这时候来了一个__kfifo_get
,就把内容拷贝出去了,这个是不允许的。所以这里我们需要用smp_wmb
。
我们可以用下面这个图来表示kfifo
在put的时候的状态:
类似的,__kfifo_get
也有几个内存操作不可以乱序:
- R(in)和R(buffer):我们需要获取最新的
in
值,否则可能会出现明明队列有内容,但是我们却读不到。这里需要用smp_rmb
。 - R(buffer)和W(out):这个顺序也是必须保证的,因为如果我们在读buffer之前就更新的out的话,则可能出现正要读buffer之前,该内容已经被
__kfifo_put
覆盖了,则读出来并不是我们想要的内容。这里需要用smp_mb
。
kfifo
在get的时候的状态可以用下面的图来表示:
所以有了上面kfifo的实现,也就有了一个非常高效的1P1C队列。当然如果是在其他的多线程场景,我们仍然需要用spinlock来保护kfifo
。
性能比较
我建了一个repo(kfifo-benchmark)来简单地比较了一下kfifo的性能。 我把kfifo port到了user space,同时简单地把spinlock_t
替换成了pthread_mutex_t
(pthread_spinlock_t
默认并不在pthread,需要另外配置)。
比较里面的三个case(可以自行到main.cc里面去看)及性能如下(我用的是real time/wall time,所以时间越短表示越快):
- 使用
__kfifo_put
和__kfifo_get
的1P1C(无锁):0m3.496s - 使用
kfifo_put
和kfifo_get
的1P1C场景(mutex):0m13.291s - 使用tpool里面的
BoundedBlockingQueue
默认特化的1P1C场景(mutex+condition variable):0m17.791s
可以看出来,在1P1C场景下,kfifo的无锁版比加锁版本要快3.8x。而就算是kfifo的加锁版本,也比tpool中的BoundedBlockingQueue
要快33%。
本作品由airekans创作,采用知识共享署名-相同方式共享 4.0 国际许可协议进行许可。
Linux内核中的队列 kfifo【转】的更多相关文章
- Linux 内核中的 Device Mapper 机制
本文结合具体代码对 Linux 内核中的 device mapper 映射机制进行了介绍.Device mapper 是 Linux 2.6 内核中提供的一种从逻辑设备到物理设备的映射框架机制,在该机 ...
- 向linux内核中添加外部中断驱动模块
本文主要介绍外部中断驱动模块的编写,包括:1.linux模块的框架及混杂设备的注册.卸载.操作函数集.2.中断的申请及释放.3.等待队列的使用.4.工作队列的使用.5.定时器的使用.6.向linux内 ...
- Linux内核中影响tcp三次握手的一些协议配置
在Linux的发行版本中,都存在一个/proc/目录,有的也称它为Proc文件系统.在 /proc 虚拟文件系统中存在一些可调节的内核参数.这个文件系统中的每个文件都表示一个或多个参数,它们可以通过 ...
- Linux内核中流量控制
linux内核中提供了流量控制的相关处理功能,相关代码在net/sched目录下:而应用层上的控制是通过iproute2软件包中的tc来实现, tc和sched的关系就好象iptables和netfi ...
- Linux内核中的list用法和实现分析
这些天在思考知识体系的完整性,发现总是对消息队列的实现不满意,索性看看内核里面的链表实现形式,这篇文章就当做是学习的i笔记吧.. 内核代码中有很多的地方使用了list,而这个list的用法又跟我们平时 ...
- Linux内核中SPI总线驱动分析
本文主要有两个大的模块:一个是SPI总线驱动的分析 (研究了具体实现的过程): 另一个是SPI总线驱动的编写(不用研究具体的实现过程). 1 SPI概述 SPI是英语Serial Peripheral ...
- Linux内核中常用的数据结构和算法(转)
知乎链接:https://zhuanlan.zhihu.com/p/58087261 Linux内核代码中广泛使用了数据结构和算法,其中最常用的两个是链表和红黑树. 链表 Linux内核代码大量使用了 ...
- [转] Linux 内核中的 Device Mapper 机制
本文结合具体代码对 Linux 内核中的 device mapper 映射机制进行了介绍.Device mapper 是 Linux 2.6 内核中提供的一种从逻辑设备到物理设备的映射框架机制,在该机 ...
- Linux内核中的软中断、tasklet和工作队列具体解释
[TOC] 本文基于Linux2.6.32内核版本号. 引言 软中断.tasklet和工作队列并非Linux内核中一直存在的机制,而是由更早版本号的内核中的"下半部"(bottom ...
随机推荐
- 【Linux笔记】阿里云服务器被暴力破解
一.关于暴力破解 前几天新购进了一台阿里云服务器,使用过程中时常会收到“主机被暴力破解”的警告,警告信息如下: 云盾用户您好!您的主机:... 正在被暴力破解,系统已自动启动破解保护.详情请登录htt ...
- 【移动端debug-1】css3中box-shadow的溢出问题
今天做项目遇到一个box-shadow的溢出父容器的问题,如下面的代码中,子容器inner的box-shadow在没有任何设置的情况下是溢出父容器的. 代码: <!DOCTYPE html> ...
- 【JQuery】使用JQuery 合并两个 json 对象
一,保存object1和2合并后产生新对象,若2中有与1相同的key,默认2将会覆盖1的值 1 var object = $.extend({}, object1, object2); 二,将2的值合 ...
- HDU 3579——Hello Kiki
好久没写什么数论,同余之类的东西了. 昨天第一次用了剩余定理解题,今天上百度搜了一下hdu中国剩余定理.于是就发现了这个题目. 题目的意思很简单.就是告诉你n个m[i],和n个a[i].表示一个数对m ...
- noip模拟题《戏》game
[问题背景] zhx 和他的妹子(们) 做游戏.[问题描述] 考虑 N 个人玩一个游戏,任意两个人之间进行一场游戏(共 N*(N-1)/2 场),且每场一定能分出胜负. ...
- 【Java并发编程】之三:线程挂起、恢复与终止的正确方法
挂起和恢复线程 Thread 的API中包含两个被淘汰的方法,它们用于临时挂起和重启某个线程,这些方法已经被淘汰,因为它们是不安全的,不稳定的.如果在不合适的时候挂起线程(比如,锁定共享资源时), ...
- 【loj2064】找相同字符
Portal --> loj2064 Solution 这里是用后缀数组做的版本!(晚点再用Sam写一遍qwq) 首先一个字符串的子串其实就是这个字符串某个后缀的前缀,所以我们有一个十分简单 ...
- Oracle 解决【ORA-01704:字符串文字太长】(转)
错误提示:oracle在toad中执行一段sql语句时,出现错误‘ORA-01704:字符串文字太长’.如下图: 原因:一般为包含有对CLOB字段的数据操作.如果CLOB字段的内容非常大的时候,会导致 ...
- Codeforces 939.E Maximize!
E. Maximize! time limit per test 3 seconds memory limit per test 256 megabytes input standard input ...
- Google Cast和ChromeCast
Google Cast类似于DLNA,AirPlayer,Miracast,就是一种投屏技术.我们ATV产品是对Google Cast和ChromeCast都是支持的. Google Cast 大致工 ...