数据结构之内核队列kfifo

kfifo概述

据wiki介绍,环形缓冲区(ring buffer),也称作圆形队列(circular queue),圆形缓冲区(circular buffer),循环缓冲区(cyclic buffer),是一种用于表示一个固定尺寸、头尾相连的缓冲区的数据结构,适合缓存数据流

圆形缓冲区的一个有用特性是:当一个数据元素被用掉后,其余数据元素不需要移动其存储位置。相反,一个非圆形缓冲区(例如一个普通的队列)在用掉一个数据元素后,其余数据元素需要向前搬移。因此,圆形缓冲区适合实现先进先出缓冲区,而非圆形缓冲区适合后进先出缓冲区。扩展一个圆形缓冲区的容量,需要搬移其中的数据。因此一个缓冲区如果需要经常调整其容量,用链表实现更为合适。

kfifo是内核里面的一个First In First Out数据结构,它采用环形循环队列的数据结构来实现;它提供一个无边界的字节流服务,最重要的一点是,它使用并行无锁编程技术即当它用于只有一个入队线程和一个出队线程的场情时,两个线程可以并发操作,而不需要任何加锁行为,就可以保证kfifo的线程安全。

本文分析的原代码版本:3.5           kfifo的定义文件:kernel/kfifo.c            kfifo的头文件:  include/linux/kfifo.h

struct __kfifo {
	unsigned int	in; // 和buffer一起构成一个循环队列。 in指向buffer中队头,out指向buffer中的队尾
	unsigned int	out;
	unsigned int	mask; // 代表index掩码,FIFO块个数减1,FIFO块个数需要为2的整数次方。
	unsigned int	esize; // 块(element)大小,FIFO按整块入队列和出队列,esize可以为1。
	void		*data; // 数据指针,可动态分配、静态分配、或用户传递数据地址。
};

struct kfifo 的定义,实际上它是由一个宏来定义的,如下。 kfifo 分为两种,record 型和非 record 型。

非 record 型分为静态和指针两种;

  • STRUCT_KFIFO(type, size)
  • STRUCT_KFIFO_PTR(type)

指针类型的buf为buf[0],数据动态分配。静态类型的,buf大小是宏传递的size,size必须为常亮。recsize为0。record 型的 recsize 的值可为1或2。(以下只关注非record型)

#define __STRUCT_KFIFO_COMMON(datatype, recsize, ptrtype) \
	union { \
		struct __kfifo	kfifo; \
		datatype	*type; \
		char		(*rectype)[recsize]; \
		ptrtype		*ptr; \
		const ptrtype	*ptr_const; \
	}
#define __STRUCT_KFIFO(type, size, recsize, ptrtype) \
{ \
	__STRUCT_KFIFO_COMMON(type, recsize, ptrtype); \
	type		buf[((size < 2) || (size & (size - 1))) ? -1 : size]; \
}

kfifo怎么和其它字段是联合呢?其它字段读写岂不是会覆盖kfifo的内容。其实这又是内核的一个技巧,其它字段不会读写数据,只是编译器用来获取相关信息 。比如:

获取recsize:

#define kfifo_recsize(fifo)     (sizeof(*(fifo)->rectype))

再比如通过kfifo_alloc分配buf存储空间时,获取块的大小

__kfifo_alloc(__kfifo, size, sizeof(*__tmp->type), gfp_mask) : \

 上述通过宏定义的struct kfifo结构体展开之后的通用格式如下:

struct
{
    union
    {
        struct __kfifo kfifo;
        datatype        *type;
        const datatype  *const_type;
        char            (*rectype)[recsize];
        ptrtype         *ptr;
        ptrtype const   *ptr_const;  
    };
    datatype buf[size];
}

关于锁定的注意事项:在不会调用kfifo_reset()的情况下,只有一个读者和一个写者使用FIFO时不需要锁定,在读者线程中可以安全地使用kfifo_reset_out()。对于多个写者和一个读者,只需要锁定写者。反之亦然,只有一个写者和多个读者只需要锁定读者。

定义及初始化

DEFINE_KFIFO(fifo, type, size) :

/*
 * helper macro to distinguish between real in place fifo where the fifo
 * array is a part of the structure and the fifo type where the array is
 * outside of the fifo structure.
 */
#define	__is_kfifo_ptr(fifo)	(sizeof(*fifo) == sizeof(struct __kfifo))
/** 
 * DEFINE_KFIFO - 定义并且初始化一个 fifo 结构
 * @fifo: name of the declared fifo datatype
 * @type: type of the fifo elements
 * @size: the number of elements in the fifo, 在初化时,将它向上扩展成2的幂
*/
#define DEFINE_KFIFO(fifo, type, size) \
	DECLARE_KFIFO(fifo, type, size) = \
	(typeof(fifo)) { \
		{ \
			{ \
			.in	= 0, \
			.out	= 0, \
			.mask	= __is_kfifo_ptr(&(fifo)) ? \
				  0 : \
				  ARRAY_SIZE((fifo).buf) - 1, \
			.esize	= sizeof(*(fifo).buf), \
			.data	= __is_kfifo_ptr(&(fifo)) ? \
				NULL : \
				(fifo).buf, \
			} \
		} \
	}
// 0 肯定是异常情况, 则 .mask = ARRAY_SIZE((fifo).buf) – 1

DEFINE_KFIFO 定义并初始化一个fifo定义的时候调用的 DECLARE_KFIFO  ,所以也是静态分配 buf 。

DECLARE_KFIFO(fifo, type, size) :

DECLARE_KFIFO_PTR(fifo, type):

#define __STRUCT_KFIFO_COMMON(datatype, recsize, ptrtype) \
	union { \
		struct __kfifo	kfifo; \
		datatype	*type; \
		char		(*rectype)[recsize]; \
		ptrtype		*ptr; \
		const ptrtype	*ptr_const; \
	}

#define __STRUCT_KFIFO(type, size, recsize, ptrtype) \
{ \
	__STRUCT_KFIFO_COMMON(type, recsize, ptrtype); \
	type		buf[((size < 2) || (size & (size - 1))) ? -1 : size]; \
}

#define STRUCT_KFIFO(type, size) \
	struct __STRUCT_KFIFO(type, size, 0, type)
/**
 * DECLARE_KFIFO - 定义一个 fifo object
 * @fifo: name of the declared fifo
 * @type: type of the fifo elements
 * @size: the number of elements in the fifo, this must be a power of 2
 */
#define DECLARE_KFIFO(fifo, type, size)	STRUCT_KFIFO(type, size) fifo
/* ------------------------ END DECLARE_KFIFO  -------------------------- */
#define __STRUCT_KFIFO_PTR(type, recsize, ptrtype) \
{ \
	__STRUCT_KFIFO_COMMON(type, recsize, ptrtype); \
	type		buf[0]; \
}
struct kfifo __STRUCT_KFIFO_PTR(unsigned char, 0, void); // kfifo以unsigned char作为元素类型 
#define STRUCT_KFIFO_PTR(type) \
	struct __STRUCT_KFIFO_PTR(type, 0, type)

#define DECLARE_KFIFO_PTR(fifo, type)	STRUCT_KFIFO_PTR(type) fifo

DECLARE_KFIFO 该宏用于静态声明一个kfifo对象, fifo 要定义的kfifo的名字, type 元素的类型, size kfifo可容纳的元素个数,必须是2的幂 。这个接口静态分配buf的存储空间,分配了一个大小为size、类型为 type 的数组。

DECLARE_KFIFO_PTR 这个接口与 DECLARE_KFIFO 的不同在于定义的buf数组的大小为0,即type buf[ 0 ],然后配合 kfifo_alloc  动态分配buf的存储空间。

INIT_KFIFO(fifo) :

/**
 * INIT_KFIFO - 初始化一个 DECLARE_KFIFO 声明的 fifo 结构
 * @fifo: fifo结构体变量
 */
#define INIT_KFIFO(fifo) \
(void)({ \
	typeof(&(fifo)) __tmp = &(fifo); \
	struct __kfifo *__kfifo = &__tmp->kfifo; \
	__kfifo->in = 0; \
	__kfifo->out = 0; \
	__kfifo->mask = __is_kfifo_ptr(__tmp) ? 0 : ARRAY_SIZE(__tmp->buf) - 1;\
	__kfifo->esize = sizeof(*__tmp->buf); \
	__kfifo->data = __is_kfifo_ptr(__tmp) ?  NULL : __tmp->buf; \
})

kfifo_init(fifo, buffer, size) : 

int __kfifo_init(struct __kfifo *fifo, void *buffer,
		unsigned int size, size_t esize)
{
	size /= esize;  // size 为 buf 中元素个数,必须为2的幂

	if (!is_power_of_2(size))
		size = rounddown_pow_of_two(size);

	fifo->in = 0;
	fifo->out = 0;
	fifo->esize = esize;
	fifo->data = buffer;

	if (size < 2) {
		fifo->mask = 0;
		return -EINVAL;
	}
	fifo->mask = size - 1;

	return 0;
}

#define kfifo_init(fifo, buffer, size) \
({ \
	typeof((fifo) + 1) __tmp = (fifo); \
	struct __kfifo *__kfifo = &__tmp->kfifo; \
	__is_kfifo_ptr(__tmp) ? \
	__kfifo_init(__kfifo, buffer, size, sizeof(*__tmp->type)) : \
	-EINVAL; \
})

kfifo_init 接口使用一个预分配好的buffer初始化fifo,size 为buffer大小,单位是字节。 

kfifo_alloc

在kfifo_alloc函数中,使用size & (size – 1)来判断size 是否为2幂,如果条件为真,则表示size不是2的幂

#define is_power_of_2(x)	((x) != 0 && (((x) & ((x) - 1)) == 0))

然后调用roundup_pow_of_two将之向上扩展为2的幂 

int __kfifo_alloc(struct __kfifo *fifo, unsigned int size,
		size_t esize, gfp_t gfp_mask)
{
	/*
	 * round down to the next power of 2, since our 'let the indices
	 * wrap' technique works only in this case.
	 */
	if (!is_power_of_2(size))
		size = rounddown_pow_of_two(size);

	fifo->in = 0;
	fifo->out = 0;
	fifo->esize = esize;

	if (size < 2) {
		fifo->data = NULL;
		fifo->mask = 0;
		return -EINVAL;
	}

	fifo->data = kmalloc(size * esize, gfp_mask);

	if (!fifo->data) {
		fifo->mask = 0;
		return -ENOMEM;
	}
	fifo->mask = size - 1;  // 即 ARRAY_SIZE((fifo).buf) – 1

	return 0;
}
/* __kfifo_int_must_check_helper是一个辅助函数, 主要是为了帮助警告用户忘记检查返回值 */
#define kfifo_alloc(fifo, size, gfp_mask) \
__kfifo_int_must_check_helper( \
({ \
	typeof((fifo) + 1) __tmp = (fifo); \
	struct __kfifo *__kfifo = &__tmp->kfifo; \
	__is_kfifo_ptr(__tmp) ? \
	__kfifo_alloc(__kfifo, size, sizeof(*__tmp->type), gfp_mask) : \
	-EINVAL; \
}) \
)

kfifo_alloc动态分配kfifo的buf成员变量的存储空间,调用的kmalloc函数。typeof((fifo) + 1)为什么要加1呢?主要的好处是帮助确定传递的参数类型是否正确, 如果传递的是结构体会产生编译错误,如果传递的是数组名, 如 int fifo[4] ,typeof(fifo)的结果为 int [4] , 而typeof(fifo + 1)的结果为 int *。

巧妙的入队和出队操作,无锁并发

kfifo_put是入队操作,它先将数据放入buffer里面,最后才修改in参数;kfifo_get是出队操作,它先将数据从buffer中移走,最后才修改out。你会发现in和out两者各司其职。计算机科学家已经证明,当只有一个读经程和一个写线程并发操作时,不需要任何额外的锁,就可以确保是线程安全的,也即kfifo使用了无锁编程技术,以提高kernel的并发。

/**
 * kfifo_put - put data into the fifo
 * @fifo: address of the fifo to be used
 * @val: the data to be added
 *
 * This macro copies the given value into the fifo.
 * It returns 0 if the fifo was full. Otherwise it returns the number
 * processed elements.
 *
 * Note that with only one concurrent reader and one concurrent
 * writer, you don't need extra locking to use these macro.
 */
#define	kfifo_put(fifo, val) \
({ \
	typeof((fifo) + 1) __tmp = (fifo); \
	typeof((val) + 1) __val = (val); \
	unsigned int __ret; \
	const size_t __recsize = sizeof(*__tmp->rectype); \
	struct __kfifo *__kfifo = &__tmp->kfifo; \
	if (0) { \
		typeof(__tmp->ptr_const) __dummy __attribute__ ((unused)); \
		__dummy = (typeof(__val))NULL; \
	} \
	if (__recsize) \   //  非record类型,此处为0 \
		__ret = __kfifo_in_r(__kfifo, __val, sizeof(*__val), \
			__recsize); \
	else { \
		__ret = !kfifo_is_full(__tmp); \
		if (__ret) { \  // 队列不满进入这个分支
			(__is_kfifo_ptr(__tmp) ? \  // 正常情况,条件为真
			((typeof(__tmp->type))__kfifo->data) : \  // 强转data指针类型为数据块的类型
			(__tmp->buf) \
			)[__kfifo->in & __tmp->kfifo.mask] = \    // 方括号中内容为计算写指针索引
				*(typeof(__tmp->type))__val; \        // 赋值了
			smp_wmb(); \                              // 考虑到处理器的优化和cpu核的跳转执行情况,添加内存屏障指令,保证了in更新前,数据已写入存储区
			__kfifo->in++; \
		} \
	} \
	__ret; \
})

kfifo要求缓冲区长度必须为2的幂。读、写指针分别是无符号整型变量。把读写指针变换为缓冲区内的索引值,仅需要“按位与”操作:(指针值 按位与 (缓冲区长度-1))。这避免了计算代价高昂的“求余”操作。且下述关系总是成立:

读指针 + 缓冲区存储的数据长度 == 写指针

即使在写指针达到了无符号整型的上界,上溢出后写指针的值小于读指针的值,上述关系仍然保持成立(这是因为无符号整型加法的性质)。 kfifo的写操作,首先计算缓冲区中当前可写入存储空间的数据长度:

len = min{待写入数据长度, 缓冲区长度 - (写指针 - 读指针)}

然后,分两段写入数据。第一段是从写指针开始向缓冲区末尾方向;第二段是从缓冲区起始处写入余下的可写入数据,这部分可能数据长度为0即并无实际数据写入。

实现技巧:

  •  size & (size - 1)  用于检测大小是否为2的幂
  • typeof(*fifo->type)  用于获取数据类型
  •  in−out   表示队列长度 ,始终成立
  • in & mask  读/写指针转化为数组索引
  • typeof((fifo) + 1)  确保传递的是fifo指针,而不是结构体或数组名
  • size - in + out 为缓冲区中空闲空间
  • in == out时缓冲区为空
  • size == (in - out)时缓冲区满了

接口

DECLARE_KFIFO_PTR(fifo, type)定义一个非record FIFO,名字为fifo,element类型为type,其数据需要动态分配。
DECLARE_KFIFO(fifo, type, size)定义一个非record FIFO,名字为fifo,element类型为type,element个数为size,其数据静态存储在结构体中,size需为常数且为2的整数次方
INIT_KFIFO(fifo)初始化DECLARE_KFIFO接口定义的fifo
DEFINE_KFIFO(fifo, type, size)定义并初始化fifo
kfifo_initialized(fifo)fifo是否初始化
kfifo_esize(fifo)返回fifo的esize
kfifo_recsize(fifo)返回fifo的recsize
kfifo_size(fifo)返回fifo的size
kfifo_reset(fifo)将in和out置0,注意:需要上锁。
kfifo_reset_out(fifo)将out设置为in,由于只修改out,因此在读者上下文,且只有一个读者时,是安全的。否则需要上锁。
kfifo_len(fifo)返回fifo中element的个数
kfifo_is_empty(fifo)fifo是否为空 (in == out)
kfifo_is_full(fifo)fifo是否满
kfifo_avail(fifo)非record FIFO,返回可容纳的element个数
record FIFO,返回除去record头能容纳的字节数,最多不超过record头能表示的字节数,如recsize为1,最多返回255。
kfifo_skip(fifo)跳过一个element或record
kfifo_peek_len(fifo)获取下一个element或者record的字节长度。
kfifo_alloc(fifo, size, gfp_mask)为指针式FIFO分配空间并初始化,成功返回0,错误则返回负数错误码
kfifo_free(fifo)释放kfifo_alloc分配的内存
kfifo_init(fifo, buffer, size)使用预分配的缓存初始化fifo,成功返回0,错误则返回负数错误码
kfifo_put(fifo, val)这是一个宏,将val赋值给一个FIFO type类型的临时变量,然后将临时变量入队。存放一个element,如果成功返回入队的elements个数。如果FIFO满,则返回0。
kfifo_get(fifo, val)val是一个指针,内部将val赋值给一个ptr指针类型的临时变量,并拷贝sizeof(*ptr)长度到val的地址。对于record FIFO,其是void类型,sizeof(void)为1,所以拷贝1个字节。非record类型,拷贝一个element。
如果FIFO为空,返回0,否则返回拷贝的element数。
kfifo_peek(fifo, val)和kfifo_get相同,除了不更新out外。
kfifo_in(fifo, but, n)入队n个elemnts。返回工程入队的elements数。
kfifo_in(fifo, buf, n, lock)加锁入队。加锁方式为spin_lock_irqsave
kfifo_out(fifo, buf, n)出队n个elements,返回成功拷贝的elements数
kfifo_out_spinlocked(fifo, buf, n, lock)加锁出队。加锁方式位spin_lock_irqsave
kfifo_from_user(fifo, from, len, copied)最多拷贝len个字节,参考record FIFO和非record FIFO的对应底层接口。
kfifo_to_user(fifo, to, len, copied)最多拷贝len个字节到用户空间,参考record FIFO和非record FIFO的对应底层接口。
kfifo_dma_in_prepare
kfifo_dma_in_finish
kfifo_dma_out_prepare
kfifo_dma_out_finish
DMA接口,略
kfifo_out_peek(fifo, buf, n)peek n个elements的数据,但是内部out不动,返回拷贝的elements个数
  • 2
    点赞
  • 23
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
您可以参考以下代码实现一个简单的Linux kfifo demo: ``` #include <linux/module.h> #include <linux/init.h> #include <linux/fs.h> #include <linux/uaccess.h> #include <linux/slab.h> #include <linux/kfifo.h> #define FIFO_SIZE 1024 MODULE_LICENSE("GPL"); MODULE_AUTHOR("Your Name"); MODULE_DESCRIPTION("A Linux kfifo demo"); static char *fifo_buffer; static struct kfifo my_fifo; static int my_open(struct inode *inode, struct file *file) { printk(KERN_INFO "my device opened\n"); return 0; } static int my_close(struct inode *inode, struct file *file) { printk(KERN_INFO "my device closed\n"); return 0; } static ssize_t my_read(struct file *file, char __user *user_buf, size_t count, loff_t *ppos) { ssize_t ret; if (kfifo_is_empty(&my_fifo)) { return -EAGAIN; } ret = kfifo_to_user(&my_fifo, user_buf, count, ppos); if (ret) { printk(KERN_INFO "Read %ld bytes\n", (long)ret); } else { printk(KERN_ERR "Failed to read\n"); } return ret; } static ssize_t my_write(struct file *file, const char __user *user_buf, size_t count, loff_t *ppos) { ssize_t ret; if (kfifo_avail(&my_fifo) < count) { return -ENOMEM; } ret = kfifo_from_user(&my_fifo, user_buf, count, ppos); if (ret) { printk(KERN_INFO "Wrote %ld bytes\n", (long)ret); } else { printk(KERN_ERR "Failed to write\n"); } return ret; } static struct file_operations my_fops = { .owner = THIS_MODULE, .open = my_open, .release = my_close, .read = my_read, .write = my_write, }; static int __init my_init(void) { int ret; fifo_buffer = kmalloc(FIFO_SIZE, GFP_KERNEL); if (!fifo_buffer) { return -ENOMEM; } ret = kfifo_init(&my_fifo, fifo_buffer, FIFO_SIZE); if (ret) { kfree(fifo_buffer); return ret; } ret = register_chrdev(0, "my_device", &my_fops); if (ret < 0) { kfifo_free(&my_fifo); kfree(fifo_buffer); return ret; } printk(KERN_INFO "my device registered with major number %d\n", ret); return 0; } static void __exit my_exit(void) { unregister_chrdev(0, "my_device"); kfifo_free(&my_fifo); kfree(fifo_buffer); printk(KERN_INFO "my device unregistered\n"); } module_init(my_init); module_exit(my_exit); ``` 该代码实现了一个简单的字符设备,它使用了Linux内核中提供的kfifo数据结构来实现队列的功能,用户可以通过该设备的文件描述符进行读写操作,读取数据会将队列中的数据写入用户缓冲区,写入数据会将用户缓冲区中的数据写入队列中。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值