深入理解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. }

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

 

 

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值