1. 程式人生 > >深入理解linux核心kfifo【轉】

深入理解linux核心kfifo【轉】

 

(轉自:http://blog.chinaunix.net/uid-18770639-id-4203078.html

專案中要用到ringbuffer,一直都是自己造輪子,除錯中才發現經常會出問題,主要是沒有加記憶體屏障。近期自己學習了linux kernel的kfifo,才發現原來核心對於ringbuffer,早就實現了一套,而且程式碼之精妙,一般人很難想到。不同於我們自己造輪子的定長ringbuffer,kfifo提供的一套介面,支援變長的ringbuffer。下面細細分析:

本文分析的原始碼:linux kernel 2.6.34.12    

kfifo.c kfifo.h

1.   Kfifo簡介

Kfifo是核心裡面的一個first in first out資料結構,它採用環形迴圈佇列(ringbuffer)的資料結構來實現;它提供一個無邊界的位元組流服務,最重要的一點是,它使用並行無鎖程式設計技術,即當它用於只有一個入隊執行緒和一個出隊執行緒的場景時,兩個執行緒可以併發操作,而不需要任何加鎖行為,就可以保證kfifo的執行緒安全。

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

 

 

  •     buffer, 用於存放資料的快取

  •     size, buffer空間的大小,必須為2的冪次方

  •     in, out,  和buffer一起構成一個迴圈佇列。 in指向buffer中資料隊頭,out指向buffer中資料的隊尾

這個結構有以下特性:

  •  初始狀態下,in == out == 0

  • 入隊導致in遞增,出隊導致out遞增。注意in和out總是遞增的,從來不會減少

  • 計算in對應的緩衝區索引的公式是fifo->in & (fifo->size-1),見__kfifo_off函式

  •  計算緩衝區中元素數量的公式是fifo->in – fifo->out,見kfifo_size函式

根據以上特性,在執行的時候in和out遲早會溢位。然而即使溢位,kfifo_size函式仍然是對的,這個就是kfifo的精妙之處。
下面我們來驗證下這個結論:

 

預備知識:在32位機器上,假設N為正整數,static_cast(-N) == 2^32 – N。也就是說,-N的補碼的位元位,如果強制解釋成正整數,其數值為2^32-N。

分情況證明:

  • 如果in和out都沒有溢位,或者in和out都溢位了,kfifo_size是對的。

  • 如果in溢位了,out沒有溢位。記in在溢位之前的數值為in_nofl,則in_nofl == 2^32+in。由於fifo.in < fifo.out,fifo.in – fifo.out為負數,因此static_cast(fifo.in-fifo.out) == 2^32 -(fifo.out-fifo.in) == (2^32+fifo.in)-fifo.out == in_nofl – fifo.out。因此kfifo_size是正確的。

  • 不可能in沒有溢位同時out溢位。

3.   kfifo_alloc 分配kfifo記憶體和初始化工作

    這個介面主要是分配kfifo的buffer核心,並初始化kfifo。kfifo->size的值總是在呼叫者傳進來的size引數的基礎上向2的冪擴充套件,這是核心一貫的做法。這樣的好處不言而喻--對kfifo->size取模運算可以轉化為與運算,如下:

kfifo->in % kfifo->size 可以轉化為 kfifo->in & (kfifo->size – 1)

 

4.   kfifo_in

kfifo in是入隊操作,其一共3個步驟:

1)     計算fifo中可寫的長度

2)     將資料放入buffer裡面

3)     修改in引數

上程式碼:

/**
 * kfifo_in - puts some data into the FIFO
 * @fifo: the fifo to be used.
 * @from: the data to be added.
 * @len: the length of the data to be added.
 *
 * This function copies at most @len bytes from the @from buffer into
 * the FIFO depending on the free space, and returns the number of
 * bytes copied.
 *
 * Note that with only one concurrent reader and one concurrent
 * writer, you don't need extra locking to use these functions.
 */
unsigned int kfifo_in(struct kfifo *fifo, const void *from,
                unsigned int len)
{
    len = min(kfifo_avail(fifo), len);

    __kfifo_in_data(fifo, from, len, 0);
    __kfifo_add_in(fifo, len);
    return len;
}

static inline void __kfifo_in_data(struct kfifo *fifo,
        const void *from, unsigned int len, unsigned int off)
{
    unsigned int l;

    /*
     * Ensure that we sample the fifo->out index -before- we
     * start putting bytes into the kfifo.
     */

    smp_mb();

    off = __kfifo_off(fifo, fifo->in + off);

    /* first put the data starting from fifo->in to buffer end */
    l = min(len, fifo->size - off);
    memcpy(fifo->buffer + off, from, l);

    /* then put the rest (if any) at the beginning of the buffer */
    memcpy(fifo->buffer, from + l, len - l);
}

/*
 * __kfifo_add_in internal helper function for updating the in offset
 */
static inline void __kfifo_add_in(struct kfifo *fifo,
                unsigned int off)
{
    smp_wmb();
    fifo->in += off;
}

從__kfifo_in_data函式中,可以明顯看到,如果fifo->in小於buffer->size,那麼先放完buffer->size-fifo->in這段 記憶體空間,剩下的部分,轉移到buffer的可用空間開頭存放;如果fifo->in大於buffer->size,那麼直接把要入佇列的數 據放到buffer可用空間開頭。

 

5.   kfifo_out

kfifo out是出隊操作,其一共3個步驟:

1)     計算fifo中可讀的長度

2)     將資料從buffer中移走

3)     修改out引數

/**
 * kfifo_out - gets some data from the FIFO
 * @fifo: the fifo to be used.
 * @to: where the data must be copied.
 * @len: the size of the destination buffer.
 *
 * This function copies at most @len bytes from the FIFO into the
 * @to buffer and returns the number of copied bytes.
 *
 * Note that with only one concurrent reader and one concurrent
 * writer, you don't need extra locking to use these functions.
 */
unsigned int kfifo_out(struct kfifo *fifo, void *to, unsigned int len)
{
    len = min(kfifo_len(fifo), len);

    __kfifo_out_data(fifo, to, len, 0);
    __kfifo_add_out(fifo, len);

    return len;
}

static inline void __kfifo_out_data(struct kfifo *fifo,
        void *to, unsigned int len, unsigned int off)
{
    unsigned int l;

    /*
     * Ensure that we sample the fifo->in index -before- we
     * start removing bytes from the kfifo.
     */

    smp_rmb();

    off = __kfifo_off(fifo, fifo->out + off);

    /* first get the data from fifo->out until the end of the buffer */
    l = min(len, fifo->size - off);
    memcpy(to, fifo->buffer + off, l);

    /* then get the rest (if any) from the beginning of the buffer */
    memcpy(to + l, fifo->buffer, len - l);
}

/*
 * __kfifo_add_out internal helper function for updating the out offset
 */
static inline void __kfifo_add_out(struct kfifo *fifo,
                unsigned int off)
{
    smp_mb();
    fifo->out += off;
}

6.   場景分析

in和out均沒有超出fifo->size,這個很好理解。下面我們主要分析兩種溢位的場景:

1)     fifo->in大於fifo->size而fifo->out小於fifo->size(即只有 fifo->in“越界”)

這種情況下,先讀取fifo->out到fifo->size-1的那一段,大小為L個byte,然後再讀取剩下的。從buffer開頭,大小為len-L個byte的資料(即先讀data A段, 再讀出data B段) 

 

2)     fifo->in和fifo->out都“越界”了

這種情況下,L = min(len, fifo->size - (fifo->out & (fifo->size - 1))); 這一語句便起作用了,此時fifo->out&fifo->size-1的結果即實際要讀的資料所在的記憶體地址相對於buffer起始地址的偏移值(如下圖所示,左邊為實際上存在於記憶體中的data A段, 而右邊虛線框為邏輯上的data A段的位置)