`
aigo
  • 浏览: 2568903 次
  • 性别: Icon_minigender_1
  • 来自: 宜昌
社区版块
存档分类
最新评论

巧夺天工的kfifo:Linux Kernel中的无锁环形缓冲讲解

阅读更多

原文:CSDN博主-海枫

http://blog.csdn.net/linyt/article/details/5764312

 

Linux kernel里面从来就不缺少简洁,优雅和高效的代码,只是我们缺少发现和品味的眼光。在Linux kernel里面,简洁并不表示代码使用神出鬼没的超然技巧,相反,它使用的不过是大家非常熟悉的基础数据结构,但是kernel开发者能从基础的数据结构中,提炼出优美的特性。

kfifo就是这样的一类优美代码,它十分简洁,绝无多余的一行代码,却非常高效。关于kfifo信息如下:

本文分析的原代码版本:2.6.24.4

kfifo的定义文件:kernel/kfifo.c

kfifo的头文件:  include/linux/kfifo.h

1. kfifo概述

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

kfifo代码既然肩负着这么多特性,那我们先一敝它的代码:

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 */   
};  

  

这是kfifo的数据结构,kfifo主要提供了两个操作,__kfifo_put(入队操作)和__kfifo_get(出队操作)。 它的各个数据成员如下:

buffer, 用于存放数据的缓存

size,      buffer空间的大小,在初化时,将它向上扩展成2的幂

lock,      如果使用不能保证任何时间最多只有一个读线程和写线程,需要使用该lock实施同步。

in, out,  和buffer一起构成一个循环队列。 in指向buffer中队头,而且out指向buffer中的队尾,它的结构如示图如下:

+--------------------------------------------------------------+ 
|           |<----------data---------->|                                    | 
+--------------------------------------------------------------+ 
            ^                                       ^  
             |                                        | 
            in                                       out

当然,内核开发者使用了一种更好的技术处理了in, out和buffer的关系,我们将在下面进行详细的分析。

2. kfifo_alloc 分配kfifo内存和初始化工作

struct kfifo *kfifo_alloc(unsigned int size, gfp_t gfp_mask, spinlock_t *lock)   
{   
    unsigned char *buffer;   
    struct kfifo *ret;   
  
    /*  
     * round up to the next power of 2, since our 'let the indices  
     * wrap' tachnique works only in this case.  
     */   
    if (size & (size - 1)) {   
        BUG_ON(size > 0x80000000);   
        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;   
}   

 

这里值得一提的是,kfifo->size的值总是在调用者传进来的size参数的基础上向2的幂扩展,这是内核一贯的做法。这样的好处不言而喻--对kfifo->size取模运算可以转化为与运算,如下:

kfifo->in % kfifo->size 可以转化为 kfifo->in & (kfifo->size – 1)

在kfifo_alloc函数中,使用size & (size – 1)来判断size 是否为2幂,如果条件为真,则表示size不是2的幂,然后调用roundup_pow_of_two将之向上扩展为2的幂。 这些都是很常用的技巧,只不过大家没有将它们结合起来使用而已,下面要分析的__kfifo_put和__kfifo_get则是将kfifo->size的特点发挥到了极致。

 

3. __kfifo_put和__kfifo_get,巧妙的入队和出队操作,无锁并发

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

下面是__kfifo_put和__kfifo_get的代码

unsigned int __kfifo_put(struct kfifo *fifo,   
             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;   
}  
  
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;   
}   

 认真读两遍吧,我也读了多次,每次总是有新发现,因为in, out和size的关系太巧妙了,竟然能利用上unsigned int回绕的特性。

原来,kfifo每次入队或出队,kfifo->in或kfifo->out只是简单地kfifo->in/kfifo->out += len,并没有对kfifo->size 进行取模运算。因此kfifo->in和kfifo->out总是一直增大,直到unsigned in最大值时,又会绕回到0这一起始端。但始终满足kfifo->out < kfifo->in,除非kfifo->in回绕到了0的那一端,即使如此,代码中计算长度的性质仍然是保持的。

我们先用简单的例子来形象说明这些性质吧:

+----------------------------------------+ 
|                                   |<—data--->|  | 
+----------------------------------------+ 
                                    ^                 ^ 
                                    |                   | 
                                    out               in

 

上图的out和in为kfifo->buffer的出队数据和入队数据的一下,方框为buffer的内存区域。当有数据入队时,那么in的值可能超过kfifo->size的值,那么我们使用另一个虚拟的方框来表示in变化后,在buffer内对kfifo->size取模的值。如下图如标:

 

     真实的buffer内存                                     虚拟的buffer内存,方便查看in对kfifo->size取模后在buffer的下标 
+----------------------------------------+ +------------------------------------+ 
|                                   |<—data-------|  |--------->|                                    | 
+----------------------------------------+ +------------------------------------+ 
                                    ^                                       ^ 
                                    |                                        | 
                                    out                                    in

当用户调用__kfifo_put函数,入队的数据使kfifo的内存关系,引起上述两图的变化时,要拷贝两次内存。

因为入队数据,一部存放在kfifo->buffer的尾部,另一部分存放在kfifo->buffer的头部,计算公式非常简单。

l = kfifo->size – kfifo->in & (kfifo->size – 1) 表示in下标到buffer末尾,还有多少空间。

如果len表示需要拷贝的长度的话,那么len - l则表示有多少字节需要拷贝到buffer开始之处。

这样,我们读__kfifo_put代码就很容易了。

len = min(len, fifo->size - fifo->in + fifo->out);

fifo->in – fifo->out表示队列里面已使用的空间大小,fifo->size - (fifo->in – fifo->out)表示队列未使用的空间,

因此en = min(…),取两者之小,表示实际要拷贝的字节数。

拷贝len个字符数,fifo->in到buffer末尾所剩的空间是多少,这里面计算:

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);

l表示len或fifo->in到buffer末尾所剩的空进行间大小的最小值,因为需要拷l字节到fifo->buffer + fifo->in的位置上;那么剩下要拷贝到buffer开始之处的长度为len – l,当然,此值可能会为0,为0 时,memcpy函数不进行任何拷贝。

所有的拷贝完成后(可能是一次,也可能是两次memcpy),fifo->in 直接+= len,不需要取模运算。

写到这里,细心的读者会发现,如果fifo->in超过了unsigned int的最大值时,而回绕到0这一端,上述的计算公式还正确吗? 答案是肯定的。

因为fifo->size的大小是2的幂,而unsigned int空间的大小是2^32,后者刚好是前者的倍数。如果从上述两个图的方式来描述,则表示unsigned int空间的数轴上刚好可以划成一定数量个kfifo->size大小方框,没有长度多余。这样,fifo->in/fifo->out对fifo->size取模后,刚好落后对应的位置上。

现在假设往kfifo加入数据后,使用fifo->in < fifo->out关系,如下:

+----------------------------------------+                                   +------------------------------------+ 
|—data—>|                                          |          ……                     |                               |<--data------| 
+----------------------------------------+                                   +------------------------------------+ 
|-------------------------------------------------------------------------------------------------------->| 
0                                                                                                                                         0xffffffff 
              ^                                                                                                                ^ 
              |                                                                                                                  | 
              in                                                                                                                out

 

假设kfifo中数据的长度为ldata,那么fifo->in和fifo->out有这样的关系:fifo->in = fifo->out + ldata,并且fifo->in < fifo->out。这说明fifo->in 回绕到0这一段了,尽管如此,fifo->in和fifo->out的差距还是保持的,没有变化。即fifo->in – fifo->out仍然是ldata, 那么此时的可用空间是fifo->size – ldata = fifo->size - (fifo->in – fifo->out) = fifo->size – fifo->in + fifo->out。

因此无论fifo->in和fifo->out谁大谁小,计算fifo剩余空间大小的公式fifo->size – fifo->in + fifo->out都正确,故可以保证__kfifo_put函数里面的长度计算均是正确的。

__kfifo_get函数使用fifo->in – fifo->out来计算fifo内数据的空间长度,然后再后需要出队的数据,是否需要两次拷贝。其中原理和方法都和__kfifo_put是一样的。

4. 总结

读完kfifo代码,令我想起那首诗“众里寻他千百度,默然回首,那人正在灯火阑珊处”。不知你是否和我一样,总想追求简洁,高质量和可读性的代码,当用尽各种方法,江郞才尽之时,才发现Linux kernel里面的代码就是我们寻找和学习的对象。

 

分享到:
评论

相关推荐

    Linux内核中的无锁队列 - kfifo

    在Linux内核中,`kfifo`(Kernel FIFO)是一种高效的无锁队列数据结构,它被设计为简单、优雅且性能卓越。`kfifo`的核心优势在于其在特定场景下能够避免锁的使用,从而提高系统的整体性能。在只有一个读线程和一个写...

    Linux设备驱动程序学习(3-补)-Linux中的循环缓冲区.pdf

    本文档“Linux设备驱动程序学习(3-补)-Linux中的循环缓冲区.pdf”主要关注Linux内核中的环形缓冲区的使用,特别是kfifo(内核FIFO)接口。Kfifo为内核提供了环形缓冲区的实现,其提供了一种标准的方式去创建和管理...

    linux下移植到应用层的 kfifo

    在Linux系统中,kfifo(Kernel FIFO)是一种高效的数据结构,用于实现内核中的先进先出(FIFO)队列。然而,有时我们可能需要在应用层也使用类似的高效FIFO队列,以便在用户空间实现数据的快速入队和出队。本篇文章...

    kfifo-master.zip_kfifo_linux 驱动

    linux kfifo 参考例程,实现单输入单输出无锁高速并发

    stm32-基于linux的kfifo移植到stm32-付详细流程教程.zip

    Linux内核中的kfifo(Kernel FIFO)是一种高效的数据缓冲机制,通常用于设备驱动程序和用户空间进程之间的数据传输。本教程将详细讲解如何将kfifo从Linux环境移植到STM32平台上,实现嵌入式系统中的数据通信。 首先...

    kfifo的demo代码

    **Linux中的Kfifo:环形缓冲区的实现与应用** Kfifo是Linux内核提供的一种环形缓冲区(ring buffer)实现,它被广泛应用于内核模块和驱动程序中,以实现高效的数据传输和同步。环形缓冲区是一种常见的数据结构,...

    linux5.8.1中的kfifo

    在Linux内核版本5.8.1中,`kfifo`(Kernel FIFO)是一个重要的数据结构,被设计为无锁队列,适用于单生产者和单消费者的多线程环境。这个设计模式允许高效的并发访问,避免了在多线程操作中常见的竞态条件和死锁问题...

    fifo.rar_fifo_kfifo_linux队列

    在Linux内核中,`kfifo`主要用于设备驱动程序或者需要进行数据缓存的场合,比如网络协议栈中的数据包缓冲。`kfifo`提供了`__kfifo_put()`和`__kfifo_get()`等函数,用于安全地将数据放入或取出FIFO缓冲区,这些函数...

    STM32F103ZET6+FreeRTOS V8.2.3+kfifo(巧夺天工)+EasyFlash

    STM32F103ZET6+FreeRTOS V8.2.3+kfifo(巧夺天工)+EasyFlash,移植 Linux 的 巧夺天工 的KFIFO 到FreeRTOS 环境, 移植 easyflash 到 FreeRTOS。

    KFIFO_Test.zip

    标题中的"KFIFO_Test.zip"表明这是一个关于KFIFO(Kernel FIFO)测试的项目,而描述提到了使用KFIFO无锁队列在PCIE(Peripheral Component Interconnect Express)环境下,实现FPGA(Field-Programmable Gate Array...

    内核源码KFIFO分析

    在Linux内核中,KFIFO(Kernel FIFO)作为一种高效的环形缓存机制,在多种场景下被广泛应用,特别是网络驱动程序开发中。从2.6.10版本开始,Linux内核就引入了这一通用的环形队列实现,并在后续版本中得到了进一步的...

    atan2查表算法、linux内核kfifo脱离平台实现、crc校验算法、数值序列化等通用库

    该工具库代码符合MISRA-C2004规范,特别适用于资源紧张、无FPU的嵌入式平台,实现效率高,接口定义清晰,注释...4、fifo.h 参考linux kernel的kfifo的实现,无锁读写,效率高 5、math_fast.h 开根号sqrt的快速算法实现

    kfifo-benchmark:基准测试表明kfifo真的超级快

    kfifo,全称为Kernel First-In-First-Out(先进先出)队列,是Linux内核中的一种数据结构,常用于实现线程间的同步和数据传递。在标题提到的“kfifo-benchmark”项目中,开发者对kfifo进行了基准测试,结果证实了...

    基于Linux的kfifo移植到STM32

    kfifo是内核里面的一个First In First Out数据结构,它采用环形循环队列的数据结构来实现;它提供一个无边界的字节流服务,最重要的一点是,它使用并行无锁编程技术,即当它用于只有一个入队线程和一个出队线程的场...

    Linux操作系统课程指导:Ch6 Kernel Data Structures.ppt

    在Linux操作系统中,内核数据结构是实现系统功能的核心组件,它们被用来高效地组织和管理内存中的数据。本课程的第六章“Kernel Data Structures”深入探讨了几个关键的数据结构,包括链表、队列、映射和二叉树。 ...

    AFrameRingBufferDemo.zip

    在给定的"AFrameRingBufferDemo"项目中,开发者使用C语言在Visual Studio 2008环境下创建了一个基于CAN帧的环形缓冲区,模仿了Linux内核中的kfifo机制。kfifo是内核提供的一种线程安全的、双端队列的数据结构,通常...

    linux 驱动开发资料

    4. Linux字符驱动模型:字符设备是Linux中实现简单、顺序访问的设备,如键盘、鼠标等。字符驱动模型是驱动开发中的核心内容之一。 5. Linux设备文件注册:解释如何在Linux系统中注册设备,创建设备文件,以便用户...

    myfs-Makefile.zip

    在Linux操作系统中,KFIFO(Kernel FIFO)是一种用于实现高效并发读写操作的环形缓冲区数据结构。这种数据结构特别适用于多线程环境,它允许多个读写线程并行工作,而无需引入复杂的锁机制,从而提高了系统的并发...

Global site tag (gtag.js) - Google Analytics