巧奪天工的 kfifo


Linux kernel裏面從來就不缺少簡潔,優雅和高效的代碼,只是我們缺少發現和品味的眼光。在Linux kernel裏面,簡潔並不表示代碼使用神出鬼沒的超然技巧,相反,它使用的不過是大家非常熟悉的基礎數據結構,但是kernel開發者能從基礎的數據結構中,提煉出優美的特性。 
kfifo就是這樣的一類優美代碼,它十分簡潔,絕無多餘的一行代碼,卻非常高效。 
關於kfifo信息如下:

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

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

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

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 */
};
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

這是kfifo的數據結構,kfifo主要提供了兩個操作,__kfifo_put(入隊操作)和__kfifo_get(出隊操作)。 它的各個數據成員如下:

buffer: 用於存放數據的緩存

size: buffer空間的大小,在初化時,將它向上擴展成2的冪

lock: 如果使用不能保證任何時間最多隻有一個讀線程和寫線程,需要使用該lock實施同步。

in, out: 和buffer一起構成一個循環隊列。 in指向buffer中隊頭,而且out指向buffer中的隊尾,它的結構如示圖如下:

+--------------------------------------------------------------+
|            |<----------data---------->|                      |
+--------------------------------------------------------------+
             ^                          ^                      ^
             |                          |                      |
            out                        in                     size
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

當然,內核開發者使用了一種更好的技術處理了in, out和buffer的關係,我們將在下面進行詳細分析。

kfifo功能描述

kfifo提供如下對外功能規格

  1. 只支持一個讀者和一個讀者併發操作
  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;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25

這裏值得一提的是,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的特點發揮到了極致。

__kfifo_put和__kfifo_get巧妙的入隊和出隊

__kfifo_put是入隊操作,它先將數據放入buffer裏面,最後才修改in參數;__kfifo_get是出隊操作,它先將數據從buffer中移走,最後才修改out。你會發現in和out兩者各司其職。

下面是__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;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33

奇怪嗎?代碼完全是線性結構,沒有任何if-else分支來判斷是否有足夠的空間存放數據。內核在這裏的代碼非常簡潔,沒有一行多餘的代碼。

l = min(len, fifo->size - (fifo->in & (fifo->size - 1)));

這個表達式計算當前寫入的空間,換成人可理解的語言就是:

l = kfifo可寫空間和預期寫入空間的最小值

使用min宏來代if-else分支

__kfifo_get也應用了同樣技巧,代碼如下:

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;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32

認真讀兩遍吧,我也讀了多次,每次總是有新發現,因爲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->in - kfifo->out <= kfifo->size

即使kfifo->in迴繞到了0的那一端,這個性質仍然是保持的。

對於給定的kfifo:

數據空間長度爲:kfifo->in - kfifo->out

而剩餘空間(可寫入空間)長度爲:kfifo->size - (kfifo->in - kfifo->out)

儘管kfifo->in和kfofo->out一直超過kfifo->size進行增長,但它對應在kfifo->buffer空間的下標卻是如下:

kfifo->in % kfifo->size (i.e. kfifo->in & (kfifo->size - 1))

kfifo->out % kfifo->size (i.e. kfifo->out & (kfifo->size - 1))

往kfifo裏面寫一塊數據時,數據空間、寫入空間和kfifo->size的關係如果滿足:

kfifo->in % size + len > size

那就要做寫拆分了,見下圖:

                                                    kfifo_put(寫)空間開始地址
                                                    |
                                                   \_/
                                                    |XXXXXXXXXX
XXXXXXXX|                                                    
+--------------------------------------------------------------+
|                        |<----------data---------->|          |
+--------------------------------------------------------------+
                         ^                          ^          ^
                         |                          |          |
                       out%size                   in%size     size
        ^
        |
      寫空間結束地址                      
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14

第一塊當然是: [kfifo->in % kfifo->size, kfifo->size] 
第二塊當然是:[0, len - (kfifo->size - kfifo->in % kfifo->size)]

下面是代碼,細細體味吧:

/* 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);  
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

對於kfifo_get過程,也是類似的,請各位自行分析。

kfifo_get和kfifo_put無鎖併發操作

計算機科學家已經證明,當只有一個讀經程和一個寫線程併發操作時,不需要任何額外的鎖,就可以確保是線程安全的,也即kfifo使用了無鎖編程技術,以提高kernel的併發。

kfifo使用in和out兩個指針來描述寫入和讀取遊標,對於寫入操作,只更新in指針,而讀取操作,只更新out指針,可謂井水不犯河水,示意圖如下:

                                               |<--寫入-->|
+--------------------------------------------------------------+
|                        |<----------data----->|               |
+--------------------------------------------------------------+
                         |<--讀取-->|
                         ^                     ^               ^
                         |                     |               |
                        out                   in              size
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

爲了避免讀者看到寫者預計寫入,但實際沒有寫入數據的空間,寫者必須保證以下的寫入順序:

  1. 往[kfifo->in, kfifo->in + len]空間寫入數據
  2. 更新kfifo->in指針爲 kfifo->in + len

在操作1完成時,讀者是還沒有看到寫入的信息的,因爲kfifo->in沒有變化,認爲讀者還沒有開始寫操作,只有更新kfifo->in之後,讀者才能看到。

那麼如何保證1必須在2之前完成,祕密就是使用內存屏障:smp_mb(),smp_rmb(), smp_wmb(),來保證對方觀察到的內存操作順序。

總結

    讀完kfifo代碼,令我想起那首詩“衆裏尋他千百度,默然回首,那人正在燈火闌珊處”。不知你是否和我一樣,總想追求簡潔,高質量和可讀性的代碼,當用盡各種方法,江郞才盡之時,才發現Linux kernel裏面的代碼就是我們尋找和學習的對象。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章