理論證明,在一個生產者和一個消費者的情況下,兩者之間的同步無需加鎖,即可併發訪問。Linux內核無鎖隊列kfifo完美踐行了該理論,性能得到極大提升。工作中如果能使用到該定論,可以有效的提升程序的性能。
內核代碼處處有驚喜,往往讓人歎爲觀止,猶如桃花源,讓你發現別有洞天的美景。kfifo的實現中使用如下幾個trick:
- 一個整數如果是2的整數冪,那麼對齊取餘操作%,可以轉化爲與其&操作,提升效率
- 使用整數自動溢出構成的一個環來替代循環數組,簡化判空、判滿及各種長度運算
- 先操作buffer,再計數。實現無鎖隊列
1. kfifo結構體信息
kfifo是內核中一個First In First Out數據結構,循壞隊列實現。
struct __kfifo {
unsigned int in;
unsigned int out;
unsigned int mask;
unsigned int esize;
void *data;
};
data:用於存放數據的緩存區
esize:緩存區每個元素的size(element size)
mask:緩衝區元素個數(size) - 1;使用&mask,替換%size,提升效率
in:隊尾下標,入隊列的offset爲(in % size 或者 in & mask)
out:隊首下標,出隊列的offset爲(out % size 或者 out & mask)
隊尾下標in在有數據入隊的時候,一直自增;隊首下標out在有數據出隊的時候,一直自增。內核使用了unsigned int溢出的特性,來實現循環隊列,即in - out不管任何情況都爲隊列的長度,即使in < out,這裏可以畫圖或者自己編程理解一下。
2. kfifo內存分配
int __kfifo_alloc(struct __kfifo *fifo, unsigned int size,
size_t esize, gfp_t gfp_mask)
{
/*
* round down to the next power of 2, since our 'let the indices
* wrap' technique works only in this case.
*/
size = roundup_pow_of_two(size);
fifo->in = 0;
fifo->out = 0;
fifo->esize = esize;
if (size < 2) {
fifo->data = NULL;
fifo->mask = 0;
return -EINVAL;
}
fifo->data = kmalloc(size * esize, gfp_mask);
if (!fifo->data) {
fifo->mask = 0;
return -ENOMEM;
}
fifo->mask = size - 1;
return 0;
}
內存分配時,需要將size向上取2的冪,同時置fifo->mask=size - 1。
3. kfifo初始化
int __kfifo_init(struct __kfifo *fifo, void *buffer,
unsigned int size, size_t esize)
{
size /= esize;
size = roundup_pow_of_two(size);
fifo->in = 0;
fifo->out = 0;
fifo->esize = esize;
fifo->data = buffer;
if (size < 2) {
fifo->mask = 0;
return -EINVAL;
}
fifo->mask = size - 1;
return 0;
}
4. 入隊列__kfifo_in
unsigned int __kfifo_in(struct __kfifo *fifo,
const void *buf, unsigned int len)
{
unsigned int l;
l = kfifo_unused(fifo);
if (len > l)
len = l;
kfifo_copy_in(fifo, buf, len, fifo->in);
fifo->in += len;
return len;
}
可以看到,先將元素入隊列:kfifo_copy_in,然後再計數fifo->in += len。
其中kfifo_unused判斷隊列中還有多少空間,代碼如下:
static inline unsigned int kfifo_unused(struct __kfifo *fifo)
{
return (fifo->mask + 1) - (fifo->in - fifo->out);
}
kfifo_copy_in爲入隊列的實體,實現如下:
static void kfifo_copy_in(struct __kfifo *fifo, const void *src,
unsigned int len, unsigned int off)
{
unsigned int size = fifo->mask + 1;
unsigned int esize = fifo->esize;
unsigned int l;
off &= fifo->mask;
if (esize != 1) {
off *= esize;
size *= esize;
len *= esize;
}
l = min(len, size - off);
memcpy(fifo->data + off, src, l);
memcpy(fifo->data, src + l, len - l);
/*
* make sure that the data in the fifo is up to date before
* incrementing the fifo->in index counter
*/
smp_wmb();
}
5. 出隊列__kfifo_out
unsigned int __kfifo_out(struct __kfifo *fifo,
void *buf, unsigned int len)
{
len = __kfifo_out_peek(fifo, buf, len);
fifo->out += len;
return len;
}
同樣可以看出,先出隊,再計數。如果先計數,在還沒有出隊前,被其他入隊操作覆蓋。
__kfifo_out_peek如下:
unsigned int __kfifo_out_peek(struct __kfifo *fifo,
void *buf, unsigned int len)
{
unsigned int l;
l = fifo->in - fifo->out;
if (len > l)
len = l;
kfifo_copy_out(fifo, buf, len, fifo->out);
return len;
}
每次去元素時,需要判斷長度是否越界,防止破壞空間。
kfifo_copy_out是出隊的實體,試下如下:
static void kfifo_copy_out(struct __kfifo *fifo, void *dst,
unsigned int len, unsigned int off)
{
unsigned int size = fifo->mask + 1;
unsigned int esize = fifo->esize;
unsigned int l;
off &= fifo->mask;
if (esize != 1) {
off *= esize;
size *= esize;
len *= esize;
}
l = min(len, size - off);
memcpy(dst, fifo->data + off, l);
memcpy(dst + l, fifo->data, len - l);
/*
* make sure that the data is copied before
* incrementing the fifo->out index counter
*/
smp_wmb();
}
6. 總結
上面分析基於linux 3.10.107內核。工程中,如果遇到1:N,或者N:1的情況,都可以轉換爲N個1:1的情況,將無鎖隊列的核心原理(先操作再計數)運用其中,來優化程序,提升逼格。