searchusermenu
  • 发布文章
  • 消息中心
点赞
收藏
评论
分享
原创

DPDK-URCU机制初探

2023-06-27 08:11:29
286
0

前言

 RCU 是一种内核同步机制,在2002年10月加入到 Linux 内核中。它属于“无锁编程”的范畴,实现的功能类似于“读写锁”。但是与“读写锁”相比,RCU有着显著的优势:

  1. 读者性能极高。特别是当RCU以QSBR方式运行时,读者同步开销为0。
  2. 无竞争、无阻塞。读者写者之间、读者读者之间均不互相阻塞,也没有死锁、活锁问题。

当然RCU也有其劣势:当写操作十分频繁时,写者开销较大。虽然写者不会被阻塞或者出现“写饥饿”情况,但是当有大量写操作时,写者的综合开销不亚于,甚至高于传统的读写锁。另外,RCU不支持多写者并发。如果有多个写者的话,写者之间需要加互斥锁(或自旋锁)。

URCU是RCU的用户态版本,目前已经在各种用户态网关中广泛应用。参考:https://github.com/urcu/userspace-rcu

DPDK做为用户态数据面开发利器,在DPDK19中首次引入。与URCU库相比,它更适合DPDK的PMD模型,使用起来更简便。

注意:本文旨在向读者初步介绍DPDK-RCU库的使用方式,并假定读者对“锁”、“RCU”、“URCU”、“临界区”、“宽限区”等概念有了充分的了解。此外,本文大部分内容翻译自DPDK-RCU文档。其版权归原作者所有。原文链接:https://doc.dpdk.org/guides/index.html#

 

DPDK-RCU库的特点

  1. DPDK RCU库 允许writer不被阻塞去干别的事情,从而尽量缩短临界区(减少静默状态汇报),并尽量延长宽限期(避免删除操作积压)。

  2. PMD模型的循环间隔十分适合做静默期,并且这个模型的临界区很长,可以将reader的开销降至最低。

  3. 与传统的RCU机制不同,DPDK RCU库支持QS变量(类似于锁实例),可以为每个需要保护的临界区创建一个QS变量,可以更好地跟踪每一个宽限期的结束。(传统的RCU没有QS变量)

DPDK-RCU的使用步骤

  1. 初始化QS,使用rte_rcu_qsbr_get_memsize(reader线程的最大数目)为QS变量分配内存(因为writer需要跟踪每个reader的经静默状态,所以分配内存的大小和reader的数目有关)。

  2. 使用rte_rcu_qsbr_init()初始化QS变量。

  3. 每一个线程需要有一个线程ID,可以使用lcore_id代替。

  4. 每一个读者需要调用rte_rcu_qsbr_thread_register()将自己注册为读者(任意读线程都行,不一定是worker)。

  5. 读线程还需要调用rte_rcu_qsbr_thread_online(),然后才能汇报自己的静默状态。

  6. 在读线程执行阻塞调用之前,必须调用rte_rcu_qsbr_thread_offline()先将自己下线。调用阻塞函数之后,还得调用rte_rcu_qsbr_thread_online()上线。

  7. 写线程调用rte_rcu_qsbr_start()触发读线程开始汇报自己的静默状态。有多个写线程的话,其他写线程也可以调用这个函数。故,这个函数将会返回一个token给调用者。

  8. 写者必须调用rte_rcu_qsbr_check()去获取当前的静默状态。注意参数就是上面返回的token。如果这个函数的返回值指示所有的读者都已经经历了静默期,那么写者就可以释放对应的资源。

  9. rte_rcu_qsbr_start()和rte_rcu_qsbr_check()是lock-free的。所以,多个写者可以同时调用。

  10. 触发报告和查询状态的分离为写入线程提供了灵活性,可以执行有用的工作,而不是阻塞读取线程进入静止状态或脱机。这可以减少持续polling状态导致的内存访问次数。但是由于资源被稍后释放,token和要被释放资源的引用要被存起来以便于今后的状态查询。

  11. rte_rcu_qsbr_synchronize()函数将rte_rcu_qsbr_start()和阻塞版本的rte_rcu_qsbr_check()功能组合在一起。这个API触发读者汇报它们的静默状态并且poll直到所有reader进入静默状态或者下线。这个API不允许writer在等待的时候做其他事情,会在持续的polling中带来额外的内存访问次数。但是,好处在于这个API不必存储token和需要delete的资源引用。资源可以在rte_rcu_qsbr_synchronize()调用结束后立即被释放。

  12. 读线程必须调用rte_rcu_qsbr_thread_offline()和rte_rcu_qsbr_thread_unregister()将自己从汇报状态的列表中移除。rte_rcu_qsbr_check() API就不会再等待这些线程了。

  13. 读者必须调用rte_rcu_qsbr_quiescent()指示自己进入了静默状态。

  14. rte_rcu_qsbr_lock()和rte_rcu_qsbr_unlock()是空函数,但是利于debug,代表临界区的开始和结束。rte_rcu_qsbr_quiescent()会检查是否所有的lock都unlock了。

DPDK-RCU的资源回收框架

Lock-free算法给应用的资源回收带来了额外的负担。当一个写者删除数据结构的条目时,这个写者:

  1. 必须开始宽限期
  2. 必须将要被删除资源的引用存放在FIFO中
  3. 应该检查所有读者经历了宽限期并删除资源

若干API被提供出来用以帮助这个过程。写者可以调用函数rte_rcu_qsbr_dq_create()来建立用以存放将要删除资源的引用。使用rte_rcu_qsbr_dq_enqueue()将资源存放进FIFO。如果FIFO满了,rte_rcu_qsbr_dq_enqueue会在enqueue之前回收资源。它还将定期回收资源,以防止先进先出制过于庞大。如果写者用尽了资源,写者可以调用rte_rcu_qsbr_dq_reclaim API去回收资源。rte_rcu_qsbr_dq_delete用于在关机时回收所有资源并将FIFO删除 。

但是,如果将这个资源回收过程集成到无锁数据结构库中,它将对应用程序隐藏这种复杂性,并使应用程序更容易采用无锁算法。

在任何DPDK应用中,使用QSBR的资源回收过程可以被分为4部分:

  1. 初始化
  2. 汇报静默状态
  3. 回收资源
  4. 关机

这里的设计建议是将这个过程的不同部分分配给客户端库和应用。术语“客户端库”指的是lock-free的数据结构,例如rte_hash, rte_lpm,等等。在DPDK里面或者在外部的库。术语“应用”指的是使用DPDK的数据包处理程序,例如L3_Forwarding, OVS,VPP,etc.

应用需要掌管“初始化”和“静默状态汇报”,所以:

  • 应用需要建立RCU变量并注册读线程去汇报静默状态
  • 应用和客户端库必须注册相同的RCU变量。
  • 应用中的读线程需要汇报静默状态。这允许应用去控制临界区的长度,以及应用汇报静默期的频率。

客户端库需要掌控这个过程的“资源回收”部分。客户端库将使用写入线程上下文来执行内存回收算法。所以:

  • 客户端库应该提供API去注册即将使用的RCU变量。它需要调用rte_rcu_qsbr_dq_create()去建立存放已经删除的条目的索引的FIFO。
  • 客户端库需要使用rte_rcu_qsbr_dq_enqueue()去将删除的资源放入FIFO队列并开始宽限期。
  • 当客户端添加条目时用尽了资源,它需要调用rte_rcu_qsbr_dq_reclaim()去回收回收资源并再次尝试分配资源。

“shutdown”该过程应该在应用和客户端库之间共享。

  • 应用需要确保读线程没有正在使用共享数结构。在调用客户端库的“shutdown”功能之前,需要从QSBR变量中unregister读线程。
  • 客户端库需要调用rte_rcu_qsbr_dq_delete去回收任何需要回收的资源,并释放FIFO。

将资源回收集成到客户端库可以消除应用的负担,并使应用可以方便地使用lock-free算法。

  1. 应用不需要专门的线程去回收资源。内存回收做为写线程的一部分而发生,并且不会影响性能。
  2. 客户端可以更好地控制资源。例如:客户端在耗尽资源后可以尝试回收。
0条评论
0 / 1000
李****一
10文章数
2粉丝数
李****一
10 文章 | 2 粉丝