原文:https://www.linuxidc.com/Linux/2016-12/137936.htm
一、簡介
1、環形隊列是一種特殊的隊列結構,保證了元素也是先進先出的,但與一般隊列的區別是,他們是環形的,即隊列頭部的上個元素是隊列尾部,通常是容納元素數固定的一個閉環。采用環形緩沖區的好處為,當一個數據元素被用掉后,其余數據元素不需要移動其存儲位置,從而減少拷貝提高效率
2、優點:
保證元素的先進先出。
元素空間可以重復利用。
為多線程數據通信提供了一種高效的機制。
3、實現
Linux kernal kfifo實現的單生產/單消費模式的共享隊列是不需要加鎖同步的。
源碼:kernel/kfifo.c
1: struct kfifo {
2: unsigned char *buffer; /* the buffer holding the data ,用於存放數據的緩存*/
3: unsigned int size; /* the size of the allocated buffer,緩沖區空間的大小,在初化時,將它向上圓整成2的冪*/
4: unsigned int in; /* data is added at offset (in % size),指向buffer中隊頭 */
5: unsigned int out; /* data is extracted from off. (out % size),指向buffer中的隊尾 */
6: spinlock_t *lock; /* protects concurrent modifications,如果使用不能保證任何時間最多只有一個讀線程和寫線程,必須使用該lock實施同步。 */
7: };
結構圖:

“取模運算”的效率並沒有 “位運算” 的效率高,可以將kfifo->size取模運算可以轉化為與運算,如下:kfifo->in % kfifo->size 可以轉化為 kfifo->in & (kfifo->size – 1)
tips:判斷n能否被2整除:
bool is_power_of_2(unsigned long n)
{
return (n != 0 && ((n & (n - 1)) == 0));
}
為什么kfifo實現的單生產/單消費模式的共享隊列是不需要加鎖同步的呢?因為上一節中講的內存屏障。
| smp_rmb | 適用於多處理器的讀內存屏障。 |
| smp_wmb | 適用於多處理器的寫內存屏障。 |
| smp_mb | 適用於多處理器的內存屏障 |
二、代碼分析
入隊__kfifo_put和出隊__kfifo_get代碼分析
1、__kfifo_put是入隊操作,它先將數據放入buffer中,然后移動in的位置,其源代碼如下:
1: unsigned int __kfifo_put(struct kfifo *fifo,
2: const unsigned char *buffer, unsigned int len)
3: {
4: unsigned int l;
5:
6: len = min(len, fifo->size - fifo->in + fifo->out);
7:
8: /*
9: * Ensure that we sample the fifo->out index -before- we
10: * start putting bytes into the kfifo.
11: */
12:
13: smp_mb();
14:
15: /* first put the data starting from fifo->in to buffer end */
16: l = min(len, fifo->size - (fifo->in & (fifo->size - 1)));
17: memcpy(fifo->buffer + (fifo->in & (fifo->size - 1)), buffer, l);
18:
19: /* then put the rest (if any) at the beginning of the buffer */
20: memcpy(fifo->buffer, buffer + l, len - l);
21:
22: /*
23: * Ensure that we add the bytes to the kfifo -before-
24: * we update the fifo->in index.
25: */
26:
27: smp_wmb();
28:
29: fifo->in += len;
30:
31: return len;
32: }
分析:
6行,環形緩沖區的剩余容量為fifo->size - fifo->in + fifo->out,讓寫入的長度取len和剩余容量中較小的,避免寫越界;
13行,加內存屏障,保證在開始放入數據之前,fifo->out取到正確的值(另一個CPU可能正在改寫out值)
16行,前面講到fifo->size已經2的次冪圓整,而且kfifo->in % kfifo->size 可以轉化為 kfifo->in & (kfifo->size – 1),所以fifo->size - (fifo->in & (fifo->size - 1)) 即位 fifo->in 到 buffer末尾所剩余的長度,l取len和剩余長度的最小值,即為需要拷貝l 字節到fifo->buffer + fifo->in的位置上。
17行,拷貝l 字節到fifo->buffer + fifo->in的位置上,如果l = len,則已拷貝完成,第20行len – l 為0,將不執行,如果l = fifo->size - (fifo->in & (fifo->size - 1)) ,則第20行還需要把剩下的 len – l 長度拷貝到buffer的頭部。
27行,加寫內存屏障,保證in 加之前,memcpy的字節已經全部寫入buffer,如果不加內存屏障,可能數據還沒寫完,另一個CPU就來讀數據,讀到的緩沖區內的數據不完全,因為讀數據是通過 in – out 來判斷的。
29行,注意這里 只是用了 fifo->in += len而未取模,這就是kfifo的設計精妙之處,這里用到了unsigned int的溢出性質,當in 持續增加到溢出時又會被置為0,這樣就節省了每次in向前增加都要取模的性能,錙銖必較,精益求精,讓人不得不佩服。
2、__kfifo_get是出隊操作,它從buffer中取出數據,然后移動out的位置,其源代碼如下:
1: unsigned int __kfifo_get(struct kfifo *fifo,
2: unsigned char *buffer, unsigned int len)
3: {
4: unsigned int l;
5:
6: len = min(len, fifo->in - fifo->out);
7:
8: /*
9: * Ensure that we sample the fifo->in index -before- we
10: * start removing bytes from the kfifo.
11: */
12:
13: smp_rmb();
14:
15: /* first get the data from fifo->out until the end of the buffer */
16: l = min(len, fifo->size - (fifo->out & (fifo->size - 1)));
17: memcpy(buffer, fifo->buffer + (fifo->out & (fifo->size - 1)), l);
18:
19: /* then get the rest (if any) from the beginning of the buffer */
20: memcpy(buffer + l, fifo->buffer, len - l);
21:
22: /*
23: * Ensure that we remove the bytes from the kfifo -before-
24: * we update the fifo->out index.
25: */
26:
27: smp_mb();
28:
29: fifo->out += len;
30:
31: return len;
32: }
分析:
6行,可去讀的長度為fifo->in – fifo->out,讓讀的長度取len和剩余容量中較小的,避免讀越界;
13行,加讀內存屏障,保證在開始取數據之前,fifo->in取到正確的值(另一個CPU可能正在改寫in值)
16行,前面講到fifo->size已經2的次冪圓整,而且kfifo->out % kfifo->size 可以轉化為 kfifo->out & (kfifo->size – 1),所以fifo->size - (fifo->out & (fifo->size - 1)) 即位 fifo->out 到 buffer末尾所剩余的長度,l取len和剩余長度的最小值,即為從fifo->buffer + fifo->in到末尾所要去讀的長度。
17行,從fifo->buffer + fifo->out的位置開始讀取l長度,如果l = len,則已讀取完成,第20行len – l 為0,將不執行,如果l =fifo->size - (fifo->out & (fifo->size - 1)) ,則第20行還需從buffer頭部讀取 len – l 長。
27行,加內存屏障,保證在修改out前,已經從buffer中取走了數據,如果不加屏障,可能先執行了增加out的操作,數據還沒取完,令一個CPU可能已經往buffer寫數據,將數據破壞,因為寫數據是通過fifo->size - (fifo->in & (fifo->size - 1))來判斷的 。
29行,注意這里 只是用了 fifo->out += len 也未取模,同樣unsigned int的溢出性質,當out 持續增加到溢出時又會被置為0,如果in先溢出,出現 in < out 的情況,那么 in – out 為負數(又將溢出),in – out 的值還是為buffer中數據的長度。
最后,多生產者/多消費者模式的共享隊列需要用到gcc4.2以上內置提供__sync_synchronize()這類的函數。
