概述

dpdk19.01提供了qsbr模式的rcu库,其具体实现在lib/librte_rcu目录中。

librte_rcu是无锁线程安全的,这个库提供了读者报告静默状态的能力,让写者知道读者是否进入过静默状态。

dpdk中QSBR具体实现是通过一个struct rte_rcu_qsbr_cnt变量qs,将多个线程共享的内存管理起来。总体思路是通过比较线程的静默期版本号与qs全局版本号的大小,判断是否所有线程进入过本次静默期。

使用librte_rcu进行内存释放的线程的基本步骤如下:

  • 解除内存的引用
  • 调用rte_rcu_qsbr_start()函数将全局版本号加1,触发所有读线程报告静默状态
  • 调用rte_rcu_qsbr_check()遍历检查所有读者线程,确保都进入过本次静默期
  • 释放内存

初始化

初始化时,会用到一些通过的工具宏,定义在在dpdk-master/lib/librte_eal/common/include/rte_common.h中。如下:


  1. #define RTE_CACHE_LINE_SIZE 64
  2. #define RTE_ALIGN_MUL_CEIL(v, mul) \
  3. ((v + 64 - 1)/64) * 64 // (64地板除 + 1)*64
  4. #define RTE_ALIGN_FLOOR(val, 64) \
  5. val & (~(64 - 1)) // 64的地板除
  6. #define RTE_ALIGN_CEIL(val, 64) \
  7. RTE_ALIGN_FLOOR(val + 64 - 1, 64) // 64的地板除 + 1
  8. #define RTE_ALIGN(val, align) RTE_ALIGN_CEIL(val, align) // 64的地板除 + 1

dpdk-master\lib\librte_rcu\rte_rcu_qsbr.h中,定义了初始化时用到的一些函数与宏。

  1. /* 工作线程计数器 */
  2. struct rte_rcu_qsbr_cnt {
  3. uint64_t cnt; // 静默态计数器,0表示下线。使用64bits,防止计数溢出
  4. uint32_t lock_cnt; // counter锁, 用于CONFIG_RTE_LIBRTE_RCU_DEBUG
  5. } __rte_cache_aligned;
  6. #define __RTE_QSBR_THRID_ARRAY_ELM_SIZE (sizeof(uint64_t) * 8) // 数组元素大小为64 B
  7. #define __RTE_QSBR_THRID_ARRAY_SIZE(max_threads)\
  8. RTE_ALIGN(RTE_ALIGN_MUL_CEIL(max_threads, 64) >> 3, RTE_CACHE_LINE_SIZE) // 计算得到线程数组的大小
  9. /*
  10. * (struct rte_rcu_qsbr_cnt *)(v + 1): 获得 v中 rte_rcu_qsbr_cnt 的地址偏移,此时指针p变为 struct rte_rcu_qsbr_cnt *类型
  11. * + v->max_threads: 得到 v中thread id_array的偏移,
  12. * + i
  13. */
  14. #define __RTE_QSBR_THRID_ARRAY_ELM(v, i) // 获得线程数组的第 i 个
  15. ((uint64_t *) ((struct rte_rcu_qsbr_cnt *)(v + 1) + v->max_threads) + i)
  16. #define __RTE_QSBR_THRID_INDEX_SHIFT 6
  17. #define __RTE_QSBR_THRID_MASK 0x3f
  18. #define RTE_QSBR_THRID_INVALID 0xffffffff
  19. /*
  20. * 获得QSBR变量的内存大小,包括rte_rcu_qsbr + thread ID bitmap array变量
  21. */
  22. size_t
  23. rte_rcu_qsbr_get_memsize(uint32_t max_threads)
  24. {
  25. size_t sz; // rcu_qsbr
  26. sz = sizeof(struct rte_rcu_qsbr);
  27. /* Add the size of quiescent state counter array */
  28. sz += sizeof(struct rte_rcu_qsbr_cnt) * max_threads;
  29. /* Add the size of the registered thread ID bitmap array */
  30. sz += __RTE_QSBR_THRID_ARRAY_SIZE(max_threads);
  31. return sz;
  32. }

qsbr rcu真正的初始化在函数rte_rcu_qsbr_init()中,主要是初始化变量的值。

  1. int
  2. rte_rcu_qsbr_init(struct rte_rcu_qsbr *v, uint32_t max_threads)
  3. {
  4. size_t sz;
  5. sz = rte_rcu_qsbr_get_memsize(max_threads);
  6. if (sz == 1)
  7. return 1;
  8. /* Set all the threads to offline */
  9. memset(v, 0, sz); // 获得大小,初始化为零
  10. v->max_threads = max_threads;
  11. v->num_elems = RTE_ALIGN_MUL_CEIL(max_threads,
  12. __RTE_QSBR_THRID_ARRAY_ELM_SIZE) /
  13. __RTE_QSBR_THRID_ARRAY_ELM_SIZE; // 根据最大线程数,获得 thread_id array的元素个数
  14. v->token = __RTE_QSBR_CNT_INIT;
  15. v->acked_token = __RTE_QSBR_CNT_INIT - 1;
  16. return 0;
  17. }

其中, rte_rcu_qsbr_init 函数中的参数中,传入了全局变量rte_rcu_qsbr,其存储了静默期版本号,以及所有注册了的线程的thread_Id与局部静默期版本号。

此变量定义如下:

  1. struct rte_rcu_qsbr {
  2. uint64_t token __rte_cache_aligned; // 允许多个并发静态查询的计数器
  3. /**< Counter to allow for multiple concurrent quiescent state queries */
  4. uint64_t acked_token;
  5. /**< Least token acked by all the threads in the last call to
  6. * rte_rcu_qsbr_check API.
  7. */
  8. uint32_t num_elems __rte_cache_aligned;
  9. /**< Number of elements in the thread ID array */
  10. uint32_t num_threads;
  11. /**< Number of threads currently using this QS variable */
  12. uint32_t max_threads;
  13. /**< Maximum number of threads using this QS variable */
  14. struct rte_rcu_qsbr_cnt qsbr_cnt[0] __rte_cache_aligned;
  15. /**< Quiescent state counter array of 'max_threads' elements */
  16. /**< Registered thread IDs are stored in a bitmap array,
  17. * after the quiescent state counter array.
  18. */
  19. } __rte_cache_aligned;

注册与注销

通过rte_rcu_qsbr_thread_register函数,注册一个读者线程的thread_id到 全局变量 rte_rcu_qsbr 的 thread 数组位图中,并更新线程数num_threads

  1. int
  2. rte_rcu_qsbr_thread_register(struct rte_rcu_qsbr *v, unsigned int thread_id)
  3. {
  4. unsigned int i, id, success;
  5. uint64_t old_bmap, new_bmap;
  6. id = thread_id & __RTE_QSBR_THRID_MASK; // thread_id%64, 表示bits<64>中位图中的哪一位
  7. i = thread_id >> __RTE_QSBR_THRID_INDEX_SHIFT; // thread_id/64,表示uint64_t数组的索引
  8. /*
  9. * 确保已注册线程的计数器不会不同步。因此,需要额外的检查。
  10. */
  11. old_bmap = __atomic_load_n(__RTE_QSBR_THRID_ARRAY_ELM(v, i),
  12. __ATOMIC_RELAXED); // 获得 thread_id所在的 bits<64>
  13. if (old_bmap & 1UL << id) // bits<64>中的id位是否为1
  14. return 0; // 等于1,表示已注册,则返回
  15. do { // 若没有注册,则注册,并对num_threads + 1
  16. new_bmap = old_bmap | (1UL << id); /
  17. success = __atomic_compare_exchange(
  18. __RTE_QSBR_THRID_ARRAY_ELM(v, i),
  19. &old_bmap, &new_bmap, 0,
  20. __ATOMIC_RELEASE, __ATOMIC_RELAXED);
  21. if (success)
  22. __atomic_fetch_add(&v->num_threads, // 加1
  23. 1, __ATOMIC_RELAXED);
  24. else if (old_bmap & (1UL << id)) // 抢注册
  25. return 0;
  26. } while (success == 0);
  27. return 0;
  28. }

通过rte_rcu_qsbr_thread_unregister函数将读线程的thread_id 从全局变量 rte_rcu_qsbr 的 thread数组位图中移除。

  1. int
  2. rte_rcu_qsbr_thread_unregister(struct rte_rcu_qsbr *v, unsigned int thread_id)
  3. {
  4. unsigned int i, id, success;
  5. uint64_t old_bmap, new_bmap;
  6. __RTE_RCU_IS_LOCK_CNT_ZERO(v, thread_id, ERR, "Lock counter %u\n",
  7. v->qsbr_cnt[thread_id].lock_cnt);
  8. id = thread_id & __RTE_QSBR_THRID_MASK;
  9. i = thread_id >> __RTE_QSBR_THRID_INDEX_SHIFT;
  10. /* Make sure that the counter for registered threads does not
  11. * go out of sync. Hence, additional checks are required.
  12. */
  13. /* Check if the thread is already unregistered */
  14. old_bmap = __atomic_load_n(__RTE_QSBR_THRID_ARRAY_ELM(v, i),
  15. __ATOMIC_RELAXED);
  16. if (!(old_bmap & (1UL << id)))
  17. return 0;
  18. do {
  19. new_bmap = old_bmap & ~(1UL << id);
  20. /* Make sure any loads of the shared data structure are
  21. * completed before removal of the thread from the list of
  22. * reporting threads.
  23. */
  24. success = __atomic_compare_exchange(
  25. __RTE_QSBR_THRID_ARRAY_ELM(v, i),
  26. &old_bmap, &new_bmap, 0,
  27. __ATOMIC_RELEASE, __ATOMIC_RELAXED);
  28. if (success)
  29. __atomic_fetch_sub(&v->num_threads,
  30. 1, __ATOMIC_RELAXED);
  31. else if (!(old_bmap & (1UL << id)))
  32. /* Someone else unregistered this thread.
  33. * Counter should not be incremented.
  34. */
  35. return 0;
  36. } while (success == 0);
  37. return 0;
  38. }

上线与下线

线程的上线通过rte_rcu_qsbr_thread_online()函数将局部静默期版本号更新到全局版本。

rte_rcu_qsbr_thread_online()函数的简化版本如下:

  1. static __rte_always_inline void
  2. rte_rcu_qsbr_thread_online(struct rte_rcu_qsbr *v, unsigned int thread_id)
  3. {
  4. uint64_t t;
  5. t = __atomic_load_n(&v->token, __ATOMIC_RELAXED); // 获得全局版本号
  6. __atomic_store_n(&v->qsbr_cnt[thread_id].cnt, // 更新本线程的局部静默期版本号
  7. t, __ATOMIC_RELAXED);
  8. }

线程的下线就是通过rte_rcu_qsbr_thread_offline()函数,将局部静默期版本号设置为0。

  1. __rte_experimental
  2. static __rte_always_inline void
  3. rte_rcu_qsbr_thread_offline(struct rte_rcu_qsbr *v, unsigned int thread_id)
  4. {
  5. __atomic_store_n(&v->qsbr_cnt[thread_id].cnt, 0, __ATOMIC_RELEASE);
  6. }

等待静默

通过rte_rcu_qsbr_synchronize()函数等待所有线程进入过静默期,其主要工作如下:

  • 首先,对全局的静默期的版本加1;
  • 然后,判断本线程局部静默期版本是否等于全局的,若不等于,则更新到最新;
  • 最后,遍历所有注册了的并且在线的线程的静默期版本号cnt的值,确定是否所有线程都进入过本次静默期,若没有,则等待所有读线程都进入过静默状态。
  1. void
  2. rte_rcu_qsbr_synchronize(struct rte_rcu_qsbr *v, unsigned int thread_id)
  3. {
  4. uint64_t t;
  5. t = rte_rcu_qsbr_start(v); // 将 v->token 加1,并存储在局部变量中
  6. /* 若当前线程还在临界区,更新其静默状态 */
  7. if (thread_id != RTE_QSBR_THRID_INVALID) // 0xffffffff
  8. rte_rcu_qsbr_quiescent(v, thread_id); // 更新本线程的 v->qsbr_cnt[thread_id].cnt 到最新token
  9. /* 等待其他读者进入静默期 */
  10. rte_rcu_qsbr_check(v, t, true);
  11. }

注意:

线程每调用一次rte_rcu_qsbr_synchronize()函数,全局的静默期版本号token就会加1。

因为多个线程同时调用此函数,线程的局部静默期版本号cnt一般会小于全局好几个版本。

事实上,若线程调用了一次rte_rcu_qsbr_synchronize(),其版本号就会大于存储在其他线程局部变量t中的全局版本号。

具体是通过rte_rcu_qsbr_check()判断所有线程是否都进行了本次静默。

  1. __rte_experimental
  2. static __rte_always_inline int
  3. rte_rcu_qsbr_check(struct rte_rcu_qsbr *v, uint64_t t, bool wait)
  4. {
  5. /* 判断是否所有线程都进入过静默期 */
  6. if (likely(t <= v->acked_token))
  7. return 1;
  8. /* 若没有确认过,则遍历线程确认。 */
  9. if (likely(v->num_threads == v->max_threads))
  10. return __rte_rcu_qsbr_check_all(v, t, wait);
  11. else
  12. return __rte_rcu_qsbr_check_selective(v, t, wait);
  13. }

其中,__rte_rcu_qsbr_check_all()函数与__rte_rcu_qsbr_check_selective()函数类似,

都是通过遍历注册在thread_id array中的所有线程的cnt,判断是否所有线程进入过静默期。下面,以函数__rte_rcu_qsbr_check_all()进行说明。

  1. static __rte_always_inline int
  2. __rte_rcu_qsbr_check_selective(struct rte_rcu_qsbr *v, uint64_t t, bool wait)
  3. {
  4. uint32_t i, j, id;
  5. uint64_t bmap;
  6. uint64_t c;
  7. uint64_t *reg_thread_id;
  8. uint64_t acked_token = __RTE_QSBR_CNT_MAX; // ((uint64_t)~0)
  9. /* 遍历注册在thread_id array中的所有线程的版本,等待所有线程进入过静默期 */
  10. for (i = 0, reg_thread_id = __RTE_QSBR_THRID_ARRAY_ELM(v, 0); // 获得第0个 thread_id array元素
  11. i < v->num_elems; // thread_id array 元素个数
  12. i++, reg_thread_id++) {
  13. /* 获得bmap所标识的所有线程id的公共前缀 */
  14. bmap = __atomic_load_n(reg_thread_id, __ATOMIC_ACQUIRE);
  15. id = i << __RTE_QSBR_THRID_INDEX_SHIFT; //
  16. while (bmap) {
  17. /* 获得线程的id,以及对应的计数器 */
  18. j = __builtin_ctzl(bmap); // bmap中的第一个注册线程
  19. c = __atomic_load_n( // 获得线程id的cnt
  20. &v->qsbr_cnt[id + j].cnt, // id + j = thread_id
  21. __ATOMIC_ACQUIRE);
  22. /* 若线程没有下线,并且静默期号小于t,则等待,直到其大于等于 */
  23. if (unlikely(c != __RTE_QSBR_CNT_THR_OFFLINE && c < t)) {
  24. /* This thread is not in quiescent state */
  25. if (!wait) // 若不等待则直接返回
  26. return 0;
  27. rte_pause(); // 暂定CPU执行一小段时间
  28. bmap = __atomic_load_n(reg_thread_id, // 重新查看未退出注册的线程,是否进入静默期
  29. __ATOMIC_ACQUIRE);
  30. continue;
  31. }
  32. /* 更新acked_token到最新版本 */
  33. if (c != __RTE_QSBR_CNT_THR_OFFLINE && acked_token > c)
  34. acked_token = c;
  35. bmap &= ~(1UL << j);
  36. }
  37. }
  38. if (acked_token != __RTE_QSBR_CNT_MAX)
  39. __atomic_store_n(&v->acked_token, acked_token, // 若所有的读者都已经进入过静默期,则将最新的静默期版本更新
  40. __ATOMIC_RELAXED);
  41. return 1;
  42. }

示例:

dpdk/app/test/test_rcu_qsbr.c中,

附录

  1. type __atomic_load_n (type *ptr, int memorder),GCC内建函数,实现原子的加载操作,返回*ptr

    有限的 memorder有:__ATOMIC_RELAXED, __ATOMIC_SEQ_CST, __ATOMIC_ACQUIRE, __ATOMIC_CONSUME

目前最新版本的gcc、clang的原子操作实现均符合c++11定义的原子操作6种内存模型:

__ATOMIC_RELAXED No barriers or synchronization.

__ATOMIC_CONSUME Data dependency only for both barrier and synchronization with another thread.

__ATOMIC_ACQUIRE Barrier to hoisting of code and synchronizes with release (or stronger) semantic stores from another thread.

__ATOMIC_RELEASE Barrier to sinking of code and synchronizes with acquire (or stronger) semantic loads from another thread.

__ATOMIC_ACQ_REL Full barrier in both directions and synchronizes with acquire loads and release stores in another thread.

__ATOMIC_SEQ_CST Full barrier in both directions and synchronizes with acquire loads and release stores in all threads.

详见 http://gcc.gnu.org/wiki/Atomic/GCCMM/AtomicSync

  1. void __atomic_store_n (type *ptr, type val, int memorder),GCC内建函数,实现原子的存操作,将val的值写入*ptr。

  2. __builtin_ctz(x):

    计算器x二进制表示,末尾有多少个0。

    例如,a = 16,其二进制表示是 00000000 00000000 00000000 00010000,输出为ctz = 4

    类似的函数有__builtin_ctzl(x)__builtin_ctzll(x),分别用于long类型,与long long类型的数据。

  3. static void rte_pause(void): 暂停CPU执行一段时间, 此调用用于轮询共享资源或等待事件的紧循环。在回路中短暂的停顿可以降低功耗。

原文阅读

微信公共号

NFVschool,关注最前沿的网络技术。

参考

dpdk中QSBR具体实现的更多相关文章

  1. 译文:ovs+dpdk中的“vHost User NUMA感知”特性

    本文描述了"vHost User NUMA感知"的概念,该特性的测试表现,以及该特性为ovs+dpdk带来的性能提升.本文的目标受众是那些希望了解ovs+dpdk底层细节的人,如果 ...

  2. dpdk中log的使用方法

    1 log简介    dpdk中通过log系统记录相关的日志信息,每一条日志除日志内容外,还有两个附加信息,log级别和log类型.开发人员可根据级别和类型对日志信息进行过滤,只记录必要的日志.1.1 ...

  3. DPDK中使用VFIO的配置

    VFIO VFIO是一个可以安全地把设备I/O.中断.DMA等暴露到用户空间(userspace),从而可以在用户空间完成设备驱动的框架.用户空间直接设备访问,虚拟机设备分配可以获得更高的IO性能. ...

  4. dpdk中kni模块

    一,什么是kni,为什么要有kni Kni(Kernel NIC Interface)内核网卡接口,是DPDK允许用户态和内核态交换报文的解决方案,模拟了一个虚拟的网口,提供dpdk的应用程序和lin ...

  5. dpdk中uio技术

    总结一下dpdk的uio技术 一:什么是uio技术 UIO(Userspace I/O)是运行在用户空间的I/O技术,Linux系统中一般的驱动设备都是运行在内核空间,而在用户空间用应用程序调用即可, ...

  6. dpdk中文文档

    Linux平台上DPDK入门指南 1. 简介 1.1. 文档地图 2. 系统要求 2.1. X86 上预先设置 BIOS 2.2. 编译DPDK 2.3. 运行DPDK应用程序 3. 使用源码编译DP ...

  7. 解决dpdk中出现IOMMU not found的问题

    问题 在使用VFIO前,需要在BIOS打开VT-x和VT-d,想把一个PCIe网卡透传到虚拟机中,发现虚拟机启动失败,提示IOMMU没有找到. 输入以下命令确定vt-d开启 dmesg | grep ...

  8. [中英对照]Introduction to DPDK: Architecture and Principles | DPDK概论: 体系结构与实现原理

    [中英对照]Introduction to DPDK: Architecture and Principles | DPDK概论: 体系结构与实现原理   Introduction to DPDK: ...

  9. DPDK在OpenStack中的实现

    随着云计算与大数据的快速发展,其对数据中心网络的性能和管理提出了更高的要求,但传统云计算架构存在多个I/O瓶颈,由于云平台基本上是采用传统的X86服务器加上虚拟化方式组建,随着40G.100G高速网卡 ...

随机推荐

  1. 2018湖南省赛B题“2018”

    题面懒得敲了,反正看这篇博客的肯定知道题面. 比赛时想按约数的一些性质分情况讨论出公式然后在合并,结果单考虑矩阵里出现2018和1009(与2互质,1009出现次数等于2)出现的情况就写了一长串公式, ...

  2. Win32下双缓冲绘图技术

    一:双缓冲原理 为了解决窗口刷新频率过快所带来的闪烁问题,利用双缓冲技术进行绘图.所谓双缓冲技术,就是将资源加载到内存,然后复制内存数据到设备DC(这个比较快),避免了直接在设备DC上绘图(这个比较慢 ...

  3. caffe之数据集介绍

    数据集:http://bigdata.51cto.com/art/201702/531276.htm 计算机视觉 MNIST: 最通用的健全检查.25x25 的数据集,中心化,B&W 手写数字 ...

  4. 吴裕雄--python学习笔记:BeautifulSoup模块

    import re import requests from bs4 import BeautifulSoup req_obj = requests.get('https://www.baidu.co ...

  5. ArrayList与LinkList对比

    本文简要总结一下java中ArrayList与LinkedList的区别,这在面试中也是常常会问到的一个知识点. 先来看一下ArrayList和LinkedList的关系是怎样的: 从继承体系可以看到 ...

  6. CPU网卡亲和绑定

    #!/bin/bash # # Copyright (c) , Intel Corporation # # Redistribution and use in source and binary fo ...

  7. 用Python搭建简单的HTTP服务 · Zhangxu's Blog

    分享一个快速用Python搭建简单的HTTP服务的方法. 平时我们可能有需要,传输某个文件到手机,或者工作中某台服务器的电脑. 假如这个手机是个测试手机/服务器,并没有微信QQ之类的软件,而且你也不想 ...

  8. 微软发布Microsoft Concept Graph和Microsoft Concept Tagging模型

    ​ Concept Graph和Microsoft Concept Tagging模型"> 当我们在讨论人工智能时,请注意,我们通常在讨论弱人工智能. 虽然我们现有的资源与之前可谓不同 ...

  9. iOS 使用系统的UITabBarController 修改展示的图片大小

    1. 设置TabBarItem图片的大小 1 - (void)configurationAppTabBarAndNavigationBar { // 选中的item普通状态图片的大小 UIImage ...

  10. 7-43 jmu-python-字符串异常处理 (20 分)

    输入一行字符串及下标,能取出相应字符.程序能对不合法数据做相应异常处理. 输入格式: 行1:输入一字符串 行2:输入字符下标 输出格式: 下标非数值异常,输出下标要整数 下标越界,输出下标越界 数据正 ...