在了解无锁队列之前,需要了解一个知识点:CAS(Compare and Swap,比较并替换)原子指令,用来保障数据的一致性。指令有三个参数,当前内存值V、旧的预期值A、更新的值B,当且仅当预期值A和内存值V相同时,将内存值修改为B并返回true,否则什么都不做,并返回false。函数原型如下:
rte_atomic32_cmpset(volatile uint32_t *dst, uint32_t exp, uint32_t src)
当dst指向内存的值和exp的值相等时,将src的值更新给dst。对于无锁队列来说,比较复杂的是多生产者和多消费者的这种情况,现在就以多生产者为例,多消费者的情况和多生产者相似,先以图形讲解
1、图解无锁队列
(1)ring的pro_head、pro_tail赋值给本地pro_head,pro_next指向表的下一个元素。
(2)这个操作是一个核心,多个生产者进入到这一步,会有CAS的一个操作,ring.prod_head和本地pro_head进行比较, CAS 操作失败,代码重走第一步。如下图所示,core1比较成功,则将新的位置索引赋值给ring.prod_head,core2由于比较失败,所以core2重新走第一步,prod_next遍历下一个环的位置,prod_head遍历同样遍历到下一个位置。
(3) 在核心 2 上重试 CAS 操作并成功。核心 1 更新环的一个元素 (obj4),核心 2 更新另一个元素 (obj5)。
(4) 现在,每个内核都想要更ring.prod_tail。 只有当ring.prod_tail等于prod_head局部变量时,内核才能更新它。 这仅适用于核心 1。该操作在核心 1 上完成
(5) 一旦核心 1 更新了ring.prod_tail,核心 2 也可以更新它。 该操作也在核心 2 上完成。
2、代码解析无锁队列
/*该函数表示多生产者的情况调用的核心__rte_ring_do_enqueue*/
static __rte_always_inline unsigned int
rte_ring_mp_enqueue_bulk(struct rte_ring *r, void * const *obj_table,
unsigned int n, unsigned int *free_space)
{
return __rte_ring_do_enqueue(r, obj_table, n, RTE_RING_QUEUE_FIXED,
__IS_MP, free_space);
}
__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;
n = __rte_ring_move_prod_head(r, is_sp, n, behavior,
&prod_head, &prod_next, &free_entries);
if (n == 0)
goto end;
ENQUEUE_PTRS(r, &r[1], prod_head, obj_table, n, void *);
update_tail(&r->prod, prod_head, prod_next, is_sp, 1);
end:
if (free_space != NULL)
*free_space = free_entries - n;
return n;
}
/*这个函数对应上图讲的第二步,更新ring.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).
*/
*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;
if (is_sp)
r->prod.head = *new_head, success = 1;
else/*由于是多生产者,调用rte_atomic32_cmpset比较r->prod.head和old_head的值,因为无锁队列是全局的,每个核都有机会获取核的机会,当没有抢占到的核用r->prod.head和old_head比较时,不一致,就会while等待,直到当前队列更新new_head,这个时候下一个核终于有机会占有这个队列,更新自己的节点*/
success = rte_atomic32_cmpset(&r->prod.head,
*old_head, *new_head);
/*相同则返回true,不相同则返回0,继续do while循环*/
} while (unlikely(success == 0));
return n;
}
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
*/
/*如果ht tail和old_value不等时,一直等待,直到其它流程处理完之后,更新自己的tail,这个就是图解的最后几步*/
if (!single)
while (unlikely(ht->tail != old_val))
rte_pause();
ht->tail = new_val;
}