深入理解dpdk rte_ring无锁队列

一、简介

同样用面向对象的思想来理解无锁队列ring。dpdk的无锁队列ring是借鉴了linux内核kfifo无锁队列。ring的实质是FIFO的环形队列。

ring的特点:

  • 无锁出入队(除了cas(compare and swap)操作)
  • 多消费/生产者同时出入队

使用方法:

1.创建一个ring对象。

接口:structrte_ring *

rte_ring_create(constchar *name, unsigned count, int socket_id,

        unsignedflags)

例如:

struct rte_ring *r = rte_ring_create(“MY_RING”, 1024,rte_socket_id(), 0);

name:ring的name

count:ring队列的长度必须是2的幂次方。原因下文再介绍。

socket_id:ring位于的socket。

flags:指定创建的ring的属性:单/多生产者、单/多消费者两者之间的组合。

0表示使用默认属性(多生产者、多消费者)。不同的属性出入队的操作会有所不同。

2.出入队

有不同的出入队方式(单、bulk、burst)都在rte_ring.h中。

例如:rte_ring_enqueue和rte_ring_dequeue

这里只是简要介绍使用方法,本文的重点是介绍ring的整体架构。

二、创建ring

struct rte_ring {
	/*
	 * Note: this field kept the RTE_MEMZONE_NAMESIZE size due to ABI
	 * compatibility requirements, it could be changed to RTE_RING_NAMESIZE
	 * next time the ABI changes
	 */
	char name[RTE_MEMZONE_NAMESIZE];    /**< Name of the ring. */
	int flags;                       /**< Flags supplied at creation. */
	const struct rte_memzone *memzone;
			/**< Memzone, if any, containing the rte_ring */

	/** Ring producer status. */
	struct prod {
		uint32_t watermark;      /**< Maximum items before EDQUOT. */
		uint32_t sp_enqueue;     /**< True, if single producer. */
		uint32_t size;           /**< Size of ring. */
		uint32_t mask;           /**< Mask (size-1) of ring. */
		volatile uint32_t head;  /**< Producer head. cgm 预生产到地方*/
		volatile uint32_t tail;  /**< Producer tail. cgm 实际生产了的数量*/
	} prod __rte_cache_aligned;

	/** Ring consumer status. */
	struct cons {
		uint32_t sc_dequeue;     /**< True, if single consumer. */
		uint32_t size;           /**< Size of the ring. */
		uint32_t mask;           /**< Mask (size-1) of ring. */
		volatile uint32_t head;  /**< Consumer head. cgm 预出队的地方*/
		volatile uint32_t tail;  /**< Consumer tail. cgm 实际出队的地方*/
#ifdef RTE_RING_SPLIT_PROD_CONS
	} cons __rte_cache_aligned;
#else
	} cons;
#endif

#ifdef RTE_LIBRTE_RING_DEBUG
	struct rte_ring_debug_stats stats[RTE_MAX_LCORE];
#endif

	void *ring[] __rte_cache_aligned;   /**< Memory space of ring starts here.
	                                     * not volatile so need to be careful
	                                     * about compiler re-ordering */
};

 

在rte_ring_list链表中创建一个rte_tailq_entry节点。在memzone中根据队列的大小count申请一块内存(rte_ring的大小加上count*sizeof(void *))。紧邻着rte_ring结构的void *数组用于放置入队的对象(单纯的赋值指针值)。rte_ring结构中有生产者结构prod、消费者结构cons。初始化参数之后,把rte_tailq_entry的data节点指向rte_ring结构地址。

可以注意到cons.head、cons.tail、prod.head、prod.tail的类型都是uint32_t(32位无符号整形)。除此之外,队列的大小count被限制为2的幂次方。这两个条件放到一起构成了一个很巧妙的情景。因为队列的大小一般不会是最大的2的32次方那么大,所以,把队列取为32位的一个窗口,当窗口的大小是2的幂次方,则32位包含整数个窗口。这样,用来存放ring对象的void *指针数组空间就可只申请一个窗口大小即可。我的另一篇文章“ 图解有符号和无符号数隐藏的含义”解释了二进制的回环性,无符号数计算距离的技巧。根据二进制的回环性,可以直接用(uint32_t)( prod_tail - cons_tail)计算队列中有多少生产的产品(即使溢出了也不会出错,如(uint32_t)5-65535 = 6)。
head/tail移动的时候,直接相加位移量即可,既是溢出了,结果也是对的。

三、实现多生产/消费者同时生产/消费

也即是同时出入队。

有个地方可能让人纳闷,为什么prod和cons都定义了head和tail。其实,我加的代码注释说明了这点。这就是为了实现同时出入队。
如图:

  • 移动prod.head表示生产者预定的生产数量
  • 当该生产者生产结束,且在此之前的生产也都结束后,移动prod.tail表示实际生产的位置
  • 同样,移动cons.head表示消费者预定的消费数量
  • 当该消费者消费结束,且在此之前的消费也都结束后,移动cons.tail表示实际消费的位置

四、出/入队列

在前面介绍的基础上,就很容易理解怎么样巧妙的出/入队列的了。

1.入队流程

多生产者入队代码:
static inline int __attribute__((always_inline))
__rte_ring_mp_do_enqueue(struct rte_ring *r, void * const *obj_table,
			 unsigned n, enum rte_ring_queue_behavior behavior)
{
	uint32_t prod_head, prod_next;
	uint32_t cons_tail, free_entries;
	const unsigned max = n;
	int success;
	unsigned i, rep = 0;
	uint32_t mask = r->prod.mask;
	int ret;

	/* Avoid the unnecessary cmpset operation below, which is also
	 * potentially harmful when n equals 0. */
	if (n == 0)
		return 0;

	/* move prod.head atomically 
	cgm
	1.检查free空间是否足够
	2.cms生产预约*/
	do {
		/* Reset n to the initial burst count */
		n = max;

		prod_head = r->prod.head;
		cons_tail = r->cons.tail;
		/* The subtraction is done between two unsigned 32bits value
		 * (the result is always modulo 32 bits even if we have
		 * prod_head > cons_tail). So 'free_entries' is always between 0
		 * and size(ring)-1. */
		 /**cgm mask+cons_tail+1到下一个窗口中*/
		free_entries = (mask + cons_tail - prod_head);

		/* check that we have enough room in ring */
		if (unlikely(n > free_entries)) {
			if (behavior == RTE_RING_QUEUE_FIXED) {
				__RING_STAT_ADD(r, enq_fail, n);
				return -ENOBUFS;
			}
			else {
				/* No free entry available */
				if (unlikely(free_entries == 0)) {
					__RING_STAT_ADD(r, enq_fail, n);
					return 0;
				}

				n = free_entries;
			}
		}

		prod_next = prod_head + n;
		success = rte_atomic32_cmpset(&r->prod.head, prod_head,
					      prod_next);
	} while (unlikely(success == 0));

	/* write entries in ring */
	ENQUEUE_PTRS();
	rte_smp_wmb();

	/* if we exceed the watermark 
	cgm 检查是否到了阈值,并添加到统计中*/
	if (unlikely(((mask + 1) - free_entries + n) > r->prod.watermark)) {
		ret = (behavior == RTE_RING_QUEUE_FIXED) ? -EDQUOT :
				(int)(n | RTE_RING_QUOT_EXCEED);
		__RING_STAT_ADD(r, enq_quota, n);
	}
	else {
		ret = (behavior == RTE_RING_QUEUE_FIXED) ? 0 : n;
		__RING_STAT_ADD(r, enq_success, n);
	}

	/*
	 * If there are other enqueues in progress that preceded us,
	 * we need to wait for them to complete
	 cgm 等待之前的入队操作完成
	 */
	while (unlikely(r->prod.tail != prod_head)) {
		rte_pause();

		/* Set RTE_RING_PAUSE_REP_COUNT to avoid spin too long waiting
		 * for other thread finish. It gives pre-empted thread a chance
		 * to proceed and finish with ring dequeue operation. */
		if (RTE_RING_PAUSE_REP_COUNT &&
		    ++rep == RTE_RING_PAUSE_REP_COUNT) {
			rep = 0;
			sched_yield();
		}
	}
	r->prod.tail = prod_next;
	return ret;
}

 

1.检查free空间是否足够

把free_entries = (mask + cons_tail - prod_head);写成free_entries = (mask + 1 + cons_tail - prod_head -1);就容易理解了。

先解释mask + 1 + cons_tail的意义:
mask + 1 + cons_tail是把cons_tail移到下一个窗口对应的位置上。那么从上面2个图中,下面图中的红色面积等于上图中红色面积(按数学的几何学是这样的,但这里会有1的差错,下面介绍)。
减1的意义:
一个四位的二进制,能表示的两个数之间最大的距离是15,也即是下图中的单元格数:
但当移到下一个窗口中时,15到0之间的一个也会加上,因为15要移动下一个窗口的0(16),必须要增加1。所以,在这里多算了1个,需要减去1。

2.生产预约

利用cas操作,移动r->prod.head,预约生产。

3.检查是否到了阈值,并添加到统计中

4.等待之前的入队操作完成,移动实际位置

检查在此生产者之前的生产者都生产完成后,移动r->prod.tail,移动实际生产了的位置。

2.出队流程

多消费者出队代码:
static inline int __attribute__((always_inline))
__rte_ring_mc_do_dequeue(struct rte_ring *r, void **obj_table,
		 unsigned n, enum rte_ring_queue_behavior behavior)
{
	uint32_t cons_head, prod_tail;
	uint32_t cons_next, entries;
	const unsigned max = n;
	int success;
	unsigned i, rep = 0;
	uint32_t mask = r->prod.mask;

	/* Avoid the unnecessary cmpset operation below, which is also
	 * potentially harmful when n equals 0. */
	if (n == 0)
		return 0;

	/* move cons.head atomically 
	cgm
	1.检查可消费空间是否足够
	2.cms消费预约*/
	do {
		/* Restore n as it may change every loop */
		n = max;

		cons_head = r->cons.head;
		prod_tail = r->prod.tail;
		/* The subtraction is done between two unsigned 32bits value
		 * (the result is always modulo 32 bits even if we have
		 * cons_head > prod_tail). So 'entries' is always between 0
		 * and size(ring)-1. */
		entries = (prod_tail - cons_head);

		/* Set the actual entries for dequeue */
		if (n > entries) {
			if (behavior == RTE_RING_QUEUE_FIXED) {
				__RING_STAT_ADD(r, deq_fail, n);
				return -ENOENT;
			}
			else {
				if (unlikely(entries == 0)){
					__RING_STAT_ADD(r, deq_fail, n);
					return 0;
				}

				n = entries;
			}
		}

		cons_next = cons_head + n;
		success = rte_atomic32_cmpset(&r->cons.head, cons_head,
					      cons_next);
	} while (unlikely(success == 0));

	/* copy in table */
	DEQUEUE_PTRS();
	rte_smp_rmb();

	/*
	 * If there are other dequeues in progress that preceded us,
	 * we need to wait for them to complete
	 cgm 等待之前的出队操作完成
	 */
	while (unlikely(r->cons.tail != cons_head)) {
		rte_pause();

		/* Set RTE_RING_PAUSE_REP_COUNT to avoid spin too long waiting
		 * for other thread finish. It gives pre-empted thread a chance
		 * to proceed and finish with ring dequeue operation. */
		if (RTE_RING_PAUSE_REP_COUNT &&
		    ++rep == RTE_RING_PAUSE_REP_COUNT) {
			rep = 0;
			sched_yield();
		}
	}
	__RING_STAT_ADD(r, deq_success, n);
	r->cons.tail = cons_next;

	return behavior == RTE_RING_QUEUE_FIXED ? 0 : n;
}
同生产者一个道理,代码中加了点注释,就不详细解释了。
 

### 回答1: rte_ring_sc_dequeue_bulk是DPDK中的一个函数,用于从单个生产者,单个消费者环形队列中批量出队一组元素。"sc"代表"single consumer",表示只有一个消费者在访问该队列。该函数的原型如下: ``` uint32_t rte_ring_sc_dequeue_bulk(struct rte_ring *r, void **obj_table, uint32_t n) ``` 其中,参数r是指向环形队列的指针,obj_table是一个指向指针数组的指针,用于存储出队的元素,n表示要出队的元素数量。 函数的返回值是实际出队的元素数量,可能小于请求的数量n。如果队列为空,则返回0。该函数是线程安全的,可以在多个线程之间并发调用。 ### 回答2: rte_ring_sc_dequeue_bulk是DPDK(Data Plane Development Kit)库中的一个函数,用于从指定的环形缓冲区中以"单一消费者"的方式批量取出元素。 该函数的作用是从环形缓冲区中按照先入先出原则取出一定数量(批量)的元素,并将它们存储到用户提供的缓冲区中。在单一消费者的情况下,该函数可以提供更高的性能。 该函数的原型为: ```c uint32_t rte_ring_sc_dequeue_bulk(struct rte_ring *r, void **obj_table, uint32_t n, const unsigned int *restrict offset) ``` 参数说明: - r:指向目标环形缓冲区的指针。 - obj_table:指向用户提供的缓冲区指针的指针,用于存储从环形缓冲区中取出的元素。 - n:用户期望从环形缓冲区中取出的元素数量。 - offset:用户提供的存储偏移量的数组指针,用于存储从缓冲区中每个元素的偏移量值。 该函数的返回值为实际取出的元素数量。 使用rte_ring_sc_dequeue_bulk函数可以实现高效地从环形缓冲区中取出一定数量的元素,可以提高数据处理的效率。需要注意的是,在使用该函数之前,必须先创建好环形缓冲区,并确保环形缓冲区中有足够的元素可供取出。 ### 回答3: rte_ring_sc_dequeue_bulk是一个函数,用于从单生产者、单消费者环形缓冲区中以原子操作的方式批量出队元素。 它的功能是从环形缓冲区中连续出队指定数量的元素,并返回实际出队的元素数量。它是无锁的,采用强制屏障以确保原子性。 使用rte_ring_sc_dequeue_bulk函数时,需要传入一个指向环形缓冲区的指针,以及一个指向存储出队元素的数组的指针,以及期望出队的元素数量。 函数会按先进先出的顺序出队元素,并将其存储到数组中。如果实际出队的元素数量小于期望的数量,则代表环形缓冲区中的元素数量不足,所有剩余的元素都将被出队。 rte_ring_sc_dequeue_bulk函数会根据环形缓冲区的状态,使用原子操作进行出队操作,避免多个线程同时修改环形缓冲区造成冲突。在出队操作的同时,它使用强制屏障来确保原子性,确保出队操作的结果对其他线程可见。 总之,rte_ring_sc_dequeue_bulk函数是一个高效、无锁的函数,用于在单生产者、单消费者环形缓冲区中以原子操作的方式批量出队元素。它可以帮助提高多线程程序的性能,并保证线程安全性。
评论 5
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值