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提供如下對外功能規格
- 只支持一個讀者和一個讀者併發操作
- 無阻塞的讀寫操作,如果空間不夠,則返回實際訪問空間
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
爲了避免讀者看到寫者預計寫入,但實際沒有寫入數據的空間,寫者必須保證以下的寫入順序:
- 往[kfifo->in, kfifo->in + len]空間寫入數據
- 更新kfifo->in指針爲 kfifo->in + len
在操作1完成時,讀者是還沒有看到寫入的信息的,因爲kfifo->in沒有變化,認爲讀者還沒有開始寫操作,只有更新kfifo->in之後,讀者才能看到。
那麼如何保證1必須在2之前完成,祕密就是使用內存屏障:smp_mb(),smp_rmb(), smp_wmb(),來保證對方觀察到的內存操作順序。
總結
讀完kfifo代碼,令我想起那首詩“衆裏尋他千百度,默然回首,那人正在燈火闌珊處”。不知你是否和我一樣,總想追求簡潔,高質量和可讀性的代碼,當用盡各種方法,江郞才盡之時,才發現Linux kernel裏面的代碼就是我們尋找和學習的對象。