深入理解dpdk rte_ring无锁队列

 

 

一、简介

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

ring的特点:

 

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

 

使用方法:

1.创建一个ring对象。

接口:structrte_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

 
  1. struct rte_ring {

  2. /*

  3. * Note: this field kept the RTE_MEMZONE_NAMESIZE size due to ABI

  4. * compatibility requirements, it could be changed to RTE_RING_NAMESIZE

  5. * next time the ABI changes

  6. */

  7. char name[RTE_MEMZONE_NAMESIZE]; /**< Name of the ring. */

  8. int flags; /**< Flags supplied at creation. */

  9. const struct rte_memzone *memzone;

  10. /**< Memzone, if any, containing the rte_ring */

  11.  
  12. /** Ring producer status. */

  13. struct prod {

  14. uint32_t watermark; /**< Maximum items before EDQUOT. */

  15. uint32_t sp_enqueue; /**< True, if single producer. */

  16. uint32_t size; /**< Size of ring. */

  17. uint32_t mask; /**< Mask (size-1) of ring. */

  18. volatile uint32_t head; /**< Producer head. cgm 预生产到地方*/

  19. volatile uint32_t tail; /**< Producer tail. cgm 实际生产了的数量*/

  20. } prod __rte_cache_aligned;

  21.  
  22. /** Ring consumer status. */

  23. struct cons {

  24. uint32_t sc_dequeue; /**< True, if single consumer. */

  25. uint32_t size; /**< Size of the ring. */

  26. uint32_t mask; /**< Mask (size-1) of ring. */

  27. volatile uint32_t head; /**< Consumer head. cgm 预出队的地方*/

  28. volatile uint32_t tail; /**< Consumer tail. cgm 实际出队的地方*/

  29. #ifdef RTE_RING_SPLIT_PROD_CONS

  30. } cons __rte_cache_aligned;

  31. #else

  32. } cons;

  33. #endif

  34.  
  35. #ifdef RTE_LIBRTE_RING_DEBUG

  36. struct rte_ring_debug_stats stats[RTE_MAX_LCORE];

  37. #endif

  38.  
  39. void *ring[] __rte_cache_aligned; /**< Memory space of ring starts here.

  40. * not volatile so need to be careful

  41. * about compiler re-ordering */

  42. };

 

在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.入队流程

多生产者入队代码:

 
  1. static inline int __attribute__((always_inline))

  2. __rte_ring_mp_do_enqueue(struct rte_ring *r, void * const *obj_table,

  3. unsigned n, enum rte_ring_queue_behavior behavior)

  4. {

  5. uint32_t prod_head, prod_next;

  6. uint32_t cons_tail, free_entries;

  7. const unsigned max = n;

  8. int success;

  9. unsigned i, rep = 0;

  10. uint32_t mask = r->prod.mask;

  11. int ret;

  12.  
  13. /* Avoid the unnecessary cmpset operation below, which is also

  14. * potentially harmful when n equals 0. */

  15. if (n == 0)

  16. return 0;

  17.  
  18. /* move prod.head atomically

  19. cgm

  20. 1.检查free空间是否足够

  21. 2.cms生产预约*/

  22. do {

  23. /* Reset n to the initial burst count */

  24. n = max;

  25.  
  26. prod_head = r->prod.head;

  27. cons_tail = r->cons.tail;

  28. /* The subtraction is done between two unsigned 32bits value

  29. * (the result is always modulo 32 bits even if we have

  30. * prod_head > cons_tail). So 'free_entries' is always between 0

  31. * and size(ring)-1. */

  32. /**cgm mask+cons_tail+1到下一个窗口中*/

  33. free_entries = (mask + cons_tail - prod_head);

  34.  
  35. /* check that we have enough room in ring */

  36. if (unlikely(n > free_entries)) {

  37. if (behavior == RTE_RING_QUEUE_FIXED) {

  38. __RING_STAT_ADD(r, enq_fail, n);

  39. return -ENOBUFS;

  40. }

  41. else {

  42. /* No free entry available */

  43. if (unlikely(free_entries == 0)) {

  44. __RING_STAT_ADD(r, enq_fail, n);

  45. return 0;

  46. }

  47.  
  48. n = free_entries;

  49. }

  50. }

  51.  
  52. prod_next = prod_head + n;

  53. success = rte_atomic32_cmpset(&r->prod.head, prod_head,

  54. prod_next);

  55. } while (unlikely(success == 0));

  56.  
  57. /* write entries in ring */

  58. ENQUEUE_PTRS();

  59. rte_smp_wmb();

  60.  
  61. /* if we exceed the watermark

  62. cgm 检查是否到了阈值,并添加到统计中*/

  63. if (unlikely(((mask + 1) - free_entries + n) > r->prod.watermark)) {

  64. ret = (behavior == RTE_RING_QUEUE_FIXED) ? -EDQUOT :

  65. (int)(n | RTE_RING_QUOT_EXCEED);

  66. __RING_STAT_ADD(r, enq_quota, n);

  67. }

  68. else {

  69. ret = (behavior == RTE_RING_QUEUE_FIXED) ? 0 : n;

  70. __RING_STAT_ADD(r, enq_success, n);

  71. }

  72.  
  73. /*

  74. * If there are other enqueues in progress that preceded us,

  75. * we need to wait for them to complete

  76. cgm 等待之前的入队操作完成

  77. */

  78. while (unlikely(r->prod.tail != prod_head)) {

  79. rte_pause();

  80.  
  81. /* Set RTE_RING_PAUSE_REP_COUNT to avoid spin too long waiting

  82. * for other thread finish. It gives pre-empted thread a chance

  83. * to proceed and finish with ring dequeue operation. */

  84. if (RTE_RING_PAUSE_REP_COUNT &&

  85. ++rep == RTE_RING_PAUSE_REP_COUNT) {

  86. rep = 0;

  87. sched_yield();

  88. }

  89. }

  90. r->prod.tail = prod_next;

  91. return ret;

  92. }

 

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.出队流程

多消费者出队代码:

 
  1. static inline int __attribute__((always_inline))

  2. __rte_ring_mc_do_dequeue(struct rte_ring *r, void **obj_table,

  3. unsigned n, enum rte_ring_queue_behavior behavior)

  4. {

  5. uint32_t cons_head, prod_tail;

  6. uint32_t cons_next, entries;

  7. const unsigned max = n;

  8. int success;

  9. unsigned i, rep = 0;

  10. uint32_t mask = r->prod.mask;

  11.  
  12. /* Avoid the unnecessary cmpset operation below, which is also

  13. * potentially harmful when n equals 0. */

  14. if (n == 0)

  15. return 0;

  16.  
  17. /* move cons.head atomically

  18. cgm

  19. 1.检查可消费空间是否足够

  20. 2.cms消费预约*/

  21. do {

  22. /* Restore n as it may change every loop */

  23. n = max;

  24.  
  25. cons_head = r->cons.head;

  26. prod_tail = r->prod.tail;

  27. /* The subtraction is done between two unsigned 32bits value

  28. * (the result is always modulo 32 bits even if we have

  29. * cons_head > prod_tail). So 'entries' is always between 0

  30. * and size(ring)-1. */

  31. entries = (prod_tail - cons_head);

  32.  
  33. /* Set the actual entries for dequeue */

  34. if (n > entries) {

  35. if (behavior == RTE_RING_QUEUE_FIXED) {

  36. __RING_STAT_ADD(r, deq_fail, n);

  37. return -ENOENT;

  38. }

  39. else {

  40. if (unlikely(entries == 0)){

  41. __RING_STAT_ADD(r, deq_fail, n);

  42. return 0;

  43. }

  44.  
  45. n = entries;

  46. }

  47. }

  48.  
  49. cons_next = cons_head + n;

  50. success = rte_atomic32_cmpset(&r->cons.head, cons_head,

  51. cons_next);

  52. } while (unlikely(success == 0));

  53.  
  54. /* copy in table */

  55. DEQUEUE_PTRS();

  56. rte_smp_rmb();

  57.  
  58. /*

  59. * If there are other dequeues in progress that preceded us,

  60. * we need to wait for them to complete

  61. cgm 等待之前的出队操作完成

  62. */

  63. while (unlikely(r->cons.tail != cons_head)) {

  64. rte_pause();

  65.  
  66. /* Set RTE_RING_PAUSE_REP_COUNT to avoid spin too long waiting

  67. * for other thread finish. It gives pre-empted thread a chance

  68. * to proceed and finish with ring dequeue operation. */

  69. if (RTE_RING_PAUSE_REP_COUNT &&

  70. ++rep == RTE_RING_PAUSE_REP_COUNT) {

  71. rep = 0;

  72. sched_yield();

  73. }

  74. }

  75. __RING_STAT_ADD(r, deq_success, n);

  76. r->cons.tail = cons_next;

  77.  
  78. return behavior == RTE_RING_QUEUE_FIXED ? 0 : n;

  79. }

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

 

 

 

<think>嗯,用户之前让我分析了一个关于`vhost_rdma_scheduler`函数的代码,现在又接着问`rte_ring_dequeue`这个函数。看起来他可能是在深入理解DPDK中的形队列操作,特别是在RDMA虚拟化境中的应用。我需要先回顾一下之前的内容,确保现在的回答和之前的分析保持一致。 首先,用户之前提到的代码中,`rte_ring_dequeue`被用来从`rx_ring`和`task_ring`中取出数据包和任务。这时候用户可能想了解这个函数的具体工作原理,或者在使用时需要注意的地方。我需要解释清楚`rte_ring_dequeue`的功能、参数、返回值,以及它在多线程境下的行为。 接下来,我需要考虑用户可能的背景。他可能在开发或优化基于DPDK的高性能网络应用,特别是涉及到RDMA的部分。因此,了解`rte_ring_dequeue`的性能特点、线程安全机制以及正确使用方式对他来说非常重要。用户可能关心如何高效地从形队列中取出元素,避免竞争条件和数据乱序的问题。 用户的问题直接指向`rte_ring_dequeue`,但可能隐含想知道如何在多线程境中正确使用它,尤其是在接收数据包和处理任务时。之前的代码分析中提到,接收数据包的单线程设计是为了避免乱序,这可能和`rte_ring_dequeue`的多生产者/消费者模式有关。需要解释为什么在接收数据包时只能用单线程,而任务处理可能允许多线程。 然后,我需要确保回答结构清晰,符合用户之前要求的逐步解析。可能需要先介绍函数的基本信息,包括参数、返回值,然后深入其内部机制,比如无锁队列的实现,多线程下的行为,最后给出使用注意事项和示例,帮助用户理解如何正确应用这个函数。 另外,用户可能对DPDK形队列实现不太熟悉,所以需要简要解释`rte_ring`的设计,比如它是如何实现高效的无锁操作的,生产者和消费者的同步机制,以及不同的部署模式(单生产者/单消费者,多生产者/多消费者)对性能的影响。 还需要注意用户提到的日志和原子操作,比如在任务处理中使用`rte_atomic16_clear`,这可能和任务状态的同步有关。在解释`rte_ring_dequeue`时,要强调其线程安全性,以及如何与原子操作配合使用,确保数据的一致性和正确性。 最后,总结部分需要回顾`rte_ring_dequeue`在整个调度器中的作用,强调其在数据包接收和任务处理中的关键地位,以及正确使用该函数对系统性能和稳定性的重要性。可能还要提到常见的错误使用场景,比如未检查返回值导致的问题,或者在多线程境中错误配置生产者和消费者模式,从而引发的性能下降或数据竞争。 总之,需要全面覆盖`rte_ring_dequeue`的功能、实现原理、使用方法和注意事项,结合用户之前的代码分析,给出有针对性的解释,帮助用户深入理解并正确应用这一函数。</think>以下是关于DPDK中`rte_ring_dequeue`函数的详细解析: --- ### **1. 函数基础信息** - **函数原型** ```c int rte_ring_dequeue(struct rte_ring *r, void **obj); ``` - **功能**: 从无锁形队列(`rte_ring`)中取出一个元素。 - **参数**: - `r`: 指向`rte_ring`队列的指针。 - `obj`: 输出参数,用于存储取出的元素地址。 - **返回值**: - **0**: 成功取出元素。 - **-ENOENT**: 队列为空,无法取出。 - **其他错误码**: 参数非法或队列未初始化。 --- ### **2. 核心设计:无锁形队列** #### **`rte_ring`结构** DPDK的`rte_ring`是一种高性能无锁队列,支持多生产者/多消费者(MPMC)或单生产者/单消费者(SPSC)模式。其核心特性包括: - **内存预分配**: 队列容量固定,初始化时分配内存。 - **原子操作**: 使用`rte_atomic`指令实现线程安全。 - **高吞吐低延迟**: 避免锁竞争,适合高并发场景。 #### **内部实现** - **生产者/消费者指针**: - `prod_head`/`prod_tail`: 生产者的头尾指针。 - `cons_head`/`cons_tail`: 消费者的头尾指针。 - **CAS操作**: 通过`Compare-And-Swap`(CAS)更新指针,确保原子性。 --- ### **3. `rte_ring_dequeue`工作流程** 1. **检查队列是否为空** 若消费者头指针(`cons_head`)与生产者尾指针(`prod_tail`)重合,则队列为空,返回`-ENOENT`。 2. **预取数据** 若启用预取(如`RTE_RING_QUEUE_FIXED`模式),将数据加载到缓存。 3. **更新消费者指针** 使用CAS操作将`cons_head`向前移动,若失败则重试(自旋锁机制)。 4. **返回元素地址** 通过`obj`参数返回元素指针,并更新`cons_tail`完成出队。 --- ### **4. 多线程行为** - **消费者模式**: - **单消费者(RTE_RING_F_SC_DEQ)**: 仅允许单一线程调用`rte_ring_dequeue`。 - **多消费者(默认)**: 允许多线程并发调用。 - **线程安全**: - 无锁设计通过原子操作保证线程安全。 - 若队列配置为单消费者模式,多线程调用会导致未定义行为。 --- ### **5. 典型使用场景** #### **示例代码** ```c struct rte_ring *rx_ring = dev->rx_ring; struct rte_mbuf *pkt = NULL; // 从队列中取出数据包 if (rte_ring_dequeue(rx_ring, (void **)&pkt) == 0) { process_packet(pkt); } else { // 队列为空,需等待或重试 } ``` #### **应用场景** 1. **网络包接收** 如`vhost_rdma_scheduler`中从`rx_ring`取出数据包。 2. **任务调度** 从任务队列(如`task_ring`)中取出任务执行。 3. **零拷贝数据传输** 生产者填充数据,消费者直接通过指针访问。 --- ### **6. 性能优化与注意事项** #### **优化技巧** - **批量操作**: 使用`rte_ring_dequeue_burst`批量取出元素,减少CAS操作次数。 - **缓存对齐**: 队列内存按缓存行对齐,避免伪共享。 - **模式选择**: 根据实际场景选择SPSC或MPMC模式(单消费者模式性能更高)。 #### **常见问题** 1. **返回值未检查** 忽略`-ENOENT`可能导致空指针异常。 ```c // 错误示例:未检查返回值 rte_ring_dequeue(ring, &obj); // 若队列为空,obj可能为非法地址 ``` 2. **模式不匹配** 初始化队列时若未正确设置标志(如单消费者模式多线程调用),会导致数据竞争。 3. **内存泄漏** 取出数据包(如`rte_mbuf`)后需手动释放内存。 --- ### **7. 与`rte_ring_enqueue`的对比** | 特性 | `rte_ring_dequeue` | `rte_ring_enqueue` | |---------------------|----------------------------------------|----------------------------------------| | **方向** | 消费者取出元素 | 生产者插入元素 | | **指针操作** | 移动`cons_head`/`cons_tail` | 移动`prod_head`/`prod_tail` | | **返回值** | 0(成功)、-ENOENT(空队列) | 0(成功)、-ENOBUFS(队列满) | | **多线程支持** | 依赖`RTE_RING_F_SC_DEQ`或默认多消费者 | 依赖`RTE_RING_F_SP_ENQ`或默认多生产者 | --- ### **8. 总结** - **核心作用**: `rte_ring_dequeue`是DPDK无锁队列的核心操作,用于高效、线程安全地消费数据。 - **适用场景**: 高并发网络处理、任务调度、零拷贝数据传输等。 - **关键设计**: 原子指针操作(CAS)、批量处理优化、多模式支持。 - **注意事项**: 检查返回值、匹配队列模式、及时释放资源。 通过合理使用`rte_ring_dequeue`,可以显著提升数据面应用(如RDMA虚拟化、NFV等)的吞吐量和响应速度。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值