昨天参加了一个面试,面试官问我dpdk的ring是怎么解决冲突的,我当时回答的不好,现在来总结下~
1.无锁的实现
既然是解决冲突,肯定至少有两个生产者或者两个消费者,以两个生产者为例。
core1和core2同时往ring中写入数据,对于core1来说,首先会在入队函数中定义局部变量prod_head和prod_next和cons_tail,prod_head由r->prod.head赋值,cons_tail由r->cons.tail赋值。在判断当前r还有无空间满足写入需求之后,会用一个局部变量new_head来指示完成数据入队之后r->prod.head所处的位置。接下来就是很关键的一步,使用__atomic_compare_exchange_n指令,来判断写入数据的这一刻是否发生冲突,如果有冲突就返回失败,重新进行局部变量prod_head的赋值以后的操作。如果没有冲突就写入数据,同时更新r->prod.head。
简单说下__atomic_compare_exchange_n这个指令:
首先这个指令是原子操作,叫CAS指令(比较并交换),bool __atomic_compare_exchange_n (type *ptr, type *expected, type desired, bool weak, int success_memorder, int failure_memorder)
ptr为要操作的指针,expected是期待的值,如果ptr和expected不相同,则指令返回失败,如果相同,则将ptr更新,更新值为desired。
core2也是同理,简单来说,就是利用CAS指令来保证每次写入数据时,写入线程读取到的r->prod.head就是当前最新的,在写入过程中,其他线程不会写入。
贴下代码和注释
//入队api
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;
//解决多生产者的冲突问题
n = __rte_ring_move_prod_head(r, is_sp, n, behavior,
&prod_head, &prod_next, &free_entries);
if (n == 0)
goto end;
//元素入队。此时就不需要考虑冲突了,因为r->prod.head已经移动了n位,为本次数据写入空出了n位
//其他线程读取r->prod.head已经是新的位置了
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;
}
//核心函数
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,也就是获取当前时刻环形队列的写入位置
*old_head = __atomic_load_n(&r->prod.head,
__ATOMIC_ACQUIRE);
//cons_tail是用来辅助判断当前空间能否写入n个obj
const uint32_t cons_tail = r->cons.tail;
//获取空闲位置数
*free_entries = (capacity + 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的值代表着写入n个object之后新的头部会移动到什么位置上
*new_head = *old_head + n;
if (is_sp)
r->prod.head = *new_head, success = 1;
else
//调用CAS指令,如果此时r->prod.head和之前获取的头位置一致,就意味着没有冲突,此时更新
//r->prod.head的位置。但是数据的写入还是在__rte_ring_do_enqueue
success = __atomic_compare_exchange_n(&r->prod.head,
old_head, *new_head,
0, __ATOMIC_ACQUIRE,
__ATOMIC_RELAXED);
} while (unlikely(success == 0));
return n;
}
2.零拷贝的实现
零拷贝的实现可以用“狸猫换太子”来形容。环形队列是用数组来实现的,数组里面存储的是指向存储报文的内存地址。当收包的时候,会另外申请一个buffer,然后对buffer进行一系列操作,包括dma映射,随后将指向这个buffer的指针替换环形队列中的指针,在报文的生存周期内,上层操作的指针始终是指向这个原始报文的内存,这样就完成了零拷贝。
总的来说,环形队列的主要操作就是零拷贝和多生产者多消费者的入队出队操作。零拷贝的核心思想就是在队列中替换指向新申请buffer的指针,冲突解决的核心就是利用CAS指令,用来保证本次操作队列的正确性和唯一性。