对dpdk的rte_ring实现原理和代码分析

本文深入解析DPDK的无锁环形队列Ring,介绍其借鉴自Linux内核kfifo的设计原理,探讨单/多生产者及消费者场景下的无锁操作机制,分析核心代码实现细节。

 

前言

dpdk的rte_ring是借鉴了linux内核的kfifo实现原理,这里统称为无锁环形缓冲队列。
环形缓冲区通常有一个读指针和一个写指针。读指针指向环形缓冲区中可读的数据,写指针指向环形缓冲区中可写的数据。通过移动读指针和写指针就可以实现缓冲区的数据读取和写入。
在通常情况下,环形缓冲区的读用户仅仅会影响读指针, 而写用户仅仅会影响写指针。如果仅仅有一个读用户和一个写用户,那么不需要添加互斥保护机制就可以保证数据的正确性。
但是,如果有多个读写用户访问环形缓冲区, 那么必须添加互斥保护机制来确保多个用户互斥访问环形缓冲区。 具体来讲,如果有多个写用户和一个读用户,那么只是需要给写用户加锁进行保护;反之,如果有一个写用户和多个读用户,那么只是需要对读用户进行加锁保护。

所以这里说它是无锁,其实我是持保留态度的,只有在单生产单消费的场景下,才能说它是正在的无锁,多生产或者多消费的场景下,必须要加锁来保护才行,kfifo提供了自旋锁保护,而dpdk提供了原子锁cas来保护。

备注:dpdk版本18.11, 以下图片来自dpdk的prog_guide-master.pdf. dpdk官网编程指南手册

本人从dpdk移植了ring实现到自己的github里面 https://github.com/air5005/usg/tree/master/libs/libring 有兴趣的可以参考

rte_ring的实现原理

单生产者入队

当只有一个生产者增加一个对象到环形缓冲区,这个时候只有一个生产者头和尾(prod_head和prod_tail)被修改。这个初始状态是有一个生产者的头和尾指向了相同的位置。

  1. 第一步
    将r->prod.head保存到变量的prod_head, prod_next = prod_head + n,如下图
    (在代码里面的话,临时变量prod_head = old_head. prod_next = new_head)
    在这里插入图片描述
  2. 第二步
    修改prod_head去指向prod_next指向的位置。 指向新增加对象的指针被拷贝到ring(obj4)。
    在这里插入图片描述
  3. 第三步
    一旦这个对象被增加到环形缓冲区中,prod_tail将要被修改成prod_head指向的位置。 至此, 这入队操作完成了。

在这里插入图片描述

单消费者出队

当只有一个消费者出队操作在环形缓冲区,这个时候只有一个消费者头和尾(cons_head和cons_tail)被修改并且这只有一个消费者。初始状态是一个消费者的头和尾指向了相同的位置。

  1. 第一步
    首先,暂时将消费者的头索引和生产者的尾部索引交给临时变量,并且将cons_next指向表中下一个对象,如果在这环形缓冲区没有足够的对象,将返回一个错误。
    在这里插入图片描述
  2. 第二步
    第二步是修改cons_head去指向cons_next指向的位置,并且指向出队对象(obj1)的指针被拷贝到一个临时用户定义的指针中。
    在这里插入图片描述
  3. 第三步
    最后,cons_tail被修改成指向cons_head指向的位置。至此,单消费者的出队操作完成了。
    在这里插入图片描述

多生产者入队

  1. 初始状态下,环形缓冲区的状态是prod_head和prod_tail都指向同一个位置,这个时候core1和core2都保存prod_head到各自的临时变量,如果core1和core2是同时执行这个动作的话,他们的临时变量保存的prod_head和prod_next应该完全相同。如下图所示:
    在这里插入图片描述
  2. 在core1上面执行cas,比较prod_head和临时变量里面的prod_head,如果相同则把prod_head移动到prod_next,这个时候再core2也是执行完全一下的动作,由于使用的是cas,原子操作的比较和设置,只能有一个是成功的,这里如果是core1成功的话,那么core2的cas就是失败的。
    所以这个时候ring里面的prod_head移动到core1的prod_next这个位置了并填充obj4到ring里面。
    在这里插入图片描述
  3. core2在执行第一次cas的时候失败了,这个时候再执行一次,重新update临时变量里面的prod_head和prod_next,由于core1已经cas成功,那么这个时候core2 update到的这两个值就相应的更新了,这个时候再执行cas,则能成功,所以这个时候ring里面的prod_head移动到core2的prod_next这个位置并填充obj5到ring里面。
    在这里插入图片描述
  4. core1在更新完ring的prod_head和obj后,这个时候临时变量里面保存的prod_head和prod_next是固定的,这个时候需要借助这两个值,当ring里面的prod_tail == prod_head(临时变量)的时候,即可直接更新prod_tail = prod_next, 这里的更新tail条件是唯一的,否则不能更新。
    在这里插入图片描述
  5. core2在更新完ring的prod_head和obj后,这个时候临时变量里面保存的prod_head和prod_next是固定的,同理,core2也需要判断当ring里面的prod_tail == prod_head(临时变量)的时候,即可直接更新prod_tail = prod_next, 如果这个时候core1还没有update这个tail值的话,core2需要一直等待,知道core1更新了tail值位置才行(因为core1更新head值在core2之前,所以core1更新tail值也需要在core2之前)。
    在这里插入图片描述

多消费者出队

多消费者出列和多生产者入列的实现逻辑大致相同,也是先更新cons head、存储obj、最后更新cons tail,具体这里就不一一罗列出来了,下面我们看具体代码的实现。

代码分析

rte_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] __rte_cache_aligned; /**< Name of the ring. */
	int flags;               /**< Flags supplied at creation. */
	const struct rte_memzone *memzone;
			/**< Memzone, if any, containing the rte_ring */
	uint32_t size;           /**< Size of ring. */
	uint32_t mask;           /**< Mask (size-1) of ring. */
	uint32_t capacity;       /**< Usable size of ring */

	char pad0 __rte_cache_aligned; /**< empty cache line */

	/** Ring producer status. */
	struct rte_ring_headtail prod __rte_cache_aligned;
	char pad1 __rte_cache_aligned; /**< empty cache line */

	/** Ring consumer status. */
	struct rte_ring_headtail cons __rte_cache_aligned;
	char pad2 __rte_cache_aligned; /**< empty cache line */
};
  •  

struct rte_ring结构体主要包含一个生产者prod和一个消费者cons,还有ring本身支持加入obj数量的容量大小,
这个过程struct rte_ring、struct rte_ring_headtail都设置了cache line对其,防止出现cache miss的情况.

struct rte_ring_headtail {
	volatile uint32_t head;  /**< Prod/consumer head. */
	volatile uint32_t tail;  /**< Prod/consumer tail. */
	uint32_t single;         /**< True if single prod/cons */
};

rte_ring_headtail 实现了head和tail,环形链表两个游标,还有一个single,标示是单操作者还是多操作者.

创建ring rte_ring_create

struct rte_ring *
rte_ring_create(const char *name, unsigned count, int socket_id,
		unsigned flags)
{
	char mz_name[RTE_MEMZONE_NAMESIZE];
	struct rte_ring *r;
	struct rte_tailq_entry *te;
	const struct rte_memzone *mz;
	ssize_t ring_size;
	int mz_flags = 0;
	struct rte_ring_list* ring_list = NULL;
	const unsigned int requested_count = count;
	int ret;

    /* 获取rte_ring_tailq链表,用来保存ring,方便定位查看系统存在的ring */
	ring_list = RTE_TAILQ_CAST(rte_ring_tailq.head, rte_ring_list);

    /* 判断flags表示是否设置了RING_F_EXACT_SZ, RING_F_EXACT_SZ的话,内部会
       把count做一个调整,调整为2的指数次方,否则要求外面带进来的count本身就是
       2的指数次方*/
	/* for an exact size ring, round up from count to a power of two */
	if (flags & RING_F_EXACT_SZ)
		count = rte_align32pow2(count + 1);

    /* 根据count获取ring需要的大小,
       ring_size的大小计算如下
       ring_size = sizeof(struct rte_ring) + count * sizeof(void *);
       ring_size = RTE_ALIGN(ring_size, RTE_CACHE_LINE_SIZE);
       即等于sizeof(struct rte_ring)+count个指针的大小,所以ring用来保存的
       数据就是指针.
    */
	ring_size = rte_ring_get_memsize(count);
	if (ring_size < 0) {
		rte_errno = ring_size;
		return NULL;
	}

    /* 填充一下ring的名字,RTE_RING_MZ_PREFIX+name */
	ret = snprintf(mz_name, sizeof(mz_name), "%s%s",
		RTE_RING_MZ_PREFIX, name);
	if (ret < 0 || ret >= (int)sizeof(mz_name)) {
		rte_errno = ENAMETOOLONG;
		return NULL;
	}

    /* 申请一下rte_ring_tailq的节点,挂到rte_ring_tailq尾部 */
	te = rte_zmalloc("RING_TAILQ_ENTRY", sizeof(*te), 0);
	if (te == NULL) {
		RTE_LOG(ERR, RING, "Cannot reserve memory for tailq\n");
		rte_errno = ENOMEM;
		return NULL;
	}

    /* 读写锁保护tailq操作 */
	rte_rwlock_write_lock(RTE_EAL_TAILQ_RWLOCK);

	/* reserve a memory zone for this ring. If we can't get rte_config or
	 * we are secondary process, the memzone_reserve function will set
	 * rte_errno for us appropriately - hence no check in this this function */
	mz = rte_memzone_reserve_aligned(mz_name, ring_size, socket_id,
					 mz_flags, __alignof__(*r));
	if (mz != NULL) {
		r = mz->addr;
		/* no need to check return value here, we already checked the
		 * arguments above */
		/* 对ring里面变量做初始化操作 */
		rte_ring_init(r, name, requested_count, flags);

		te->data = (void *) r;
		r->memzone = mz;

		TAILQ_INSERT_TAIL(ring_list, te, next);
	} else {
		r = NULL;
		RTE_LOG(ERR, RING, "Cannot reserve memory\n");
		rte_free(te);
	}
	rte_rwlock_write_unlock(RTE_EAL_TAILQ_RWLOCK);

	return r;
}
int
rte_ring_init(struct rte_ring *r, const char *name, unsigned count,
	unsigned flags)
{
	int ret;

	/* compilation-time checks */
	RTE_BUILD_BUG_ON((sizeof(struct rte_ring) &
			  RTE_CACHE_LINE_MASK) != 0);
	RTE_BUILD_BUG_ON((offsetof(struct rte_ring, cons) &
			  RTE_CACHE_LINE_MASK) != 0);
	RTE_BUILD_BUG_ON((offsetof(struct rte_ring, prod) &
			  RTE_CACHE_LINE_MASK) != 0);

	/* init the ring structure */
	memset(r, 0, sizeof(*r));
	ret = snprintf(r->name, sizeof(r->name), "%s", name);
	if (ret < 0 || ret >= (int)sizeof(r->name))
		return -ENAMETOOLONG;
	r->flags = flags;
	r->prod.single = (flags & RING_F_SP_ENQ) ? __IS_SP : __IS_MP;
	r->cons.single = (flags & RING_F_SC_DEQ) ? __IS_SC : __IS_MC;

	if (flags & RING_F_EXACT_SZ) {
		r->size = rte_align32pow2(count + 1);
		r->mask = r->size - 1;
		r->capacity = count;
	} else {
		if ((!POWEROF2(count)) || (count > RTE_RING_SZ_MASK)) {
			RTE_LOG(ERR, RING,
				"Requested size is invalid, must be power of 2, and not exceed the size limit %u\n",
				RTE_RING_SZ_MASK);
			return -EINVAL;
		}
		r->size = count;
		r->mask = count - 1;
		r->capacity = r->mask;
	}
	r->prod.head = r->cons.head = 0;
	r->prod.tail = r->cons.tail = 0;

	return 0;
}

具体创建一个ring的函数流程如上,具体我在代码里面加了每个操作的注释,主要分为以下几个流程:

  1. 确定count数量,需要满足是2的指数次方
  2. 申请内存,具体ring size=sizeof(struct rte_ring) + count * sizeof(void *),通过rte_memzone_reserve_aligned申请内存
  3. 申请tailq节点,加入rte_ring_tailq链表里面,用来保存ring,方便定位查看系统存在的ring
  4. 使用rte_ring_init初始化ring结构体

入队列

入队列操作主要分为三个流程

  1. 更新r->prod.head
static __rte_always_inline unsigned int
__rte_ring_move_prod_head(struct rte_ring *r, unsigned int is_sp,
		unsigned int n, enum rte_ring_queue_behavior behavior,
		uint32_t *old_head, uint32_t *new_head,
		uint32_t *free_entries)
{
	const uint32_t capacity = r->capacity;
	unsigned int max = n;
	int success;

	do {
		/* Reset n to the initial burst count */
		n = max;

		*old_head = r->prod.head;

		/* add rmb barrier to avoid load/load reorder in weak
		 * memory model. It is noop on x86
		 */
		rte_smp_rmb();

		/*
		 *  The subtraction is done between two unsigned 32bits value
		 * (the result is always modulo 32 bits even if we have
		 * *old_head > cons_tail). So 'free_entries' is always between 0
		 * and capacity (which is < size).
		 */
		/* 计算当前可用容量,
		   cons.tail是小于等于prod.head, 所以r->cons.tail - *old_head得到一个
		   负数,capacity减这个差值就得到剩余的容量 */
		*free_entries = (capacity + r->cons.tail - *old_head);

		/* check that we have enough room in ring */
		if (unlikely(n > *free_entries))
			n = (behavior == RTE_RING_QUEUE_FIXED) ?
					0 : *free_entries;

		if (n == 0)
			return 0;

        /* 新头的位置 */
		*new_head = *old_head + n;
        /* 如果是单生产者,直接更新r->prod.head即可,不需要加锁 */
		if (is_sp)
			r->prod.head = *new_head, success = 1;
        /* 如果是多生产者,需要使用cmpset比较,如果&r->prod.head == *old_head
           则&r->prod.head = *new_head
           否则重新循环,获取新的*old_head = r->prod.head,知道成功位置*/
		else
			success = rte_atomic32_cmpset(&r->prod.head,
					*old_head, *new_head);
	} while (unlikely(success == 0));
	return n;
}
  1. 将obj存放到r指定位置
    /* r经过__rte_ring_move_prod_head处理后,r->prod.head已经移动到想要的位置
       &r[1]是数据的位置, prod_head是旧的r->prod.head,obj_table是要加入的obj
       ENQUEUE_PTRS的处理目的是把对应个数的obj存放到r的指定位置里面,由于
       obj在r里面坑已经站好,所以这里只要按指定填充即可,不需要加锁
    */
#define ENQUEUE_PTRS(r, ring_start, prod_head, obj_table, n, obj_type) do { \
	unsigned int i; \
	const uint32_t size = (r)->size; \
	uint32_t idx = prod_head & (r)->mask; \
	obj_type *ring = (obj_type *)ring_start; \
	if (likely(idx + n < size)) { \
		for (i = 0; i < (n & ((~(unsigned)0x3))); i+=4, idx+=4) { \
			ring[idx] = obj_table[i]; \
			ring[idx+1] = obj_table[i+1]; \
			ring[idx+2] = obj_table[i+2]; \
			ring[idx+3] = obj_table[i+3]; \
		} \
		switch (n & 0x3) { \
		case 3: \
			ring[idx++] = obj_table[i++]; /* fallthrough */ \
		case 2: \
			ring[idx++] = obj_table[i++]; /* fallthrough */ \
		case 1: \
			ring[idx++] = obj_table[i++]; \
		} \
	} else { \
		for (i = 0; idx < size; i++, idx++)\
			ring[idx] = obj_table[i]; \
		for (idx = 0; i < n; i++, idx++) \
			ring[idx] = obj_table[i]; \
	} \
} while (0)
  1. 更新r->prod.tail
static __rte_always_inline void
update_tail(struct rte_ring_headtail *ht, uint32_t old_val, uint32_t new_val,
		uint32_t single, uint32_t enqueue)
{
	if (enqueue)
		rte_smp_wmb();
	else
		rte_smp_rmb();
	/*
	 * If there are other enqueues/dequeues in progress that preceded us,
	 * we need to wait for them to complete
	 */

    /* 如果是多生产者流程,这里需要等待tail等于我们想要的old val
       因为是多生产者,这里需要等其他prod把这个tail update,这里的
       能成立*/
	if (!single)
		while (unlikely(ht->tail != old_val))
			rte_pause();

    /* 更新tail */
	ht->tail = new_val;
}

完整函数如下:

static __rte_always_inline unsigned int
__rte_ring_do_enqueue(struct rte_ring *r, void * const *obj_table,
		 unsigned int n, enum rte_ring_queue_behavior behavior,
		 unsigned int is_sp, unsigned int *free_space)
{
	uint32_t prod_head, prod_next;
	uint32_t free_entries;

    /* 更新r->prod.head指针操作 */
	n = __rte_ring_move_prod_head(r, is_sp, n, behavior,
			&prod_head, &prod_next, &free_entries);
	if (n == 0)
		goto end;

    /* r经过__rte_ring_move_prod_head处理后,r->prod.head已经移动到想要的位置
       &r[1]是数据的位置, prod_head是旧的r->prod.head,obj_table是要加入的obj
       ENQUEUE_PTRS的处理目的是把对应个数的obj存放到r的指定位置里面,由于
       obj在r里面坑已经站好,所以这里只要按指定填充即可,不需要加锁
    */
	ENQUEUE_PTRS(r, &r[1], prod_head, obj_table, n, void *);

    /* 更新r->prod.tail指针操作 */
	update_tail(&r->prod, prod_head, prod_next, is_sp, 1);
end:
	if (free_space != NULL)
		*free_space = free_entries - n;
	return n;
}

出队列

出队列操作流程和入队列流程基本一致,也是分为三部分,不过操作的是cons指针

  1. 更新r->cons.head

  2. 将obj从r指定位置移除

  3. 更新r->cons.tail

static __rte_always_inline unsigned int
__rte_ring_do_dequeue(struct rte_ring *r, void **obj_table,
		 unsigned int n, enum rte_ring_queue_behavior behavior,
		 unsigned int is_sc, unsigned int *available)
{
	uint32_t cons_head, cons_next;
	uint32_t entries;

    /* 更新r->cons.head */
	n = __rte_ring_move_cons_head(r, (int)is_sc, n, behavior,
			&cons_head, &cons_next, &entries);
	if (n == 0)
		goto end;

	DEQUEUE_PTRS(r, &r[1], cons_head, obj_table, n, void *);

    /* 更新r->cons.tail */
	update_tail(&r->cons, cons_head, cons_next, is_sc, 0);

end:
	if (available != NULL)
		*available = entries - n;
	return n;
}

ring衍生出来的api

rte_ring_mp_enqueue_bulk         多生产者批量入队列,数量要求固定
rte_ring_sp_enqueue_bulk         单生产者批量入队列,数量要求固定
rte_ring_enqueue_bulk            生产者批量入队列,同时支持单生产和多生产,数量要求固定
rte_ring_mp_enqueue              多生产者单次入队列
rte_ring_sp_enqueue              单生产者单次入队列
rte_ring_enqueue                 生产者单次入队列,同时支持单生产和多生产
                                 
rte_ring_mc_dequeue_bulk         多消费者批量出队列,数量要求固定
rte_ring_sc_dequeue_bulk         单消费者批量出队列,数量要求固定
rte_ring_dequeue_bulk            消费者批量出队列,同时支持单消费和多消费,数量要求固定
rte_ring_mc_dequeue              多消费者单次出队列
rte_ring_sc_dequeue              单消费者单次出队列
rte_ring_dequeue                 消费者单次出队列,同时支持单消费和多消费
                                 
rte_ring_count                   已经加入的obj数量
rte_ring_free_count              剩余容量
rte_ring_full                    判断ring是否已经满了,没有容量了
rte_ring_empty                   判断ring是否为空
rte_ring_get_size                获取ring的大小
rte_ring_get_capacity            获取ring的最大容量
rte_ring_list_dump               打印系统所有ring的信息
rte_ring_lookup                  根据名字查找ring是否存在
rte_ring_mp_enqueue_burst        多生产者批量入队列,如果ring可用容量不够,加入最大能加入的数量
rte_ring_sp_enqueue_burst        单生产者批量入队列,如果ring可用容量不够,加入最大能加入的数量
rte_ring_enqueue_burst           生产者批量入队列,同时支持单生产和多生产,如果ring可用容量不够,加入最大能加入的数量
rte_ring_mc_dequeue_burst        多消费者批量出队列,如果ring可用容量不够,加入最大能加入的数量
rte_ring_sc_dequeue_burst        单消费者批量出队列,如果ring可用容量不够,加入最大能加入的数量
rte_ring_dequeue_burst           消费者批量出队列,同时支持单消费和多消费,如果ring可用容量不够,加入最大能加入的数量

已知问题

rte_ring是不可抢占的,如果遇到多任务抢占的情况下,可能导致死锁。

  1. 在同一个ring执行多生产者入队列操作的时候,该pthread不能被在同一环上执行多生产者队列的其他pthread抢占
  2. 在同一个ring执行多消费者出队列操作的时候,该pthread不能被另一个在同一环上执行多消费者出队列操作的pthread抢占
    如果出现以上两种情况,可能会导致第二个pthread旋转,直到第一个pthread再次被调度。甚至如果第一个pthread被具有更高优先级的上下文抢占,它甚至可能导致死锁。

使用ring的时候,需要谨慎考虑任务的抢占,单生产和单消费都是支持抢占的,但是多生产或者多消费在抢占情况下会有概率出现死锁。

 

-----------------------------------------------------------------------------------------------------------------------------------------------------------------------

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

ring的特点:

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

使用方法:

1.创建一个ring对象。
接口:struct rte_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


在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;
}


同生产者一个道理,代码中加了点注释,就不详细解释了。

<think>我们被要求详细解释`rte_ring_dump`函数,包括其功能、用法示例。`rte_ring_dump`是DPDK中用于调试环形队列(ring)的函数,它可以将环形队列的内部状态输出到文件或标准输出。 ### 功能 `rte_ring_dump`函数用于打印环形队列的详细内部状态,包括: - 队列的名称 - 队列的容量当前使用情况 - 生产者消费者的头尾指针位置 - 队列的同步模式(单生产者/多生产者,单消费者/多消费者) ### 函数原型 ```c void rte_ring_dump(FILE *f, const struct rte_ring *r); ``` - `f`: 文件指针,指定输出位置(如`stdout`表示标准输出) - `r`: 指向要检查的环形队列的指针 ### 使用场景 当怀疑环形队列出现异常(如死锁、数据不一致、指针错乱)时,使用此函数可以快速获取队列的当前状态。 ### 使用示例 ```c #include <stdio.h> #include <rte_ring.h> int main() { struct rte_ring *my_ring = rte_ring_create("my_ring", 1024, rte_socket_id(), RTE_RING_F_SP_ENQ | RTE_RING_F_SC_DEQ); if (my_ring == NULL) { rte_exit(EXIT_FAILURE, "无法创建环形队列\n"); } // 假设队列进行了一些操作后,我们想检查其状态 rte_ring_dump(stdout, my_ring); // 将队列状态输出到控制台 return 0; } ``` ### 输出内容详解 调用`rte_ring_dump`会输出类似以下信息: ``` ring <my_ring>@0x7f8b1a02d2c0 flags=0x0003 RING_F_SP_ENQ: 单生产者模式 RING_F_SC_DEQ: 单消费者模式 size=1024 capacity=1023 ct=0 producer.head=0, tail=0 consumer.head=0, tail=0 ``` - `flags`: 队列的创建标志,如`RING_F_SP_ENQ`(单生产者)`RING_F_SC_DEQ`(单消费者) - `size`: 队列的总大小(实际可用大小为size-1) - `capacity`: 实际可存储的元素个数(等于size-1) - `ct`: 当前队列中的元素个数(计数模式启用时有效,默认不启用) - `producer.head`, `producer.tail`: 生产者的头尾指针 - `consumer.head`, `consumer.tail`: 消费者的头尾指针 ### 注意事项 1. **调试开销**:此函数会遍历队列的元数据,在性能关键路径上调用可能影响性能,建议仅在调试时使用。 2. **线程安全**:该函数内部不锁定队列,因此在多线程环境下,输出的状态可能是中间状态(但通常仍可用于分析问题)。 3. **计数模式**:如果创建队列时指定了`RING_F_EXACT_SZ`标志,则队列容量会调整为2的幂次,此时`size``capacity`会不同。 ### 实际应用案例 假设在开发中发现某个队列无法入队,可以这样排查: ```c // 入队失败时打印队列状态 if (rte_ring_enqueue_burst(ring, obj, 1) == 0) { fprintf(stderr, "入队失败!当前队列状态:\n"); rte_ring_dump(stderr, ring); // 输出到标准错误 } ``` ### 相关函数 - `rte_ring_list_dump`: 打印程序中的所有环形队列列表 - `rte_ring_lookup`: 通过名称查找环形队列 §§相关问题§§ 1. `rte_ring_dump`的输出中`ct`为0是什么原因? 2. 如何在线程安全的情况下获取环形队列的当前元素数量? 3. `rte_ring`在什么情况下会返回`ENOBUFS`错误? 4. 多生产者模式下的环形队列是如何避免冲突的? 5. 如何配置环形队列以支持计数功能(精确统计元素个数)? </think> ### `rte_ring_dump` 函数详解 `rte_ring_dump` 是 DPDK 中用于**诊断环形队列(ring)状态**的关键调试函数。它将环形队列的内部元数据(如头尾指针、元素计数、同步标志等)以可读格式输出,帮助开发者快速定位队列异常问题。以下从功能到实操全面解析: --- ### 一、核心功能与输出结构 **函数原型**: ```c void rte_ring_dump(FILE *f, const struct rte_ring *r); ``` - **`f`**:输出目标(如 `stdout`/`stderr` 或文件指针) - **`r`**:待诊断的环形队列指针 **典型输出示例**: ```bash ring <my_ring>@0x7f8b1a02d2c0 flags=0x0003 # 队列标志位 RING_F_SP_ENQ: 单生产者模式 RING_F_SC_DEQ: 单消费者模式 size=1024 # 队列总容量 capacity=1023 # 实际可用容量(通常为 size-1) ct=0 # 当前元素计数(需启用计数功能) producer.head=0 # 生产者头指针 producer.tail=0 consumer.head=0 # 消费者头指针 consumer.tail=0 ``` --- ### 二、关键输出字段解析 | **字段** | **说明** | |-------------------|--------------------------------------------------------------------------| | `flags` | 队列同步模式标志(十六进制),例如 `0x0003` 表示单生产者(`SP`) + 单消费者(`SC`) | | `size` | 队列的总槽位数(创建时指定) | | `capacity` | 实际可用槽位数($size - 1$,因环形队列需保留一个空位)[^1] | | `ct` | 当前队列中元素数量(仅当创建时启用 `RING_F_EXACT_SZ` 标志时有效) | | `producer.head/tail` | 生产者的头/尾指针(反映数据写入位置) | | `consumer.head/tail` | 消费者的头/尾指针(反映数据读取位置) | > 指针异常示例:若发现 `producer.tail` 与 `consumer.head` 差值超过 `capacity`,表明存在指针越界。 --- ### 三、使用场景与示例 #### 1. 基础诊断(控制台输出) ```c #include <rte_ring.h> struct rte_ring *ring = rte_ring_lookup("MY_RING"); if (ring != NULL) { rte_ring_dump(stdout, ring); // 输出到标准输出 } ``` #### 2. 结合错误处理(日志记录) ```c if (rte_ring_enqueue_burst(ring, objs, count) == 0) { FILE *log = fopen("/tmp/ring_error.log", "a"); fprintf(log, "Enqueue failed! Ring state:\n"); rte_ring_dump(log, ring); // 记录队列状态到文件 fclose(log); } ``` #### 3. 多队列批量诊断 ```c void dump_all_rings() { struct rte_ring *ring = NULL; RTE_RING_FOREACH(ring) { // 遍历所有已创建的队列 printf("==== Ring <%s> ====\n", ring->name); rte_ring_dump(stdout, ring); } } ``` --- ### 四、诊断指针损坏的典型场景 通过 `rte_ring_dump` 可识别以下异常: 1. **死锁状态** - 现象:`producer.tail - consumer.head == capacity`(队列满)但消费者未取数据 - 原因:消费者线程卡死或逻辑错误 2. **指针越界** - 现象:`producer.tail > size` 或 `consumer.head > size` - 原因:内存越界写入破坏队列元数据 3. **模式冲突** - 现象:`flags` 显示 `MP`(多生产者)但实际为单线程操作 - 修复:重建队列 `rte_ring_create(..., RTE_RING_F_SP_ENQ)` --- ### 五、注意事项 1. **性能影响** 函数涉及锁操作文件I/O,**禁止在数据面高频调用**,仅用于调试。 2. **线程安全** 输出期间不锁定队列,诊断结果可能是**瞬时快照**(需结合日志时间戳分析)。 3. **计数模式限制** 字段 `ct`(元素计数)需创建队列时指定标志: ```c rte_ring_create("count_ring", size, socket_id, RING_F_EXACT_SZ); ``` --- ### 六、扩展工具 - **`rte_ring_list_dump`**:打印所有已创建队列的摘要信息(名称、大小、内存地址) - **GDB 脚本**:离线分析队列内存(需配合 DPDK 符号表) ```gdb dpdk dump ring <ring_addr> # 自定义 GDB 命令 ```
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值