DPDK中实现了无锁环形缓冲区,支持单生产者或者多生产者入队列,单消费者或多消费者出队列。下面会记录dpdk中是如何管理所有使用的无锁环形缓冲区以及无锁环形缓冲区中支持的一些操作。
无锁环形缓冲区的组织
DPDK中定义的无锁环形缓冲区对象如下:
1 | struct rte_ring { |
从上面的定义可以看出,无锁环形缓冲区对象中定义了一个生产者对象和一个消费者对象,对应的也就是缓冲区的写对象和读对象。另外,也可以看出无锁环形缓冲区在内存中的组织形式是前面是无锁环形缓冲区对象本身,然后紧接着就是实际用于存储内容的环形队列,在某一时刻其内存布局如下图所示:
无锁环形缓冲区是一种通用的数据结构,所以可能会在多个地方使用,在dpdk中就会有多种情况会使用,比如内存池。所以随之而来的一个疑问就是dpdk是如何管理其所使用的所有的无锁环形缓冲区的呢?从它的源码实现(rte_ring.c/rte_ring.h)中我们可以找到答案,与此相关的部分源码如下:
1 | TAILQ_HEAD(rte_ring_list, rte_tailq_entry); |
从上面的源码我们可以知道,dpdk是用尾队列来管理其所使用的所有无锁环形缓冲区的,也就是说一个尾队列中的元素就是一个无锁环形缓冲区对象。那用来管理所有无锁环形缓冲区的尾队列,dpdk又是如何管理的呢?在函数rte_ring_create()中可以看到管理着无锁环形缓冲区的尾队列头部是存放在一个类型为struct rte_tailq_elem的全局变量rte_ring_tailq的head成员中,其中struct rte_tailq_elem定义如下:
1 | struct rte_tailq_elem { |
从struct rte_tailq_elem的定义可以看到,管理着无锁环形缓冲区尾队列的头部是另外一个尾队列的一个元素,类似的管理方式还用在了dpdk的内存池等数据结构中,所以在dpdk中采用了两级尾队列来管理所使用的数据结构。其实这样说也不完整,因为除了用二级尾队列来管理所使用的数据结构之外,dpdk还用了一个全局共享内存中的列表数组rte_config.mem_config->tailq_head[RTE_MAX_TAILQ]来分别存储这个二级尾队列中低维尾队列存放的元素,即管理某一种特定数据结构的尾队列头部。
无锁环形缓冲区的操作
记录完了dpdk中对无锁环形缓冲区的管理方式,接下来对dpdk中实现的无锁环形缓冲区所支持的操作做一个记录。从一开始说过无锁环形缓冲区支持单生产者或者多生产者入队列,单消费者或多消费者出队列等操作。其实从另外一个角度还可以说dpdk中的无锁环形缓冲区中支持两种出入队列的模式,即出入队列元素数目固定模式和尽力而为模式,出入队列元素数目固定模式就是说只有进入队列的元素数目达到指定数目才算操着成功,否则失败;而出入队列元素数目尽力而为模式就是说对于指定的数目,如果当时队列状态并不能满足,则以当时队列状态为准,尽可能满足指定的数目,举个例子,如果参数指定需要入队列3个元素,但队列中只剩下2个空闲空间,那么就将其中2个元素入队列,出队列情况同理。
下面以两个核同时往队列各写入一个元素来介绍无锁环形缓冲区支持的多生产者入队列功能,其他的操作方式都可以从这里推演出来。在代码中多生产者入队列相关的函数为__rte_ring_mp_do_enqueue(),下面的流程也是根据这个函数整理出来的。
1) 初始状态下生产者的头和尾指向了同一个位置。如下图所示: