內核無鎖隊列 -- kfifo

理論證明,在一個生產者和一個消費者的情況下,兩者之間的同步無需加鎖,即可併發訪問。Linux內核無鎖隊列kfifo完美踐行了該理論,性能得到極大提升。工作中如果能使用到該定論,可以有效的提升程序的性能。

內核代碼處處有驚喜,往往讓人歎爲觀止,猶如桃花源,讓你發現別有洞天的美景。kfifo的實現中使用如下幾個trick:

  1. 一個整數如果是2的整數冪,那麼對齊取餘操作%,可以轉化爲與其&操作,提升效率
  2. 使用整數自動溢出構成的一個環來替代循環數組,簡化判空、判滿及各種長度運算
  3. 先操作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的情況,將無鎖隊列的核心原理(先操作再計數)運用其中,來優化程序,提升逼格。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章