Linux内核数据结构之kfifo详解
本文分析的原代码版本: 2.6.24.4
kfifo的定义文件: kernel/kfifo.c
kfifo的头文件: include/linux/kfifo.h
kfifo是内核里面的一个First In First Out数据结构,它采用环形循环队列的数据结构来实现,提供一个无边界的字节流服务,并且使用并行无锁编程技术,即当它用于只有一个入队线程和一个出队线程的场情时,两个线程可以并发操作,而不需要任何加锁行为,就可以保证kfifo的线程安全。
下文着重于代码剖析,各部分代码后面有关键点说明,同时可参考注释进行理解:
struct kfifo { unsigned char *buffer; /* the buffer holding the data : 用于存放数据的缓存 */ unsigned int size; /* the size of the allocated buffer : 空间的大小,在初化时将它向上扩展成2的幂,为了高效的进行与操作取余,后面会详解 */ unsigned int in; /* data is added at offset (in % size) : 如果使用不能保证任何时间最多只有一个读线程和写线程,需要使用该lock实施同步*/ unsigned int out; /* data is extracted from off. (out % size) :一起构成一个循环队列。 in指向buffer中队头,而且out指向buffer中的队尾 */ spinlock_t *lock; /* protects concurrent modifications : 用于put和get过程中加锁防止并发*/ };
以上是kfifo的数据结构,kfifo主要提供了如下操作:
//根据给定buffer创建一个kfifo struct kfifo *kfifo_init(unsigned char *buffer, unsigned int size, gfp_t gfp_mask, spinlock_t *lock);
//给定size分配buffer和kfifo struct kfifo *kfifo_alloc(unsigned int size, gfp_t gfp_mask, spinlock_t *lock); //释放kfifo空间 void kfifo_free(struct kfifo *fifo);
//向kfifo中添加数据 unsigned int kfifo_put(struct kfifo *fifo, const unsigned char *buffer, unsigned int len); //从kfifo中取数据 unsigned int kfifo_get(struct kfifo *fifo, unsigned char *buffer, unsigned int len);
//获取kfifo中有数据的buffer大小 unsigned int kfifo_len(struct kfifo *fifo);
(1)初始化部分:
/* 创建队列 */
struct kfifo *kfifo_init(unsigned char *buffer, unsigned int size, gfp_t gfp_mask, spinlock_t *lock) { struct kfifo *fifo; /* size must be a power of 2 :判断是否为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; }
/* 分配空间 */
struct kfifo *kfifo_alloc(unsigned int size, gfp_t gfp_mask, spinlock_t *lock) { unsigned char *buffer; struct kfifo *ret; if (!is_power_of_2(size)) { /* 判断是否为2的幂 */ BUG_ON(size > 0x80000000); size = roundup_pow_of_two(size); /* 如果不是则向上扩展成2的幂 */ } 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; }
巧妙点①:保证buffer size为2的幂
通常循环队列入队和出队操作要不断的对size 进行求余,一般采用 mInOffset % size(其他类似) 的方法,但是乘、除和求余等会执行多次加法器运算,它们没有单纯的加法运算效率高,更没有位运算效率高。
所以kfifo->size的值总是在调用者传进来的size参数的基础上向2的幂扩展,目的是使 kfifo->size 取模运算可以转化为与运算(提高运行效率):kfifo->in % kfifo->size 可以转化为 kfifo->in & (kfifo->size – 1);
例如 size 为 16,求 3 % size 通常的做法是 3 % size = 3 , 现在变为 3 & (size - 1) = 3 size - 1 = 16 - 1 = 10000 - 1 = 01111 , 3 = 00011 , 3 & (size - 1) = 00011 & 01111 = 00011 = 3.
再看其他几个例子 :
5 & (size - 1) = 00101 & 01111 = 00101 = 5
8 & (size - 1) = 01000 & 01111 = 01000 = 8
15 & (size -1) = 01111 & 01111 = 01111 = 15
16 & (size - 1) = 10000 & 01111 = 00000 = 0
26 & (size - 1 ) = 11010 & 01111 = 01010 = 10 (26 % 16 = 10)
所以保证size是2的幂的前提下可以通过位运算的方式求余,在频繁操作队列的情况下可以大大提高效率。
(2)入队、出队操作:
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; } 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; } 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); /* * Ensure that we sample the fifo->out index -before- we * start putting bytes into the kfifo. */ smp_mb(); // 内存屏障:smp_mb(),smp_rmb(), smp_wmb(),来保证对内存操作顺序 /* 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; } 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; }
巧妙点②:使用spin_lock_irqsave&spin_unlock_irqrestore 实现同步
Linux内核中通常有spin_lock、spin_lock_irq 和 spin_lock_irqsave,分别何时使用呢?
spin_lock的调用关系:
spin_lock | + -----> raw_spin_lock static inline void __raw_spin_lock(raw_spinlock_t *lock) { preempt_disable(); spin_acquire(&lock->dep_map, 0, 0, _RET_IP_); LOCK_CONTENDED(lock, do_raw_spin_trylock, do_raw_spin_lock); }
spin_lock_irq的调用关系:
spin_lock_irq
|
+-------> raw_spin_lock_irq
static inline void __raw_spin_lock_irq(raw_spinlock_t *lock)
{
local_irq_disable();
preempt_disable();
spin_acquire(&lock->dep_map, 0, 0, _RET_IP_);
LOCK_CONTENDED(lock, do_raw_spin_trylock, do_raw_spin_lock);
}
可以看出来他们两者只有一个差别:是否调用local_irq_disable()函数, 即是否禁止本地中断。在任何情况下使用spin_lock_irq都是安全的。因为它既禁止本地中断,又禁止内核抢占。spin_lock比spin_lock_irq速度快,但是它并不是任何情况下都是安全的。
举个例子:
进程A中调用了spin_lock(&lock) 然后进入临界区,此时来了一个中断(interrupt),该中断也运行在和进程A相同的CPU上,并且在该中断处理程序中恰巧也会spin_lock(&lock) 试图获取同一个锁。由于是在同一个CPU上被中断,进程A会被设置为TASK_INTERRUPT状态,中断处理程序无法获得锁,会不停的忙等,由于进程A被设置为中断状态,schedule()进程调度就无法再调度进程A运行,这样就导致了死锁!
但是如果该中断处理程序运行在不同的CPU上就不会触发死锁。 因为在不同的CPU上出现中断不会导致进程A的状态被设为TASK_INTERRUPT,当中断处理程序忙等被换出后,进程A还是有机会获得CPU,执行并退出临界区。所以在使用spin_lock时要明确知道该锁不会在中断处理程序中使用。
而spin_lock_irqsave是基于spin_lock_irq实现的一个便利接口,在于你不期望在离开临界区后,改变中断的开启/关闭状态!进入临界区是关闭的,离开后它同样应该是关闭的!
如果自旋锁在中断处理函数中被用到,那么在获取该锁之前需要关闭本地中断,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。
对于给定的kfifo空间大小: kfifo->size 数据空间长度为:kfifo->in - kfifo->out 而剩余空间长度为:kfifo->size - (kfifo->in - kfifo->out)
以 __kfifo_get为例(__kfifo_get类似):
len = min(len, fifo->size - fifo->in + fifo->out); // 此处用min宏代替if-else判断剩余空间,len赋值为实际要写入的数据大小 ...... /* first put the data starting from fifo->in to buffer end */ l = min(len, fifo->size - (fifo->in & (fifo->size - 1))); // l = 准备写入数据的大小和 当前fifo->in在队列中的游标位置到队尾之间长度的最小值,其中(fifo->in & (fifo->size - 1) 等价于 fifo->in%fifo->size(注:fifo->size为2的幂) memcpy(fifo->buffer + (fifo->in & (fifo->size - 1)), buffer, l); // 如果要写入的数据长度大于当前fifo->in游标到队尾的距离,则先拷贝一部分数据填满至队尾 /* then put the rest (if any) at the beginning of the buffer */ memcpy(fifo->buffer, buffer + l, len - l); // 再从队头写入剩余的数据
巧妙点④:使用内存屏障(Memory Barriers):smp_mb(),smp_rmb(), smp_wmb()
- 内存屏障API函数说明:
mb() 适用于多处理器和单处理器的内存屏障。
rmb() 适用于多处理器和单处理器的读内存屏障。
wmb() 适用于多处理器和单处理器的写内存屏障。
smp_mb() 适用于多处理器的内存屏障。
smp_rmb() 适用于多处理器的读内存屏障。
smp_wmb() 适用于多处理器的写内存屏障。
- Memory barrier 常用场合包括:
1.实现同步原语(synchronization primitives) 2.实现无锁数据结构(lock-free data structures) 3.驱动程序
程序在运行时内存实际的访问顺序和程序代码编写的访问顺序不一定一致,这就是内存乱序访问。内存乱序访问行为出现的理由是为了提升程序运行时的性能。内存乱序访问主要发生在两个阶段:
1.编译时,编译器优化导致内存乱序访问(指令重排) 2.运行时,多CPU间交互引起内存乱序访问
Memory barrier 能够让 CPU 或编译器在内存访问上有序。一个 Memory barrier 之前的内存访问操作必定先于其之后的完成。Memory barrier 包括两类:
1.编译器 barrier 2.CPU Memory barrier
很多时候,编译器和 CPU 引起内存乱序访问不会带来什么问题,但一些特殊情况下,程序逻辑的正确性依赖于内存访问顺序,这时候内存乱序访问会带来逻辑上的错误。
(1)编译时内存乱序访问:在编译时,编译器对代码做出优化时可能改变实际执行指令的顺序(例如 gcc 下 O2 或 O3 都会改变实际执行指令的顺序):
// test.cpp int x, y, r; void f() { x = r; y = 1; }
编译器优化的结果可能导致 y = 1 在 x = r 之前执行完成。
(2)运行时内存乱序访问:在运行时,CPU 虽然会乱序执行指令,但是在单个 CPU 的上,硬件能够保证程序执行时所有的内存访问操作看起来像是按程序代码编写的顺序执行的,这时候 Memory barrier 没有必要使用(不考虑编译器优化的情况下)。CPU执行指令分:取址、译码等等,为了更快的执行指令,CPU采取了流水线的执行方式,编译器在编译代码时为了使指令更适合CPU的流水线执行方式以及多CPU执行,原本的指令就会出现乱序的情况。在乱序执行时,一个处理器真正执行指令的顺序由可用的输入数据决定,而非程序员编写的顺序。
// thread 1 while (!ok); do(x); // thread 2 x = 42; ok = 1;
此段代码中,ok 初始化为 0,线程 1 等待 ok 被设置为 1 后执行 do 函数。假如线程 2 对内存的写操作乱序执行,也就是 x 赋值后于 ok 赋值完成,那么 do 函数接受的实参就很可能出乎程序员的意料,不为 42!
-end-
posted on 2019-03-21 12:29 sheldon_blogs 阅读(10812) 评论(0) 编辑 收藏 举报