深入理解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
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段的位置)