linux内核无锁缓冲队列kfifo原理

Linux kernel里面从来就不缺少简洁,优雅和高效的代码

比如,通过限定写入的数据不能溢出和内存屏障实现在单线程写单线程读的情况下不使用锁。因为锁是使用在共享资源可能存在冲突的情况下。还用设置buffer缓冲区的大小为2的幂次方,以简化求模运算,这样求模运算就演变为 (fifo->in & (fifo->size - 1))。通过使用unsigned int为kfifo的下标,可以不用考虑每次下标超过size时对下表进行取模运算赋值,这里使用到了无符号整数的溢出回零的特性。由于指示读写指针的下标一直在增加,没有进行取模运算,知道其溢出,在这种情况下写满和读完就是不一样的标志,写满是两者指针之差为fifo->size,读完的标志是两者指针相等。后面有一篇博客还介绍了VxWorks下的环形缓冲区的实现机制点击打开链接,从而可以看出linux下的fifo的灵巧性和高效性。

kfifo主要有以下特点:

  • 保证缓冲空间的大小为2的次幂,不是的向上取整为2的次幂。
  • 使用无符号整数保存输入(in)和输出(out)的位置,在输入输出时不对in和out的值进行模运算,而让其自然溢出,并能够保证in-out的结果为缓冲区中已存放的数据长度,这也是最能体现kfifo实现技巧的地方;
  • 使用内存屏障(Memory Barrier)技术,实现单消费者和单生产者对kfifo的无锁并发访问,多个消费者、生产者的并发访问还是需要加锁的。

本文主要以下三个部分:

  • 关于2的次幂问题,判断是不是2的次幂以及向上取整为2的次幂
  • Linux内核中kfifo的实现及简要分析
  • 根据kfifo实现的循环缓冲区,并进行一些测试

关于内存屏障的本文不作过多分析,可以参考WikiMemory Barrier。另外,本文所涉及的整数都默认为无符号整数,不再做一一说明。

1. 2的次幂

  • 判断一个数是不是2的次幂
    kfifo要保证其缓存空间的大小为2的次幂,如果不是则向上取整为2的次幂。其对于2的次幂的判断方式也是很巧妙的。如果一个整数n是2的次幂,则二进制模式必然是1000...,而n-1的二进制模式则是0111...,也就是说n和n-1的每个二进制位都不相同,例如:8(1000)和7(0111);n不是2的次幂,则n和n-1的二进制必然有相同的位都为1的情况,例如:7(0111)和6(0110)。这样就可以根据 n & (n-1)的结果来判断整数n是不是2的次幂,实现如下:
/*
    判断n是否是2的幂
    若n为2的次幂,   则 n & (n-1) == 0,也就是n和n-1的各个位都不相同。例如 8(1000)和7(0111)
    若n不是2的次幂, 则 n & (n-1) != 0,也就是n和n-1的各个位肯定有相同的,例如7(0111)和6(0110)
*/
static inline bool is_power_of_2(uint32_t n)
{
    return (n != 0 && ((n & (n - 1)) == 0));
}
  • 将数字向上取整为2的次幂
    如果设定的缓冲区大小不是2的次幂,则向上取整为2的次幂,例如:设定为5,则向上取为8。上面提到整数n是2的次幂,则其二进制模式为100...,故如果正数k不是n的次幂,只需找到其最高的有效位1所在的位置(从1开始计数)pos,然后1 << pos即可将k向上取整为2的次幂。实现如下:
static inline uint32_t roundup_power_of_2(uint32_t a)
{
    if (a == 0)
        return 0;

    uint32_t position = 0;
    for (int i = a; i != 0; i >>= 1)
        position++;

    return static_cast<uint32_t>(1 << position);
}

fifo->in & (fifo->size - 1) 再比如这种写法取模,获取已用的大小。这样用逻辑与的方式相较于加减法更有效率
二:
Linux内核中kfifo实现技巧,主要集中在放入数据的put方法和取数据的get方法。代码如下:
 1 unsigned int __kfifo_put(struct kfifo *fifo, unsigned char *buffer, unsigned int len)   
 2 {   
 3     unsigned int l;   
 4   
 5     len = min(len, fifo->size - fifo->in + fifo->out);   
 6   
 7     /*  
 8      * Ensure that we sample the fifo->out index -before- we  
 9      * start putting bytes into the kfifo.  
10      */   
11   
12     smp_mb();   
13   
14     /* first put the data starting from fifo->in to buffer end */   
15     l = min(len, fifo->size - (fifo->in & (fifo->size - 1)));   
16     memcpy(fifo->buffer + (fifo->in & (fifo->size - 1)), buffer, l);   
17   
18     /* then put the rest (if any) at the beginning of the buffer */   
19     memcpy(fifo->buffer, buffer + l, len - l);   
20   
21     /*  
22      * Ensure that we add the bytes to the kfifo -before-  
23      * we update the fifo->in index.  
24      */   
25   
26     smp_wmb();   
27   
28     fifo->in += len;   
29   
30     return len;   
31 }  
32   
33 unsigned int __kfifo_get(struct kfifo *fifo,unsigned char *buffer, unsigned int len)   
34 {   
35     unsigned int l;   
36   
37     len = min(len, fifo->in - fifo->out);   
38   
39     /*  
40      * Ensure that we sample the fifo->in index -before- we  
41      * start removing bytes from the kfifo.  
42      */   
43   
44     smp_rmb();   
45   
46     /* first get the data from fifo->out until the end of the buffer */   
47     l = min(len, fifo->size - (fifo->out & (fifo->size - 1)));   
48     memcpy(buffer, fifo->buffer + (fifo->out & (fifo->size - 1)), l);   
49   
50     /* then get the rest (if any) from the beginning of the buffer */   
51     memcpy(buffer + l, fifo->buffer, len - l);   
52   
53     /*  
54      * Ensure that we remove the bytes from the kfifo -before-  
55      * we update the fifo->out index.  
56      */   
57   
58     smp_mb();   
59   
60     fifo->out += len;   
61   
62     return len;   
63 }   

put返回实际保存到缓冲区中的数据长度,get返回的是实际取到的数据长度。在上面代码中,需要注意到在写入、取出时候的两次min运算。关于kfifo的分析,已有很多资料了,也可参考眉目传情之匠心独运的kfifo 。

Linux内核实现的kfifo的有以下特点:

  • 使用内存屏障 Memory Barrier
  • 初始化缓冲区空间时要保证缓冲区的大小为2的次幂
  • 使用无符号整数保存in和out(输入输出的指针),并且在放入取出数据的时候不做模运算,让其自然溢出。

优点:

  1. 实现单消费者和单生产者的无锁并发访问。多消费者和多生产者的时候还是需要加锁的。
  2. 使用与运算in & (size-1)代替模运算
  3. 在更新in或者out的值时不做模运算,而是让其自动溢出。这应该是kfifo实现最牛叉的地方了,利用溢出后的值参与运算,并且能够保证结果的正确。溢出运算保证了以下几点:

    • in - out为缓冲区中的数据长度
    • size - in + out 为缓冲区中空闲空间
    • in == out时缓冲区为空
    • size == (in - out)时缓冲区满了

讲解最详细的一篇https://blog.csdn.net/linyt/article/details/53355355
另外一个https://www.cnblogs.com/wangguchangqing/p/6070286.html
原文地址:https://www.cnblogs.com/wangshaowei/p/11559522.html