程序员指南一:数据平面开发套件

这个部分提供了 Data Plane Development Kit(DPDK)架构的全局概述。
DPDK 的主要目标是为数据平面应用程序提供快速数据包处理的简单完整框架。用户可以使用这些代码来理解其中使用的一些技术,用于原型设计,或者添加自己的协议栈。还提供了使用 DPDK 的替代生态系统选项。

该框架通过创建一个环境抽象层(EAL)为特定环境创建一组库,可能特定于英特尔®架构的模式(32位或64位)、Linux*用户空间编译器或特定平台。这些环境是通过使用 make 文件和配置文件创建的。一旦 EAL 库创建完成,用户可以链接该库以创建自己的应用程序。除了 EAL 之外,还提供了其他库,包括 Hash、最长前缀匹配(LPM)和环形队列库。

提供示例应用程序来帮助用户了解如何使用 DPDK 的各种功能。

DPDK 实现了用于数据包处理的运行至完成模型,在调用数据平面应用程序之前必须分配所有资源,这些应用程序作为逻辑处理核心上的执行单元运行。该模型不支持调度程序,并且所有设备都通过轮询访问。不使用中断的主要原因是中断处理带来的性能开销。

除了运行至完成模型外,还可以通过环形队列在处理器核心之间传递数据包或消息来使用管道模型。这允许工作在多个阶段进行,并且可能更有效地利用处理器核心上的代码。

2.1 开发环境

DPDK 项目的安装需要 Linux 及相关的工具链,例如一个或多个编译器、汇编器、make 工具、编辑器以及各种库来创建 DPDK 组件和库。

一旦针对特定环境和架构创建了这些库,它们就可以用于创建用户的数据平面应用程序。

在为 Linux 用户空间创建应用程序时,会使用 glibc 库。对于 DPDK 应用程序,在编译应用程序之前必须配置两个环境变量(RTE_SDK 和 RTE_TARGET)。以下是设置这些变量的示例:

export RTE_SDK=/home/user/DPDK
export RTE_TARGET=x86_64-native-linuxapp-gcc

2.2 环境抽象层

环境抽象层(EAL)提供了一个通用接口,将环境特定的细节隐藏在应用程序和库之外。EAL 提供的服务包括:

  • DPDK 加载和启动
  • 支持多进程和多线程执行类型
  • 核心亲和性/分配过程
  • 系统内存分配/释放
  • 原子/锁操作
  • 时间参考
  • PCI 总线访问
  • 跟踪和调试功能
  • CPU 特性识别
  • 中断处理
  • 警报操作

有关环境抽象层的详细信息,请参阅《环境抽象层》。

2.3 核心组件

核心组件是一组库,提供了构建高性能数据包处理应用程序所需的所有元素。

2.3.1 内存管理器(librte_malloc)

librte_malloc 库提供了从巨页的内存区域分配内存的 API,而不是从堆中分配。在分配大量项目时,使用来自 Linux 用户空间环境的典型 4k 堆页面可能会导致 TLB 未命中,而这时使用巨页可以帮助解决这个问题。这个内存分配器的详细信息在《Malloc Library》中有完整描述。

2.3.2 环形队列管理器(librte_ring)

环形结构提供了一个无锁的多生产者、多消费者 FIFO API,存在于有限大小的表中。它相比于无锁队列具有一些优势:更易实现、适用于批量操作以及速度更快。环形队列被内存池管理器(librte_mempool)使用,并且可以作为连接在逻辑核心上的核心和/或执行块之间的一般通信机制。
在这里插入图片描述
这个环形缓冲区及其使用方法在《环形库》中有详细描述

2.3.3 内存池管理器(librte_mempool)

内存池管理器负责在内存中分配对象池。一个池通过名称标识,并使用环形队列存储空闲对象。它还提供了一些其他可选服务,比如每个核心的对象缓存,以及一个对齐辅助程序,确保对象填充以在所有 RAM 通道上均匀分布。
这个内存池分配器在《Mempool Library》中有详细描述。

2.3.4 网络数据包缓冲管理(librte_mbuf)

mbuf 库提供了创建和销毁缓冲区的功能,DPDK 应用程序可用于存储消息缓冲区。消息缓冲区在启动时创建,并使用 DPDK 内存池库存储在内存池中。
此库提供了分配/释放 mbufs、操作控制消息缓冲区(ctrlmbuf,通用消息缓冲区)和数据包缓冲区(pktmbuf,用于携带网络数据包)的 API
网络数据包缓冲管理在《Mbuf Library》中有详细描述。

2.3.5 定时器管理器(librte_timer)

此库为 DPDK 执行单元提供定时器服务,提供异步执行函数的能力。它可以是周期性的函数调用,也可以是一次性调用。它使用环境抽象层(EAL)提供的定时器接口获取精确的时间参考,并可以根据需要以每个核心为基础进行初始化。
有关库的文档可在《Timer Library》中找到。

2.4 以太网轮询模式驱动程序架构

DPDK 包括用于 1GbE、10GbE 和 40GbE 的轮询模式驱动程序(PMDs),以及用于无异步、基于中断的信号机制工作的虚拟化 virtio 以太网控制器。
请参阅 Poll Mode Driver。

2.5 数据包转发算法支持

DPDK 包括 Hash(librte_hash)和 Longest Prefix Match(LPM,librte_lpm)库,支持相应的数据包转发算法。
有关更多信息,请参阅 Hash Library 和 LPM Library。

2.6 librte_net

librte_net 库是一组 IP 协议定义和便利宏集合。它基于 FreeBSD* IP 栈的代码,包含协议号(用于 IP 头)、IP 相关的宏、IPv4/IPv6 头结构以及 TCP、UDP 和 SCTP 头结构。

环境抽象层 (Environment Abstraction Layer - EAL)

环境抽象层 (EAL) 负责访问低级资源,例如硬件和内存空间。它提供了一个通用接口,隐藏了环境特定的细节,使应用程序和库不必关心环境细节。初始化例程的责任是决定如何分配这些资源(例如内存空间、PCI 设备、定时器、控制台等)。

EAL 通常提供的服务包括:

  • DPDK 加载和启动:DPDK 及其应用程序被链接为单个应用程序,并必须通过某种方式加载。
  • 核心亲和性/分配程序:EAL 提供了将执行单元分配到特定核心以及创建执行实例的机制。
  • 系统内存保留:EAL 促进了不同内存区域的保留,例如用于设备交互的物理内存区域。
  • PCI 地址抽象:EAL 提供了访问 PCI 地址空间的接口。
  • 跟踪和调试功能:日志、dump_stack、panic 等。
  • 实用功能:自旋锁和原子计数器,这些在 libc 中没有提供。
  • CPU 特性识别:在运行时确定特定特性(例如 Intel® AVX)是否受支持。确定当前 CPU 是否支持二进制编译的特性集。
  • 中断处理:用于注册/注销特定中断源的回调函数接口。
  • 警报功能:设置/删除在特定时间运行的回调函数接口。

3.1 在 Linux 用户空间执行环境中的 EAL

在 Linux 用户空间环境中,DPDK 应用程序作为一个用户空间应用程序运行,使用 pthread 库。有关设备和地址空间的 PCI 信息是通过 /sys 内核接口和内核模块(例如 uio_pci_genericigb_uio)发现的。请参考 Linux 内核中的 UIO:用户空间驱动程序文档。这些内存区域在应用程序中进行 mmap()。

EAL 使用mmap()在 hugetlbfs 中执行物理内存分配(使用大页大小以提高性能)。这些内存区域暴露给 DPDK 服务层,如内存池库。

在这一点上,DPDK 服务层将被初始化,然后通过 pthread setaffinity 调用,每个执行单元将分配给特定的逻辑核心作为用户级线程运行。

时间参考由 CPU 时间戳计数器(TSC)或通过 mmap() 调用 HPET 内核 API 提供。

3.1.1 初始化和核心启动

部分初始化由 glibc 的 start 函数完成。还会在初始化时进行检查,以确保配置文件中选择的微体系结构类型受 CPU 支持。然后调用 main() 函数。核心的初始化和启动在 rte_eal_init() 中完成(参阅 API 文档)。它包括对 pthread 库的调用(更具体地说,pthread_self()、pthread_create() 和 pthread_setaffinity_np())。

注意对象的初始化,比如内存区域、环形队列、内存池、LPM 表和哈希表,应作为整个应用程序初始化的一部分在主 lcore 上完成。这些对象的创建和初始化函数不是多线程安全的。然而,一旦初始化完成,这些对象本身可以安全地在多个线程中同时使用。

3.1.2 多进程支持

Linuxapp EAL 允许多进程和多线程(pthread)部署模型。更多详情请参阅第 2.20 节《多进程支持》。

3.1.3 内存映射发现和内存保留

使用 hugetlbfs 内核文件系统来分配大块连续的物理内存。EAL 提供了一个 API,在这个连续内存中保留命名的内存区域。该内存区域的保留内存的物理地址也通过内存区域保留 API 返回给用户。

注意:使用 rte_malloc 库提供的 API 进行的内存保留也是由 hugetlbfs 文件系统中的页面支持的。但是,以这种方式分配的内存块的物理地址信息是不可用的。

3.1.4 不使用 hugepages 的 Xen Dom0 支持

现有的内存管理实现基于 Linux 内核的 hugepage 机制。然而,Xen Dom0 不支持 hugepages,因此添加了一个新的 Linux 内核模块 rte_dom0_mm 来解决这个限制。

EAL 使用 IOCTL 接口来通知 Linux 内核模块 rte_dom0_mm 分配指定大小的内存,并从模块中获取所有内存段的信息,然后使用 MMAP 接口来映射分配的内存。对于每个内存段,物理地址在其中是连续的,但实际硬件地址在 2MB 内是连续的。
在这里插入图片描述

3.1.5 PCI 访问

EAL 使用内核提供的 /sys/bus/pci 实用工具来扫描 PCI 总线上的内容。要访问 PCI 内存,一个名为 uio_pci_generic 的内核模块提供了一个 /dev/uioX 设备文件和 /sys 中的资源文件,可以通过 mmap() 来获取应用程序对 PCI 地址空间的访问。DPDK 的特定模块 igb_uio 也可用于此目的。这两个驱动程序都使用了 uio 内核特性(用户态驱动程序)。

3.1.6 Per-lcore 和 Shared 变量

注意:lcore 是指处理器的逻辑执行单元,有时也称为硬件线程。

共享变量是默认行为。Per-lcore 变量使用线程局部存储(TLS)来提供每个线程的本地存储。

3.1.7 日志

EAL 提供了一个日志 API。在 Linux 应用程序中,默认情况下,日志会发送到 syslog,并同时显示在控制台上。但是用户可以覆盖 log 函数以使用不同的日志记录机制。

3.1.7 跟踪和调试函数

有一些调试函数可以在 glibc 中输出堆栈信息。rte_panic() 函数可以自愿地触发 SIG_ABORT,这可以触发核心文件的生成,可由 gdb 读取。

3.1.8 CPU 特性识别

EAL 可以在运行时查询 CPU(使用 rte_cpu_get_feature() 函数),以确定可用的 CPU 特性。

3.1.9 用户空间中断和警报处理

EAL 创建一个主机线程来轮询 UIO 设备文件描述符以检测中断。EAL 函数可以注册或取消注册特定中断事件的回调函数,并在主机线程中异步调用。EAL 还允许使用定时回调,与 NIC 中断一样。

注意:DPDK 轮询模式驱动程序仅支持用于链接状态更改的中断,即链接上和链接下的通知。

3.1.10 黑名单

EAL PCI 设备黑名单功能可用于将某些网卡端口标记为黑名单,因此 DPDK 将忽略它们。要列入黑名单的端口使用 PCIe* 描述标识(Domain:Bus:Device.Function)。

3.1.11 杂项功能

锁和原子操作是按架构分的(i686 和 x86_64)。

3.2 内存段和内存区域(memzone)

EAL 中提供的这个特性用于映射物理内存。由于物理内存可能存在间隙,内存在描述符表中描述,每个描述符(称为 rte_memseg)描述了内存的连续部分。
在此基础上,memzone 分配器的作用是保留物理内存的连续部分。这些区域在保留内存时通过唯一名称进行标识。
rte_memzone 描述符也位于配置结构中。可以使用 rte_eal_get_configuration() 访问此结构。通过名称查找内存区域将返回一个包含内存区域物理地址的描述符。
可以通过提供 align 参数来保留具有特定起始地址对齐的内存区域(默认情况下,它们对齐到缓存行大小)。对齐值应为二的幂,并且不应少于缓存行大小(64 字节)。可以从 2 MB 或 1 GB 的大页中保留内存区域,只要系统上都有这两种。

3.3 多个 pthread

DPDK 通常将一个 pthread 固定到一个核心以避免任务切换的开销。这可以带来显著的性能提升,但缺乏灵活性且不始终高效。
功耗管理有助于通过限制 CPU 运行频率来提高 CPU 效率。然而,也可以利用空闲周期来充分利用 CPU 的全部能力。
通过利用 cgroup,可以简单地分配 CPU 利用率配额。这提供了另一种提高 CPU 效率的方式,但前提是 DPDK 必须处理多个核心间的上下文切换。
为了进一步增强灵活性,将 pthread 亲和性设置不仅限于一个 CPU,还可以设置为一个 CPU 集合

3.3.1 EAL pthread 和 lcore 亲和性

术语“lcore”指的是一个 EAL 线程,实际上是一个 Linux/FreeBSD pthread。由 EAL 创建和管理的“EAL pthreads”执行由 remote_launch 发出的任务。
在每个 EAL pthread 中,有一个 TLS(线程局部存储)称为 _lcore_id 用于唯一标识。由于 EAL pthread 通常绑定到物理 CPU 的 1:1,因此 _lcore_id 通常等于 CPU ID。
然而,当使用多个 pthread 时,EAL pthread 与指定物理 CPU 之间的绑定不再总是 1:1。EAL pthread 可能与 CPU 集合具有亲和性,因此 _lcore_id 将与 CPU ID 不同。因此,有一个 EAL 长选项 ‘–lcores’ 用于分配 lcores 的 CPU 亲和性。对于指定的 lcore ID 或 ID 组,该选项允许设置该 EAL pthread 的 CPU 集合。
格式模式:–lcores=’<lcore_set>[@cpu_set][,<lcore_set>[@cpu_set],…]’
‘lcore_set’ 和 ‘cpu_set’ 可以是单个数字、范围或一组。
数字是“digit([0-9]+)”,范围是“-”,组是“(<num-
ber|range>[,<number|range>,…])”。
如果未提供 ‘@cpu_set’ 值,则 ‘cpu_set’ 的值将默认为 ‘lcore_set’ 的值。

For example, "--lcores='1,2@(5-7),(3-5)@(0,2),(0,6),7-8'"
which means start 9 EAL thread;
lcore 0 runs on cpuset 0x41 (cpu 0,6);
lcore 1 runs on cpuset 0x2 (cpu 1);
lcore 2 runs on cpuset 0xe0 (cpu 5,6,7);
lcore 3,4,5 runs on cpuset 0x5 (cpu 0,2);
lcore 6 runs on cpuset 0x41 (cpu 0,6);
lcore 7 runs on cpuset 0x80 (cpu 7);
lcore 8 runs on cpuset 0x100 (cpu 8).

3.3.2 非-EAL pthread 支持

可以在任何用户 pthread(也称为非-EAL pthread)中使用 DPDK 执行上下文。在非-EAL pthread 中,_lcore_id 始终为 LCORE_ID_ANY,用于标识它不是具有有效唯一 _lcore_id 的 EAL 线程。某些库将使用替代唯一标识符(例如 TID),有些则不受影响,有些将会受到限制(例如 timer 和 mempool 库)。所有这些影响都在已知问题部分提到。

3.3.3 公共线程 API

有两个公共 API:rte_thread_set_affinity()rte_pthread_get_affinity()用于线程。当它们在任何 pthread 上下文中使用时,将设置/获取线程局部存储(TLS)。
这些 TLS 包括 _cpuset 和 _socket_id:

  • _cpuset 存储了 pthread 亲和的 CPU 位图。
  • _socket_id 存储了 CPU 集合的 NUMA 节点。如果 CPU 集合中的 CPU 属于不同的 NUMA 节点,_socket_id 将被设置为 SOCKET_ID_ANY。

3.3.4 已知问题

  • rte_mempool
    rte_mempool 在内存池内部使用了 per-lcore 缓存。对于非-EAL pthreads,rte_lcore_id() 将不会返回有效数值。因此,目前当 rte_mempool 与非-EAL pthreads 一起使用时,put/get 操作将绕过内存池缓存,这会导致性能损失。支持非-EAL 内存池缓存目前正在启用。
  • rte_ring
    rte_ring 支持多生产者入队和多消费者出队。然而,它是非抢占的,这导致了 rte_mempool 也是非抢占的。
    注意:“非抢占” 约束意味着:
    • 在给定环形队列上执行多生产者入队的 pthread 不应该被执行另一个在同一环形队列上执行多生产者入队的 pthread 抢占。
    • 在给定环形队列上执行多消费者出队的 pthread 不应该被执行另一个在同一环形队列上执行多消费者出队的 pthread 抢占。
      忽略此约束可能导致第二个 pthread 自旋,直到第一个 pthread 再次被调度。此外,如果第一个 pthread 被具有更高优先级的上下文所抢占,甚至可能导致死锁。
      这并不意味着它不能使用,只是需要限制多个 pthread 在同一核心上使用时的情况。
    1. 它可以用于任何单生产者或单消费者情况。
    2. 可以由调度策略都是 SCHED_OTHER(cfs) 的多生产者/消费者 pthread 使用。在使用前,用户应该意识到可能存在的性能损失。
    3. 不应该由调度策略是 SCHED_FIFO 或 SCHED_RR 的多生产者/消费者 pthread 使用。
      RTE_RING_PAUSE_REP_COUNT 被定义为 rte_ring,用于减少争用。它主要是用于情况 2,在一定次数的暂停重复后发出一个 yield。
      如果线程在等待其他线程在环形队列上完成其操作时自旋时间太长,则会添加 sched_yield() 系统调用。这会给被抢占的线程一个机会来执行并完成对环形队列的入队/出队操作。
  • rte_timer
    在非-EAL pthread 上不允许运行 rte_timer_manager()。但是,允许从非-EAL pthread 重置/停止计时器。
  • rte_log
    在非-EAL pthread 中,没有每线程的日志级别和日志类型,使用全局日志级别。
  • 其他
    rte_ring、rte_mempool 和 rte_timer 的调试统计不支持非-EAL pthread。

3.3.5 cgroup 控制

以下是 cgroup 控制使用的简单示例,有两个 pthread(t0 和 t1)在同一个核心上进行数据包 I/O($CPU)。我们期望仅有 50% 的 CPU 用于数据包 I/O。

# 创建两个 cgroup:pkt_io 用于 CPU 限制
mkdir /sys/fs/cgroup/cpu/pkt_io
mkdir /sys/fs/cgroup/cpuset/pkt_io

# 设置 CPU 和线程的关联
echo $cpu > /sys/fs/cgroup/cpuset/cpuset.cpus
echo $t0 > /sys/fs/cgroup/cpu/pkt_io/tasks
echo $t0 > /sys/fs/cgroup/cpuset/pkt_io/tasks
echo $t1 > /sys/fs/cgroup/cpu/pkt_io/tasks
echo $t1 > /sys/fs/cgroup/cpuset/pkt_io/tasks

# 设置 CPU 时间周期和配额
cd /sys/fs/cgroup/cpu/pkt_io
echo 100000 > pkt_io/cpu.cfs_period_us  # CPU 时间周期
echo 50000 > pkt_io/cpu.cfs_quota_us   # CPU 时间配额

MALLOC LIBRARY

librte_malloc库提供了一个API,用于分配任意大小的内存。

该库的目标是提供类似于 malloc 的函数,允许从大页内存中分配,并简化应用程序的移植。DPDK API 参考手册描述了可用的函数。

通常在数据平面处理中不应进行这类分配,因为它们比基于内存池的分配慢,并在分配和释放路径中使用了锁。但是,它们可以在配置代码中使用。

有关更多信息,请参阅 DPDK API 参考手册中的 rte_malloc() 函数描述。

4.1 Cookies

当启用 CONFIG_RTE_MALLOC_DEBUG 时,分配的内存包含覆写保护字段,以帮助识别缓冲区溢出。

4.2 对齐和NUMA限制

rte_malloc() 接受一个对齐参数,可以用于请求按该值的倍数对齐的内存区域(该值必须是二的幂)。

在支持NUMA的系统上,调用 rte_malloc() 函数将返回由调用该函数的核心的NUMA套接字上分配的内存。还提供了一组API,允许直接在NUMA套接字上分配内存,或者在另一个核心所在的NUMA套接字上分配内存,以便在执行内存分配的逻辑核心不同于使用内存的逻辑核心的情况下进行内存分配。

4.3 使用情况

此库需要在初始化时需要类似于 malloc 的功能的应用程序,并且不需要对单个内存块的物理地址信息。

对于在运行时分配/释放数据,在应用程序的快速路径中,应使用内存池库。

如果需要具有已知物理地址的内存块(例如,供硬件设备使用),应使用内存区域。

4.4 内部实现

4.4.1 数据结构

内部使用了两种数据结构类型来管理 malloc 库:

  • struct malloc_heap:用于按套接字跟踪空闲空间
  • struct malloc_elem:在库内部用于分配和空闲空间跟踪的基本元素。
结构:malloc_heap

malloc_heap 结构在库中用于按套接字管理空闲空间。在库内部,每个NUMA节点都有一个堆结构,这允许根据此线程运行的NUMA节点为线程分配内存。虽然这并不保证内存将在该NUMA节点上使用,但它不比总是在固定或随机节点上分配内存更糟糕。

malloc_heap 结构的关键字段及其功能如下(也参见上图):
  • mz_count:用于计算在此NUMA节点上为堆内存分配的内存区域数量。此值的唯一用途是与 numa_socket 值结合使用,为每个内存区域生成一个合适的唯一名称。
  • lock:需要锁定字段以同步对堆的访问。由于堆中的空闲空间是使用链表跟踪的,因此我们需要一个锁来防止两个线程同时操作链表。
  • free_head:这指向此 malloc 堆的空闲节点列表中的第一个元素。

注意:malloc_heap 结构既不跟踪已分配的内存区域(因为它们不能被释放),也不跟踪正在使用的内存块(因为除非它们再次被释放,否则永远不会触及指向该块的指针)。

结构:malloc_elem

malloc_elem 结构在内存区块中具有三种不同的用法,如上图所示:

  1. 作为空闲或已分配内存块上的头部 - 正常情况
  2. 作为内存块内的填充头部
  3. 作为内存区块结束标记

在这里插入图片描述
Figure 4.1 展示了 malloc 库中的 malloc 堆和 malloc 元素的示例

4.4. 内部实现

以下描述了结构中最重要的字段以及它们的使用情况。

注意:如果上述三种用法中某个特定字段的用法未经描述,则可以假定该字段在该情况下具有未定义的值,例如,对于填充头部,只有 “state” 和 “pad” 字段具有有效值。

  • heap:此指针是指向分配此块的堆结构的引用。用于释放正常内存块时,将新释放的块添加到堆的空闲列表中。
  • prev:该指针指向当前元素后面的 memzone 中的头元素/块。释放块时,使用此指针引用前一个块,以检查该块是否也是空闲的。如果是,则合并两个空闲块以形成一个更大的块。
  • next_free:该指针用于链接未分配内存块的空闲列表。同样,它仅在正常内存块中使用 - 在 malloc() 中找到适合分配的空闲块,并在 free() 中将新释放的元素添加到空闲列表中。
  • state:此字段可以具有三个值:“Free”、“Busy”或“Pad”。前两者用于指示正常内存块的分配状态,后者用于指示元素结构是填充头部中的虚拟结构(即数据的起始地址不是块本身的起始地址,因为存在对齐约束)。对于 memzone 的结束结构,此值始终为“busy”,以确保释放的元素不会搜索超出 memzone 的末尾以寻找其他块以合并成更大的空闲区域。
  • pad:此字段保存块起始处的填充长度。对于正常块头,它会添加到头部结束地址,以得到数据区域的起始地址,即在 malloc() 返回的值。在填充内部的虚拟头部中,存储了相同的值,并且从虚拟头部地址中减去此值,可以得到实际块头的地址。
  • size:数据块的大小,包括头部本身。对于 memzone 的结束结构,此大小为零,尽管实际上永远不会检查它。对于被释放的正常块,此大小值用作“next”指针的替代,以标识下一个内存块的位置(因此,如果它也是空闲的,则可以将两个空闲块合并为一个)。
  • 4.4.2 内存分配

当应用程序调用类似于 malloc 的函数时,malloc 函数将首先索引 lcore_config 结构以确定调用线程的 NUMA 节点标识。这用于索引 malloc_heap 结构的数组,并调用 heap_alloc() 函数,同时将该堆作为参数,以及请求的大小、类型和对齐参数。

heap_alloc() 函数将扫描堆的 free_list,并尝试找到一个适合存储请求大小数据的空闲块,并满足请求的对齐约束。如果找不到合适的块 - 例如,第一次为节点调用 malloc 时,free-list 为 NULL - 将保留并设置一个新的内存区域作为堆元素。设置过程涉及在内存区域末尾放置一个虚拟结构作为哨兵,以防止访问超出末尾(因为哨兵标记为 BUSY,malloc 库代码将不会进一步尝试引用它),并在内存区域开头放置一个适当的元素头。后者将标识内存区域中除了末尾的哨兵值外的所有空间,作为单个自由堆元素,并将其添加到堆的 free_list 中。

一旦设置了新的内存区域,就会重新扫描堆的自由列表,并且这次应该会找到新创建的适当元素,因为内存区域中保留的大小至少设置为请求的数据块大小加上对齐方式 - 受 DPDK 编译时配置中指定的最小大小的限制。

当确定了一个合适的空闲元素后,将计算要返回给用户的指针,用户可用空间位于空闲块的末尾。紧随此空间之前的内存缓存行填充有一个 struct malloc_elem 头部:如果块内剩余空间较小,例如 <=128 字节,则使用一个 pad 头部,并浪费剩余空间。然而,如果剩余空间大于此值,则单个空闲元素块会被分成两部分,并在返回的数据空间之前放置一个新的、正确的 malloc_elem 头部。​【oaicite:0】

4.4.3 释放内存

要释放一个内存区域,需要将数据区域起始指针传递给 free 函数。从该指针减去 malloc_elem 结构的大小,以获得块的元素头部。如果此头部是“PAD”类型,则进一步从指针中减去 pad 长度,以获得整个块的正确元素头部。

从这个元素头部,我们得到了来自该块的堆的指针 - 需要释放的位置,以及指向前一个元素的指针,通过大小字段,我们可以计算出指向下一个元素的指针。然后检查这些下一个和前一个元素,以查看它们是否也是空闲的,如果是,则将它们与当前元素合并。这意味着我们永远不会有两个相邻的空闲内存块,它们总是合并成一个单一的块。

RING LIBRARY

环形队列(ring)允许管理队列。与具有无限大小的链表不同,rte_ring 具有以下特性:

  • 先进先出(FIFO)
  • 最大大小固定,指针存储在表中
  • 无锁实现
  • 多消费者或单消费者出列(dequeue)
  • 多生产者或单生产者入列(enqueue)
  • 批量出列 - 如果成功,则出列指定数量的对象;否则失败
  • 批量入列 - 如果成功,则入列指定数量的对象;否则失败
  • 批次出列 - 如果无法满足指定数量,则出列最大可用对象
  • 批次入列 - 如果无法满足指定数量,则入列最大可用对象

这种数据结构相对于链表队列的优势如下:

  • 更快;只需要一次 sizeof(void *) 的 Compare-And-Swap 指令,而不是多次双 Compare-And-Swap 指令。
  • 比完全无锁队列更简单。
  • 适用于批量入列/出列操作。由于指针存储在表中,多个对象的出列不会像链式队列那样产生很多缓存未命中。此外,许多对象的批量出列与简单对象的出列成本相同。

缺点包括:

  • 大小固定
  • 拥有许多环形队列在内存方面的成本比链表队列更高。一个空的环至少包含 N 个指针。

环形结构的简化表示如下,具有消费者和生产者头部和尾部指针,指向存储在数据结构中的对象。

在这里插入图片描述

5.1 FreeBSD 环形缓冲实现的参考资料

以下代码是在 FreeBSD 8.0 中添加的,在某些网络设备驱动程序中使用(至少在 Intel 驱动程序中):

  • bufring.h 在 FreeBSD 中
  • bufring.c 在 FreeBSD 中

5.2 Linux 中的无锁环形缓冲区

以下是描述 Linux 无锁环形缓冲区设计的链接

5.3 额外功能

5.3.1 名称

环形缓冲由唯一名称标识。不可能创建两个具有相同名称的环形缓冲(如果尝试这样做,rte_ring_create() 将返回 NULL)。

5.3.2 水印标记

环形缓冲可以具有高水印(阈值)。一旦入列操作达到高水印,如果已配置水印,则会通知生产者。
例如,此机制可用于对 I/O 施加反压力以通知 LAN 进行暂停。

5.3.3 调试

当启用调试(CONFIG_RTE_LIBRTE_RING_DEBUG 已设置)时,库会存储关于入列/出列数量的一些环形缓冲统计计数器。这些统计数据是每个核心的,以避免并发访问或原子操作。

5.4 使用案例

环形缓冲库的使用案例包括:

  • 在DPDK中应用程序之间的通信
  • 被内存池分配器使用

5.5 环形缓冲的构成

本节解释了环形缓冲的操作方式。环结构由两组头部和尾部组成;一组由生产者使用,另一组由消费者使用。下面各节的图示中将它们表示为 prod_head、prod_tail、cons_head 和 cons_tail。
每个图示表示了一个简化的环的状态,这是一个循环缓冲区。函数局部变量的内容显示在图的顶部,环结构的内容显示在图的底部。

5.5.1 单生产者入列

本节解释了当生产者向环添加对象时发生的情况。在这个例子中,只有生产者头部和尾部(prod_head 和 prod_tail)被修改,并且只有一个生产者。

初始状态是有一个 prod_head 和 prod_tail 指向相同的位置。
入列第一步
首先,将 ring->prod_head 和 ring->cons_tail 复制到本地变量中。prod_next 本地变量指向表中的下一个元素,或者在批量入列的情况下可能是后面的多个元素。
如果环中没有足够的空间(通过检查 cons_tail 检测到),则返回错误。

入列第二步
第二步是修改环结构中的 ring->prod_head,使其指向与 prod_next 相同的位置。
将指向添加对象的指针复制到环中(obj4)。

入列最后一步
一旦对象添加到环中,就会修改环结构中的 ring->prod_tail,使其指向与 ring->prod_head 相同的位置。入列操作完成。
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

5.5.2 单消费者出列

本节解释了当消费者从环中出列一个对象时会发生什么。在这个例子中,只有消费者头部和尾部(cons_head 和 cons_tail)被修改,并且只有一个消费者。
初始状态是有一个 cons_head 和 cons_tail 指向相同的位置。
出列第一步
首先,将 ring->cons_head 和 ring->prod_tail 复制到本地变量中。cons_next 本地变量指向表中的下一个元素,或者在批量出列的情况下可能是后面的多个元素。
如果环中没有足够的对象(通过检查 prod_tail 检测到),则返回错误。
出列第二步
第二步是修改环结构中的 ring->cons_head,使其指向与 cons_next 相同的位置。
将指向出列对象的指针(obj1)复制到用户提供的指针中。
在这里插入图片描述
在这里插入图片描述

5.5.2 单消费者出列

本节解释了当消费者从环中出列一个对象时会发生什么。在这个例子中,只有消费者头部和尾部(cons_head 和 cons_tail)被修改,并且只有一个消费者。
初始状态是有一个 cons_head 和 cons_tail 指向相同的位置。
出列第一步
首先,将 ring->cons_head 和 ring->prod_tail 复制到本地变量中。cons_next 本地变量指向表中的下一个元素,或者在批量出列的情况下可能是后面的多个元素。
如果环中没有足够的对象(通过检查 prod_tail 检测到),则返回错误。
出列第二步
第二步是修改环结构中的 ring->cons_head,使其指向与 cons_next 相同的位置。
将指向出列对象的指针(obj1)复制到用户提供的指针中。
出列最后一步
最后,修改环结构中的 ring->cons_tail,使其指向与 ring->cons_head 相同的位置。出列操作完成。
在这里插入图片描述

5.5.3 多生产者入列

本节解释了当两个生产者同时向环中添加对象时会发生什么。在这个例子中,只有生产者头部和尾部(prod_head 和 prod_tail)被修改。
初始状态是有一个 prod_head 和 prod_tail 指向相同的位置。
多消费者入列第一步
在两个核心上,将 ring->prod_head 和 ring->cons_tail 复制到本地变量中。prod_next 本地变量指向表中的下一个元素,或者在批量入列的情况下可能是后面的多个元素。
如果环中没有足够的空间(通过检查 cons_tail 检测到),则返回错误。
在这里插入图片描述

5.5.3 多消费者入列

多消费者入列第二步

第二步是修改环结构中的 ring->prod_head,使其指向与 prod_next 相同的位置。此操作使用 Compare And Swap (CAS) 指令执行,该指令以原子方式执行以下操作:

  • 如果 ring->prod_head 与本地变量 prod_head 不同,则 CAS 操作失败,代码将重新从第一步开始。
  • 否则,将 ring->prod_head 设置为本地 prod_next,CAS 操作成功,并继续处理。

在图中,操作在核心1上成功,而核心2上的第一步重新开始。

多消费者入列第三步

在核心2上重新尝试 CAS 操作,并成功执行。
核心1更新环的一个元素(obj4),核心2更新另一个元素(obj5)。

多消费者入列第四步

现在每个核心都想要更新 ring->prod_tail。只有当 ring->prod_tail 等于 prod_head 本地变量时,核心才能更新它。这只在核心1上为真。操作在核心1上完成。
在这里插入图片描述

5.5.3 多消费者入列 最后一步

一旦 ring->prod_tail 被核心1更新,核心2也被允许进行更新。操作也在核心2上完成。

5.5.4 32位取模索引

在之前的图示中,prod_headprod_tailcons_headcons_tail 索引用箭头表示。在实际实现中,这些值不是在0到 size(ring) - 1 之间,这是人们会假定的。这些索引的范围是在0到 2^32 -1 之间,并且当我们访问指针表(即环本身)时,我们会对它们的值进行掩码处理。32位取模还意味着对索引的操作(例如加法/减法)将在结果溢出32位数字范围时自动执行 2^32 取模。

以下是两个示例,有助于解释索引在环中的使用。

  • 这个环包含11000个条目。
  • 这个环包含12536个条目。
    在这里插入图片描述

注意:

为了更容易理解,在上述示例中,我们使用了模65536的操作。在实际执行中,这对效率低下,但当结果溢出时会自动执行该操作。

代码始终保持生产者和消费者之间的距离在0到 size(ring) - 1 之间。由于这个特性,我们可以在32位取模基础上对两个索引值进行减法运算:这就是为什么索引的溢出不是一个问题。

任何时候,entriesfree_entries 都在0到 size(ring) - 1 之间,即使只有减法的第一个术语已经溢出:

uint32_t entries = (prod_tail - cons_head);
uint32_t free_entries = (mask + cons_tail - prod_head);

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

写一封情书

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值