前言
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)被修改。这个初始状态是有一个生产者的头和尾指向了相同的位置。
- 第一步
将r->prod.head保存到变量的prod_head, prod_next = prod_head + n,如下图
(在代码里面的话,临时变量prod_head = old_head. prod_next = new_head)
- 第二步
修改prod_head去指向prod_next指向的位置。 指向新增加对象的指针被拷贝到ring(obj4)。
- 第三步
一旦这个对象被增加到环形缓冲区中,prod_tail将要被修改成prod_head指向的位置。 至此, 这入队操作完成了。

单消费者出队
当只有一个消费者出队操作在环形缓冲区,这个时候只有一个消费者头和尾(cons_head和cons_tail)被修改并且这只有一个消费者。初始状态是一个消费者的头和尾指向了相同的位置。
- 第一步
首先,暂时将消费者的头索引和生产者的尾部索引交给临时变量,并且将cons_next指向表中下一个对象,如果在这环形缓冲区没有足够的对象,将返回一个错误。
- 第二步
第二步是修改cons_head去指向cons_next指向的位置,并且指向出队对象(obj1)的指针被拷贝到一个临时用户定义的指针中。
- 第三步
最后,cons_tail被修改成指向cons_head指向的位置。至此,单消费者的出队操作完成了。
多生产者入队
- 初始状态下,环形缓冲区的状态是prod_head和prod_tail都指向同一个位置,这个时候core1和core2都保存prod_head到各自的临时变量,如果core1和core2是同时执行这个动作的话,他们的临时变量保存的prod_head和prod_next应该完全相同。如下图所示:

- 在core1上面执行cas,比较prod_head和临时变量里面的prod_head,如果相同则把prod_head移动到prod_next,这个时候再core2也是执行完全一下的动作,由于使用的是cas,原子操作的比较和设置,只能有一个是成功的,这里如果是core1成功的话,那么core2的cas就是失败的。
所以这个时候ring里面的prod_head移动到core1的prod_next这个位置了并填充obj4到ring里面。
- core2在执行第一次cas的时候失败了,这个时候再执行一次,重新update临时变量里面的prod_head和prod_next,由于core1已经cas成功,那么这个时候core2 update到的这两个值就相应的更新了,这个时候再执行cas,则能成功,所以这个时候ring里面的prod_head移动到core2的prod_next这个位置并填充obj5到ring里面。

- core1在更新完ring的prod_head和obj后,这个时候临时变量里面保存的prod_head和prod_next是固定的,这个时候需要借助这两个值,当ring里面的prod_tail == prod_head(临时变量)的时候,即可直接更新prod_tail = prod_next, 这里的更新tail条件是唯一的,否则不能更新。

- 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的函数流程如上,具体我在代码里面加了每个操作的注释,主要分为以下几个流程:
- 确定count数量,需要满足是2的指数次方
- 申请内存,具体ring size=sizeof(struct rte_ring) + count * sizeof(void *),通过rte_memzone_reserve_aligned申请内存
- 申请tailq节点,加入rte_ring_tailq链表里面,用来保存ring,方便定位查看系统存在的ring
- 使用rte_ring_init初始化ring结构体
入队列
入队列操作主要分为三个流程
- 更新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;
}
- 将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)
- 更新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指针
-
更新r->cons.head
-
将obj从r指定位置移除
-
更新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是不可抢占的,如果遇到多任务抢占的情况下,可能导致死锁。
- 在同一个ring执行多生产者入队列操作的时候,该pthread不能被在同一环上执行多生产者队列的其他pthread抢占
- 在同一个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;
}
同生产者一个道理,代码中加了点注释,就不详细解释了。
本文深入解析DPDK的无锁环形队列Ring,介绍其借鉴自Linux内核kfifo的设计原理,探讨单/多生产者及消费者场景下的无锁操作机制,分析核心代码实现细节。
1086

被折叠的 条评论
为什么被折叠?



