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

| Minimum Threshold | 0 | 1022 | 1/4 x queue size |
| Maximum Threshold | 1 | 1023 | 1/2 x queue size |
| Inverse Mark Probability | 1 | 255 | 10 |
| EWMA Filter Weight | 1 | 12 | 9 |

这些参数的含义在后续章节中将会详细解释。这些参数按照其指定给丢弃器模块API的格式,对应于思科*在其RED实现中所使用的格式。最小和最大阈值参数以数据包数量的形式提供给丢弃器模块。标记概率参数以逆值指定,例如,标记概率参数值为10对应于标记概率为1/10(即,10个数据包中将丢弃1个)。EWMA滤波器权重参数以逆对数值指定,例如,滤波器权重参数值为9对应于滤波器权重为1/29。

21.3.2 入队操作

在图21.9中所示的示例中,q(实际队列大小)是输入值,avg(平均队列大小)和count(自上次丢弃以来的数据包数量)是运行时值,decision是输出值,其余数值为配置参数。

在这里插入图片描述
图 21.8:通过滴管的流量

在这里插入图片描述
图 21.9:通过 Dropper 的数据流示例

EWMA滤波器微块

EWMA滤波器微块的目的是对队列大小值进行滤波,以平滑“突发”流量所导致的瞬态变化。输出值为平均队列大小,从而更稳定地反映了队列当前的拥塞水平。

EWMA滤波器具有一个配置参数,即滤波器权重,它决定了平均队列大小输出对实际队列大小变化的快慢响应。滤波器权重值越高,平均队列大小对实际队列大小的变化响应就越快。

队列非空时的平均队列大小计算

EWMA滤波器的定义如下方程所示:
在这里插入图片描述

其中:

  • avg = 平均队列大小
  • wq = 滤波器权重
  • q = 实际队列大小
  • R = 固定整数,表示除法的位移量

队列为空时的平均队列大小计算

当队列为空时,EWMA滤波器不会读取时间戳,而是假定入队操作会相当定期地发生。当队列变为空时,需要进行特殊处理,因为队列可能短时间或长时间为空。队列变为空时,平均队列大小应该逐渐衰减到零,而不是突然降为零或停留在上次计算的值上。当在空队列上入队一个数据包时,平均队列大小使用以下公式计算:
avg = (m * avg) / (2^R)

其中:

  • m = 在队列为空时可能发生的入队操作数

在dropper模块中,m被定义为:
在这里插入图片描述

其中:

  • time = 当前时间
  • qtime = 队列变为空的时间
  • s = 在该队列上连续入队操作之间的典型时间

时间参考以字节为单位,其中每个字节表示物理接口在传输介质上发送一个字节所需的时间(参见"内部时间参考"部分)。参数s在dropper模块中被定义为一个常数,其值为:s=2^22。这对应于在具有64K个叶节点的层次结构中,每个叶节点传输一个64字节数据包到传输介质上所需的时间,并代表了最坏情况。对于规模较小的调度器层次结构,可能需要减小参数s,在red头文件源文件(rte_red.h)中定义为:
#define RTE_RED_S

由于时间参考是以字节为单位的,因此端口速度被包含在表达式time-qtime中。dropper无需配置实际端口速度,它会自动适应低速和高速链路。

实现

采用数值方法来计算方程2中出现的(1-wq)^m因子。这个方法基于以下恒等式:
在这里插入图片描述

这使我们能够表达如下:
在这里插入图片描述

在dropper模块中,使用查找表来为dropper模块支持的每个wq值计算log2(1-wq)。然后,可以通过将表值乘以m并进行位移操作来获得(1-wq)m因子。为了避免乘法中的溢出,值m和查找表值都限制在16位。查找表的总大小为56字节。一旦使用这种方法获得了(1-wq)m因子,就可以从方程2中计算出平均队列大小。

替代方法

考虑了其他计算在队列为空时(方程2)计算平均队列大小所需的因子(1-wq)^m的方法。这些方法包括:

  • 浮点数计算
  • 使用小查找表(512B)和最多16次乘法的定点数计算(这是FreeBSD* ALTQ RED实现中使用的方法)
  • 使用小查找表(512B)和16个SSE乘法的定点数计算(FreeBSD* ALTQ RED实现的SSE优化版本)
  • 大查找表(76 KB)

最终选择的方法(在上面的“实现”部分中描述)在运行时性能和内存需求方面优于所有这些方法,并且在精度上也达到了与浮点数计算相当的水平。表17列出了这些替代方法相对于dropper使用的方法的性能。可以看到,浮点数实现性能最差。

表21.17:替代方法的相对性能

MethodRelative Performance
Current dropper method (see Dropper)100%
Fixed-point method with small (512B) look-up table148%
SSE method with small (512B) look-up table114%
Large (76KB) look-up table118%
Floating-point595%
注意:

在这种情况下,由于性能是以在特定条件下执行操作所花费的时间来表示的,任何超过100%的相对性能值都比参考方法运行得更慢。

丢弃决策模块

丢弃决策模块:

  • 比较平均队列大小与最小和最大阈值
  • 计算丢包概率
  • 随机决定是否对到达的数据包进行入队或丢弃

丢包概率的计算分为两个阶段。首先根据平均队列大小、最小和最大阈值以及标记概率计算初始丢包概率。然后,从初始丢包概率计算实际丢包概率。实际丢包概率考虑了计数的运行时值,因此随着自上次丢包以来到达队列的数据包越来越多,实际丢包概率也会增加。

初始数据包丢包概率

初始丢包概率使用以下方程计算:
在这里插入图片描述

其中:

  • maxp = 标记概率
  • avg = 平均队列大小
  • minth = 最小阈值
  • maxth = 最大阈值

方程3中使用平均队列大小计算数据包丢失概率的过程如图21.10所示。如果平均队列大小低于最小阈值,则将到达的数据包入队。如果平均队列大小达到或超过最大阈值,则将到达的数据包丢弃。如果平均队列大小介于最小和最大阈值之间,则计算丢包概率以确定是否应将数据包入队或丢弃。

实际丢包概率

如果平均队列大小介于最小和最大阈值之间,则实际丢包概率由以下方程计算:
在这里插入图片描述

其中:

  • Pb = 初始丢包概率(来自方程3)
  • count = 自上次丢包以来到达的数据包数量

方程4中的常数2是与参考文档给出的丢包概率公式唯一的偏差,参考文档中使用了值1。需要注意的是,从计算得到的pa可能为负值或大于1。如果是这种情况,则应该使用值1。

图21.11显示了初始和实际丢包概率。实际丢包概率分别使用了参考文档中给出的公式(蓝色曲线)和dropper模块中实现的公式(红色曲线)。与用户指定的标记概率配置参数相比,参考文档中的公式导致了明显更高的丢包率。与参考文档的偏差只是一个设计决策,其他RED实现(例如FreeBSD* ALTQ RED)也做出了类似的选择。
在这里插入图片描述
图 21.10:给定 RED 配置的数据包丢弃概率
在这里插入图片描述
图 21.11:使用 a 计算的初始掉落概率 (pb)、实际掉落概率 (pa)
因子 1(蓝色曲线)和因子 2(红色曲线)

21.3.3 队列为空操作

记录数据包队列变为空的时间,并将其保存到RED运行时数据中,以便EWMA过滤器块在下一次入队操作时计算平均队列大小。通过API通知dropper模块队列已经变为空是调用应用程序的责任。

21.3.4 源文件位置

DPDK dropper的源文件位于:

  • DPDK/lib/librte_sched/rte_red.h
  • DPDK/lib/librte_sched/rte_red.c
21.3.5 与DPDK QoS调度器的集成

DPDK QoS调度器中的RED功能默认情况下是禁用的。要启用它,请使用DPDK配置参数:
CONFIG_RTE_SCHED_RED=y
必须将此参数设置为y。该参数位于DPDK/config目录中的构建配置文件中,例如,DPDK/config/common_linuxapp。在初始化过程中,RED配置参数在传递给调度器的rte_sched_port_params结构中的rte_red_params结构中进行指定。RED参数分别针对四个流量类别和三种数据包颜色(绿色、黄色和红色)进行指定,允许调度器实现加权随机早期检测(WRED)。

21.3.6 与DPDK QoS调度器示例应用程序的集成

DPDK QoS调度器应用程序在启动时读取一个配置文件。配置文件包括一个包含RED参数的部分。这些参数的格式在“配置”中有描述。下面是一个示例RED配置。在此示例中,队列大小为64个数据包。
注意:为了正确运行,应该在相同流量类别(tc)中的每种数据包颜色(绿色、黄色、红色)中使用相同的EWMA过滤器权重参数(wred weight)。

; RED params per traffic class and color (Green / Yellow / Red)
[red]
tc 0 wred min = 28 22 16
tc 0 wred max = 32 32 32
tc 0 wred inv prob = 10 10 10
tc 0 wred weight = 9 9 9
tc 1 wred min = 28 22 16
tc 1 wred max = 32 32 32
tc 1 wred inv prob = 10 10 10
tc 1 wred weight = 9 9 9
tc 2 wred min = 28 22 16
tc 2 wred max = 32 32 32
tc 2 wred inv prob = 10 10 10
tc 2 wred weight = 9 9 9
tc 3 wred min = 28 22 16
tc 3 wred max = 32 32 32
tc 3 wred inv prob = 10 10 10
tc 3 wred weight = 9 9 9

通过此配置文件,适用于绿色、黄色和红色数据包的 RED 配置
流量类别 0 的情况如表 18 所示。
表 21.18:与 RED 配置相对应的 RED 配置文件

Parameter NameGreenYellowRed
Minimum Threshold282216
Maximum Threshold323232
Mark Probability101010
EWMA Filter Weight999
21.3.7 应用程序编程接口(API)

Enqueue API
enqueue API 的语法如下:

int rte\_red\_enqueue(const struct rte\_red\_config \*red_cfg,
struct rte\_red \*red,
const unsigned q,
const uint64\_t time)

传递给 enqueue API 的参数包括配置数据、运行时数据、数据包队列的当前大小(以数据包为单位)以及表示当前时间的值。时间参考以字节为单位,其中一个字节表示物理接口在传输介质上发送一个字节所需的时间持续。(请参阅内部时间参考部分)。为了性能考虑,丢弃器重用调度器的时间戳。
Empty API
empty API 的语法如下:

void rte\_red\_mark\_queue\_empty(struct rte\_red \*red, const uint64\_t time)

传递给 empty API 的参数包括运行时数据和以字节表示的当前时间。

21.4 交通计量

交通计量组件实现了由 IETF RFC 2697 和 2698 定义的单速率三色标记器(srTCM)和双速率三色标记器(trTCM)算法。这些算法根据预先为每个流量流定义的允许量来测量传入数据包流。因此,每个传入的数据包根据其所属流的监测消耗被标记为绿色、黄色或红色。

21.4.1 功能概述

srTCM 算法为每个流量流定义了两个令牌桶,这两个桶共享相同的令牌更新速率:
• 承诺(C)桶:以承诺信息速率(CIR)参数定义的速率提供令牌(以每秒 IP 数据包字节为单位)。C 桶的大小由承诺突发大小(CBS)参数定义(以字节为单位);
• 过剩(E)桶:以与 C 桶相同的速率提供令牌。E 桶的大小由过剩突发大小(EBS)参数定义(以字节为单位)。
trTCM 算法为每个流量流定义了两个令牌桶,这两个桶以独立的速率更新令牌:
• 承诺(C)桶:以承诺信息速率(CIR)参数定义的速率提供令牌(以每秒 IP 数据包字节为单位)。C 桶的大小由承诺突发大小(CBS)参数定义(以字节为单位);
• 峰值(P)桶:以峰值信息速率(PIR)参数定义的速率提供令牌(以每秒 IP 数据包字节为单位)。P 桶的大小由峰值突发大小(PBS)参数定义(以字节为单位)。
请参阅 RFC 2697(用于 srTCM)和 RFC 2698(用于 trTCM)以获取有关如何从桶中消耗令牌以及确定数据包颜色的详细信息。
色盲和色感知模式
对于这两种算法,色盲模式在功能上等同于输入颜色设置为绿色的色感知模式。对于色感知模式,具有红色输入颜色的数据包只能获得红色输出颜色,而具有黄色输入颜色的数据包只能获得黄色或红色输出颜色。
色盲模式仍然与色感知模式有明显区别的原因是,与色感知模式相比,色盲模式可以用更少的操作来实现。

21.4.2 实现概述

对于每个输入数据包,srTCM / trTCM 算法的步骤如下:
• 更新 C 和 E / P 令牌桶。这是通过读取当前时间(从 CPU 时间戳计数器获取),识别自上次桶更新以来的时间量,并根据预先配置的桶速率计算关联的令牌数量来完成的。桶中的令牌数量受到预先配置的桶大小的限制;
• 根据 IP 数据包的大小和 C 和 E / P 令牌桶中当前可用的令牌数量,确定当前数据包的输出颜色;对于仅色感知模式,还考虑数据包的输入颜色。当输出颜色不为红色时,从 C 或 E / P 桶中减去与 IP 数据包长度相等的令牌数量,取决于算法和数据包的输出颜色。

POWER MANAGEMENT

DPDK 的电源管理功能允许用户空间应用程序通过动态调整 CPU 频率或进入不同的 C 状态来节省电力。
• 根据 RX 队列的利用率动态调整 CPU 频率。
• 根据自适应算法进入不同的深度 C 状态,用于猜测短暂的挂起应用程序的时间,如果未收到数据包。
用于调整操作 CPU 频率的接口位于电源管理库中。C 状态控制根据不同的使用情况在应用程序中实现。

22.1 CPU 频率调节

Linux 内核为每个逻辑核心提供了一个 cpufreq 模块用于 CPU 频率调节。例如,对于 cpuX,/sys/devices/system/cpu/cpuX/cpufreq/ 包含以下与频率调节相关的 sys 文件:
• affected_cpus
• bios_limit
• cpuinfo_cur_freq
• cpuinfo_max_freq
• cpuinfo_min_freq
• cpuinfo_transition_latency
• related_cpus
• scaling_available_frequencies
• scaling_available_governors
• scaling_cur_freq
• scaling_driver
• scaling_governor
• scaling_max_freq
• scaling_min_freq
• scaling_setspeed
在 DPDK 中,scaling_governor 在用户空间中配置。然后,用户空间应用程序可以通过写入 scaling_setspeed 来提示内核根据用户空间应用程序定义的策略调整 CPU 频率。

22.2 通过 C 状态对核心负载进行节流

当指定的逻辑核心没有任务时,核心状态可以通过猜测性的睡眠来改变。在 DPDK 中,如果轮询后未收到任何数据包,则可以根据用户空间应用程序定义的策略触发猜测性睡眠。

22.3 电源库的 API 概述

电源库导出的主要方法是用于 CPU 频率调节的,包括以下内容:
• Freq up:提示内核增加特定逻辑核心的频率。
• Freq down:提示内核降低特定逻辑核心的频率。
• Freq max:提示内核将特定逻辑核心的频率调至最大。
• Freq min:提示内核将特定逻辑核心的频率调至最小。
• Get available freqs:从 sys 文件中读取特定逻辑核心的可用频率。
• Freq get:获取特定逻辑核心的当前频率。
• Freq set:提示内核设置特定逻辑核心的频率。

22.4 用户案例

电源管理机制用于在执行 L3 转发时节省功耗。

22.5 参考资料

• l3fwd-power:DPDK 中执行带有电源管理的 L3 转发的示例应用程序。
• DPDK 示例应用程序用户指南中的“带有电源管理的 L3 转发示例应用程序”章节。

PACKET CLASSIFICATION AND ACCESS CONTROL

DPDK 提供了一个访问控制库,它具有基于一组分类规则对输入数据包进行分类的能力。
ACL 库用于在具有多个类别的一组规则上执行 N 元搜索,并为每个类别找到最佳匹配(最高优先级)。库 API 提供了以下基本操作:
• 创建新的访问控制(AC)上下文。
• 将规则添加到上下文中。
• 对上下文中的所有规则,构建执行数据包分类所需的运行时结构。
• 执行输入数据包的分类。
• 销毁 AC 上下文及其运行时结构,并释放相关内存。

23.1 概述
23.1.1 规则定义

当前实现允许用户为每个 AC 上下文指定其自己的规则(字段集),以执行数据包分类。尽管规则字段布局上有一些限制:
• 规则定义中的第一个字段必须是一个字节长。
• 所有后续字段必须分组为连续 4 个字节的集合。
这主要是出于性能考虑 - 搜索函数将第一个输入字节作为流程设置的一部分处理,然后搜索函数的内部循环展开为一次处理四个输入字节。
为定义 AC 规则内的每个字段,使用以下结构:

struct rte\_acl\_field\_def {
uint8\_t type; /\*< type - ACL\_FIELD\_TYPE. \*/
uint8\_t size; /\*< size of field 1,2,4, or 8. \*/
uint8\_t field_index; /\*< index of field inside the rule. \*/
uint8\_t input_index; /\*< 0-N input index. \*/
uint32\_t offset; /\*< offset to start of field. \*/
};

• 类型:字段类型有三种选择之一:
– _MASK - 用于具有值和定义相关位数的掩码的字段,例如 IP 地址。
– _RANGE - 用于具有字段的下限和上限值的字段,例如端口。
– _BITMASK - 用于具有值和位掩码的协议标识符字段。
• 大小:大小参数定义字段的字节长度。允许的值为 1、2、4 或 8 字节。注意,由于输入字节的分组,1 或 2 字节字段必须定义为组成 4 个连续输入字节的连续字段。此外,最好将 8 字节或更多字节的字段定义为 4 字节字段,以便构建过程可以消除所有为通配符的字段。
• 字段索引:表示规则内字段位置的从零开始的值;对于 N 个字段,范围为 0 到 N-1。
• 输入索引:如上所述,除了第一个字段外,所有输入字段必须分组为 4 个连续字节。输入索引指定该字段属于哪个输入组。
• 偏移量:偏移字段定义字段的偏移量。这是从搜索的 buffer 参数开始的偏移量。
例如,要定义以下 IPv4 5-tuple 结构的分类:

struct ipv4\_5tuple {
uint8\_t proto;
uint32\_t ip_src;
uint32\_t ip_dst;
uint16\_t port_src;
uint16\_t port_dst;
};

可以使用以下字段定义数组:

#include <rte\_acl.h> // 假设相关的头文件

// 定义一个包含5个 rte\_acl\_field\_def 结构的数组,用于 ACL 匹配
struct rte\_acl\_field\_def ipv4_defs[5] = {
    /\* 第一个输入字段 - 总是一个字节长。 \*/
    {
        .type = RTE_ACL_FIELD_TYPE_BITMASK,
        .size = sizeof(uint8\_t),
        .field_index = 0,
        .input_index = 0,
        .offset = offsetof(struct ipv4\_5tuple, proto),
    },
    /\* 下一个输入字段(IPv4 源地址)- 连续的4个字节。 \*/
    {
        .type = RTE_ACL_FIELD_TYPE_MASK,
        .size = sizeof(uint32\_t),
        .field_index = 1,
        .input_index = 1,
        .offset = offsetof(struct ipv4\_5tuple, ip_src),
    },
    /\* 下一个输入字段(IPv4 目标地址)- 连续的4个字节。 \*/
    {
        .type = RTE_ACL_FIELD_TYPE_MASK,
        .size = sizeof(uint32\_t),
        .field_index = 2,
        .input_index = 2,
        .offset = offsetof(struct ipv4\_5tuple, ip_dst),
    },
    /\*
 \* 接下来的2个字段(源和目标端口)共同占用4个连续字节。
 \* 它们共享相同的输入索引。
 \*/
    {
        .type = RTE_ACL_FIELD_TYPE_RANGE,
        .size = sizeof(uint16\_t),
        .field_index = 3,
        .input_index = 3,
        .offset = offsetof(struct ipv4\_5tuple, port_src),
    },
    {
        .type = RTE_ACL_FIELD_TYPE_RANGE,
        .size = sizeof(uint16\_t),
        .field_index = 4,
        .input_index = 3,
        .offset = offsetof(struct ipv4\_5tuple, port_dst),
    },
};


这种 IPv4 5 元组规则的典型示例如下:

source addr/mask destination addr/mask source ports dest ports protocol/mask
192.168.1.0/24 192.168.2.31/32 0:65535 1234:1234 17/0xff

协议 ID 17 (UDP)、源地址 192.168.1.[0-255]、目标地址的任何 IPv4 数据包
地址192.168.2.31,源端口[0-65535]和目标端口1234与上面匹配规则。
要定义 IPv6 2 元组的分类:<协议,IPv6 源地址> 基于以下内容
IPv6 标头结构:

struct struct ipv6_hdr {
uint32\_t vtc_flow; /\* IP version, traffic class & flow label. \*/
uint16\_t payload_len; /\* IP packet length - includes sizeof(ip\_header). \*/
uint8\_t proto; /\* Protocol, next header. \*/
uint8\_t hop_limits; /\* Hop limits. \*/
uint8\_t src_addr[16]; /\* IP address of source host. \*/
uint8\_t dst_addr[16]; /\* IP address of destination host(s). \*/
} \_\_attribute\_\_((__packed__));

可以使用以下字段定义数组:

#include <rte\_acl.h> // 可能需要包含相关的头文件

// 定义一个包含5个 rte\_acl\_field\_def 结构的数组,用于 IPv6 两元组匹配
struct rte\_acl\_field\_def ipv6_2tuple_defs[5] = {
    /\* 第一个输入字段 - 总是一个字节长,用于IPv6协议类型。 \*/
    {
        .type = RTE_ACL_FIELD_TYPE_BITMASK,
        .size = sizeof(uint8\_t),
        .field_index = 0,
        .input_index = 0,
        .offset = offsetof(struct ipv6\_hdr, proto),
    },
    /\* 下一个输入字段(IPv6源地址的前4个字节)。 \*/
    {
        .type = RTE_ACL_FIELD_TYPE_MASK,
        .size = sizeof(uint32\_t),
        .field_index = 1,
        .input_index = 1,
        .offset = offsetof(struct ipv6\_hdr, src_addr[0]),
    },
    /\* 接下来的输入字段(IPv6源地址的第5至第8字节)。 \*/
    {
        .type = RTE_ACL_FIELD_TYPE_MASK,
        .size = sizeof(uint32\_t),
        .field_index = 2,
        .input_index = 2,
        .offset = offsetof(struct ipv6\_hdr, src_addr[4]),
    },
    /\* 后续的输入字段(IPv6源地址的第9至第12字节)。 \*/
    {
        .type = RTE_ACL_FIELD_TYPE_MASK,
        .size = sizeof(uint32\_t),
        .field_index = 3,
        .input_index = 3,
        .offset = offsetof(struct ipv6\_hdr, src_addr[8]),
    },
    /\* 最后的输入字段(IPv6源地址的第13至第16字节)。 \*/
    {
        .type = RTE_ACL_FIELD_TYPE_MASK,
        .size = sizeof(uint32\_t),
        .field_index = 4,
        .input_index = 4,
        .offset = offsetof(struct ipv6\_hdr, src_addr[12]),
    },
};


这种 IPv6 2 元组规则的典型示例如下:

source addr/mask                         		protocol/mask
2001:db8:1234:0000:0000:0000:0000:0000/48 		6/0xff

任何 IPv6 数据包的协议 ID 为 6(TCP),且源地址位于范围 [2001:db8🔢0000:0000:0000:0000:0000 - 2001:db8🔢ffff:ffff:ffff:ffff:ffff] 内,都符合上述规则。
在创建一组规则时,对于每个规则,还必须提供额外的信息:
• 优先级:用于衡量规则优先级的权重(值越高越好)。如果输入元组匹配多个规则,则返回优先级较高的规则。请注意,如果输入元组匹配多个具有相同优先级的规则,则未定义将返回哪个规则作为匹配。建议为每个规则分配唯一的优先级。
• 类别掩码:每个规则使用位掩码值来选择规则的相关类别。当执行查找时,将为每个类别返回结果。这有效地通过使单个搜索能够返回多个结果来提供“并行查找”,例如,如果有四个不同的 ACL 规则集,一个用于访问控制,一个用于路由等。每个集合可以分配其自己的类别,并通过将它们合并为单个数据库,使单个查找返回每个集合的结果。
• 用户数据:一个用户定义的字段,可以是除零之外的任何值。对于每个类别,成功匹配将返回具有最高优先级的匹配规则的 userdata 字段。
注意:将新规则添加到 ACL 上下文时,所有字段必须是主机字节顺序(LSB)。当对输入元组执行搜索时,元组中的所有字段必须是网络字节顺序(MSB)。

23.1.2 RT 内存大小限制

构建阶段(rte_acl_build())为给定规则集创建用于进一步运行时遍历的内部结构。使用当前实现,它是一组多位 Trie(stride == 8)。根据规则集,这可能会消耗大量内存。为了节省一些空间,ACL 构建过程尝试将给定的规则集拆分为几个不相交的子集,并为每个子集构建单独的 Trie。根据规则集,这可能会减少 RT 内存需求,但可能会增加分类时间。在构建时,可以为给定 AC 上下文的内部 RT 结构指定最大内存限制。可以通过 rte_acl_config 结构的 max_size 字段来完成。将其设置为大于零的值,指示 rte_acl_build():
• 尝试最小化 RT 表中 Trie 的数量,但
• 确保 RT 表的大小不超过给定值。
将其设置为零会使 rte_acl_build() 使用默认行为:尝试最小化 RT 结构的大小,但不会施加任何硬限制。
这使用户能够自行决定性能/空间折衷的方案。例如:

#include <rte\_acl.h> // 可能需要包含相关的头文件

struct rte\_acl\_ctx \*acx; // 定义指向 ACL 上下文的指针
struct rte\_acl\_config cfg; // 定义 ACL 配置结构体
int ret; // 定义用于存储函数返回值的变量

/\*
 \* 假设 acx 指向已创建并填充了规则的 ACL 上下文,
 \* 并且 cfg 被正确填充。
 \*/

/\* 尝试构建 ACL 上下文,RT 结构小于 8MB。\*/
cfg.max_size = 0x800000; // 设置最大大小为 8MB
ret = rte\_acl\_build(acx, &cfg); // 尝试构建 ACL 上下文

/\*
 \* 如果给定上下文的 RT 结构超出了 8MB。
 \* 尝试构建时不暴露任何硬限制。
 \*/
if (ret == -ERANGE) {
    cfg.max_size = 0; // 设置最大大小为 0,不暴露硬限制
    ret = rte\_acl\_build(acx, &cfg); // 再次尝试构建 ACL 上下文
}


23.1.3 分类方法

在给定的 AC 上下文上成功完成 rte_acl_build() 后,可以用于执行分类 - 在输入数据上搜索具有最高优先级的规则。有几种分类算法的实现:
• RTE_ACL_CLASSIFY_SCALAR:通用实现,不需要任何特定的硬件支持。
• RTE_ACL_CLASSIFY_SSE:矢量实现,可以并行处理多达 8 个流。需要 SSE 4.1 支持。
• RTE_ACL_CLASSIFY_AVX2:矢量实现,可以并行处理多达 16 个流。需要 AVX2 支持。
纯粹是一个运行时的决策选择哪种方法,没有构建时的差异。所有实现都在相同的内部 RT 结构上操作,并使用类似的原理。主要区别在于矢量实现可以手动利用 IA SIMD 指令,并并行处理多个输入数据流。在启动时,ACL 库确定给定平台的最高可用分类方法,并将其设置为默认方法。尽管用户可以覆盖给定 ACL 上下文的默认分类器函数或使用非默认的分类方法执行特定搜索。在这种情况下,用户有责任确保给定平台支持所选的分类实现。

23.2 应用程序编程接口(API)使用

注意:有关访问控制 API 的更多详细信息,请参阅 DPDK API 参考。
以下示例更详细地演示了上述定义的带有多个类别的 IPv4 5-tuple 分类规则的示例。

23.2.1 多类别分类
#include <rte\_acl.h> // 可能需要包含相关的头文件

struct rte\_acl\_ctx \*acx; // 定义指向 ACL 上下文的指针
struct rte\_acl\_config cfg; // 定义 ACL 配置结构体
int ret; // 用于存储函数返回值的变量

/\* 定义一个包含多达5个字段的规则结构。\*/
RTE\_ACL\_RULE\_DEF(acl_ipv4_rule, RTE\_DIM(ipv4_defs));

/\* AC 上下文创建参数。\*/
struct rte\_acl\_param prm = {
    .name = "ACL\_example", // ACL 上下文的名称
    .socket_id = SOCKET_ID_ANY, // 指定的套接字 ID
    .rule_size = RTE\_ACL\_RULE\_SZ(RTE\_DIM(ipv4_defs)), // 每个规则的大小
    .max_rule_num = 8, // AC 上下文中规则的最大数量
};

/\* 定义 ACL 规则数组。\*/
struct acl\_ipv4\_rule acl_rules[] = {
    /\* 匹配前往 192.168.0.0/16 的所有数据包,适用于类别:0,1 \*/
    {
        .data = {.userdata = 1, .category_mask = 3, .priority = 1},
        /\* 目标 IPv4 \*/
        .field[2] = {.value.u32 = IPv4(192,168,0,0), .mask_range.u32 = 16,},
        /\* 源端口 \*/
        .field[3] = {.value.u16 = 0, .mask_range.u16 = 0xffff,},
        /\* 目标端口 \*/
        .field[4] = {.value.u16 = 0, .mask_range.u16 = 0xffff,},
    },
    // 其他规则...

    // 依次添加更多规则...
};

/\* 创建一个空的 AC 上下文 \*/
if ((acx = rte\_acl\_create(&prm)) == NULL) {
    /\* 处理上下文创建失败的情况。\*/
}

/\* 向上下文中添加规则 \*/
ret = rte\_acl\_add\_rules(acx, acl_rules, RTE\_DIM(acl_rules));
if (ret != 0) {
    /\* 处理添加 ACL 规则时的错误。\*/
}

/\* 准备 AC 构建配置。\*/
cfg.num_categories = 2; // 规定的类别数量
cfg.num_fields = RTE\_DIM(ipv4_defs); // 规则中字段的数量
memcpy(cfg.defs, ipv4_defs, sizeof(ipv4_defs)); // 复制字段定义

/\* 构建已添加规则的运行时结构,使用2个类别。 \*/
ret = rte\_acl\_build(acx, &cfg);
if (ret != 0) {
    /\* 处理为 ACL 上下文构建运行时结构时的错误。\*/
}


对于源 IP 地址:10.1.1.1 和目标 IP 地址:192.168.1.15 的元组,一次
执行以下几行:

uint32\_t results[4]; /\* make classify for 4 categories. \*/
rte\_acl\_classify(acx, data, results, 1, 4);

那么 results[] 数组包含:

results[4] = {2, 3, 0, 0};

• 对于类别 0,规则 1 和规则 2 都匹配,但规则 2 优先级更高,因此 results[0] 包含规则 2 的 userdata。
• 对于类别 1,规则 1 和规则 3 都匹配,但规则 3 优先级更高,因此 results[1] 包含规则 3 的 userdata。
• 对于类别 2 和 3,没有匹配,因此 results[2] 和 results[3] 包含零,表示这些类别没有匹配项。
对于源 IP 地址为 192.168.1.1 和目标 IP 地址为 192.168.2.11 的元组,
一旦执行以下行:

uint32\_t results[4]; /\* make classify by 4 categories. \*/
rte\_acl\_classify(acx, data, results, 1, 4);

the results[] array contains:

results[4] = {1, 1, 0, 0};

• 对于类别 0 和 1,只有规则 1 匹配。
• 对于类别 2 和 3,没有匹配项。
对于源 IP 地址为 10.1.1.1 和目标 IP 地址为 201.212.111.12 的元组,
一旦执行以下行:

uint32\_t results[4]; /\* make classify by 4 categories. \*/
rte\_acl\_classify(acx, data, results, 1, 4);

the results[] array contains:

results[4] = {0, 3, 0, 0};

• 对于类别 1,只有规则 3 匹配。
• 对于类别 0、2 和 3,没有匹配项。

24.1 设计目标

DPDK 数据包框架的主要设计目标包括:
• 提供标准方法来构建复杂的数据包处理流水线。为常用的流水线功能模块提供可重用和可扩展的模板;
• 允许在同一个流水线功能模块中切换纯软件和硬件加速的实现方式;
• 在灵活性和性能之间寻求最佳平衡。硬编码的流水线通常提供最佳性能,但不灵活,而开发灵活的框架通常不成问题,但性能通常较低;
• 提供一个在逻辑上类似于 Open Flow 的框架。

24.2 概述

数据包处理应用通常被构建为多个阶段的流水线,每个阶段的逻辑都围绕着查找表进行。对于每个传入的数据包,该表定义了要应用于数据包的一组操作,以及发送数据包到下一个阶段的逻辑。

DPDK 数据包框架通过定义流水线开发的标准方法以及提供可重用模板库,最大限度地减少了构建数据包处理流水线所需的开发工作量。

流水线通过将一组输入端口与一组输出端口通过一组表连接在树状拓扑中构建。作为当前数据包在当前表中查找操作的结果,表的一个条目(查找命中时)或默认表条目(查找未命中时)提供了应用于当前数据包的一组操作,以及数据包的下一跳,可以是另一个表、一个输出端口或数据包丢弃。

数据包处理流水线的示例如图 24.1 所示:
在这里插入图片描述图 24.1:连接输入端口 0 和 1 的数据包处理管道示例
具有输出端口 0、1 和 2 至表 0 和 1

24.3 端口库设计
24.3.1 端口类型

表 19 是可以使用数据包框架实现的端口的非详尽列表。
表 24.1:端口类型

Port类型描述
1SW ring:用于在应用程序线程之间进行消息传递的SW环形缓冲区。使用DPDK rte_ring 原语。预计是最常用的端口类型。
2HW ring:用于与NIC、交换机或加速器端口交互的缓冲区描述符队列。对于NIC端口,使用DPDK rte_eth_rx_queue 或 rte_eth_tx_queue 原语。
3IP重组:输入数据包可以是IP片段或完整的IP数据报。输出数据包是完整的IP数据报。
4IP分段:输入数据包是巨大的(长度大于MTU的IP数据报)或非巨大的数据包。输出数据包是非巨大的数据包。
5流量管理器:连接到特定NIC输出端口的流量管理器,根据预定义的SLA执行拥塞管理和分层调度。
6KNI:发送/接收数据包到/从Linux内核空间。
7Source:作为数据包生成器使用的输入端口。类似于Linux内核的 /dev/zero 字符设备。
8Sink:用于丢弃所有输入数据包的输出端口。类似于Linux内核的 /dev/null 字符设备。

24.3.2 端口接口

每个端口是单向的,即输入端口或输出端口。每个输入/输出端口都需要实现一个抽象接口,该接口定义了端口的初始化和运行时操作。端口的抽象接口描述如下。
表 24.2: 20 端口抽象接口

#Port OperationDescription
1Create创建低级端口对象(例如队列)。可以在内部分配内存。
2Free释放低级端口对象使用的资源(例如内存)。
3RX读取一组输入数据包。非阻塞操作。仅适用于输入端口。
4TX写入一组输入数据包。非阻塞操作。仅适用于输出端口。
5Flush刷新输出缓冲区。仅适用于输出端口。

24.4 表格库设计

24.4.1 表格类型
表 21 是一个非详尽列举的可使用 Packet Framework 实现的表格类型列表。
表 24.3: 表格类型

表格类型描述
Hash表 Hash table键查找是基于n元组的。通常,查找键被散列以产生一个签名,该签名用于识别下一个进行查找的条目存储桶。与每个输入数据包的查找键相关联的签名要么从数据包描述符(预先计算的签名)中读取,要么在表查找时计算。表查找、添加条目和删除条目操作,以及预先计算签名的任何其他流水线块都必须使用相同的散列算法来生成签名。通常用于实现流分类表、ARP缓存、隧道协议的路由表等。
最长前缀匹配(LPM)Longest Prefix Match键查找是IP地址。每个表条目都有一个关联的IP前缀(IP和深度)。表查找操作选择与查找键匹配的IP前缀;在多次匹配的情况下,具有最长前缀深度的条目获胜。通常用于实现IP路由表。
访问控制列表(ACL)Access Control List键查找是两个VLAN/MPLS标签、IP目标地址、IP源地址、L4协议、L4目标端口、L4源端口的7元组。每个表条目都有一个关联的ACL和优先级。ACL包含VLAN/MPLS标签的位掩码,IP目标地址的IP前缀,IP源地址的IP前缀,L4协议和位掩码,L4目标端口和位掩码,L4源端口和位掩码。表查找操作选择与查找键匹配的ACL;在多次匹配的情况下,具有最高优先级的条目获胜。通常用于实现防火墙等的规则数据库。
模式匹配搜索 Pattern matching search键查找是数据包有效负载。表是具有分配的每个模式的优先级的模式数据库。表查找操作选择与输入数据包匹配的模式;在多次匹配的情况下,具有最高优先级的匹配模式获胜。
数组 Array键查找是表条目索引本身。

24.4.2 表格接口
每个表格都需要实现一个抽象接口,该接口定义了表格的初始化和运行时操作。表格的抽象接口描述在表 29 中。
表 24.4: 表格抽象接口

操作描述
Create创建查找表的低级数据结构。可以在内部分配内存。
Free释放查找表使用的所有资源。
Add entry向查找表添加新条目。
Delete entry从查找表中删除特定条目。
Lookup查找一组输入数据包,并返回指定每个数据包查找操作结果的位掩码:设置位表示相应数据包的查找命中,清除位表示查找未命中。对于每个查找命中的数据包,查找操作还会返回指向被命中的表条目的指针,其中包含要应用于数据包的操作和任何相关元数据。对于每个查找未命中的数据包,要应用于数据包的操作和任何相关元数据由预先配置的用于查找未命中的默认表条目指定。

24.4.3 哈希表设计

哈希表概述

哈希表很重要,因为键查找操作被优化为速度:不需要通过表中的所有键进行线性搜索查找键,而是仅限于在单个表桶中存储的键。

关联数组

关联数组是一个可以指定为一组(键、值)对的函数,每个键最多出现一次。对于给定的关联数组,可能的操作有:

  1. 添加(键,值):当键当前未关联任何值时,创建(键,值)关联。当键已与值 value0 关联时,删除(键,value0)关联并创建(键,value)关联;
  2. 删除键:当键当前未关联任何值时,此操作不起作用。当键已与值关联时,删除(键,值)关联;
  3. 查找键:当键当前未关联任何值时,此操作返回空值(查找未命中)。当键与值关联时,此操作返回值。不更改(键,值)关联。
    用于比较输入键与关联数组中的键的匹配标准是精确匹配,因为键的大小(字节数)和键值(字节数组)必须与比较中的两个键完全匹配。
哈希函数

哈希函数将可变长度(键)的数据确定性地映射到固定大小的数据(哈希值或键签名)。通常,键的大小大于键签名的大小。哈希函数基本上将长键压缩为短签名。多个键可以共享相同的签名(碰撞)。
高质量的哈希函数具有均匀的分布。对于大量的键,将签名值空间划分为固定数量的相等间隔(桶)时,希望键签名均匀分布在这些间隔之间(均匀分布),而不是大多数签名只进入少数间隔,其余间隔基本上未使用(非均匀分布)。

哈希表

哈希表是使用哈希函数进行操作的关联数组。使用哈希函数的原因是通过最小化与输入键进行比较的表键数量来优化查找操作的性能。
哈希表不是将(键,值)对存储在单个列表中,而是维护多个列表(桶)。对于任何给定的键,存在一个单独的桶,其中该键可能存在,并且该桶基于键签名是唯一标识的。一旦计算了键签名并标识了哈希表桶,要么键位于此桶中,要么根本不在哈希表中,因此键搜索可以从当前表中所有键的完整集合缩小为仅位于识别的表桶中的键集合。
只要表键均匀分布在哈希表桶中,哈希表查找操作的性能就会大大提高,这可以通过使用具有均匀分布的哈希函数来实现。将键映射到其桶的规则可以简单地使用键签名(对表桶数量取模)作为表桶ID:
bucket_id = f_hash(key) % n_buckets;
通过选择桶的数量为2的幂,取模运算符可以被位与逻辑运算符取代:
bucket_id = f_hash(key) & (n_buckets - 1);
考虑到 n_bits 为 bucket_mask = n_buckets - 1 中设置的位数,这意味着所有最终位于相同哈希表桶中的键都具有其签名的较低 n_bits 相同。为了减少相同桶中键的数量(碰撞),需要增加哈希表桶的数量。
在数据包处理上下文中,哈希表操作涉及的操作序列在图 24.2 中描述:
在这里插入图片描述

图 24.2:数据包处理上下文中哈希表操作的步骤序列

哈希表用例
流分类

描述:对每个输入数据包至少执行一次流分类。此操作将每个传入数据包映射到流数据库中的已知流量流之一,流数据库通常包含数百万个流。
哈希表名称:流分类表
键数:数百万
键格式:唯一标识流量流/连接的数据包字段的 n 元组。例如:DiffServ 的 5 元组(源 IP 地址、目标 IP 地址、L4 协议、L4 协议源端口、L4 协议目标端口)。对于 IPv4 协议和 TCP、UDP 或 SCTP 等 L4 协议,DiffServ 5 元组的大小为 13 字节,而对于 IPv6 则为 37 字节。
键值(键数据):描述当前流量流数据包要应用的处理的操作和操作元数据。与每个流量流相关的数据大小可从 8 字节到千字节不等。

地址解析协议(ARP)

描述:一旦为 IP 数据包确定了路由(因此知道了输出接口和下一跳站点的 IP 地址),则需要下一跳站点的 MAC 地址,以便将该数据包发送到其目的地的旅程的下一段(由其目标 IP 地址标识)。下一跳站点的 MAC 地址成为出站以太网帧的目的地 MAC 地址。
哈希表名称:ARP 表
键数:数千个
键格式:输出接口和下一跳 IP 地址的一对(Typically 5 字节用于 IPv4,17 字节用于 IPv6)。
键值(键数据):下一跳站点的 MAC 地址(6 字节)。

哈希表类型

表 22 列出了所有不同哈希表类型共享的哈希表配置参数。
表 24.5:所有哈希表类型共有的配置参数

参数名称详细说明
键大小 Key size以字节数表示。所有键都具有相同的大小。
键值(键数据)大小 Key value (key data) size以字节数表示。
桶的数量 Number of buckets必须是二的幂。
最大键数量 Maximum number of keys必须是二的幂。
散列函数 Hash function例如:jhash,CRC hash等。
散列函数种子 Hash function seed传递给散列函数的参数。
键偏移量 Key offset包中存储的数据包元数据中查找键字节数组的偏移量。

桶满问题

在初始化时,每个哈希表桶为确切地分配了 4 个键的空间。随着键被添加到表中,当需要向某个桶中添加新键时,可能发生这样的情况:该桶已经有 4 个键了。可能的选项有:

  1. 最近最少使用(LRU)哈希表。桶中的现有键之一被删除,并在其位置添加新键。每个桶中的键数量永远不会超过 4。选择要从桶中删除的键的逻辑是 LRU。哈希表查找操作维护相同桶中的键被命中的顺序,因此每次键被命中时,它都成为新的最近使用(MRU)键,即下一个可能被删除的键。当将键添加到桶时,它也成为新的 MRU 键。当需要选择并删除键时,始终选择第一个被命中的键,即当前的 LRU 键。LRU 逻辑要求为每个桶单独维护特定的数据结构。
  2. 可扩展桶哈希表。桶被扩展,为其增加了 4 个键的空间。这是通过在表初始化时分配额外内存来实现的,用于创建一组空闲键(此池的大小可配置,始终是 4 的倍数)。在添加键操作中,只有在空闲键的限制范围内才能成功地分配一组 4 个键,否则添加键操作将失败。在删除键操作中,当要删除的键是其组内唯一使用的键时,一组 4 个键将释放回空闲键池中。在键查找操作中,如果当前桶处于扩展状态且在第一组 4 个键中找不到匹配项,则搜索将继续超出第一组 4 个键,潜在地直到检查此桶中的所有键。可扩展桶逻辑要求为每个表和每个桶单独维护特定的数据结构。
    表 24.6:可扩展桶哈希表特定的配置参数
参数名称详细说明
附加键的数量 Number of additional keys必须是二的幂,至少等于4。

签名计算

用于键签名计算的可能选项包括:

  1. 预先计算的键签名。键查找操作在两个 CPU 核心之间进行分割。第一个 CPU 核心(通常是执行数据包 RX 的 CPU 核心)从输入数据包中提取键,计算键签名,并将键和键签名保存在数据包缓冲区中作为数据包元数据。第二个 CPU 核心从数据包元数据中读取键和键签名,并执行键查找操作的桶搜索步骤。
  2. 查找时计算键签名(“do-sig” 版本)。同一个 CPU 核心从数据包元数据中读取键,用它计算键签名,并执行键查找操作的桶搜索步骤。
    表 24.7:预先计算的键签名哈希表特定的配置参数
参数名称详细说明
签名偏移量 Signature offset在数据包元数据中预先计算的键签名的偏移量。

键大小优化的哈希表

对于特定键大小,键查找操作的数据结构和算法可以进行专门设计以进一步提高性能,因此有以下选项:

  1. 支持可配置键大小的实现。
  2. 支持单个键大小的实现。典型的键大小为 8 字节和 16 字节。
    适用于可配置键大小哈希表的桶搜索逻辑
    桶搜索逻辑的性能是影响键查找操作性能的主要因素之一。数据结构和算法旨在充分利用英特尔 CPU 架构资源,例如:缓存内存空间、缓存内存带宽、外部内存带宽、多个并行工作的执行单元、乱序指令执行、特殊 CPU 指令等。
    桶搜索逻辑并行处理多个输入数据包。它构建为几个阶段的流水线(3 或 4 阶段),每个流水线阶段处理来自输入数据包突发的两个不同数据包。在每个流水线迭代中,数据包被推送到下一个流水线阶段:对于 4 阶段流水线,两个刚完成第 3 阶段的数据包退出流水线,正在执行第 3 阶段的两个数据包,正在执行第 2 阶段的两个数据包,正在执行第 1 阶段的两个数据包以及进入流水线以执行第 0 阶段的下两个数据包(从输入数据包突发中读取的下两个数据包)。流水线迭代将继续,直到来自输入数据包突发的所有数据包执行了流水线的最后一个阶段。
    桶搜索逻辑在下一个内存访问边界处分为流水线阶段。每个流水线阶段使用的数据结构通常存储在当前 CPU 核心的 L1 或 L2 缓存内存中(高概率),并在下一个算法所需的内存访问之前中断。当前流水线阶段通过预取下一个流水线阶段所需的数据结构来结束,从而为预取完成提供足够的时间。因此,当下一个流水线阶段最终对相同数据包执行时,它将从 L1 或 L2 缓存内存中读取它所需的数据结构,并避免因 L2 或 L3 缓存缺失而产生的重大性能损失。
    通过提前预取下一个流水线阶段所需的数据结构(在实际使用之前),并切换到执行不同数据包的另一个流水线阶段,大大减少了 L2 或 L3 缓存缺失的数量,这是性能改进的主要原因之一。这是因为由于指令之间的数据依赖性,由于 CPU 执行单元必须等待从 L3 缓存或外部 DRAM 内存完成读取操作,因此内存读取访问的 L2/L3 缓存缺失成本较高。
    通过将处理分成几个在不同数据包上执行的阶段(输入突发的数据包交错使用),可以创建足够的工作量,以使预取指令成功完成(在实际访问预取的数据结构之前),并减少指令之间的数据依赖性。例如,对于 4 阶段流水线,阶段 0 在数据包 0 和 1 上执行,然后在使用相同数据包 0 和 1(即在数据包 0 和 1 上执行阶段 1 之前)之前,使用不同数据包:数据包 2 和 3(执行阶段 1),数据包 4 和 5(执行阶段 2)以及数据包 6 和 7(执行阶段 3)。通过在将数据结构引入 L1 或 L2 缓存内存时执行有用的工作,隐藏了读取内存访问的延迟。通过增加对同一数据结构的两次连续访问之间的间隔,放宽了指令之间的数据依赖性;这允许充分利用超标量和乱序执行的 CPU 架构,因为活动的 CPU 核心执行单元数量被最大化(而不是由于指令之间的数据依赖性约束而处于空闲或停滞状态)。
    桶搜索逻辑还实现为不使用任何分支指令。这避免了在每次分支错误预测时刷新 CPU 核心执行流水线所产生的重要成本。

可配置键大小哈希表

图 24.3、表 25 和表 26 详细说明了用于实现可配置键大小哈希表(LRU 或可扩展桶,以及预先计算签名或“do-sig”)的主要数据结构。
Figure 24.3: Data Structures for Configurable Key Size Hash Tables
在这里插入图片描述

表 24.8:用于可配置键大小哈希表的主要大型数据结构(数组)

数组名称条目数量条目大小(字节)描述
Bucket arrayn_buckets(可配置)散列表的32个桶。
Bucket extensionsn_buckets_ext(可配置)仅为可扩展桶表创建的数组。
Key arrayn_keyskey_size (可配置)添加到散列表的键。
Data arrayn_keysentry_size (可配置)与散列表键相关联的键值(键数据)。

表 24.9:桶数组条目的字段描述(可配置键大小哈希表)

字段名字段大小(字节)描述
Next Ptr/LRU8对于LRU表,该字段表示当前存储的桶作为4个2字节条目数组的LRU列表。条目0存储MRU键的索引(0 … 3),而条目3存储LRU键的索引。 对于可扩展的桶表,此字段表示下一个指针(即指向与当前桶链接的4个键组的下一组)。 如果桶当前被扩展,则下一个指针不为NULL;否则为NULL。 为了帮助无分支实现,此字段的位0(最低有效位)如果下一个指针不为NULL,则将其设置为1,否则设置为0。
Sig[0 … 3]4 x 2如果键X有效(X = 0 … 3),则sig X位15到1存储键X签名的最高15位,并且sig X位0设置为1。 如果键X无效,则sig X设置为零。
Key Pos [0 … 3]4 x 4如果键X有效(X = 0 … 3),则Key Pos X表示键X存储的键数组中的索引,以及存储与键X关联的值的数据数组中的索引。 如果键X无效,则Key Pos X的值未定义。

图 24.4 和表 27 详细说明了桶搜索流水线阶段(LRU 或可扩展桶,以及预先计算签名或“do-sig”)。对于每个流水线阶段,所描述的操作应用于该阶段处理的两个数据包中的每一个。
在这里插入图片描述
Figure 24.4: Bucket Search Pipeline for Key Lookup Operation (Configurable Key Size Hash
Tables)

表 24.10:桶搜索流水线阶段描述(可配置键大小哈希表)

阶段名称描述
0 预取数据包元数据Prefetch packet meta-data从输入数据包的突发中选择下两个数据包。预取包含键和键签名的数据包元数据。
1 预取表桶 Prefetch table bucket从数据包元数据中读取键签名(对于可扩展桶哈希表),或者从数据包元数据中读取键并计算键签名(对于LRU表)。使用键签名标识桶ID。设置签名的位0为1(仅匹配表中有效键的签名)。预取桶。
2 预取表键 Prefetch table key从桶中读取键签名。将输入键的签名与数据包的4个键签名进行比较。结果如下:如果至少有一个签名匹配,则 match = 等于TRUE,否则在无签名匹配的情况下为FALSE;如果有多个签名匹配(在最坏情况下可以有多达4个签名匹配),则 match_many = 等于TRUE,否则为FALSE;match_pos = 第一个产生签名匹配的键的索引(仅在match为true时有效)。仅对于可扩展桶哈希表,如果下一个指针有效,则将 match_many 设置为TRUE。预取由 match_pos 指示的桶键(即使 match_pos 没有指向有效键)。
3 预取表数据 Prefetch table data读取由 match_pos 指示的桶键。将桶键与输入键进行比较。结果如下:如果两个键匹配,则 match_key = 等于TRUE,否则为FALSE。仅当 match 和 match_key 都等于TRUE时,将输入键报告为查找命中,否则报告为查找未命中。仅对于LRU表,仅当查找命中时,使用无分支逻辑更新桶LRU列表(当前键成为新的MRU)。预取与当前键关联的键值(键数据),以避免分支(这在查找命中和未命中时都要执行)。

额外注意事项:

  1. 桶搜索算法的流水线版本仅在输入数据包突发中至少有 7 个数据包时执行。如果输入数据包突发中的数据包少于 7 个,则执行非优化版本的桶搜索算法。
  2. 一旦针对输入数据包突发中的所有数据包执行了桶搜索算法的流水线版本,还将针对未产生查找命中但设置了 match_many 标志的任何数据包执行桶搜索算法的非优化版本。执行非优化版本的结果可能导致其中一些数据包产生查找命中或查找未命中。这不会影响键查找操作的性能,因为在相同的 4 个键组中匹配超过一个签名或具有扩展状态的桶的概率(仅适用于可扩展桶哈希表)相对较小。

键签名比较逻辑在表 28 中描述。
表 24.11:用于匹配、Match_Many 和 Match_Pos 的查找表
在这里插入图片描述
如果输入签名等于存储桶签名 X,则输入掩码哈希位 X (X = 0 … 3) 设置为 1
否则设置为 0。 输出 match、match_many 和 match_pos 分别为 1 位、1 位和 2
位的大小及其含义已在上面解释过。
如表 29 所示,match 和 match_many 的查找表可以折叠为
单个 32 位值和 match_pos 的查找表可以折叠为 64 位值。
给定输入掩码,match、match_many 和 match_pos 的值可以通过以下方式获得
对各自的位数组进行索引,以无分支方式分别提取 1 位、1 位和 2 位
逻辑。
表 24.12:Match、Match_Many 和 Match_Pos 的折叠查找表
在这里插入图片描述
match、match_many 和 match_pos 的伪代码是:

match = (0xFFFELLU >> mask) & 1;
match_many = (0xFEE8LLU >> mask) & 1;
match_pos = (0x12131210LLU >> (mask << 1)) & 3;

单键大小哈希表

图 24.5、图 24.6、表 30 和表 31 详细说明了用于实现 8 字节和 16 字节键哈希表(LRU 或可扩展桶,以及预先计算签名或“do-sig”)的主要数据结构。
在这里插入图片描述
图 24.5:8 字节密钥哈希表的数据结构
在这里插入图片描述
图 24.6:16 字节密钥哈希表的数据结构

表 24.13:用于 8 字节和 16 字节键大小哈希表的主要大型数据结构(数组)

数组名称条目数量条目大小(字节)描述
Bucket arrayn_buckets8字节键大小:64 + 4 x entry_size 16字节键大小:128 + 4 x entry_size散列表的桶。
Bucket extensions arrayn_buckets_ext8字节键大小:64 + 4 x entry_size 16字节键大小:128 + 4 x entry_size仅为可扩展桶表创建的数组。

表 24.14:桶数组条目的字段描述(8 字节和 16 字节键哈希表),

字段名字段大小(字节)描述
Valid8位 X (X = 0 … 3) 设置为 1 表示键 X 有效,否则为 0。第 4 位仅用于可扩展桶表,用于帮助实现无分支逻辑。在这种情况下,如果下一个指针有效(非NULL),则第 4 位设置为 1,否则为 0。
Next Ptr/LRU8对于LRU表,此字段表示当前存储的桶作为4个2字节条目数组的LRU列表。条目0存储MRU键的索引(0 … 3),而条目3存储LRU键的索引。 对于可扩展桶表,此字段表示下一个指针(即指向与当前桶链接的4个键组的下一组)。如果桶当前被扩展,则下一个指针不为NULL;否则为NULL。
Key [0 … 3]4 x key_size完整键。
Data [0 … 3]4 x entry_size与键 0 … 3 相关联的完整键值(键数据)。

以及用于实现 8 字节和 16 字节键哈希表的桶搜索流水线细节(LRU 或可扩展桶,以及预先计算签名或“do-sig”)。对于每个流水线阶段,所描述的操作应用于该阶段处理的两个数据包中的每一个。
在这里插入图片描述
图 24.7:用于键查找操作的存储桶搜索管道(单键大小哈希表)

表 24.15:桶搜索流水线阶段描述(8 字节和 16 字节键哈希表)

阶段名称描述
0 预取数据包元数据1. 从输入数据包的突发中选择下两个数据包。 2. 预取包含键和键签名的数据包元数据。
1 预取表桶1. 从数据包元数据中读取键签名(对于可扩展桶哈希表),或者从数据包元数据中读取键并计算键签名(对于LRU表)。 2. 使用键签名标识桶ID。 3. 预取桶。
2 预取表数据1. 读取桶。 2. 将所有 4 个桶键与输入键进行比较。 3. 仅当识别到匹配时(不可能存在多个键匹配时)将输入键报告为查找命中。 4. 仅对于LRU表,仅在查找命中时使用无分支逻辑更新桶LRU列表(当前键成为新的MRU)。 5. 预取与匹配键关联的键值(键数据),以避免分支(这在查找命中和未命中时都要执行)。

额外注意事项:

  1. 桶搜索算法的流水线版本仅在输入数据包突发中至少有 5 个数据包时执行。如果输入数据包突发中的数据包少于 5 个,则执行非优化版本的桶搜索算法。
  2. 仅适用于可扩展桶哈希表,在针对输入数据包突发中的所有数据包执行了桶搜索算法的流水线版本后,还将针对未产生查找命中但具有扩展状态桶的任何数据包执行桶搜索算法的非优化版本。执行非优化版本的结果可能导致其中一些数据包产生查找命中或查找未命中。这不会影响键查找操作的性能,因为具有扩展状态桶的概率相对较小。

最后

面试题千万不要死记,一定要自己理解,用自己的方式表达出来,在这里预祝各位成功拿下自己心仪的offer。
开源分享:【大厂前端面试题解析+核心总结学习笔记+真实项目实战+最新讲解视频】

大厂面试题

面试题目录

的桶搜索流水线细节(LRU 或可扩展桶,以及预先计算签名或“do-sig”)。对于每个流水线阶段,所描述的操作应用于该阶段处理的两个数据包中的每一个。
在这里插入图片描述
图 24.7:用于键查找操作的存储桶搜索管道(单键大小哈希表)

表 24.15:桶搜索流水线阶段描述(8 字节和 16 字节键哈希表)

阶段名称描述
0 预取数据包元数据1. 从输入数据包的突发中选择下两个数据包。 2. 预取包含键和键签名的数据包元数据。
1 预取表桶1. 从数据包元数据中读取键签名(对于可扩展桶哈希表),或者从数据包元数据中读取键并计算键签名(对于LRU表)。 2. 使用键签名标识桶ID。 3. 预取桶。
2 预取表数据1. 读取桶。 2. 将所有 4 个桶键与输入键进行比较。 3. 仅当识别到匹配时(不可能存在多个键匹配时)将输入键报告为查找命中。 4. 仅对于LRU表,仅在查找命中时使用无分支逻辑更新桶LRU列表(当前键成为新的MRU)。 5. 预取与匹配键关联的键值(键数据),以避免分支(这在查找命中和未命中时都要执行)。

额外注意事项:

  1. 桶搜索算法的流水线版本仅在输入数据包突发中至少有 5 个数据包时执行。如果输入数据包突发中的数据包少于 5 个,则执行非优化版本的桶搜索算法。
  2. 仅适用于可扩展桶哈希表,在针对输入数据包突发中的所有数据包执行了桶搜索算法的流水线版本后,还将针对未产生查找命中但具有扩展状态桶的任何数据包执行桶搜索算法的非优化版本。执行非优化版本的结果可能导致其中一些数据包产生查找命中或查找未命中。这不会影响键查找操作的性能,因为具有扩展状态桶的概率相对较小。

最后

面试题千万不要死记,一定要自己理解,用自己的方式表达出来,在这里预祝各位成功拿下自己心仪的offer。
开源分享:【大厂前端面试题解析+核心总结学习笔记+真实项目实战+最新讲解视频】

[外链图片转存中…(img-7dhsGLJs-1714521117292)]

[外链图片转存中…(img-bnE0lVLL-1714521117292)]

[外链图片转存中…(img-Tfj7KP8T-1714521117293)]

[外链图片转存中…(img-bWOtpsaC-1714521117294)]

  • 7
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值