Linux kernel里面從來就不缺少簡潔,優雅和高效的代碼
比如,通過限定寫入的數據不能溢出和內存屏障實現在單線程寫單線程讀的情況下不使用鎖。因為鎖是使用在共享資源可能存在沖突的情況下。還用設置buffer緩沖區的大小為2的冪次方,以簡化求模運算,這樣求模運算就演變為 (fifo->in & (fifo->size - 1))。通過使用unsigned int為kfifo的下標,可以不用考慮每次下標超過size時對下表進行取模運算賦值,這里使用到了無符號整數的溢出回零的特性。由於指示讀寫指針的下標一直在增加,沒有進行取模運算,知道其溢出,在這種情況下寫滿和讀完就是不一樣的標志,寫滿是兩者指針之差為fifo->size,讀完的標志是兩者指針相等。后面有一篇博客還介紹了VxWorks下的環形緩沖區的實現機制點擊打開鏈接,從而可以看出linux下的fifo的靈巧性和高效性。
kfifo主要有以下特點:
- 保證緩沖空間的大小為2的次冪,不是的向上取整為2的次冪。
- 使用無符號整數保存輸入(in)和輸出(out)的位置,在輸入輸出時不對in和out的值進行模運算,而讓其自然溢出,並能夠保證
in-out的結果為緩沖區中已存放的數據長度,這也是最能體現kfifo實現技巧的地方; - 使用內存屏障(Memory Barrier)技術,實現單消費者和單生產者對
kfifo的無鎖並發訪問,多個消費者、生產者的並發訪問還是需要加鎖的。
本文主要以下三個部分:
- 關於2的次冪問題,判斷是不是2的次冪以及向上取整為2的次冪
- Linux內核中
kfifo的實現及簡要分析 - 根據
kfifo實現的循環緩沖區,並進行一些測試
關於內存屏障的本文不作過多分析,可以參考WikiMemory Barrier。另外,本文所涉及的整數都默認為無符號整數,不再做一一說明。
1. 2的次冪
- 判斷一個數是不是2的次冪
kfifo要保證其緩存空間的大小為2的次冪,如果不是則向上取整為2的次冪。其對於2的次冪的判斷方式也是很巧妙的。如果一個整數n是2的次冪,則二進制模式必然是1000...,而n-1的二進制模式則是0111...,也就是說n和n-1的每個二進制位都不相同,例如:8(1000)和7(0111);n不是2的次冪,則n和n-1的二進制必然有相同的位都為1的情況,例如:7(0111)和6(0110)。這樣就可以根據n & (n-1)的結果來判斷整數n是不是2的次冪,實現如下:
/* 判斷n是否是2的冪 若n為2的次冪, 則 n & (n-1) == 0,也就是n和n-1的各個位都不相同。例如 8(1000)和7(0111) 若n不是2的次冪, 則 n & (n-1) != 0,也就是n和n-1的各個位肯定有相同的,例如7(0111)和6(0110) */ static inline bool is_power_of_2(uint32_t n) { return (n != 0 && ((n & (n - 1)) == 0)); }
- 將數字向上取整為2的次冪
如果設定的緩沖區大小不是2的次冪,則向上取整為2的次冪,例如:設定為5,則向上取為8。上面提到整數n是2的次冪,則其二進制模式為100...,故如果正數k不是n的次冪,只需找到其最高的有效位1所在的位置(從1開始計數)pos,然后1 << pos即可將k向上取整為2的次冪。實現如下:
static inline uint32_t roundup_power_of_2(uint32_t a) { if (a == 0) return 0; uint32_t position = 0; for (int i = a; i != 0; i >>= 1) position++; return static_cast<uint32_t>(1 << position); }
fifo->in & (fifo->size - 1) 再比如這種寫法取模,獲取已用的大小。這樣用邏輯與的方式相較於加減法更有效率
二:
Linux內核中kfifo實現技巧,主要集中在放入數據的put方法和取數據的get方法。代碼如下:
1 unsigned int __kfifo_put(struct kfifo *fifo, unsigned char *buffer, unsigned int len) 2 { 3 unsigned int l; 4 5 len = min(len, fifo->size - fifo->in + fifo->out); 6 7 /* 8 * Ensure that we sample the fifo->out index -before- we 9 * start putting bytes into the kfifo. 10 */ 11 12 smp_mb(); 13 14 /* first put the data starting from fifo->in to buffer end */ 15 l = min(len, fifo->size - (fifo->in & (fifo->size - 1))); 16 memcpy(fifo->buffer + (fifo->in & (fifo->size - 1)), buffer, l); 17 18 /* then put the rest (if any) at the beginning of the buffer */ 19 memcpy(fifo->buffer, buffer + l, len - l); 20 21 /* 22 * Ensure that we add the bytes to the kfifo -before- 23 * we update the fifo->in index. 24 */ 25 26 smp_wmb(); 27 28 fifo->in += len; 29 30 return len; 31 } 32 33 unsigned int __kfifo_get(struct kfifo *fifo,unsigned char *buffer, unsigned int len) 34 { 35 unsigned int l; 36 37 len = min(len, fifo->in - fifo->out); 38 39 /* 40 * Ensure that we sample the fifo->in index -before- we 41 * start removing bytes from the kfifo. 42 */ 43 44 smp_rmb(); 45 46 /* first get the data from fifo->out until the end of the buffer */ 47 l = min(len, fifo->size - (fifo->out & (fifo->size - 1))); 48 memcpy(buffer, fifo->buffer + (fifo->out & (fifo->size - 1)), l); 49 50 /* then get the rest (if any) from the beginning of the buffer */ 51 memcpy(buffer + l, fifo->buffer, len - l); 52 53 /* 54 * Ensure that we remove the bytes from the kfifo -before- 55 * we update the fifo->out index. 56 */ 57 58 smp_mb(); 59 60 fifo->out += len; 61 62 return len; 63 }
put返回實際保存到緩沖區中的數據長度,get返回的是實際取到的數據長度。在上面代碼中,需要注意到在寫入、取出時候的兩次min運算。關於kfifo的分析,已有很多資料了,也可參考眉目傳情之匠心獨運的kfifo 。
Linux內核實現的kfifo的有以下特點:
- 使用內存屏障 Memory Barrier
- 初始化緩沖區空間時要保證緩沖區的大小為2的次冪
- 使用無符號整數保存in和out(輸入輸出的指針),並且在放入取出數據的時候不做模運算,讓其自然溢出。
優點:
- 實現單消費者和單生產者的無鎖並發訪問。多消費者和多生產者的時候還是需要加鎖的。
- 使用與運算
in & (size-1)代替模運算 -
在更新in或者out的值時不做模運算,而是讓其自動溢出。這應該是kfifo實現最牛叉的地方了,利用溢出后的值參與運算,並且能夠保證結果的正確。溢出運算保證了以下幾點:
- in - out為緩沖區中的數據長度
- size - in + out 為緩沖區中空閑空間
- in == out時緩沖區為空
- size == (in - out)時緩沖區滿了
講解最詳細的一篇https://blog.csdn.net/linyt/article/details/53355355
另外一個https://www.cnblogs.com/wangguchangqing/p/6070286.html
