目录
第一部分:DPDK插件的作用和意义
- 第1章:DPDK插件概述
- 1.1 DPDK插件在VPP中的作用和意义
- 1.2 DPDK插件与DPDK库的关系
- 1.3 DPDK插件在VPP数据包转发流程中的位置
- 1.4 DPDK插件的主要功能概述
- 1.5 与其他输入/输出模块的对比
第二部分:DPDK插件的整体架构
-
- 2.1 DPDK插件的文件组织结构
- 2.2 各文件的功能和职责
- 2.3 模块间的依赖关系
- 2.4 模块与外部系统的关系
-
- 3.1 DPDK设备结构体(dpdk_device_t)
- 3.2 接收队列结构体(dpdk_rx_queue_t)
- 3.3 发送队列结构体(dpdk_tx_queue_t)
- 3.4 每线程数据结构体(dpdk_per_thread_data_t)
- 3.5 DPDK主结构体(dpdk_main_t)
- 3.6 流表相关结构体
- 3.7 数据结构之间的关系
第三部分:DPDK插件的初始化和管理
-
- 4.1 插件注册和入口(VLIB_PLUGIN_REGISTER)
- 4.2 DPDK EAL初始化(dpdk_lib_init)
- 4.3 DPDK设备发现和枚举
- 4.4 缓冲区池创建(dpdk_buffer_pool_init)
- 4.5 设备接口创建(vnet_eth_register_interface)
- 4.6 RX/TX队列分配和配置(一起分配)
- 4.7 dpdk-input节点注册
- 4.8 dpdk-output节点注册(VNET_DEVICE_CLASS)
- 4.9 线程初始化(dpdk_worker_thread_init)
- 4.10 后台进程注册(dpdk_process、admin_up_down_process)
-
- 5.1 设备发现和枚举
- 5.2 设备配置(队列数、描述符数等)
- 5.3 设备设置(dpdk_device_setup)
- 5.4 RX/TX硬件卸载配置(offload配置)
- 5.5 设备启动和停止(dpdk_device_start/stop)
- 5.6 设备状态管理(ADMIN_UP标志)
- 5.7 MAC地址管理(添加/删除MAC地址)
- 5.8 链路状态管理(dpdk_update_link_state)
- 5.9 统计信息管理(dpdk_update_counters)
- 5.10 子接口(Sub-interface)和VLAN管理
- 5.11 中断模式配置(dpdk_setup_interrupts)
- 5.12 admin_up_down_process后台进程
- 5.13 dpdk_process后台进程(统计和链路状态轮询)
-
- 6.1 驱动匹配机制
- 6.2 驱动特性配置
- 6.3 支持的驱动列表
- 6.4 驱动特定优化
-
- 7.1 DPDK mbuf与VPP buffer的转换
- 7.2 缓冲区池(mempool)的创建和管理
- 7.3 缓冲区模板(buffer template)的使用
- 7.4 缓冲区预取(prefetch)优化
- 7.5 内存布局和兼容性
- 7.6 mbuf验证(dpdk_validate_rte_mbuf)
第四部分:数据包接收(Input)
-
- 8.1 dpdk-input节点的注册和类型
- 8.2 节点的主要处理函数(dpdk_input_node)
- 8.3 轮询向量(poll vector)的获取
- 8.4 多设备轮询机制
- 8.5 节点在VLIB图中的位置
-
- 9.1 dpdk_device_input函数详解
- 9.2 rte_eth_rx_burst调用和批量接收
- 9.3 dpdk_process_rx_burst函数处理接收的数据包
- 9.4 mbuf到vlib_buffer的转换
- 9.5 数据包元数据设置(sw_if_index、flags等)
- 9.6 缓冲区模板的应用
-
- 10.1 DPDK硬件卸载标志(ol_flags)的提取
- 10.2 IP校验和卸载(IP checksum offload)
- 10.3 L4校验和卸载(L4 checksum offload)
- 10.4 VLAN处理
- 10.5 RSS哈希处理
- 10.6 硬件卸载标志的传递和使用
-
- 11.1 多段数据包(multi-segment packet)的识别
- 11.2 dpdk_process_subseq_segs函数处理后续段
- 11.3 缓冲区链的构建
- 11.4 VLIB_BUFFER_NEXT_PRESENT标志的使用
- 11.5 总长度计算
-
[第12章:流表处理(Flow Offload)](#第12章:流表处理(Flow Offload))
- 12.1 Flow Offload的概念和作用
- 12.2 dpdk_process_flow_offload函数处理流表
- 12.3 FDIR(Flow Director)标志的处理
- 12.4 流表查找和下一跳选择
- 12.5 流ID和buffer advance的处理
-
[第13章:LRO处理(Large Receive Offload)](#第13章:LRO处理(Large Receive Offload))
- 13.1 LRO的概念和作用
- 13.2 dpdk_process_lro_offload函数处理LRO
- 13.3 GSO(Generic Segmentation Offload)标志的设置
- 13.4 L4头部大小的计算(dpdk_lro_find_l4_hdr_sz)
- 13.5 GSO相关元数据的设置
-
- 14.1 下一跳节点的选择机制(默认ethernet-input)
- 14.2 默认下一跳:ethernet-input节点
- 14.3 ethernet-input的优化标志(ETH_INPUT_FRAME_F_IP4_CKSUM_OK)
- 14.4 Feature Arc的处理(vnet_feature_start_device_input)
- 14.5 每接口下一跳索引(per_interface_next_index)重定向
- 14.6 Flow Offload的每包下一跳选择
- 14.7 数据包到下一节点的入队机制
- 14.8 单一下一跳vs多下一跳的处理
第五部分:数据包发送(Output)
-
- 15.1 dpdk-output节点的注册和类型
- 15.2 节点的主要处理函数(VNET_DEVICE_CLASS_TX_FN)
- 15.3 发送队列的选择机制
- 15.4 节点在VLIB图中的位置
- 15.5 Input和Output的协同工作
-
- 16.1 tx_burst_vector_internal函数详解
- 16.2 rte_eth_tx_burst调用和批量发送
- 16.3 vlib_buffer到mbuf的转换
- 16.4 发送队列锁定机制
- 16.5 批量发送优化
-
- 17.1 dpdk_buffer_tx_offload函数详解
- 17.2 TX硬件卸载标志的设置
- 17.3 IP校验和卸载
- 17.4 L4校验和卸载
- 17.5 TSO(TCP Segmentation Offload)处理
- 17.6 VXLAN隧道卸载
- 17.7 头部长度计算(l2_len、l3_len、l4_len)
-
- 18.1 发送队列的分配和配置
- 18.2 队列与线程的绑定关系
- 18.3 共享队列和独占队列
- 18.4 队列锁定机制
- 18.5 发送失败处理和mbuf释放
- 18.6 mbuf验证(dpdk_validate_rte_mbuf)
第六部分:高级功能和优化
-
[第19章:流表管理(Flow Offload)](#第19章:流表管理(Flow Offload))
- 19.1 Flow Offload流表的创建和删除
- 19.2 流表条目的管理
- 19.3 流表匹配结果的处理
- 19.4 VPP流表规则到DPDK流表规则的转换
-
- 20.1 接收数据包和字节数的统计
- 20.2 发送数据包和字节数的统计
- 20.3 接口计数器的更新
- 20.4 DPDK统计信息的获取(dpdk_update_counters)
- 20.5 XSTATS统计的处理
- 20.6 统计更新的时机和频率
-
- 21.1 多线程支持机制
- 21.2 每线程数据结构(per_thread_data)
- 21.3 接收队列(RX queue)的分配和管理
- 21.4 发送队列(TX queue)的分配和管理
- 21.5 队列与线程的绑定关系
- 21.6 轮询向量(poll vector)的构建
- 21.7 RSS(Receive Side Scaling)配置
- 21.8 RSS队列配置(dpdk_interface_set_rss_queues)
- 21.9 RETA(Redirection Table)配置
-
- 22.1 批量处理优化(burst processing)
- 22.2 预取(prefetch)优化
- 22.3 缓冲区模板优化
- 22.4 向量化处理
- 22.5 缓存行对齐
- 22.6 分支预测优化(PREDICT_TRUE/PREDICT_FALSE)
- 22.7 零拷贝技术
-
- 23.1 DPDK错误类型定义
- 23.2 错误处理机制
- 23.3 错误统计和报告
- 23.4 数据包丢弃的原因和处理
- 23.5 设备错误恢复
第七部分:管理和接口
-
- 24.1 DPDK相关的CLI命令
- 24.2 设备配置命令
- 24.3 统计查询命令
- 24.4 调试命令
- 24.5 API接口(如果有)
-
- 25.1 Cryptodev的概念和作用
- 25.2 Cryptodev的初始化
- 25.3 加密设备的管理
- 25.4 加密操作的数据路径
第八部分:总结
- 第26章:DPDK插件总结
- 26.1 DPDK插件的关键特点
- 26.2 在VPP数据包转发中的作用
- 26.3 性能优化要点
- 26.4 与其他模块的关系
- 26.5 最佳实践和注意事项
第8章:dpdk-input节点核心处理
8.1 dpdk-input节点概述
作用和实现原理:
dpdk-input节点是VPP中DPDK插件的核心输入节点,负责从DPDK网卡接收数据包并转换为VPP可以处理的格式。就像"快递分拣中心"的"收货窗口",负责从"网卡"(DPDK设备)接收"包裹"(数据包),并将"包裹"转换为"分拣中心"可以处理的格式。
dpdk-input节点的核心作用:
- 轮询网卡:定期轮询DPDK网卡,检查是否有新数据包到达
- 批量接收 :使用
rte_eth_rx_burst批量接收数据包(一次最多接收32个) - 格式转换 :将DPDK的
rte_mbuf格式转换为VPP的vlib_buffer_t格式 - 元数据设置:设置数据包的元数据(接口索引、标志等)
- 分发数据包:将数据包分发到下一个处理节点(如ethernet-input)
dpdk-input节点在VPP数据包处理流程中的位置:
┌─────────────────────────────────────────────────────────────┐
│ VPP数据包接收处理流程(简化版) │
└─────────────────────────────────────────────────────────────┘
网卡硬件(DPDK设备)
│
│ 硬件接收数据包
▼
┌─────────────────────┐
│ dpdk-input节点 │ ← 本章重点讲解
│ (轮询、接收、转换) │
└──────────┬──────────┘
│
│ 转换为VPP格式
▼
┌─────────────────────┐
│ ethernet-input节点 │
│ (以太网处理) │
└──────────┬──────────┘
│
│ 解析以太网头
▼
┌─────────────────────┐
│ ip4-input节点 │
│ (IP层处理) │
└──────────┬──────────┘
│
│ 路由查找、转发
▼
后续处理节点...
通俗理解:
就像"快递分拣中心"的"收货窗口":
- 轮询网卡:定期"检查"网卡是否有"新包裹"(数据包)
- 批量接收:一次"接收多个包裹"(批量接收),提高"效率"
- 格式转换:将"DPDK格式"的"包裹"转换为"VPP格式"的"包裹"
- 分发处理:将"包裹"分发到"下一个处理窗口"(下一个节点)
8.2 dpdk-input节点的注册和类型
作用和实现原理:
dpdk-input节点通过VLIB_REGISTER_NODE宏注册到VPP的节点系统中。节点类型为VLIB_NODE_TYPE_INPUT,表示这是一个输入节点,由VPP调度器定期调用。就像"快递分拣中心"的"窗口注册",将"收货窗口"注册到"分拣中心"的"窗口管理系统"中。
节点注册代码 (src/plugins/dpdk/device/node.c:563):
c
VLIB_REGISTER_NODE (dpdk_input_node) = {
.type = VLIB_NODE_TYPE_INPUT, // 节点类型:输入节点
.name = "dpdk-input", // 节点名称
.sibling_of = "device-input", // 兄弟节点:device-input
.flags = VLIB_NODE_FLAG_TRACE_SUPPORTED, // 节点标志:支持跟踪
/* Will be enabled if/when hardware is detected. */
.state = VLIB_NODE_STATE_DISABLED, // 初始状态:禁用(检测到硬件后启用)
.format_buffer = format_ethernet_header_with_length, // 格式化buffer函数
.format_trace = format_dpdk_rx_trace, // 格式化跟踪函数
.n_errors = DPDK_N_ERROR, // 错误数量
.error_strings = dpdk_error_strings, // 错误字符串数组
};
节点注册关键字段说明:
| 字段 | 值 | 说明 |
|---|---|---|
type |
VLIB_NODE_TYPE_INPUT |
输入节点类型,由调度器定期调用 |
name |
"dpdk-input" |
节点名称,用于节点图显示和CLI |
sibling_of |
"device-input" |
兄弟节点,表示与其他设备输入节点同级 |
state |
VLIB_NODE_STATE_DISABLED |
初始状态为禁用,检测到硬件后启用 |
flags |
VLIB_NODE_FLAG_TRACE_SUPPORTED |
支持数据包跟踪功能 |
节点状态说明:
VLIB_NODE_STATE_DISABLED:节点禁用,不会被调度器调用VLIB_NODE_STATE_POLLING:节点启用,调度器会定期轮询调用VLIB_NODE_STATE_INTERRUPT:节点启用中断模式,由中断触发调用
节点启用时机 (src/plugins/dpdk/device/init.c:1617):
c
// 当检测到DPDK设备时,启用dpdk-input节点
vlib_node_set_state (vm, dpdk_input_node.index, VLIB_NODE_STATE_POLLING);
通俗理解:
就像"快递分拣中心"的"窗口注册":
- 注册窗口:将"收货窗口"注册到"窗口管理系统"
- 窗口类型:标记为"输入窗口"(输入节点),由"调度系统"定期调用
- 初始状态:初始状态为"关闭"(禁用),检测到"设备"后"开启"(启用)
- 窗口名称:命名为"dpdk-input",方便识别和管理
8.3 dpdk-input节点的主要处理函数
作用和实现原理:
dpdk_input_node是dpdk-input节点的主处理函数,由VPP调度器定期调用。它负责轮询所有分配给当前线程的DPDK设备,接收数据包并处理。就像"快递分拣中心"的"收货窗口"的"主处理流程",定期"检查"所有"分配给当前窗口的设备","接收包裹"并"处理"。
主处理函数 (src/plugins/dpdk/device/node.c:539):
c
VLIB_NODE_FN (dpdk_input_node) (vlib_main_t * vm, vlib_node_runtime_t * node,
vlib_frame_t * f)
{
dpdk_main_t *dm = &dpdk_main; // 获取DPDK主结构体
dpdk_device_t *xd; // DPDK设备指针
uword n_rx_packets = 0; // 接收到的数据包总数
vnet_hw_if_rxq_poll_vector_t *pv; // 轮询向量指针
u32 thread_index = vm->thread_index; // 当前线程索引
/*
* Poll all devices on this cpu for input/interrupts.
* 轮询当前CPU上的所有设备,检查输入/中断
*/
// 获取当前线程需要轮询的所有设备队列列表(轮询向量)
pv = vnet_hw_if_get_rxq_poll_vector (vm, node);
// 遍历轮询向量中的每个设备队列
for (int i = 0; i < vec_len (pv); i++)
{
// 根据设备实例索引获取DPDK设备
xd = vec_elt_at_index (dm->devices, pv[i].dev_instance);
// 调用设备输入函数,处理该设备的指定队列
// 参数说明:
// - vm: VPP主结构体
// - dm: DPDK主结构体
// - xd: DPDK设备结构体
// - node: 节点运行时结构体
// - thread_index: 线程索引
// - pv[i].queue_id: 队列ID
n_rx_packets +=
dpdk_device_input (vm, dm, xd, node, thread_index, pv[i].queue_id);
}
// 返回接收到的数据包总数
return n_rx_packets;
}
函数处理流程:
┌─────────────────────────────────────────────────────────────┐
│ dpdk_input_node函数处理流程 │
└─────────────────────────────────────────────────────────────┘
开始
│
▼
获取DPDK主结构体(dm)
│
▼
获取当前线程索引(thread_index)
│
▼
获取轮询向量(pv = vnet_hw_if_get_rxq_poll_vector)
│
│ 轮询向量包含当前线程需要处理的所有设备队列
│ 例如:[设备0队列0, 设备0队列1, 设备1队列0, ...]
│
▼
遍历轮询向量中的每个设备队列
│
├── 获取DPDK设备(xd = dm->devices[pv[i].dev_instance])
│ │
│ ▼
│ 调用dpdk_device_input处理该设备的队列
│ │
│ ├── 从网卡接收数据包(rte_eth_rx_burst)
│ ├── 转换为VPP格式(mbuf → buffer)
│ ├── 设置元数据(sw_if_index、flags等)
│ ├── 处理硬件卸载(校验和、LRO等)
│ └── 分发到下一个节点
│ │
│ ▼
│ 累加接收到的数据包数
│
└── 继续下一个设备队列
│
▼
返回接收到的数据包总数
关键步骤说明:
-
获取轮询向量 :
pv = vnet_hw_if_get_rxq_poll_vector(vm, node)- 获取当前线程需要轮询的所有设备队列列表
- 轮询向量是一个数组,每个元素包含
dev_instance(设备实例)和queue_id(队列ID)
-
遍历设备队列 :
for (int i = 0; i < vec_len(pv); i++)- 遍历轮询向量中的每个设备队列
- 每个设备队列都需要单独处理
-
获取设备 :
xd = vec_elt_at_index(dm->devices, pv[i].dev_instance)- 根据设备实例索引获取DPDK设备结构体
- 设备结构体包含设备的所有信息(队列、配置等)
-
处理设备输入 :
dpdk_device_input(...)- 调用设备输入函数处理该设备的指定队列
- 该函数负责接收数据包、转换格式、设置元数据、分发数据包
通俗理解:
就像"快递分拣中心"的"收货窗口"的"主处理流程":
- 获取任务列表:获取"当前窗口"需要处理的"所有设备队列"(轮询向量)
- 遍历设备:遍历"任务列表"中的每个"设备队列"
- 处理设备:对每个"设备队列"执行"收货流程"(dpdk_device_input)
- 统计结果:统计"接收到的包裹总数"
8.4 轮询向量(Poll Vector)机制
作用和实现原理:
轮询向量(Poll Vector)是一个数组,包含了当前线程需要轮询的所有设备队列。每个元素包含设备实例索引和队列ID。VPP使用轮询向量来管理多设备、多队列的轮询。就像"快递分拣中心"的"任务列表",记录了"当前窗口"需要处理的"所有设备队列"。
轮询向量数据结构 (src/vnet/interface.h:778):
c
typedef struct
{
u32 dev_instance; // 设备实例索引(在devices数组中的索引)
u16 queue_id; // 队列ID(设备上的队列编号)
} vnet_hw_if_rxq_poll_vector_t;
轮询向量获取函数 (src/vnet/interface/rx_queue_funcs.h:69):
c
static_always_inline vnet_hw_if_rxq_poll_vector_t *
vnet_hw_if_get_rxq_poll_vector (vlib_main_t *vm, vlib_node_runtime_t *node)
{
vnet_hw_if_rx_node_runtime_t *rt = (void *) node->runtime_data;
vnet_hw_if_rxq_poll_vector_t *pv = rt->rxq_vector_int;
// 如果节点处于中断模式,生成中断轮询向量
if (PREDICT_FALSE (node->state == VLIB_NODE_STATE_INTERRUPT))
pv = vnet_hw_if_generate_rxq_int_poll_vector (vm, node);
// 如果节点处于自适应模式,使用轮询向量
else if (node->flags & VLIB_NODE_FLAG_ADAPTIVE_MODE)
pv = rt->rxq_vector_poll;
return pv;
}
轮询向量示例:
假设当前线程需要处理以下设备队列:
- 设备0(eth0)的队列0
- 设备0(eth0)的队列1
- 设备1(eth1)的队列0
那么轮询向量可能是:
c
pv[0] = {dev_instance: 0, queue_id: 0} // 设备0队列0
pv[1] = {dev_instance: 0, queue_id: 1} // 设备0队列1
pv[2] = {dev_instance: 1, queue_id: 0} // 设备1队列0
轮询向量示意图:
┌─────────────────────────────────────────────────────────────┐
│ 轮询向量(Poll Vector)示意图 │
└─────────────────────────────────────────────────────────────┘
线程0的轮询向量:
┌─────────────┬─────────────┬─────────────┐
│ dev:0 q:0 │ dev:0 q:1 │ dev:1 q:0 │
└─────────────┴─────────────┴─────────────┘
│ │ │
▼ ▼ ▼
┌──────────┐ ┌──────────┐ ┌──────────┐
│ 设备0 │ │ 设备0 │ │ 设备1 │
│ 队列0 │ │ 队列1 │ │ 队列0 │
└──────────┘ └──────────┘ └──────────┘
线程1的轮询向量:
┌─────────────┬─────────────┐
│ dev:1 q:1 │ dev:2 q:0 │
└─────────────┴─────────────┘
│ │
▼ ▼
┌──────────┐ ┌──────────┐
│ 设备1 │ │ 设备2 │
│ 队列1 │ │ 队列0 │
└──────────┘ └──────────┘
轮询向量的作用:
- 多设备支持:一个线程可以轮询多个设备的队列
- 多队列支持:一个设备可以有多个队列,每个队列可以分配给不同的线程
- 负载均衡:通过将不同队列分配给不同线程,实现负载均衡
- 动态管理:轮询向量可以根据设备状态动态更新
通俗理解:
就像"快递分拣中心"的"任务列表":
- 任务列表:记录了"当前窗口"需要处理的"所有设备队列"
- 多设备:一个"窗口"可以处理"多个设备"的"包裹"
- 多队列:一个"设备"可以有"多个队列",每个"队列"可以分配给不同的"窗口"
- 负载均衡:通过"分配不同队列到不同窗口",实现"负载均衡"
8.5 dpdk_device_input函数详解
作用和实现原理:
dpdk_device_input是DPDK插件中处理单个设备队列数据包接收的核心函数。它负责从DPDK网卡接收数据包、转换为VPP格式、处理硬件卸载、设置元数据,并分发到下一个处理节点。就像"快递分拣中心"的"单个设备处理流程",负责处理"一个设备队列"的"完整收货流程"。
函数签名 (src/plugins/dpdk/device/node.c:342):
c
static_always_inline u32
dpdk_device_input (vlib_main_t * vm, // VPP主结构体
dpdk_main_t * dm, // DPDK主结构体
dpdk_device_t * xd, // DPDK设备结构体
vlib_node_runtime_t * node, // 节点运行时结构体
u32 thread_index, // 线程索引
u16 queue_id) // 队列ID
函数返回值:
- 返回接收到的数据包数量(
n_rx_packets) - 如果没有接收到数据包,返回0
函数完整处理流程:
┌─────────────────────────────────────────────────────────────┐
│ dpdk_device_input函数完整处理流程 │
└─────────────────────────────────────────────────────────────┘
开始
│
▼
步骤1:初始化和状态检查
│
├── 获取RX队列和线程数据
├── 检查设备是否处于ADMIN_UP状态
└── 如果未启动,直接返回0
│
▼
步骤2:从DPDK网卡批量接收数据包
│
├── 循环调用rte_eth_rx_burst(最多DPDK_RX_BURST_SZ个)
├── 每次最多接收32个数据包
└── 如果接收数量小于请求数量,停止接收
│
▼
步骤3:更新缓冲区模板
│
├── 设置sw_if_index(接收接口索引)
├── 设置错误码、标志、缓冲区池索引
└── 设置引用计数、特性弧索引等
│
▼
步骤4:确定下一个处理节点
│
├── 检查是否有每接口重定向(per_interface_next_index)
├── 检查是否有特性弧(feature arc)
└── 确定最终的下一个节点索引
│
▼
步骤5:处理接收的数据包(dpdk_process_rx_burst)
│
├── 将mbuf转换为buffer
├── 复制缓冲区模板
├── 设置current_data和current_length
├── 提取硬件卸载标志(ol_flags)
└── 处理多段数据包(如果需要)
│
▼
步骤6:处理硬件卸载
│
├── 处理LRO卸载(Large Receive Offload)
├── 处理L4校验和错误
└── 更新缓冲区标志
│
▼
步骤7:处理流卸载(Flow Offload)
│
├── 检查是否有Flow Director标志
├── 查找流表项
└── 设置下一个节点和流ID
│
▼
步骤8:分发数据包到下一个节点
│
├── 如果启用Flow Director:使用ptd->next数组
└── 否则:使用单一next_index
│
▼
步骤9:处理数据包跟踪(如果启用)
│
├── 获取跟踪计数
├── 添加跟踪信息
└── 记录mbuf和buffer信息
│
▼
步骤10:更新统计信息
│
├── 更新接口接收统计(数据包数、字节数)
└── 更新设备接收统计
│
▼
返回接收到的数据包数量
8.5.1 步骤1:初始化和状态检查
作用和实现原理:
在开始处理之前,需要获取必要的结构体指针,并检查设备是否处于可用状态。如果设备未启动,直接返回0,避免无效操作。
初始化代码 (src/plugins/dpdk/device/node.c:346):
c
/* 步骤1.1:声明局部变量 */
uword n_rx_packets = 0, n_rx_bytes; // 接收到的数据包数和字节数
dpdk_rx_queue_t *rxq; // RX队列指针
u32 n_left, n_trace; // 剩余数据包数、跟踪计数
u32 *buffers; // 缓冲区索引数组
u32 next_index = VNET_DEVICE_INPUT_NEXT_ETHERNET_INPUT; // 默认下一个节点
struct rte_mbuf **mb; // mbuf指针数组
vlib_buffer_t *b0; // buffer指针
u16 *next; // 下一个节点索引数组
u32 or_flags; // 硬件卸载标志的OR结果
u32 n; // 循环计数器
int single_next = 0; // 是否使用单一next节点
/* 步骤1.2:获取RX队列结构体 */
/* 根据队列ID从设备的RX队列数组中获取对应的队列结构体 */
rxq = vec_elt_at_index (xd->rx_queues, queue_id);
/* 步骤1.3:获取线程数据 */
/* 每个线程都有自己的线程数据,包含mbuf数组、buffer数组、next数组等 */
dpdk_per_thread_data_t *ptd = vec_elt_at_index (dm->per_thread_data,
thread_index);
/* 步骤1.4:获取缓冲区模板 */
/* 缓冲区模板用于快速初始化新接收的数据包 */
vlib_buffer_t *bt = &ptd->buffer_template;
/* 步骤1.5:检查设备状态 */
/* DPDK_DEVICE_FLAG_ADMIN_UP标志表示设备已经启动(admin up) */
if ((xd->flags & DPDK_DEVICE_FLAG_ADMIN_UP) == 0)
return 0; // 如果设备未启动,直接返回0
关键说明:
-
dpdk_per_thread_data_t:每个线程的私有数据,包含:mbufs[DPDK_RX_BURST_SZ]:mbuf指针数组buffers[DPDK_RX_BURST_SZ]:buffer索引数组next[DPDK_RX_BURST_SZ]:下一个节点索引数组flags[DPDK_RX_BURST_SZ]:硬件卸载标志数组buffer_template:缓冲区模板
-
设备状态检查 :只有设备处于
ADMIN_UP状态时,才会接收数据包
通俗理解:
就像"快递分拣中心"的"准备工作":
- 获取工具:获取"队列信息"、"线程数据"、"模板"等"工具"
- 检查状态:检查"设备"是否"启动",如果"未启动"就不处理
8.5.2 步骤2:从DPDK网卡批量接收数据包
作用和实现原理:
使用rte_eth_rx_burst函数从DPDK网卡的指定队列批量接收数据包。为了提高效率,会循环调用直到达到最大批量大小或没有更多数据包。
批量接收代码 (src/plugins/dpdk/device/node.c:365):
c
/* 步骤2.1:循环接收数据包,直到达到最大批量大小 */
/* DPDK_RX_BURST_SZ通常是256(VLIB_FRAME_SIZE) */
while (n_rx_packets < DPDK_RX_BURST_SZ)
{
/* 步骤2.2:计算本次要接收的数据包数量 */
/* 每次最多接收32个,但不超过剩余容量 */
u32 n_to_rx = clib_min (DPDK_RX_BURST_SZ - n_rx_packets, 32);
/* 步骤2.3:调用DPDK的接收函数 */
/* rte_eth_rx_burst参数说明:
- xd->port_id: DPDK端口ID(网卡ID)
- queue_id: 队列ID
- ptd->mbufs + n_rx_packets: mbuf指针数组的起始位置
- n_to_rx: 要接收的数据包数量
返回值:实际接收到的数据包数量(可能小于n_to_rx)
*/
n = rte_eth_rx_burst (xd->port_id, queue_id,
ptd->mbufs + n_rx_packets, n_to_rx);
/* 步骤2.4:累加接收到的数据包数 */
n_rx_packets += n;
/* 步骤2.5:如果接收数量小于请求数量,说明没有更多数据包了 */
/* 此时应该停止接收,避免无效的轮询 */
if (n < n_to_rx)
break;
}
/* 步骤2.6:如果没有接收到任何数据包,直接返回 */
if (n_rx_packets == 0)
return 0;
关键说明:
- 批量接收 :每次最多接收32个数据包,循环接收直到达到
DPDK_RX_BURST_SZ(通常256个) - 提前退出:如果某次接收的数量小于请求数量,说明网卡队列已空,停止接收
- 零数据包处理:如果没有接收到任何数据包,直接返回0,避免后续无效处理
接收过程示例:
第1次调用:请求32个,实际接收32个 → n_rx_packets = 32
第2次调用:请求32个,实际接收32个 → n_rx_packets = 64
第3次调用:请求32个,实际接收32个 → n_rx_packets = 96
...
第8次调用:请求32个,实际接收32个 → n_rx_packets = 256(达到上限,退出)
或者
第5次调用:请求32个,实际接收8个 → n_rx_packets = 136(小于请求数,退出)
通俗理解:
就像"快递分拣中心"的"接收包裹":
- 批量接收:一次"接收多个包裹"(最多32个),"循环接收"直到"达到上限"或"没有更多包裹"
- 提前退出:如果"接收的包裹数"小于"请求的包裹数",说明"没有更多包裹了",停止接收
8.5.3 步骤3:更新缓冲区模板
作用和实现原理:
缓冲区模板用于快速初始化新接收的数据包。模板包含了所有数据包共有的信息(如接口索引、错误码、标志等),通过复制模板可以快速设置这些字段,避免逐个设置。
更新模板代码 (src/plugins/dpdk/device/node.c:381):
c
/* 步骤3.1:设置接收接口索引(sw_if_index) */
/* sw_if_index是VPP中接口的软件索引,用于标识数据包来自哪个接口 */
vnet_buffer (bt)->sw_if_index[VLIB_RX] = xd->sw_if_index;
/* 步骤3.2:设置错误码 */
/* DPDK_ERROR_NONE表示没有错误,数据包正常接收 */
bt->error = node->errors[DPDK_ERROR_NONE];
/* 步骤3.3:设置缓冲区标志 */
/* xd->buffer_flags包含设备的缓冲区标志,如校验和卸载标志等 */
bt->flags = xd->buffer_flags;
/* 步骤3.4:设置缓冲区池索引 */
/* 每个RX队列都有对应的缓冲区池,DPDK从这个池中分配缓冲区 */
/* 由于DPDK在接口启动前就为每个队列分配了缓冲区池,所以可以安全地存储在模板中 */
bt->buffer_pool_index = rxq->buffer_pool_index;
/* 步骤3.5:设置引用计数 */
/* 初始引用计数为1,表示当前有一个引用 */
bt->ref_count = 1;
/* 步骤3.6:设置特性弧索引 */
/* 特性弧(feature arc)用于数据包处理流程的扩展 */
vnet_buffer (bt)->feature_arc_index = 0;
/* 步骤3.7:设置当前配置索引 */
/* 用于特性弧的配置索引 */
bt->current_config_index = 0;
关键说明:
- 模板的作用:模板包含了所有数据包共有的信息,通过复制模板可以快速初始化
- 接口索引 :
sw_if_index[VLIB_RX]标识数据包的接收接口 - 缓冲区池索引:标识数据包来自哪个缓冲区池,用于后续回收
通俗理解:
就像"快递分拣中心"的"标准模板":
- 设置模板:为"所有包裹"设置"共同信息"(接口、错误码、标志等)
- 快速初始化:后续"新包裹"可以直接"复制模板",快速设置这些信息
8.5.4 步骤4:确定下一个处理节点
作用和实现原理:
根据接口配置和特性弧,确定数据包的下一个处理节点。默认是ethernet-input节点,但可以通过配置或特性弧改变。
确定下一个节点代码 (src/plugins/dpdk/device/node.c:392):
c
/* 步骤4.1:检查是否有每接口重定向 */
/* per_interface_next_index允许为每个接口配置不同的下一个节点 */
/* 例如:某些接口可能需要直接发送到ip4-input而不是ethernet-input */
if (PREDICT_FALSE (xd->per_interface_next_index != ~0))
next_index = xd->per_interface_next_index;
/* 步骤4.2:检查是否有特性弧(feature arc) */
/* 特性弧允许在数据包处理流程中插入额外的处理节点 */
/* 例如:QoS、ACL、NAT等功能可以通过特性弧实现 */
/* 由于所有数据包都属于同一个接口,特性弧查找只需要执行一次,结果存储在模板中 */
if (PREDICT_FALSE (vnet_device_input_have_features (xd->sw_if_index)))
vnet_feature_start_device_input (xd->sw_if_index, &next_index, bt);
关键说明:
- 默认节点 :
VNET_DEVICE_INPUT_NEXT_ETHERNET_INPUT是默认的下一个节点 - 每接口重定向 :
per_interface_next_index允许为每个接口配置不同的下一个节点 - 特性弧:特性弧允许在数据包处理流程中插入额外的处理节点,实现功能扩展
下一个节点的可能值:
| 节点 | 说明 | 使用场景 |
|---|---|---|
ethernet-input |
以太网输入节点 | 默认场景,处理以太网帧 |
ip4-input-no-checksum |
IPv4输入节点(无校验和) | 硬件已校验IP校验和 |
ip4-input |
IPv4输入节点 | 需要软件校验IP校验和 |
ip6-input |
IPv6输入节点 | IPv6数据包处理 |
mpls-input |
MPLS输入节点 | MPLS数据包处理 |
通俗理解:
就像"快递分拣中心"的"确定下一站":
- 默认路径:默认"包裹"会送到"以太网处理窗口"(ethernet-input)
- 特殊配置:某些"接口"可能需要"特殊路径"(per_interface_next_index)
- 功能扩展:通过"特性弧"可以在"路径"中插入"额外处理窗口"(QoS、ACL等)
8.5.5 步骤5:处理接收的数据包(dpdk_process_rx_burst)
作用和实现原理:
dpdk_process_rx_burst函数负责将DPDK的mbuf转换为VPP的buffer,设置数据偏移和长度,提取硬件卸载标志,并处理多段数据包。这是数据包处理的核心步骤。
调用代码 (src/plugins/dpdk/device/node.c:401):
c
/* 步骤5.1:根据设备是否支持多段数据包选择不同的处理函数 */
/* DPDK_DEVICE_FLAG_MAYBE_MULTISEG标志表示设备可能接收多段数据包 */
if (xd->flags & DPDK_DEVICE_FLAG_MAYBE_MULTISEG)
/* 支持多段数据包:传入maybe_multiseg=1,会处理多段数据包 */
n_rx_bytes = dpdk_process_rx_burst (vm, ptd, n_rx_packets, 1, &or_flags);
else
/* 不支持多段数据包:传入maybe_multiseg=0,跳过多段处理 */
n_rx_bytes = dpdk_process_rx_burst (vm, ptd, n_rx_packets, 0, &or_flags);
dpdk_process_rx_burst函数详解 (src/plugins/dpdk/device/node.c:167):
c
static_always_inline uword
dpdk_process_rx_burst (vlib_main_t *vm, // VPP主结构体
dpdk_per_thread_data_t *ptd, // 线程数据
uword n_rx_packets, // 接收到的数据包数
int maybe_multiseg, // 是否可能有多段数据包
u32 *or_flagsp) // 硬件卸载标志的OR结果
{
u32 n_left = n_rx_packets; // 剩余待处理的数据包数
vlib_buffer_t *b[4]; // buffer指针数组(用于批量处理)
struct rte_mbuf **mb = ptd->mbufs; // mbuf指针数组
uword n_bytes = 0; // 总字节数
u32 *flags, or_flags = 0; // 硬件卸载标志数组和OR结果
vlib_buffer_t bt; // 本地缓冲区模板
mb = ptd->mbufs;
flags = ptd->flags;
/* 步骤5.1.1:复制模板到本地变量 */
/* 这样可以避免每次访问模板时都从ptd中加载,提高性能 */
vlib_buffer_copy_template (&bt, &ptd->buffer_template);
/* 步骤5.1.2:批量处理数据包(每次处理4个) */
while (n_left >= 8) // 至少8个数据包才开始批量处理
{
/* 步骤5.1.2.1:预取下一批buffer(优化性能) */
dpdk_prefetch_buffer_x4 (mb + 4);
/* 步骤5.1.2.2:将mbuf转换为buffer */
/* 通过指针偏移,从mbuf指针获取对应的buffer指针 */
b[0] = vlib_buffer_from_rte_mbuf (mb[0]);
b[1] = vlib_buffer_from_rte_mbuf (mb[1]);
b[2] = vlib_buffer_from_rte_mbuf (mb[2]);
b[3] = vlib_buffer_from_rte_mbuf (mb[3]);
/* 步骤5.1.2.3:复制模板到每个buffer */
/* 快速设置buffer的公共字段(接口索引、错误码、标志等) */
vlib_buffer_copy_template (b[0], &bt);
vlib_buffer_copy_template (b[1], &bt);
vlib_buffer_copy_template (b[2], &bt);
vlib_buffer_copy_template (b[3], &bt);
/* 步骤5.1.2.4:预取下一批mbuf数据(优化性能) */
dpdk_prefetch_mbuf_x4 (mb + 4);
/* 步骤5.1.2.5:提取硬件卸载标志 */
/* 提取每个mbuf的ol_flags,并计算OR结果 */
or_flags |= dpdk_ol_flags_extract (mb, flags, 4);
flags += 4;
/* 步骤5.1.2.6:设置数据偏移和长度 */
/* current_data:当前数据在buffer中的偏移量 */
/* current_length:当前数据的长度 */
/* mbuf的data_off是相对于buf_addr的偏移,需要减去HEADROOM得到相对于data的偏移 */
b[0]->current_data = mb[0]->data_off - RTE_PKTMBUF_HEADROOM;
n_bytes += b[0]->current_length = mb[0]->data_len;
b[1]->current_data = mb[1]->data_off - RTE_PKTMBUF_HEADROOM;
n_bytes += b[1]->current_length = mb[1]->data_len;
b[2]->current_data = mb[2]->data_off - RTE_PKTMBUF_HEADROOM;
n_bytes += b[2]->current_length = mb[2]->data_len;
b[3]->current_data = mb[3]->data_off - RTE_PKTMBUF_HEADROOM;
n_bytes += b[3]->current_length = mb[3]->data_len;
/* 步骤5.1.2.7:处理多段数据包(如果需要) */
if (maybe_multiseg)
{
/* 处理每个buffer的多段数据包 */
n_bytes += dpdk_process_subseq_segs (vm, b[0], mb[0], &bt);
n_bytes += dpdk_process_subseq_segs (vm, b[1], mb[1], &bt);
n_bytes += dpdk_process_subseq_segs (vm, b[2], mb[2], &bt);
n_bytes += dpdk_process_subseq_segs (vm, b[3], mb[3], &bt);
}
/* 步骤5.1.2.8:移动到下一批数据包 */
mb += 4;
n_left -= 4;
}
/* 步骤5.1.3:处理剩余的数据包(不足4个的) */
while (n_left)
{
/* 转换mbuf为buffer */
b[0] = vlib_buffer_from_rte_mbuf (mb[0]);
/* 复制模板 */
vlib_buffer_copy_template (b[0], &bt);
/* 提取硬件卸载标志 */
or_flags |= dpdk_ol_flags_extract (mb, flags, 1);
flags += 1;
/* 设置数据偏移和长度 */
b[0]->current_data = mb[0]->data_off - RTE_PKTMBUF_HEADROOM;
n_bytes += b[0]->current_length = mb[0]->data_len;
/* 处理多段数据包(如果需要) */
if (maybe_multiseg)
n_bytes += dpdk_process_subseq_segs (vm, b[0], mb[0], &bt);
/* 移动到下一个数据包 */
mb += 1;
n_left -= 1;
}
/* 步骤5.1.4:返回硬件卸载标志的OR结果和总字节数 */
*or_flagsp = or_flags;
return n_bytes;
}
关键说明:
- 批量处理:每次处理4个数据包,提高处理效率
- 预取优化:在处理当前批次时,预取下一批次的数据,减少内存访问延迟
- 数据偏移计算 :
current_data = mbuf->data_off - RTE_PKTMBUF_HEADROOMmbuf->data_off是相对于buf_addr的偏移RTE_PKTMBUF_HEADROOM是mbuf的头部空间(128字节)- 相减后得到相对于
buffer->data的偏移
- 多段处理:如果设备支持多段数据包,需要处理后续段
通俗理解:
就像"快递分拣中心"的"格式化包裹":
- 批量处理:一次"格式化多个包裹"(4个一批),提高"效率"
- 复制模板:使用"标准模板"快速"设置包裹信息"
- 设置数据位置:记录"包裹数据"在"包裹"中的"位置"和"长度"
- 处理大包裹:如果"包裹"太大(多段),需要"分段处理"
8.5.6 步骤6:处理硬件卸载
作用和实现原理:
硬件卸载是指网卡硬件完成某些处理任务(如校验和计算、LRO等),软件只需要检查结果。这一步处理LRO(Large Receive Offload)和L4校验和错误。
处理LRO卸载代码 (src/plugins/dpdk/device/node.c:406):
c
/* 步骤6.1:处理LRO卸载(Large Receive Offload) */
/* LRO是网卡将多个TCP数据包合并成一个大数据包的功能 */
/* 如果硬件卸载标志中包含RTE_MBUF_F_RX_LRO,说明数据包经过了LRO处理 */
if (PREDICT_FALSE ((or_flags & RTE_MBUF_F_RX_LRO)))
dpdk_process_lro_offload (xd, ptd, n_rx_packets);
dpdk_process_lro_offload函数 (src/plugins/dpdk/device/node.c:325):
c
static_always_inline void
dpdk_process_lro_offload (dpdk_device_t *xd,
dpdk_per_thread_data_t *ptd,
uword n_rx_packets)
{
uword n;
vlib_buffer_t *b0;
/* 遍历所有数据包,检查是否有LRO标志 */
for (n = 0; n < n_rx_packets; n++)
{
/* 获取buffer指针 */
b0 = vlib_buffer_from_rte_mbuf (ptd->mbufs[n]);
/* 如果mbuf的硬件卸载标志中包含RTE_MBUF_F_RX_LRO */
if (ptd->flags[n] & RTE_MBUF_F_RX_LRO)
{
/* 设置GSO标志(Generic Segmentation Offload) */
/* VPP使用GSO标志表示数据包需要分段处理 */
b0->flags |= VNET_BUFFER_F_GSO;
/* 设置GSO大小(每个段的长度) */
vnet_buffer2 (b0)->gso_size = ptd->mbufs[n]->tso_segsz;
/* 设置L4头部大小(用于GSO分段) */
vnet_buffer2 (b0)->gso_l4_hdr_sz = dpdk_lro_find_l4_hdr_sz (b0);
}
}
}
处理L4校验和错误代码 (src/plugins/dpdk/device/node.c:409):
c
/* 步骤6.2:处理L4校验和错误 */
/* 如果硬件检测到L4校验和错误,但VPP之前设置了L4_CHECKSUM_CORRECT标志 */
/* 需要清除这个标志,因为硬件已经检测到错误了 */
if (PREDICT_FALSE ((or_flags & RTE_MBUF_F_RX_L4_CKSUM_BAD) &&
(xd->buffer_flags & VNET_BUFFER_F_L4_CHECKSUM_CORRECT)))
{
/* 遍历所有数据包,检查L4校验和错误 */
for (n = 0; n < n_rx_packets; n++)
{
/* 获取buffer指针 */
b0 = vlib_buffer_from_rte_mbuf (ptd->mbufs[n]);
/* 如果mbuf的硬件卸载标志中包含RTE_MBUF_F_RX_L4_CKSUM_BAD */
/* 需要清除VPP的L4_CHECKSUM_CORRECT标志 */
/* 这里使用XOR操作来切换标志位 */
/* 注意:RTE_MBUF_F_RX_L4_CKSUM_BAD的位号是3 */
/* VNET_BUFFER_F_LOG2_L4_CHECKSUM_CORRECT是L4_CHECKSUM_CORRECT标志的位号 */
b0->flags ^= (ptd->flags[n] & RTE_MBUF_F_RX_L4_CKSUM_BAD)
<< (VNET_BUFFER_F_LOG2_L4_CHECKSUM_CORRECT - 3);
}
}
关键说明:
- LRO处理:LRO是网卡将多个TCP数据包合并的功能,需要设置GSO标志和大小
- 校验和错误:如果硬件检测到校验和错误,需要清除VPP的校验和正确标志
- 标志位操作:使用XOR操作切换标志位,避免直接设置/清除
通俗理解:
就像"快递分拣中心"的"检查包裹质量":
- LRO处理:如果"包裹"是"合并的大包裹"(LRO),需要标记"需要分段"(GSO)
- 校验和错误:如果"硬件检查"发现"包裹"有问题(校验和错误),需要清除"正确标志"
8.5.7 步骤7:处理流卸载(Flow Offload)
作用和实现原理:
流卸载(Flow Offload)是指网卡硬件根据数据包的特征(如5元组)将数据包分发到不同的处理路径。如果启用了Flow Director,需要查找流表并设置下一个节点。
处理流卸载代码 (src/plugins/dpdk/device/node.c:426):
c
/* 步骤7.1:检查是否有Flow Director标志 */
/* RTE_MBUF_F_RX_FDIR标志表示数据包经过了Flow Director处理 */
if (PREDICT_FALSE (or_flags & RTE_MBUF_F_RX_FDIR))
{
/* 步骤7.1.1:初始化所有数据包的next节点为默认值 */
/* 因为Flow Director可能会改变某些数据包的next节点 */
for (n = 0; n < n_rx_packets; n++)
ptd->next[n] = next_index;
/* 步骤7.1.2:如果启用了RX流卸载,处理流卸载 */
/* DPDK_DEVICE_FLAG_RX_FLOW_OFFLOAD标志表示设备启用了RX流卸载 */
if (PREDICT_FALSE ((xd->flags & DPDK_DEVICE_FLAG_RX_FLOW_OFFLOAD) &&
(or_flags & RTE_MBUF_F_RX_FDIR)))
dpdk_process_flow_offload (xd, ptd, n_rx_packets);
/* 步骤7.1.3:分发数据包到下一个节点(使用ptd->next数组) */
/* 因为Flow Director可能导致不同数据包去往不同的节点 */
vlib_get_buffer_indices_with_offset (vm, (void **) ptd->mbufs,
ptd->buffers, n_rx_packets,
sizeof (struct rte_mbuf));
vlib_buffer_enqueue_to_next (vm, node, ptd->buffers, ptd->next,
n_rx_packets);
}
dpdk_process_flow_offload函数 (src/plugins/dpdk/device/node.c:250):
c
static_always_inline void
dpdk_process_flow_offload (dpdk_device_t * xd,
dpdk_per_thread_data_t * ptd,
uword n_rx_packets)
{
uword n;
dpdk_flow_lookup_entry_t *fle; // 流查找表项
vlib_buffer_t *b0;
/* 遍历所有数据包,查找流表项 */
for (n = 0; n < n_rx_packets; n++)
{
/* 步骤7.2.1:检查是否有Flow Director ID */
/* RTE_MBUF_F_RX_FDIR_ID标志表示mbuf包含Flow Director ID */
if ((ptd->flags[n] & RTE_MBUF_F_RX_FDIR_ID) == 0)
continue; // 如果没有Flow Director ID,跳过
/* 步骤7.2.2:从流查找表中查找对应的流表项 */
/* mbuf->hash.fdir.hi是Flow Director的高位ID */
fle = pool_elt_at_index (xd->flow_lookup_entries,
ptd->mbufs[n]->hash.fdir.hi);
/* 步骤7.2.3:如果流表项指定了下一个节点,设置它 */
if (fle->next_index != (u16) ~ 0)
ptd->next[n] = fle->next_index;
/* 步骤7.2.4:如果流表项指定了流ID,设置它 */
if (fle->flow_id != ~0)
{
b0 = vlib_buffer_from_rte_mbuf (ptd->mbufs[n]);
b0->flow_id = fle->flow_id;
}
/* 步骤7.2.5:如果流表项指定了缓冲区前进量,应用它 */
if (fle->buffer_advance != ~0)
{
b0 = vlib_buffer_from_rte_mbuf (ptd->mbufs[n]);
vlib_buffer_advance (b0, fle->buffer_advance);
}
}
}
关键说明:
- Flow Director:网卡硬件根据数据包特征(如5元组)将数据包分发到不同的处理路径
- 流查找表:每个Flow Director ID对应一个流表项,包含下一个节点、流ID等信息
- 不同next节点 :Flow Director可能导致不同数据包去往不同的节点,所以使用
ptd->next数组
通俗理解:
就像"快递分拣中心"的"智能分拣":
- 硬件分拣:网卡硬件根据"包裹特征"(5元组)将"包裹"分发到"不同路径"
- 查找规则:根据"包裹ID"(Flow Director ID)查找"分拣规则"(流表项)
- 应用规则:根据"规则"设置"下一站"(next节点)、"流ID"、"前进量"等
8.5.8 步骤8:分发数据包到下一个节点
作用和实现原理:
如果没有Flow Director,所有数据包都去往同一个下一个节点,可以使用优化的单一next节点路径。否则,需要使用ptd->next数组,因为不同数据包可能去往不同的节点。
单一next节点路径代码 (src/plugins/dpdk/device/node.c:446):
c
else // 没有Flow Director
{
u32 *to_next, n_left_to_next;
/* 步骤8.1:获取下一个节点的frame */
vlib_get_new_next_frame (vm, node, next_index, to_next, n_left_to_next);
/* 步骤8.2:获取buffer索引数组 */
/* 通过偏移从mbuf指针获取buffer索引 */
vlib_get_buffer_indices_with_offset (vm, (void **) ptd->mbufs, to_next,
n_rx_packets,
sizeof (struct rte_mbuf));
/* 步骤8.3:如果是ethernet-input节点,设置frame的特殊标志 */
if (PREDICT_TRUE (next_index == VNET_DEVICE_INPUT_NEXT_ETHERNET_INPUT))
{
vlib_next_frame_t *nf;
vlib_frame_t *f;
ethernet_input_frame_t *ef;
/* 获取next frame结构 */
nf = vlib_node_runtime_get_next_frame (vm, node, next_index);
f = vlib_get_frame (vm, nf->frame);
/* 设置frame标志:所有数据包来自同一个接口 */
f->flags = ETH_INPUT_FRAME_F_SINGLE_SW_IF_IDX;
/* 设置frame的标量参数(接口索引) */
ef = vlib_frame_scalar_args (f);
ef->sw_if_index = xd->sw_if_index; // 软件接口索引
ef->hw_if_index = xd->hw_if_index; // 硬件接口索引
/* 如果PMD支持IP4校验和检查,且没有数据包标记为IP4校验和错误 */
/* 可以通知ethernet-input节点,让它发送数据包到ip4-input-no-checksum节点 */
if (xd->flags & DPDK_DEVICE_FLAG_RX_IP4_CKSUM &&
(or_flags & RTE_MBUF_F_RX_IP_CKSUM_BAD) == 0)
f->flags |= ETH_INPUT_FRAME_F_IP4_CKSUM_OK;
/* 标记frame不再追加数据包(已经追加完毕) */
vlib_frame_no_append (f);
}
/* 步骤8.4:更新frame的剩余容量 */
n_left_to_next -= n_rx_packets;
/* 步骤8.5:将frame归还到节点系统 */
vlib_put_next_frame (vm, node, next_index, n_left_to_next);
single_next = 1; // 标记使用了单一next节点
}
关键说明:
- 单一next节点优化:如果所有数据包都去往同一个节点,可以使用优化的路径,避免逐个设置next节点
- ethernet-input优化:如果是ethernet-input节点,可以设置frame标志,通知它所有数据包来自同一个接口,且IP4校验和已检查
- frame管理 :使用
vlib_get_new_next_frame获取frame,使用vlib_put_next_frame归还frame
通俗理解:
就像"快递分拣中心"的"分发包裹":
- 单一路径:如果"所有包裹"都去"同一个窗口"(单一next节点),可以使用"快速路径"
- 特殊优化:如果是"以太网处理窗口"(ethernet-input),可以"提前告知"一些信息(接口、校验和状态)
- 多路径:如果"不同包裹"去"不同窗口"(Flow Director),需要使用"数组"记录每个包裹的"下一站"
8.5.9 步骤9:处理数据包跟踪(如果启用)
作用和实现原理:
如果启用了数据包跟踪功能,需要记录数据包的详细信息(mbuf、buffer、数据等),用于调试和性能分析。
处理跟踪代码 (src/plugins/dpdk/device/node.c:481):
c
/* 步骤9.1:检查是否启用了跟踪 */
/* vlib_get_trace_count返回当前节点的跟踪计数 */
if (PREDICT_FALSE ((n_trace = vlib_get_trace_count (vm, node))))
{
/* 步骤9.1.1:如果使用了单一next节点,需要获取buffer索引数组 */
if (single_next)
vlib_get_buffer_indices_with_offset (vm, (void **) ptd->mbufs,
ptd->buffers, n_rx_packets,
sizeof (struct rte_mbuf));
/* 步骤9.1.2:准备跟踪相关的变量 */
n_left = n_rx_packets;
buffers = ptd->buffers;
mb = ptd->mbufs;
next = ptd->next;
/* 步骤9.1.3:遍历数据包,添加跟踪信息 */
while (n_trace && n_left)
{
/* 获取buffer指针 */
b0 = vlib_get_buffer (vm, buffers[0]);
/* 如果没有使用单一next节点,从next数组获取next节点 */
if (single_next == 0)
next_index = next[0];
/* 检查是否需要跟踪这个buffer */
if (PREDICT_TRUE
(vlib_trace_buffer (vm, node, next_index, b0,
/* follow_chain */ 0)))
{
/* 添加跟踪数据 */
dpdk_rx_trace_t *t0 =
vlib_add_trace (vm, node, b0, sizeof t0[0]);
/* 记录队列索引、设备索引、缓冲区索引 */
t0->queue_index = queue_id;
t0->device_index = xd->device_index;
t0->buffer_index = vlib_get_buffer_index (vm, b0);
/* 复制mbuf信息 */
clib_memcpy_fast (&t0->mb, mb[0], sizeof t0->mb);
/* 复制buffer信息(不包括pre_data) */
clib_memcpy_fast (&t0->buffer, b0,
sizeof b0[0] - sizeof b0->pre_data);
/* 复制pre_data */
clib_memcpy_fast (t0->buffer.pre_data, b0->data,
sizeof t0->buffer.pre_data);
/* 复制数据包数据 */
clib_memcpy_fast (&t0->data, mb[0]->buf_addr + mb[0]->data_off,
sizeof t0->data);
n_trace--; // 减少跟踪计数
}
/* 移动到下一个数据包 */
n_left--;
buffers++;
mb++;
next++;
}
/* 步骤9.1.4:更新跟踪计数 */
vlib_set_trace_count (vm, node, n_trace);
}
关键说明:
- 跟踪功能:跟踪功能用于调试和性能分析,记录数据包的详细信息
- 跟踪数据:包括mbuf信息、buffer信息、数据包数据等
- 跟踪计数 :
n_trace限制跟踪的数据包数量,避免跟踪过多数据包影响性能
通俗理解:
就像"快递分拣中心"的"记录包裹信息":
- 启用跟踪:如果需要"记录包裹信息"(跟踪),就"记录"
- 记录信息:记录"包裹"的"详细信息"(mbuf、buffer、数据等)
- 限制数量:只"记录"一定数量的"包裹",避免"记录太多"影响"效率"
8.5.10 步骤10:更新统计信息
作用和实现原理:
更新接口和设备的接收统计信息,包括数据包数和字节数。这些统计信息用于监控和性能分析。
更新统计信息代码 (src/plugins/dpdk/device/node.c:529):
c
/* 步骤10.1:更新接口的接收统计信息 */
/* 更新组合计数器(combined counter),包括数据包数和字节数 */
vlib_increment_combined_counter
(vnet_get_main ()->interface_main.combined_sw_if_counters
+ VNET_INTERFACE_COUNTER_RX, // 接收计数器
thread_index, // 线程索引
xd->sw_if_index, // 接口索引
n_rx_packets, // 数据包数
n_rx_bytes); // 字节数
/* 步骤10.2:更新设备的接收统计信息 */
/* 更新设备的聚合接收数据包数 */
vnet_device_increment_rx_packets (thread_index, n_rx_packets);
关键说明:
- 接口统计:更新接口的接收统计(数据包数、字节数)
- 设备统计:更新设备的接收统计(数据包数)
- 线程安全:统计更新是线程安全的,每个线程更新自己的统计
通俗理解:
就像"快递分拣中心"的"统计信息":
- 更新统计:更新"接收的包裹数"和"总重量"(数据包数、字节数)
- 分类统计:分别统计"接口"和"设备"的"接收情况"
8.5.11 dpdk_device_input函数总结
函数处理的关键步骤:
| 步骤 | 函数/操作 | 作用 | 重要性 |
|---|---|---|---|
| 1. 初始化 | 获取队列、线程数据 | 准备处理环境 | ⭐⭐⭐⭐⭐ |
| 2. 接收数据包 | rte_eth_rx_burst |
从网卡接收数据包 | ⭐⭐⭐⭐⭐ |
| 3. 更新模板 | 设置模板字段 | 准备快速初始化 | ⭐⭐⭐⭐ |
| 4. 确定next节点 | vnet_feature_start_device_input |
确定处理路径 | ⭐⭐⭐⭐⭐ |
| 5. 处理数据包 | dpdk_process_rx_burst |
转换格式、设置元数据 | ⭐⭐⭐⭐⭐ |
| 6. 硬件卸载 | dpdk_process_lro_offload |
处理LRO、校验和 | ⭐⭐⭐⭐ |
| 7. 流卸载 | dpdk_process_flow_offload |
处理Flow Director | ⭐⭐⭐ |
| 8. 分发数据包 | vlib_buffer_enqueue_to_next |
发送到下一个节点 | ⭐⭐⭐⭐⭐ |
| 9. 跟踪 | vlib_add_trace |
记录跟踪信息 | ⭐⭐ |
| 10. 统计 | vlib_increment_combined_counter |
更新统计信息 | ⭐⭐⭐ |
函数设计特点:
- 批量处理:批量接收和处理数据包,提高效率
- 优化路径:针对常见场景(单一next节点、ethernet-input)提供优化路径
- 硬件卸载:充分利用网卡硬件卸载功能,减少CPU负担
- 灵活分发:支持Flow Director等高级功能,灵活分发数据包
函数性能优化:
- 预取优化:在处理当前批次时预取下一批次数据
- 批量处理:每次处理4个数据包,减少循环开销
- 模板机制:使用模板快速初始化,避免逐个设置字段
- 单一next节点优化:如果所有数据包去往同一节点,使用优化路径
通俗理解:
dpdk_device_input就像"快递分拣中心"的"完整收货流程":
- 接收包裹:从"网卡"接收"包裹"(数据包)
- 格式化:将"DPDK格式"转换为"VPP格式"
- 检查质量:检查"包裹质量"(硬件卸载、校验和)
- 智能分拣:根据"规则"(Flow Director)"分拣包裹"
- 分发:将"包裹"分发到"下一个处理窗口"
- 记录:记录"统计信息"和"跟踪信息"
8.6 dpdk-input节点在VLIB图中的位置
作用和实现原理:
dpdk-input节点在VPP的VLIB节点图中处于数据包处理的入口位置。它接收来自DPDK网卡的数据包,处理后分发到下一个节点(通常是ethernet-input)。就像"快递分拣中心"的"收货窗口"在"分拣流程"中的位置,处于"流程的入口"。
dpdk-input节点在VLIB图中的位置:
┌─────────────────────────────────────────────────────────────┐
│ VPP数据包处理节点图(简化版) │
└─────────────────────────────────────────────────────────────┘
┌──────────────┐
│ VPP调度器 │
│ (Scheduler) │
└──────┬───────┘
│
│ 定期调用
▼
┌──────────────┐
│ dpdk-input │ ← 本章讲解的节点
│ (输入节点) │
└──────┬───────┘
│
│ 分发数据包
▼
┌──────────────┐
│ethernet-input│
│ (以太网处理) │
└──────┬───────┘
│
│ 解析以太网头
├─────────────────┐
│ │
▼ ▼
┌──────────────┐ ┌──────────────┐
│ ip4-input │ │ ip6-input │
│ (IPv4处理) │ │ (IPv6处理) │
└──────┬───────┘ └──────┬───────┘
│ │
│ │
▼ ▼
后续处理节点...
节点关系说明:
dpdk-input:输入节点,由调度器定期调用ethernet-input:以太网处理节点,处理以太网帧ip4-input/ip6-input:IP层处理节点,处理IP数据包
节点类型说明:
| 节点类型 | 说明 | 调用方式 |
|---|---|---|
VLIB_NODE_TYPE_INPUT |
输入节点 | 由调度器定期轮询调用 |
VLIB_NODE_TYPE_INTERNAL |
内部节点 | 由其他节点显式调用 |
VLIB_NODE_TYPE_PROCESS |
进程节点 | 由调度器在后台运行 |
通俗理解:
就像"快递分拣中心"的"窗口位置":
- 入口窗口 :
dpdk-input是"分拣流程"的"入口窗口"(输入节点) - 下一窗口:数据包会被分发到"下一个窗口"(ethernet-input)
- 流程位置:处于"分拣流程"的"最前端",负责"接收包裹"
8.7 dpdk-input节点的调度机制
作用和实现原理:
dpdk-input节点是输入节点(VLIB_NODE_TYPE_INPUT),由VPP调度器定期调用。调度器会根据节点的优先级和负载情况决定调用频率。就像"快递分拣中心"的"调度系统",定期"调用"各个"窗口"处理"包裹"。
输入节点的调度特点:
- 定期调用:调度器会定期调用输入节点,检查是否有新数据包
- 优先级:输入节点通常具有较高的优先级,确保及时处理数据包
- 负载感知:调度器会根据节点返回的数据包数调整调用频率
调度器调用示例:
时间轴:0ms 10ms 20ms 30ms 40ms 50ms
│ │ │ │ │ │
▼ ▼ ▼ ▼ ▼ ▼
调度器:调用 调用 调用 调用 调用 调用
│ │ │ │ │ │
▼ ▼ ▼ ▼ ▼ ▼
dpdk-input: 处理 处理 处理 处理 处理 处理
│ │ │ │ │ │
▼ ▼ ▼ ▼ ▼ ▼
返回: 23个 15个 0个 8个 12个 0个
通俗理解:
就像"快递分拣中心"的"调度系统":
- 定期调用:每隔一段时间(例如10ms),"调度系统"会"调用"各个"窗口"
- 检查包裹:每次"调用"时,"窗口"会"检查"是否有"新包裹"
- 返回结果:如果"有包裹",返回"包裹数";如果"没有包裹",返回0
- 调整频率:如果"经常有包裹","调度系统"可能会"增加调用频率"
8.8 本章总结
dpdk-input节点功能总结:
| 功能 | 函数/机制 | 作用 | 重要性 |
|---|---|---|---|
| 节点注册 | VLIB_REGISTER_NODE |
注册节点到VPP系统 | ⭐⭐⭐⭐⭐ |
| 主处理函数 | dpdk_input_node |
节点的主处理逻辑 | ⭐⭐⭐⭐⭐ |
| 轮询向量 | vnet_hw_if_get_rxq_poll_vector |
获取需要轮询的设备队列列表 | ⭐⭐⭐⭐⭐ |
| 多设备轮询 | for循环遍历轮询向量 |
支持多设备、多队列轮询 | ⭐⭐⭐⭐⭐ |
| 设备输入 | dpdk_device_input |
处理单个设备队列的数据包接收 | ⭐⭐⭐⭐⭐ |
dpdk-input节点设计特点:
- 输入节点类型:由调度器定期调用,确保及时处理数据包
- 批量处理:支持批量接收和处理数据包,提高效率
- 多设备支持:一个线程可以处理多个设备的多个队列
- 负载均衡:通过RSS将不同数据流分配到不同队列和线程
dpdk-input节点完整流程:
VPP调度器
│
│ 定期调用(例如每10ms)
▼
dpdk_input_node
│
├── 获取轮询向量(pv)
│ │
│ ▼
│ 遍历轮询向量中的每个设备队列
│ │
│ ├── 获取DPDK设备(xd)
│ │ │
│ │ ▼
│ │ 调用dpdk_device_input处理该设备队列
│ │ │
│ │ ├── 从网卡接收数据包(rte_eth_rx_burst)
│ │ ├── 转换为VPP格式(mbuf → buffer)
│ │ ├── 设置元数据(sw_if_index、flags等)
│ │ ├── 处理硬件卸载(校验和、LRO等)
│ │ └── 分发到下一个节点(ethernet-input等)
│ │ │
│ │ ▼
│ │ 返回接收到的数据包数
│ │
│ └── 累加数据包数
│
▼
返回总数据包数
关键概念总结:
- 输入节点:由调度器定期调用的节点,用于接收外部数据
- 轮询向量:包含当前线程需要处理的所有设备队列的数组
- 批量接收:一次接收多个数据包,提高处理效率
- 多设备轮询:一个线程可以处理多个设备的多个队列
后续章节预告:
- 第9章 :详细讲解
dpdk_device_input函数,讲解如何从网卡接收数据包并处理 - 第10章:详细讲解硬件卸载处理,讲解校验和、LRO等硬件卸载功能
- 第11章:详细讲解多段数据包处理,讲解如何处理大于单个缓冲区大小的数据包
相关源码文件:
src/plugins/dpdk/device/node.c:539-dpdk_input_node函数src/plugins/dpdk/device/node.c:563- 节点注册src/vnet/interface/rx_queue_funcs.h:69- 轮询向量获取函数
第9章:数据包接收处理
本章概述:
第9章详细讲解DPDK插件中数据包接收处理的核心机制。虽然第8章已经讲解了dpdk_device_input函数的整体流程,但本章将深入讲解其中的关键技术细节:rte_eth_rx_burst批量接收、dpdk_process_rx_burst数据处理、mbuf到vlib_buffer的零拷贝转换、数据包元数据设置以及缓冲区模板的应用。
通俗理解:
就像"快递分拣中心"的"接收包裹流程":
- 批量接收 (
rte_eth_rx_burst):从"快递车"(网卡)批量"卸货"(接收数据包) - 格式转换(mbuf→buffer):将"快递公司格式"(mbuf)转换为"分拣中心格式"(buffer)
- 标签设置(元数据设置):为每个"包裹"贴上"标签"(接口索引、标志等)
- 模板应用(缓冲区模板):使用"标准模板"快速"初始化包裹信息"
9.1 rte_eth_rx_burst调用和批量接收
作用和实现原理:
rte_eth_rx_burst是DPDK提供的核心接收函数,用于从网卡的指定接收队列批量接收数据包。该函数实现了高效的数据包接收,一次最多可以接收32个数据包,避免了频繁的系统调用开销。就像"快递分拣中心"从"快递车"批量"卸货",一次"卸多个包裹"(最多32个),而不是"一个一个卸"。
函数签名(DPDK API):
c
/**
* 从网卡的指定接收队列批量接收数据包
*
* @param port_id 网卡端口ID(在DPDK中注册的网卡ID)
* @param queue_id 接收队列ID(网卡上的队列编号,0表示第一个队列)
* @param rx_pkts 输出参数:接收到的mbuf指针数组
* @param nb_pkts 输入参数:要接收的最大数据包数量(通常不超过32)
*
* @return 实际接收到的数据包数量(0到nb_pkts之间)
*/
uint16_t rte_eth_rx_burst(uint16_t port_id,
uint16_t queue_id,
struct rte_mbuf **rx_pkts,
uint16_t nb_pkts);
批量接收代码实现 (src/plugins/dpdk/device/node.c:365):
c
/* 步骤1:循环接收数据包,直到达到最大批量大小 */
/* DPDK_RX_BURST_SZ通常是256(VLIB_FRAME_SIZE),这是VPP一次处理的最大数据包数 */
while (n_rx_packets < DPDK_RX_BURST_SZ)
{
/* 步骤2:计算本次要接收的数据包数量 */
/* 每次最多接收32个(DPDK的限制),但不超过剩余容量 */
/* clib_min(a, b)返回a和b中的较小值 */
u32 n_to_rx = clib_min (DPDK_RX_BURST_SZ - n_rx_packets, 32);
/* 步骤3:调用DPDK的接收函数,从网卡批量接收数据包 */
/* 参数说明:
* - xd->port_id: DPDK端口ID(网卡ID),在设备初始化时从DPDK获取
* - queue_id: 队列ID,标识要接收哪个队列的数据包
* - ptd->mbufs + n_rx_packets: mbuf指针数组的起始位置
* 例如:如果已经接收了50个数据包,则从ptd->mbufs[50]开始存放
* - n_to_rx: 要接收的最大数据包数量(本次请求的数量)
* 返回值n:实际接收到的数据包数量(可能小于n_to_rx,表示网卡队列已空)
*/
n = rte_eth_rx_burst (xd->port_id, queue_id,
ptd->mbufs + n_rx_packets, n_to_rx);
/* 步骤4:累加接收到的数据包数 */
n_rx_packets += n;
/* 步骤5:如果接收数量小于请求数量,说明网卡队列已空,停止接收 */
/* 这个优化避免了无效的轮询,提高效率 */
if (n < n_to_rx)
break;
}
/* 步骤6:如果没有接收到任何数据包,直接返回,避免后续无效处理 */
if (n_rx_packets == 0)
return 0;
批量接收过程示例:
假设DPDK_RX_BURST_SZ = 256(最大批量大小)
场景1:网卡队列有足够的数据包
第1次调用:请求32个,实际接收32个 → n_rx_packets = 32
第2次调用:请求32个,实际接收32个 → n_rx_packets = 64
第3次调用:请求32个,实际接收32个 → n_rx_packets = 96
...
第8次调用:请求32个,实际接收32个 → n_rx_packets = 256(达到上限,退出循环)
场景2:网卡队列数据包不足
第1次调用:请求32个,实际接收32个 → n_rx_packets = 32
第2次调用:请求32个,实际接收32个 → n_rx_packets = 64
第3次调用:请求32个,实际接收8个 → n_rx_packets = 72(n < n_to_rx,退出循环)
场景3:网卡队列为空
第1次调用:请求32个,实际接收0个 → n_rx_packets = 0(退出循环,直接返回)
关键设计要点:
- 批量接收优化 :每次最多接收32个数据包,循环接收直到达到
DPDK_RX_BURST_SZ(256个)或网卡队列为空 - 提前退出机制:如果某次接收的数量小于请求数量,说明网卡队列已空,立即停止接收,避免无效轮询
- 零拷贝接收 :
rte_eth_rx_burst返回的mbuf指针直接指向DPDK缓冲区池中的内存,无需数据拷贝 - 队列隔离:不同队列的数据包互不干扰,支持多队列并行接收
通俗理解:
就像"快递分拣中心"的"批量卸货":
- 批量卸货:一次"卸多个包裹"(最多32个),而不是"一个一个卸"
- 循环卸货:如果"快递车"上还有"包裹",继续"卸货",直到"达到上限"或"没有更多包裹"
- 提前停止:如果"卸的包裹数"小于"请求的包裹数",说明"没有更多包裹了",停止"卸货"
- 效率提升:通过"批量卸货"和"提前停止",减少"等待时间",提高"处理效率"
9.2 dpdk_process_rx_burst函数处理接收的数据包
作用和实现原理:
dpdk_process_rx_burst是DPDK插件中处理接收数据包的核心函数。它负责将DPDK的mbuf转换为VPP的buffer,设置数据偏移和长度,提取硬件卸载标志,并处理多段数据包。该函数采用批量处理优化,一次处理多个数据包(通常是4个或8个),提高处理效率。就像"快递分拣中心"的"包裹处理流程",将"快递公司格式的包裹"转换为"分拣中心格式",并提取"包裹标签信息"。
函数签名 (src/plugins/dpdk/device/node.c:167):
c
/**
* 批量处理接收到的数据包
*
* @param vm VPP主结构体指针
* @param ptd 线程数据指针(包含mbuf数组、flags数组等)
* @param n_rx_packets 接收到的数据包数量
* @param maybe_multiseg 是否可能有多段数据包(1=可能,0=不可能)
* @param or_flagsp 输出参数:所有数据包硬件卸载标志的OR结果
*
* @return 处理的总字节数(所有数据包的总长度)
*/
static_always_inline uword
dpdk_process_rx_burst (vlib_main_t *vm, // VPP主结构体
dpdk_per_thread_data_t *ptd, // 线程数据
uword n_rx_packets, // 接收到的数据包数
int maybe_multiseg, // 是否可能有多段数据包
u32 *or_flagsp) // 硬件卸载标志的OR结果
函数完整实现 (src/plugins/dpdk/device/node.c:167):
c
static_always_inline uword
dpdk_process_rx_burst (vlib_main_t *vm, dpdk_per_thread_data_t *ptd,
uword n_rx_packets, int maybe_multiseg, u32 *or_flagsp)
{
u32 n_left = n_rx_packets; // 剩余待处理的数据包数
vlib_buffer_t *b[4]; // buffer指针数组(用于批量处理4个数据包)
struct rte_mbuf **mb = ptd->mbufs; // mbuf指针数组(从rte_eth_rx_burst接收的)
uword n_bytes = 0; // 总字节数(所有数据包的总长度)
u32 *flags, or_flags = 0; // 硬件卸载标志数组和OR结果
vlib_buffer_t bt; // 本地缓冲区模板(从线程数据复制,避免每次访问ptd)
/* 步骤1:获取mbuf数组和flags数组的指针 */
mb = ptd->mbufs; // mbuf指针数组
flags = ptd->flags; // flags数组,用于存储每个数据包的硬件卸载标志
/* 步骤2:将缓冲区模板从线程数据复制到本地变量 */
/* 这样做的好处是:后续访问模板时,CPU可以从寄存器或L1缓存快速读取,而不需要访问内存 */
/* 这是一个重要的性能优化:减少内存访问延迟 */
vlib_buffer_copy_template (&bt, &ptd->buffer_template);
/* 步骤3:批量处理循环(每次处理4个数据包) */
/* 处理8个或更多数据包时,使用优化的批量处理路径 */
while (n_left >= 8)
{
/* 步骤3.1:预取优化 - 提前加载后续4个mbuf的数据到CPU缓存 */
/* 这样可以隐藏内存访问延迟,提高处理效率 */
/* 预取的是mb+4,即当前处理的4个数据包之后的下4个数据包 */
dpdk_prefetch_buffer_x4 (mb + 4);
/* 步骤3.2:mbuf到buffer的转换(零拷贝) */
/* vlib_buffer_from_rte_mbuf是宏定义,通过指针运算实现零拷贝转换 */
/* 由于mbuf和buffer在内存中连续存储,只需计算指针偏移即可 */
b[0] = vlib_buffer_from_rte_mbuf (mb[0]); // mb[0]是rte_mbuf*,b[0]是vlib_buffer_t*
b[1] = vlib_buffer_from_rte_mbuf (mb[1]);
b[2] = vlib_buffer_from_rte_mbuf (mb[2]);
b[3] = vlib_buffer_from_rte_mbuf (mb[3]);
/* 步骤3.3:复制缓冲区模板到每个buffer */
/* 模板包含了所有数据包共有的信息:接口索引、错误码、标志、缓冲区池索引等 */
/* 通过复制模板,快速初始化这些字段,避免逐个设置 */
vlib_buffer_copy_template (b[0], &bt);
vlib_buffer_copy_template (b[1], &bt);
vlib_buffer_copy_template (b[2], &bt);
vlib_buffer_copy_template (b[3], &bt);
/* 步骤3.4:预取mbuf的数据区域(payload)到CPU缓存 */
/* 此时处理当前4个数据包,预取它们的payload数据 */
dpdk_prefetch_mbuf_x4 (mb + 4);
/* 步骤3.5:提取硬件卸载标志(ol_flags) */
/* dpdk_ol_flags_extract从每个mbuf的ol_flags字段提取硬件卸载标志 */
/* 这些标志包括:IP校验和、L4校验和、LRO、RSS哈希等 */
/* or_flags是所有数据包标志的OR结果,用于快速判断是否有某些卸载标志 */
or_flags |= dpdk_ol_flags_extract (mb, flags, 4);
flags += 4; // flags指针前进4个位置,为下一批数据包准备
/* 步骤3.6:设置buffer的数据偏移和长度 */
/* mb[0]->data_off是mbuf中数据在buffer中的偏移量 */
/* RTE_PKTMBUF_HEADROOM是mbuf的头部空间(通常128字节) */
/* current_data是buffer中当前数据的起始位置(相对于buffer起始地址的偏移) */
/* current_length是当前数据的长度(从mbuf的data_len字段获取) */
b[0]->current_data = mb[0]->data_off - RTE_PKTMBUF_HEADROOM;
n_bytes += b[0]->current_length = mb[0]->data_len;
b[1]->current_data = mb[1]->data_off - RTE_PKTMBUF_HEADROOM;
n_bytes += b[1]->current_length = mb[1]->data_len;
b[2]->current_data = mb[2]->data_off - RTE_PKTMBUF_HEADROOM;
n_bytes += b[2]->current_length = mb[2]->data_len;
b[3]->current_data = mb[3]->data_off - RTE_PKTMBUF_HEADROOM;
n_bytes += b[3]->current_length = mb[3]->data_len;
/* 步骤3.7:处理多段数据包(如果需要) */
/* maybe_multiseg参数指示是否可能有多段数据包 */
/* 多段数据包是指数据包大小超过单个mbuf容量,需要多个mbuf链接的情况 */
/* dpdk_process_subseq_segs处理后续段,构建缓冲区链 */
if (maybe_multiseg)
{
n_bytes += dpdk_process_subseq_segs (vm, b[0], mb[0], &bt);
n_bytes += dpdk_process_subseq_segs (vm, b[1], mb[1], &bt);
n_bytes += dpdk_process_subseq_segs (vm, b[2], mb[2], &bt);
n_bytes += dpdk_process_subseq_segs (vm, b[3], mb[3], &bt);
}
/* 步骤3.8:准备下一批数据包 */
mb += 4; // mb指针前进4个位置
n_left -= 4; // 剩余数据包数减4
}
/* 步骤4:处理剩余的数据包(少于8个的情况) */
/* 使用单循环处理,每次处理1个数据包 */
while (n_left)
{
/* 步骤4.1:mbuf到buffer的转换 */
b[0] = vlib_buffer_from_rte_mbuf (mb[0]);
/* 步骤4.2:复制缓冲区模板 */
vlib_buffer_copy_template (b[0], &bt);
/* 步骤4.3:提取硬件卸载标志 */
or_flags |= dpdk_ol_flags_extract (mb, flags, 1);
flags += 1;
/* 步骤4.4:设置buffer的数据偏移和长度 */
b[0]->current_data = mb[0]->data_off - RTE_PKTMBUF_HEADROOM;
n_bytes += b[0]->current_length = mb[0]->data_len;
/* 步骤4.5:处理多段数据包(如果需要) */
if (maybe_multiseg)
n_bytes += dpdk_process_subseq_segs (vm, b[0], mb[0], &bt);
/* 步骤4.6:准备下一个数据包 */
mb += 1;
n_left -= 1;
}
/* 步骤5:返回结果 */
*or_flagsp = or_flags; // 将所有数据包硬件卸载标志的OR结果写入输出参数
return n_bytes; // 返回处理的总字节数
}
关键设计要点:
- 批量处理优化:每次处理4个或8个数据包,利用CPU的SIMD指令和缓存局部性提高效率
- 预取优化:在处理当前数据包的同时,预取后续数据包的数据到CPU缓存,隐藏内存访问延迟
- 模板复制优化:将模板复制到本地变量,减少内存访问次数
- 零拷贝转换:mbuf到buffer的转换通过指针运算实现,无需数据拷贝
- 多段数据包支持 :通过
maybe_multiseg参数和dpdk_process_subseq_segs函数处理大于单个mbuf容量的数据包
函数处理流程图:
开始
│
▼
复制缓冲区模板到本地变量(减少内存访问)
│
▼
批量处理循环(每次4个数据包)
│
├── 预取后续4个mbuf(隐藏内存延迟)
├── mbuf转换为buffer(零拷贝)
├── 复制模板初始化buffer
├── 预取mbuf payload数据
├── 提取硬件卸载标志
├── 设置数据偏移和长度
└── 处理多段数据包(如果需要)
│
▼
处理剩余数据包(少于8个的情况)
│
├── mbuf转换为buffer
├── 复制模板初始化buffer
├── 提取硬件卸载标志
├── 设置数据偏移和长度
└── 处理多段数据包(如果需要)
│
▼
返回总字节数和硬件卸载标志OR结果
通俗理解:
就像"快递分拣中心"的"包裹处理流程":
- 批量处理:一次"处理多个包裹"(4个或8个),而不是"一个一个处理"
- 提前准备:在"处理当前包裹"时,"提前准备"后续包裹,减少"等待时间"
- 格式转换:将"快递公司格式的包裹"转换为"分拣中心格式"(无需重新包装,只需"调整标签")
- 标签提取:从"包裹"上提取"标签信息"(硬件卸载标志)
- 大包裹处理:如果"包裹"很大(多段数据包),需要"链接多个容器"
9.3 mbuf到vlib_buffer的转换机制
作用和实现原理:
mbuf到vlib_buffer的转换是DPDK插件的核心机制之一。VPP和DPDK使用不同的缓冲区结构(rte_mbuf和vlib_buffer_t),但通过精心设计的内存布局,实现了零拷贝转换。转换只需要简单的指针运算,无需数据拷贝,这是VPP高性能的关键之一。就像"快递分拣中心"和"快递公司"使用不同的"包裹格式",但通过"统一的内存布局",可以实现"零拷贝转换"。
内存布局设计:
为了实现零拷贝转换,DPDK的rte_mbuf和VPP的vlib_buffer_t在内存中连续存储。内存布局为:
┌─────────────────────────────────────────────────────────────┐
│ 缓冲区内存布局(单个缓冲区) │
└─────────────────────────────────────────────────────────────┘
[rte_mempool_objhdr][rte_mbuf][vlib_buffer_t][pre_data][data]
↑ ↑ ↑ ↑ ↑
DPDK内部 DPDK结构体 VPP结构体 预数据区 实际数据
管理结构 区域
内存地址:低地址 ←────────────────────────────→ 高地址
转换宏定义 (src/plugins/dpdk/buffer.h:19):
c
/**
* 从VPP buffer指针获取DPDK mbuf指针
*
* 原理:由于rte_mbuf在vlib_buffer_t之前,只需将buffer指针向前偏移一个rte_mbuf大小
*
* @param x vlib_buffer_t指针
* @return struct rte_mbuf指针
*
* 示例:
* vlib_buffer_t *b = ...;
* struct rte_mbuf *mb = rte_mbuf_from_vlib_buffer(b);
* // 此时mb指向内存中b之前的rte_mbuf结构体
*/
#define rte_mbuf_from_vlib_buffer(x) (((struct rte_mbuf *)x) - 1)
/**
* 从DPDK mbuf指针获取VPP buffer指针
*
* 原理:由于vlib_buffer_t在rte_mbuf之后,只需将mbuf指针向后偏移一个rte_mbuf大小
*
* @param x struct rte_mbuf指针
* @return vlib_buffer_t指针
*
* 示例:
* struct rte_mbuf *mb = ...;
* vlib_buffer_t *b = vlib_buffer_from_rte_mbuf(mb);
* // 此时b指向内存中mb之后的vlib_buffer_t结构体
*/
#define vlib_buffer_from_rte_mbuf(x) ((vlib_buffer_t *)(x+1))
转换过程详解:
假设我们有一个rte_mbuf指针mb,指向内存地址0x1000:
内存地址: 0x1000 0x1200 0x1400 0x1600
│ │ │ │
▼ ▼ ▼ ▼
内存布局:[rte_mempool_objhdr][rte_mbuf][vlib_buffer_t][pre_data][data]
↑ ↑ ↑
│ │ │
(隐藏) mb指向这里 b指向这里
0x1200 0x1400
步骤1:从mb获取buffer指针
vlib_buffer_t *b = vlib_buffer_from_rte_mbuf(mb);
计算过程:(vlib_buffer_t *)(mb + 1)
= (vlib_buffer_t *)(0x1200 + sizeof(rte_mbuf))
= (vlib_buffer_t *)0x1400
步骤2:从b获取mbuf指针(反向转换)
struct rte_mbuf *mb2 = rte_mbuf_from_vlib_buffer(b);
计算过程:((struct rte_mbuf *)b) - 1
= ((struct rte_mbuf *)0x1400) - 1
= (struct rte_mbuf *)(0x1400 - sizeof(rte_mbuf))
= (struct rte_mbuf *)0x1200
= mb (与原始mb相同)
实际使用示例 (从dpdk_process_rx_burst函数):
c
/* 步骤1:从rte_eth_rx_burst接收的mbuf数组 */
struct rte_mbuf **mb = ptd->mbufs; // mb[0], mb[1], ... 是rte_mbuf*指针
/* 步骤2:将mbuf转换为buffer(零拷贝) */
vlib_buffer_t *b[4];
b[0] = vlib_buffer_from_rte_mbuf (mb[0]); // mb[0]是rte_mbuf*,b[0]是vlib_buffer_t*
b[1] = vlib_buffer_from_rte_mbuf (mb[1]);
b[2] = vlib_buffer_from_rte_mbuf (mb[2]);
b[3] = vlib_buffer_from_rte_mbuf (mb[3]);
/* 步骤3:现在可以使用VPP的buffer API操作数据包 */
/* 例如:设置数据偏移和长度 */
b[0]->current_data = mb[0]->data_off - RTE_PKTMBUF_HEADROOM;
b[0]->current_length = mb[0]->data_len;
/* 步骤4:如果需要访问mbuf字段,可以从buffer反向获取mbuf */
struct rte_mbuf *mb_again = rte_mbuf_from_vlib_buffer(b[0]);
/* 此时mb_again == mb[0] */
关键设计要点:
- 零拷贝转换:转换只需要指针运算,无需数据拷贝,这是高性能的关键
- 内存布局保证:转换依赖于mbuf和buffer在内存中的连续存储,这由缓冲区池的创建过程保证
- 双向转换:支持从mbuf到buffer和从buffer到mbuf的双向转换
- 类型安全:使用宏定义实现类型转换,编译时展开,无运行时开销
缓冲区池创建时的内存布局保证 (src/plugins/dpdk/buffer.c):
c
/* 缓冲区池创建时,每个元素的大小计算 */
elt_size = sizeof(struct rte_mbuf) + // DPDK mbuf结构体大小
sizeof(vlib_buffer_t) + // VPP buffer结构体大小
bp->data_size; // 数据区域大小
/* 创建DPDK内存池时,告诉DPDK mbuf的私有数据大小 */
priv.mbuf_priv_size = VLIB_BUFFER_HDR_SIZE; // vlib_buffer_t的大小
/* 这样DPDK在分配mbuf时,会在mbuf之后预留vlib_buffer_t的空间 */
/* 内存布局:[rte_mbuf][vlib_buffer_t预留空间][pre_data][data] */
通俗理解:
就像"快递分拣中心"和"快递公司"的"包裹格式转换":
- 统一内存布局:两个系统使用"统一的内存布局","mbuf"和"buffer"在"同一个容器"中,只是"标签位置不同"
- 零拷贝转换:转换时不需要"重新包装包裹",只需"调整指针位置",指向"正确的标签区域"
- 双向转换:可以从"快递公司格式"转换为"分拣中心格式",也可以反向转换
- 性能优势:由于"无需重新包装",转换速度极快,几乎无开销
9.4 数据包元数据设置(sw_if_index、flags等)
作用和实现原理:
数据包元数据设置是VPP数据包处理的关键步骤。每个数据包都需要设置正确的元数据,包括接收接口索引(sw_if_index[VLIB_RX])、缓冲区标志(flags)、错误码(error)、缓冲区池索引(buffer_pool_index)等。这些元数据用于后续的数据包处理和路由决策。就像"快递分拣中心"为每个"包裹"贴上"标签",包括"来源窗口"、"包裹类型"、"错误标记"等。
元数据设置代码 (src/plugins/dpdk/device/node.c:381):
c
/* 步骤1:获取缓冲区模板指针 */
/* 缓冲区模板包含了所有数据包共有的元数据信息 */
vlib_buffer_t *bt = &ptd->buffer_template;
/* 步骤2:设置接收接口索引(sw_if_index[VLIB_RX]) */
/* sw_if_index是VPP中接口的软件索引,用于标识数据包来自哪个接口 */
/* VLIB_RX表示接收方向,VLIB_TX表示发送方向 */
/* xd->sw_if_index是DPDK设备对应的VPP接口索引,在接口创建时分配 */
vnet_buffer (bt)->sw_if_index[VLIB_RX] = xd->sw_if_index;
/* 注意:sw_if_index[VLIB_TX]通常保持为~0(无效值),直到数据包被路由 */
/* 步骤3:设置错误码 */
/* DPDK_ERROR_NONE表示没有错误,数据包正常接收 */
/* node->errors是错误码数组,DPDK_ERROR_NONE是数组索引 */
/* 这个错误码会在数据包处理出错时使用,用于统计和错误报告 */
bt->error = node->errors[DPDK_ERROR_NONE];
/* 步骤4:设置缓冲区标志(flags) */
/* xd->buffer_flags包含设备的缓冲区标志,这些标志在设备初始化时设置 */
/* 常见的标志包括:
* - VNET_BUFFER_F_IP4_CHECKSUM_CORRECT: IP校验和已由硬件校验
* - VNET_BUFFER_F_L4_CHECKSUM_CORRECT: L4校验和已由硬件校验
* - VNET_BUFFER_F_IS_IP4: 数据包是IPv4
* - VNET_BUFFER_F_IS_IP6: 数据包是IPv6
* 这些标志用于优化后续的数据包处理(例如跳过校验和计算)
*/
bt->flags = xd->buffer_flags;
/* 步骤5:设置缓冲区池索引 */
/* 每个RX队列都有对应的缓冲区池,DPDK从这个池中分配缓冲区 */
/* 由于DPDK在接口启动前就为每个队列分配了缓冲区池,所以可以安全地存储在模板中 */
/* buffer_pool_index用于缓冲区回收时,将缓冲区返回到正确的池中 */
bt->buffer_pool_index = rxq->buffer_pool_index;
/* 步骤6:设置引用计数 */
/* 初始引用计数为1,表示当前有一个引用(当前节点持有) */
/* 当数据包被复制或克隆时,引用计数会增加 */
/* 当数据包处理完成时,引用计数会减少,当为0时缓冲区会被回收 */
bt->ref_count = 1;
/* 步骤7:设置特性弧索引 */
/* 特性弧(feature arc)用于数据包处理流程的扩展 */
/* 初始值为0,表示从特性弧的起始节点开始处理 */
/* 例如:device-input特性弧可能包含:acl-input、qos-input等节点 */
vnet_buffer (bt)->feature_arc_index = 0;
/* 步骤8:设置当前配置索引 */
/* 用于特性弧的配置索引,初始值为0 */
/* 这个索引用于选择特性弧中的哪个配置(例如:哪个ACL规则集) */
bt->current_config_index = 0;
元数据字段详解:
1. sw_if_index(接口索引):
c
/* sw_if_index是VPP中接口的软件索引,是一个32位整数 */
/* VPP为每个接口分配一个唯一的sw_if_index,用于标识接口 */
vnet_buffer (bt)->sw_if_index[VLIB_RX] = xd->sw_if_index; // 接收接口索引
/* sw_if_index的使用场景:
* - 接口查找:通过sw_if_index快速查找接口信息
* - 路由决策:根据接收接口选择路由表
* - 统计更新:更新接口的接收/发送统计
* - ACL匹配:根据接口匹配ACL规则
*/
2. flags(缓冲区标志):
c
/* flags是一个32位的位掩码,每个位表示一个标志 */
/* 常见的标志位定义(src/vlib/buffer.h): */
#define VLIB_BUFFER_NEXT_PRESENT (1 << 0) // 多段缓冲区标志
#define VLIB_BUFFER_IS_TRACED (1 << 1) // 跟踪标志
#define VLIB_BUFFER_TOTAL_LENGTH_VALID (1 << 2) // 总长度有效标志
/* VPP网络层标志(src/vnet/buffer.h): */
#define VNET_BUFFER_F_IP4_CHECKSUM_CORRECT (1 << 16) // IP校验和正确
#define VNET_BUFFER_F_L4_CHECKSUM_CORRECT (1 << 17) // L4校验和正确
#define VNET_BUFFER_F_IS_IP4 (1 << 18) // IPv4数据包
#define VNET_BUFFER_F_IS_IP6 (1 << 19) // IPv6数据包
/* flags的使用场景:
* - 性能优化:跳过已校验的校验和计算
* - 协议识别:快速识别数据包类型
* - 多段处理:标识多段数据包
*/
3. error(错误码):
c
/* error字段存储错误码,用于标识数据包处理过程中的错误 */
/* 错误码是一个枚举值,定义在节点注册时 */
bt->error = node->errors[DPDK_ERROR_NONE]; // 无错误
/* 常见的错误码:
* - DPDK_ERROR_NONE: 无错误
* - DPDK_ERROR_NO_BUFFER: 无可用缓冲区
* - DPDK_ERROR_UNHANDLED: 未处理的错误
*
* 错误码的使用场景:
* - 错误统计:统计各种错误的数量
* - 错误处理:根据错误码进行相应的错误处理
* - 日志记录:记录错误信息用于调试
*/
4. buffer_pool_index(缓冲区池索引):
c
/* buffer_pool_index标识缓冲区来自哪个缓冲区池 */
/* 每个RX队列都有对应的缓冲区池,DPDK从这个池中分配缓冲区 */
bt->buffer_pool_index = rxq->buffer_pool_index;
/* buffer_pool_index的使用场景:
* - 缓冲区回收:将缓冲区返回到正确的池中
* - 统计管理:统计每个池的使用情况
* - 内存管理:管理不同池的内存分配
*/
5. ref_count(引用计数):
c
/* ref_count是缓冲区的引用计数,用于跟踪缓冲区的使用情况 */
bt->ref_count = 1; // 初始引用计数为1
/* 引用计数的使用场景:
* - 缓冲区共享:多个节点共享同一个缓冲区时,增加引用计数
* - 缓冲区克隆:克隆缓冲区时,增加引用计数
* - 缓冲区回收:引用计数为0时,缓冲区可以被回收
*/
元数据设置的时机:
- 模板更新阶段 :在
dpdk_device_input函数中,更新缓冲区模板,设置所有数据包共有的元数据 - 模板复制阶段 :在
dpdk_process_rx_burst函数中,将模板复制到每个buffer,快速初始化元数据 - 后续处理阶段:在数据包处理的各个阶段,根据处理结果更新元数据(例如:设置协议标志、更新错误码等)
通俗理解:
就像"快递分拣中心"为每个"包裹"贴上"标签":
- 来源窗口(sw_if_index):标识"包裹"来自哪个"接收窗口"(接口)
- 包裹类型(flags):标识"包裹"的类型(IPv4、IPv6等)和"处理状态"(校验和已校验等)
- 错误标记(error):标识"包裹"是否有"问题"(无错误、无缓冲区等)
- 容器编号(buffer_pool_index):标识"包裹"来自哪个"容器池"
- 使用计数(ref_count):标识"包裹"被"多少人"使用(引用计数)
9.5 缓冲区模板的应用
作用和实现原理:
缓冲区模板(Buffer Template)机制是VPP性能优化的关键技术之一。模板包含了所有数据包共有的元数据信息(接口索引、错误码、标志、缓冲区池索引等),通过复制模板可以快速初始化多个缓冲区,避免逐个设置字段的开销。就像"快递分拣中心"的"标准标签模板",可以快速为多个"包裹"贴上"相同的标签"。
模板结构定义 (src/vlib/buffer.h:109):
c
/**
* 缓冲区模板字段宏定义
* 这些字段是所有数据包共有的元数据信息
*/
#define vlib_buffer_template_fields \
/** 当前数据在data[]或pre_data[]中的偏移量(有符号) \
* 如果为负,表示当前头部指向pre_data区域 \
*/ \
i16 current_data; \
\
/** 当前数据到缓冲区末尾的字节数 \
*/ \
u16 current_length; \
\
/** 缓冲区标志: \
* VLIB_BUFFER_FREE_LIST_INDEX_MASK: 空闲列表索引位 \
* VLIB_BUFFER_IS_TRACED: 跟踪此缓冲区 \
* VLIB_BUFFER_NEXT_PRESENT: 这是多段缓冲区 \
* VLIB_BUFFER_TOTAL_LENGTH_VALID: 总长度有效 \
*/ \
u32 flags; \
\
/** 通用流标识符 \
*/ \
u32 flow_id; \
\
/** 引用计数 \
*/ \
volatile u8 ref_count; \
\
/** 缓冲区池索引 \
*/ \
u8 buffer_pool_index; \
\
/** 错误码 \
*/ \
vlib_error_t error; \
\
/** 下一个缓冲区索引(用于多段缓冲区) \
*/ \
u32 next_buffer; \
\
/* 更多字段... */
模板复制函数 (src/vlib/buffer_funcs.h:184):
c
/**
* 复制缓冲区模板到目标缓冲区
*
* 这个函数使用向量化指令(如果可用)或快速内存拷贝,实现高效的模板复制
*
* @param b 目标缓冲区指针(要初始化的缓冲区)
* @param bt 源模板指针(包含要复制的元数据)
*/
static_always_inline void
vlib_buffer_copy_template (vlib_buffer_t * b, vlib_buffer_t * bt)
{
#if defined CLIB_HAVE_VEC512
/* 如果有512位向量指令(AVX-512),使用向量化复制 */
/* 一次复制64字节(512位) */
b->as_u8x64[0] = bt->as_u8x64[0];
#elif defined (CLIB_HAVE_VEC256)
/* 如果有256位向量指令(AVX2),使用向量化复制 */
/* 分两次复制,每次32字节(256位) */
b->as_u8x32[0] = bt->as_u8x32[0];
b->as_u8x32[1] = bt->as_u8x32[1];
#elif defined (CLIB_HAVE_VEC128)
/* 如果有128位向量指令(SSE),使用向量化复制 */
/* 分四次复制,每次16字节(128位) */
b->as_u8x16[0] = bt->as_u8x16[0];
b->as_u8x16[1] = bt->as_u8x16[1];
b->as_u8x16[2] = bt->as_u8x16[2];
b->as_u8x16[3] = bt->as_u8x16[3];
#else
/* 如果没有向量指令,使用快速内存拷贝 */
/* 复制模板的前64字节(包含所有关键字段) */
clib_memcpy_fast (b, bt, 64);
#endif
}
模板应用流程:
步骤1:更新缓冲区模板 (src/plugins/dpdk/device/node.c:381):
c
/* 在dpdk_device_input函数中,更新缓冲区模板 */
/* 模板存储在每线程数据中:ptd->buffer_template */
vlib_buffer_t *bt = &ptd->buffer_template;
/* 设置模板的所有字段(所有数据包共有的信息) */
vnet_buffer (bt)->sw_if_index[VLIB_RX] = xd->sw_if_index; // 接收接口索引
bt->error = node->errors[DPDK_ERROR_NONE]; // 错误码
bt->flags = xd->buffer_flags; // 缓冲区标志
bt->buffer_pool_index = rxq->buffer_pool_index; // 缓冲区池索引
bt->ref_count = 1; // 引用计数
vnet_buffer (bt)->feature_arc_index = 0; // 特性弧索引
bt->current_config_index = 0; // 配置索引
步骤2:复制模板到本地变量 (src/plugins/dpdk/device/node.c:182):
c
/* 在dpdk_process_rx_burst函数中,将模板复制到本地变量 */
/* 这样做的好处:后续访问模板时,CPU可以从寄存器或L1缓存快速读取 */
vlib_buffer_t bt; // 本地模板变量
vlib_buffer_copy_template (&bt, &ptd->buffer_template);
步骤3:批量复制模板到每个buffer (src/plugins/dpdk/device/node.c:192):
c
/* 批量处理循环中,为每个buffer复制模板 */
/* 每次处理4个数据包,使用向量化指令提高效率 */
b[0] = vlib_buffer_from_rte_mbuf (mb[0]); // 先转换mbuf到buffer
b[1] = vlib_buffer_from_rte_mbuf (mb[1]);
b[2] = vlib_buffer_from_rte_mbuf (mb[2]);
b[3] = vlib_buffer_from_rte_mbuf (mb[3]);
/* 然后复制模板初始化每个buffer */
vlib_buffer_copy_template (b[0], &bt); // 复制模板到buffer[0]
vlib_buffer_copy_template (b[1], &bt); // 复制模板到buffer[1]
vlib_buffer_copy_template (b[2], &bt); // 复制模板到buffer[2]
vlib_buffer_copy_template (b[3], &bt); // 复制模板到buffer[3]
步骤4:设置数据包特有的字段 (src/plugins/dpdk/device/node.c:202):
c
/* 在复制模板后,设置每个数据包特有的字段 */
/* 这些字段不能放在模板中,因为它们对每个数据包都不同 */
b[0]->current_data = mb[0]->data_off - RTE_PKTMBUF_HEADROOM; // 数据偏移
b[0]->current_length = mb[0]->data_len; // 数据长度
/* flags字段可能需要根据硬件卸载标志进一步更新 */
模板应用的优势:
- 减少内存访问:模板复制到本地变量后,后续访问从CPU缓存读取,比访问全局内存快得多
- 批量初始化:一次复制模板可以初始化多个字段,比逐个设置字段效率高
- 向量化优化:使用SIMD指令(SSE、AVX等)实现向量化复制,一次复制多个字节
- 代码简化:避免在每个数据包处理中重复设置相同的字段
模板应用示例:
c
/* 不使用模板的方式(低效): */
for (i = 0; i < n_rx_packets; i++) {
vlib_buffer_t *b = vlib_buffer_from_rte_mbuf (mb[i]);
vnet_buffer(b)->sw_if_index[VLIB_RX] = xd->sw_if_index; // 每次访问全局变量
b->error = node->errors[DPDK_ERROR_NONE]; // 每次访问全局变量
b->flags = xd->buffer_flags; // 每次访问全局变量
b->buffer_pool_index = rxq->buffer_pool_index; // 每次访问全局变量
b->ref_count = 1; // 每次设置
/* ... 更多字段设置 ... */
}
/* 使用模板的方式(高效): */
/* 步骤1:更新模板一次(在循环外) */
vlib_buffer_t *bt = &ptd->buffer_template;
vnet_buffer(bt)->sw_if_index[VLIB_RX] = xd->sw_if_index;
bt->error = node->errors[DPDK_ERROR_NONE];
bt->flags = xd->buffer_flags;
bt->buffer_pool_index = rxq->buffer_pool_index;
bt->ref_count = 1;
/* 步骤2:复制模板到本地变量(在循环外) */
vlib_buffer_t local_bt;
vlib_buffer_copy_template (&local_bt, bt); // 从全局内存复制到本地
/* 步骤3:在循环中快速复制模板(从本地内存复制) */
for (i = 0; i < n_rx_packets; i++) {
vlib_buffer_t *b = vlib_buffer_from_rte_mbuf (mb[i]);
vlib_buffer_copy_template (b, &local_bt); // 从本地变量复制,使用向量化指令
/* 只设置数据包特有的字段 */
b->current_data = mb[i]->data_off - RTE_PKTMBUF_HEADROOM;
b->current_length = mb[i]->data_len;
}
通俗理解:
就像"快递分拣中心"的"标准标签模板":
- 创建模板:从"标准包裹"创建"标签模板",包含"所有包裹共有的标签信息"
- 复制模板:使用"模板"快速为多个"包裹"贴上"相同的标签"
- 调整差异:为每个"包裹"单独设置"特有的标签信息"(例如:包裹大小)
- 效率提升:通过"批量贴标签"和"使用模板",减少"贴标签时间",提高"处理效率"
9.6 本章总结
数据包接收处理核心流程总结:
| 步骤 | 函数/机制 | 作用 | 关键点 |
|---|---|---|---|
| 1. 批量接收 | rte_eth_rx_burst |
从网卡批量接收数据包 | 一次最多32个,循环接收直到256个或队列为空 |
| 2. 格式转换 | vlib_buffer_from_rte_mbuf |
mbuf转换为buffer | 零拷贝转换,只需指针运算 |
| 3. 模板复制 | vlib_buffer_copy_template |
快速初始化buffer元数据 | 使用向量化指令,批量复制 |
| 4. 元数据设置 | 模板字段设置 | 设置接口索引、标志等 | 所有数据包共有的信息放在模板中 |
| 5. 数据设置 | current_data、current_length |
设置数据偏移和长度 | 每个数据包特有的信息单独设置 |
| 6. 标志提取 | dpdk_ol_flags_extract |
提取硬件卸载标志 | 用于后续的硬件卸载处理 |
关键性能优化技术:
- 批量处理:一次处理多个数据包(4个或8个),利用CPU的SIMD指令
- 预取优化:在处理当前数据包时,预取后续数据包的数据到CPU缓存
- 零拷贝转换:mbuf到buffer的转换通过指针运算实现,无需数据拷贝
- 模板机制:使用模板快速初始化多个缓冲区,避免逐个设置字段
- 本地变量优化:将模板复制到本地变量,减少全局内存访问
相关源码文件:
src/plugins/dpdk/device/node.c:342-dpdk_device_input函数src/plugins/dpdk/device/node.c:167-dpdk_process_rx_burst函数src/plugins/dpdk/device/node.c:152-dpdk_ol_flags_extract函数src/plugins/dpdk/buffer.h:19- mbuf/buffer转换宏定义src/vlib/buffer_funcs.h:184-vlib_buffer_copy_template函数
第10章:硬件卸载处理(接收侧)
本章概述:
第10章详细讲解DPDK插件中硬件卸载处理(接收侧)的核心机制。现代网卡支持多种硬件卸载功能,包括IP校验和校验、L4(TCP/UDP)校验和校验、VLAN处理、RSS(Receive Side Scaling)哈希计算、LRO(Large Receive Offload)等。这些硬件卸载功能可以显著提高数据包处理性能,减少CPU开销。本章将详细讲解这些硬件卸载功能的提取、处理和使用。
通俗理解:
就像"快递分拣中心"的"自动化处理":
- 校验和验证(IP/L4校验和):网卡"自动检查包裹完整性"(校验和),无需CPU计算
- VLAN标签:网卡"自动识别包裹标签"(VLAN标签),无需软件解析
- 负载均衡(RSS):网卡"自动分发包裹到不同窗口"(队列),实现负载均衡
- 大包裹合并(LRO):网卡"自动合并多个小包裹"(TCP分段),减少处理次数
10.1 DPDK硬件卸载标志(ol_flags)的提取
作用和实现原理:
DPDK的rte_mbuf结构体包含一个ol_flags字段(offload flags),用于存储网卡硬件处理后的各种卸载标志。这些标志指示了网卡对数据包进行了哪些硬件处理,例如校验和校验结果、VLAN标签信息、RSS哈希计算等。dpdk_ol_flags_extract函数负责从mbuf中提取这些标志,并计算所有数据包标志的OR结果,用于快速判断是否有某些卸载标志。就像"快递分拣中心"从"包裹标签"中提取"处理信息",包括"完整性检查结果"、"标签信息"等。
函数实现 (src/plugins/dpdk/device/node.c:152):
c
/**
* 从mbuf数组中提取硬件卸载标志
*
* @param mb mbuf指针数组
* @param flags 输出参数:提取的标志数组(每个数据包一个)
* @param count 要处理的数据包数量
*
* @return 所有数据包标志的OR结果(用于快速判断是否有某些标志)
*/
static_always_inline u32
dpdk_ol_flags_extract (struct rte_mbuf **mb, u32 *flags, int count)
{
u32 rv = 0; // OR结果,初始化为0
int i;
/* 遍历每个mbuf,提取其ol_flags */
for (i = 0; i < count; i++)
{
/* 提取mbuf的ol_flags字段
* 注释说明:我们感兴趣的标志都在低8位,但这可能会改变
* ol_flags是uint64_t类型,但通常只有低8位包含我们关心的标志
*/
flags[i] = (u32) mb[i]->ol_flags;
/* 计算所有标志的OR结果
* 这样可以通过检查rv来快速判断是否有某些标志
* 例如:if (rv & RTE_MBUF_F_RX_LRO) 判断是否有LRO卸载
*/
rv |= flags[i];
}
return rv; // 返回所有数据包标志的OR结果
}
DPDK硬件卸载标志定义 (src/plugins/dpdk/device/dpdk_priv.h:127):
c
/* 接收侧硬件卸载标志 */
#define RTE_MBUF_F_RX_IP_CKSUM_BAD PKT_RX_IP_CKSUM_BAD // IP校验和错误
#define RTE_MBUF_F_RX_IP_CKSUM_GOOD PKT_RX_IP_CKSUM_GOOD // IP校验和正确
#define RTE_MBUF_F_RX_IP_CKSUM_NONE PKT_RX_IP_CKSUM_GOOD // 未校验IP校验和(视为正确)
#define RTE_MBUF_F_RX_L4_CKSUM_BAD PKT_RX_L4_CKSUM_BAD // L4校验和错误
#define RTE_MBUF_F_RX_L4_CKSUM_GOOD PKT_RX_L4_CKSUM_GOOD // L4校验和正确
#define RTE_MBUF_F_RX_L4_CKSUM_NONE PKT_RX_L4_CKSUM_GOOD // 未校验L4校验和(视为正确)
#define RTE_MBUF_F_RX_LRO PKT_RX_LRO // LRO卸载标志
#define RTE_MBUF_F_RX_VLAN PKT_RX_VLAN // VLAN标签存在
#define RTE_MBUF_F_RX_VLAN_STRIPPED PKT_RX_VLAN_STRIPPED // VLAN标签已剥离
#define RTE_MBUF_F_RX_RSS_HASH PKT_RX_RSS_HASH // RSS哈希已计算
#define RTE_MBUF_F_RX_FDIR PKT_RX_FDIR // Flow Director匹配
#define RTE_MBUF_F_RX_FDIR_ID PKT_RX_FDIR_ID // Flow Director ID
标志提取流程:
步骤1:接收数据包
│
▼
步骤2:在dpdk_process_rx_burst中调用dpdk_ol_flags_extract
│
├── 提取每个mbuf的ol_flags → flags[i]
├── 计算OR结果:rv |= flags[i]
└── 返回所有标志的OR结果
│
▼
步骤3:使用OR结果快速判断
│
├── if (or_flags & RTE_MBUF_F_RX_LRO) → 处理LRO
├── if (or_flags & RTE_MBUF_F_RX_L4_CKSUM_BAD) → 处理校验和错误
└── if (or_flags & RTE_MBUF_F_RX_FDIR) → 处理Flow Director
OR结果的使用优势:
使用OR结果可以快速判断一批数据包中是否有某些卸载标志,而不需要逐个检查每个数据包。这提高了处理效率:
c
/* 示例:快速判断是否有LRO卸载 */
u32 or_flags = dpdk_ol_flags_extract(mb, flags, n_packets);
/* 只有当至少一个数据包有LRO卸载时,才进行处理 */
if (PREDICT_FALSE(or_flags & RTE_MBUF_F_RX_LRO))
{
/* 处理LRO卸载 */
dpdk_process_lro_offload(xd, ptd, n_rx_packets);
}
/* 如果没有数据包有LRO卸载,这个检查的开销非常小 */
关键设计要点:
- 批量提取:一次提取多个数据包的标志,减少函数调用开销
- OR结果优化:通过OR结果快速判断是否有某些标志,避免逐个检查
- 标志存储:将提取的标志存储在数组中,供后续处理使用
通俗理解:
就像"快递分拣中心"的"标签提取":
- 提取标签:从每个"包裹"的"标签"中提取"处理信息"(ol_flags)
- 快速判断:通过"汇总信息"(OR结果)快速判断"是否有特殊包裹"(特殊标志)
- 批量处理:一次"提取多个包裹的标签",提高效率
10.2 IP校验和卸载(IP Checksum Offload)
作用和实现原理:
IP校验和卸载是网卡硬件自动计算和验证IPv4数据包的校验和的功能。当网卡支持IP校验和卸载时,它会在接收数据包时自动验证IP头的校验和,并将验证结果存储在ol_flags中。如果校验和正确,VPP可以跳过软件校验和验证,直接使用数据包。如果校验和错误,VPP可以立即丢弃数据包。就像"快递分拣中心"的"自动检查包裹完整性",如果"检查通过"就可以"直接使用",如果"检查失败"就"立即丢弃"。
IP校验和卸载标志提取:
c
/* 在dpdk_ol_flags_extract中提取的IP校验和标志 */
flags[i] = (u32) mb[i]->ol_flags;
/* 可能的IP校验和标志值:
* - RTE_MBUF_F_RX_IP_CKSUM_BAD: IP校验和错误
* - RTE_MBUF_F_RX_IP_CKSUM_GOOD: IP校验和正确
* - RTE_MBUF_F_RX_IP_CKSUM_NONE: 未校验IP校验和(视为正确)
*/
IP校验和卸载的使用 (src/plugins/dpdk/device/node.c:471):
c
/* 在dpdk_device_input函数中,判断是否可以优化ethernet-input节点 */
if (PREDICT_TRUE (next_index == VNET_DEVICE_INPUT_NEXT_ETHERNET_INPUT))
{
vlib_next_frame_t *nf;
vlib_frame_t *f;
ethernet_input_frame_t *ef;
/* 获取下一帧的运行时结构 */
nf = vlib_node_runtime_get_next_frame (vm, node, next_index);
f = vlib_get_frame (vm, nf->frame);
f->flags = ETH_INPUT_FRAME_F_SINGLE_SW_IF_IDX;
ef = vlib_frame_scalar_args (f);
ef->sw_if_index = xd->sw_if_index;
ef->hw_if_index = xd->hw_if_index;
/* 如果PMD支持IP4校验和校验,并且没有数据包被标记为IP校验和错误
* 则可以通知ethernet-input节点,使其可以直接将数据包发送到
* ip4-input-no-checksum节点,跳过软件校验和验证
*/
if (xd->flags & DPDK_DEVICE_FLAG_RX_IP4_CKSUM && // 设备支持IP4校验和卸载
(or_flags & RTE_MBUF_F_RX_IP_CKSUM_BAD) == 0) // 没有IP校验和错误的数据包
f->flags |= ETH_INPUT_FRAME_F_IP4_CKSUM_OK; // 设置标志,通知ethernet-input
vlib_frame_no_append (f);
}
设备标志设置 (src/plugins/dpdk/device/common.c:259):
c
/* 在设备启动时,根据设备能力设置标志 */
dpdk_device_flag_set (xd, DPDK_DEVICE_FLAG_RX_IP4_CKSUM,
rxo & RTE_ETH_RX_OFFLOAD_IPV4_CKSUM);
/* rxo是接收卸载能力,从rte_eth_dev_get_info获取
* 如果设备支持RTE_ETH_RX_OFFLOAD_IPV4_CKSUM,则设置DPDK_DEVICE_FLAG_RX_IP4_CKSUM标志
*/
IP校验和卸载处理流程:
接收数据包
│
▼
网卡硬件验证IP校验和
│
├── 校验和正确 → 设置RTE_MBUF_F_RX_IP_CKSUM_GOOD
├── 校验和错误 → 设置RTE_MBUF_F_RX_IP_CKSUM_BAD
└── 未校验 → 设置RTE_MBUF_F_RX_IP_CKSUM_NONE
│
▼
提取ol_flags标志
│
▼
检查是否有IP校验和错误
│
├── 有错误 → 后续节点可以丢弃数据包
└── 无错误 → 设置ETH_INPUT_FRAME_F_IP4_CKSUM_OK标志
→ ethernet-input可以直接跳转到ip4-input-no-checksum
→ 跳过软件校验和验证,提高性能
性能优化效果:
- 跳过软件校验:如果硬件校验和正确,可以跳过软件的IP校验和验证,节省CPU周期
- 直接路由 :ethernet-input节点可以直接跳转到
ip4-input-no-checksum节点,减少节点跳转 - 批量处理:通过检查OR结果,可以批量判断是否有校验和错误
通俗理解:
就像"快递分拣中心"的"自动完整性检查":
- 自动检查:网卡"自动检查包裹完整性"(IP校验和),无需人工检查
- 快速处理:如果"检查通过","包裹"可以"直接进入下一环节",无需"再次检查"
- 错误处理:如果"检查失败","包裹"会被"标记为错误",后续可以"丢弃"
10.3 L4校验和卸载(L4 Checksum Offload)
作用和实现原理:
L4校验和卸载是网卡硬件自动计算和验证TCP/UDP数据包的校验和的功能。当网卡支持L4校验和卸载时,它会在接收数据包时自动验证TCP/UDP头的校验和,并将验证结果存储在ol_flags中。与IP校验和类似,如果校验和正确,VPP可以在buffer的flags中设置相应标志,供后续节点使用。如果校验和错误,需要清除之前设置的"校验和正确"标志。就像"快递分拣中心"的"自动检查包裹内容完整性",如果"检查通过"就"标记为正确",如果"检查失败"就"清除正确标记"。
L4校验和卸载标志提取:
c
/* 在dpdk_ol_flags_extract中提取的L4校验和标志 */
flags[i] = (u32) mb[i]->ol_flags;
/* 可能的L4校验和标志值:
* - RTE_MBUF_F_RX_L4_CKSUM_BAD: L4校验和错误
* - RTE_MBUF_F_RX_L4_CKSUM_GOOD: L4校验和正确
* - RTE_MBUF_F_RX_L4_CKSUM_NONE: 未校验L4校验和(视为正确)
*/
设备buffer_flags设置 (src/plugins/dpdk/device/common.c:254):
c
/* 在设备启动时,如果设备支持TCP和UDP校验和卸载,设置buffer_flags */
if ((rxo & (RTE_ETH_RX_OFFLOAD_TCP_CKSUM | RTE_ETH_RX_OFFLOAD_UDP_CKSUM)) ==
(RTE_ETH_RX_OFFLOAD_TCP_CKSUM | RTE_ETH_RX_OFFLOAD_UDP_CKSUM))
{
/* 如果设备同时支持TCP和UDP校验和卸载,则设置以下标志:
* - VNET_BUFFER_F_L4_CHECKSUM_COMPUTED: L4校验和已计算
* - VNET_BUFFER_F_L4_CHECKSUM_CORRECT: L4校验和正确
*
* 这些标志会被复制到缓冲区模板中,所有数据包默认都会继承这些标志
*/
xd->buffer_flags |=
(VNET_BUFFER_F_L4_CHECKSUM_COMPUTED | VNET_BUFFER_F_L4_CHECKSUM_CORRECT);
}
L4校验和错误处理 (src/plugins/dpdk/device/node.c:409):
c
/* 在dpdk_device_input函数中,处理L4校验和错误 */
if (PREDICT_FALSE ((or_flags & RTE_MBUF_F_RX_L4_CKSUM_BAD) && // 至少有一个数据包的L4校验和错误
(xd->buffer_flags & VNET_BUFFER_F_L4_CHECKSUM_CORRECT))) // 且设备默认设置L4校验和正确标志
{
/* 需要清除那些L4校验和错误的数据包的"校验和正确"标志
* 因为buffer模板中已经设置了VNET_BUFFER_F_L4_CHECKSUM_CORRECT标志
* 但如果有数据包的L4校验和错误,需要清除这个标志
*/
for (n = 0; n < n_rx_packets; n++)
{
/* 获取buffer指针 */
b0 = vlib_buffer_from_rte_mbuf (ptd->mbufs[n]);
/* 使用XOR操作清除标志:
* 如果ptd->flags[n] & RTE_MBUF_F_RX_L4_CKSUM_BAD为真,则清除VNET_BUFFER_F_L4_CHECKSUM_CORRECT标志
*
* 注释说明:
* - 数字3是RTE_MBUF_F_RX_L4_CKSUM_BAD的位号(在DPDK中定义)
* - VNET_BUFFER_F_LOG2_L4_CHECKSUM_CORRECT是VNET_BUFFER_F_L4_CHECKSUM_CORRECT的位号(log2)
* - 通过位移和XOR操作,将RTE_MBUF_F_RX_L4_CKSUM_BAD的位映射到VNET_BUFFER_F_L4_CHECKSUM_CORRECT的位
*
* 代码中有一个STATIC_ASSERT确保RTE_MBUF_F_RX_L4_CKSUM_BAD == (1ULL << 3)
*/
b0->flags ^= (ptd->flags[n] & RTE_MBUF_F_RX_L4_CKSUM_BAD)
<< (VNET_BUFFER_F_LOG2_L4_CHECKSUM_CORRECT - 3);
}
}
位操作详解:
c
/* 假设:
* - RTE_MBUF_F_RX_L4_CKSUM_BAD = (1ULL << 3) = 0x08 = 0b00001000(第3位)
* - VNET_BUFFER_F_L4_CHECKSUM_CORRECT = (1ULL << 17) = 0x20000 = 0b...010...(第17位)
* - VNET_BUFFER_F_LOG2_L4_CHECKSUM_CORRECT = 17
*
* 计算过程:
* 位移量 = VNET_BUFFER_F_LOG2_L4_CHECKSUM_CORRECT - 3 = 17 - 3 = 14
*
* 如果ptd->flags[n] & RTE_MBUF_F_RX_L4_CKSUM_BAD为真(即第3位为1):
* 需要清除的标志位 = 1 << 14 = 0x4000(第17位,对应VNET_BUFFER_F_L4_CHECKSUM_CORRECT)
*
* b0->flags ^= 0x4000 // XOR操作,如果该位为1则清除,为0则保持不变
*
* 这样可以将RTE_MBUF_F_RX_L4_CKSUM_BAD(第3位)映射到VNET_BUFFER_F_L4_CHECKSUM_CORRECT(第17位)
*/
L4校验和卸载处理流程:
设备启动
│
▼
检查设备是否支持TCP/UDP校验和卸载
│
├── 支持 → 设置buffer_flags: VNET_BUFFER_F_L4_CHECKSUM_COMPUTED | VNET_BUFFER_F_L4_CHECKSUM_CORRECT
└── 不支持 → 不设置标志
│
▼
接收数据包
│
▼
网卡硬件验证L4校验和
│
├── 校验和正确 → 设置RTE_MBUF_F_RX_L4_CKSUM_GOOD
├── 校验和错误 → 设置RTE_MBUF_F_RX_L4_CKSUM_BAD
└── 未校验 → 设置RTE_MBUF_F_RX_L4_CKSUM_NONE
│
▼
提取ol_flags标志
│
▼
检查是否有L4校验和错误
│
├── 有错误 → 清除VNET_BUFFER_F_L4_CHECKSUM_CORRECT标志(使用XOR操作)
└── 无错误 → 保持VNET_BUFFER_F_L4_CHECKSUM_CORRECT标志
→ 后续节点可以跳过软件L4校验和验证
关键设计要点:
- 默认设置正确标志:如果设备支持L4校验和卸载,默认设置"校验和正确"标志
- 错误时清除标志:只有当硬件检测到校验和错误时,才清除"校验和正确"标志
- 位操作优化:使用XOR和位移操作,高效地清除标志位
通俗理解:
就像"快递分拣中心"的"内容完整性检查":
- 默认信任:如果"检查设备"支持"自动检查",默认"标记为正确"
- 错误清除:如果"检查发现错误","清除正确标记"
- 性能优化:后续环节可以"跳过再次检查",提高处理速度
10.4 VLAN处理
作用和实现原理:
VLAN(Virtual LAN)处理是网卡硬件识别和处理VLAN标签的功能。当数据包包含VLAN标签时,网卡可以在接收时识别VLAN信息,并将相关信息存储在mbuf中。VPP可以利用这些信息进行VLAN处理,例如VLAN路由、VLAN过滤等。虽然DPDK插件中VLAN信息的提取相对简单,但VLAN标签的处理涉及数据包解析和路由决策。就像"快递分拣中心"的"标签识别",网卡可以"自动识别包裹的标签"(VLAN标签),用于"路由和分类"。
VLAN卸载标志 (src/plugins/dpdk/device/dpdk_priv.h:143):
c
#define RTE_MBUF_F_RX_VLAN PKT_RX_VLAN // VLAN标签存在
#define RTE_MBUF_F_RX_VLAN_STRIPPED PKT_RX_VLAN_STRIPPED // VLAN标签已剥离
#define RTE_MBUF_F_RX_QINQ PKT_RX_QINQ // 双层VLAN(Q-in-Q)
#define RTE_MBUF_F_RX_QINQ_STRIPPED PKT_RX_QINQ_STRIPPED // 双层VLAN标签已剥离
VLAN信息存储:
c
/* DPDK mbuf中包含VLAN信息 */
struct rte_mbuf {
/* ... 其他字段 ... */
/* VLAN相关信息 */
union {
uint32_t hash; // RSS哈希值(如果没有VLAN)
struct {
uint16_t vlan_tci; // VLAN TCI(Tag Control Information)
uint16_t reserved; // 保留字段
};
};
/* ... 其他字段 ... */
};
VLAN处理流程:
接收数据包
│
▼
网卡硬件识别VLAN标签
│
├── 单层VLAN → 设置RTE_MBUF_F_RX_VLAN
├── 双层VLAN → 设置RTE_MBUF_F_RX_QINQ
└── 标签已剥离 → 设置RTE_MBUF_F_RX_VLAN_STRIPPED
│
▼
提取VLAN信息
│
├── mb->vlan_tci: VLAN TCI(包含VLAN ID和优先级)
└── mb->ol_flags: VLAN相关标志
│
▼
VPP处理VLAN
│
├── 解析VLAN标签(从数据包中或从mbuf中)
├── 设置VNET_BUFFER_F_VLAN_1_DEEP或VNET_BUFFER_F_VLAN_2_DEEP标志
└── 根据VLAN ID进行路由和过滤
VLAN在LRO处理中的应用 (src/plugins/dpdk/device/node.c:295):
c
/* 在dpdk_lro_find_l4_hdr_sz函数中,处理VLAN标签以正确计算L4头部大小 */
static_always_inline u16
dpdk_lro_find_l4_hdr_sz (vlib_buffer_t *b)
{
/* ... 其他代码 ... */
ethernet_header_t *e;
u16 ethertype;
e = (void *) data;
current_offset += sizeof (e[0]);
ethertype = clib_net_to_host_u16 (e->type);
/* 检查是否有VLAN标签 */
if (ethernet_frame_is_tagged (ethertype))
{
ethernet_vlan_header_t *vlan = (ethernet_vlan_header_t *) (e + 1);
ethertype = clib_net_to_host_u16 (vlan->type);
current_offset += sizeof (*vlan); // 单层VLAN,增加VLAN头部大小
/* 检查是否有双层VLAN(Q-in-Q) */
if (ethertype == ETHERNET_TYPE_VLAN)
{
vlan++;
current_offset += sizeof (*vlan); // 双层VLAN,再增加VLAN头部大小
ethertype = clib_net_to_host_u16 (vlan->type);
}
}
/* ... 后续处理 ... */
}
关键设计要点:
- 硬件识别:网卡硬件可以自动识别VLAN标签,无需软件解析
- 信息存储:VLAN信息存储在mbuf的vlan_tci字段和ol_flags中
- 标签剥离:网卡可以配置为自动剥离VLAN标签,标志在ol_flags中指示
通俗理解:
就像"快递分拣中心"的"标签识别":
- 自动识别:网卡"自动识别包裹的标签"(VLAN标签),无需人工查看
- 信息提取:从"标签"中提取"VLAN ID"和"优先级"信息
- 路由决策:根据"VLAN ID"进行"路由和分类"
10.5 RSS哈希处理
作用和实现原理:
RSS(Receive Side Scaling)哈希处理是网卡硬件计算数据包哈希值的功能。RSS用于将数据包分发到不同的接收队列,实现负载均衡。网卡根据数据包的源IP、目标IP、源端口、目标端口等信息计算哈希值,然后根据哈希值将数据包分发到对应的队列。哈希值存储在mbuf的hash字段中,RSS标志存储在ol_flags中。就像"快递分拣中心"的"自动分拣",根据"包裹信息"(IP和端口)计算"分拣码"(哈希值),将"包裹"分发到"不同的窗口"(队列)。
RSS哈希标志 (src/plugins/dpdk/device/dpdk_priv.h:140):
c
#define RTE_MBUF_F_RX_RSS_HASH PKT_RX_RSS_HASH // RSS哈希已计算
RSS哈希信息存储:
c
/* DPDK mbuf中包含RSS哈希信息 */
struct rte_mbuf {
/* ... 其他字段 ... */
/* 哈希相关信息 */
union {
uint32_t hash; // RSS哈希值(如果设置了RTE_MBUF_F_RX_RSS_HASH)
struct {
uint16_t vlan_tci; // VLAN TCI(如果没有RSS哈希)
uint16_t reserved; // 保留字段
};
};
/* ... 其他字段 ... */
};
RSS哈希提取和使用:
c
/* 在dpdk_ol_flags_extract中,RSS哈希标志会被提取 */
flags[i] = (u32) mb[i]->ol_flags;
/* 如果设置了RTE_MBUF_F_RX_RSS_HASH,可以访问哈希值 */
if (mb[i]->ol_flags & RTE_MBUF_F_RX_RSS_HASH)
{
u32 rss_hash = mb[i]->hash.rss; // RSS哈希值(32位)
/* 可以使用哈希值进行负载均衡、流识别等 */
}
RSS配置 (src/plugins/dpdk/device/init.c:328):
c
/* 在DPDK主配置中,设置默认的RSS哈希函数 */
dm->default_port_conf.rss_hf =
RTE_ETH_RSS_IP | // IPv4和IPv6哈希
RTE_ETH_RSS_UDP | // UDP哈希
RTE_ETH_RSS_TCP; // TCP哈希
/* RSS哈希函数类型(src/plugins/dpdk/device/dpdk.h:416) */
#define foreach_dpdk_rss_hf \
_ (0, RTE_ETH_RSS_FRAG_IPV4, "ipv4-frag") \
_ (1, RTE_ETH_RSS_NONFRAG_IPV4_TCP, "ipv4-tcp") \
_ (2, RTE_ETH_RSS_NONFRAG_IPV4_UDP, "ipv4-udp") \
_ (3, RTE_ETH_RSS_NONFRAG_IPV4_SCTP, "ipv4-sctp") \
_ (4, RTE_ETH_RSS_NONFRAG_IPV4_OTHER, "ipv4-other") \
_ (5, RTE_ETH_RSS_IPV4, "ipv4") \
_ (6, RTE_ETH_RSS_IPV6_TCP_EX, "ipv6-tcp-ex") \
_ (7, RTE_ETH_RSS_IPV6_UDP_EX, "ipv6-udp-ex") \
_ (8, RTE_ETH_RSS_FRAG_IPV6, "ipv6-frag") \
_ (9, RTE_ETH_RSS_NONFRAG_IPV6_TCP, "ipv6-tcp") \
_ (10, RTE_ETH_RSS_NONFRAG_IPV6_UDP, "ipv6-udp") \
_ (11, RTE_ETH_RSS_NONFRAG_IPV6_SCTP, "ipv6-sctp") \
_ (12, RTE_ETH_RSS_NONFRAG_IPV6_OTHER, "ipv6-other") \
_ (13, RTE_ETH_RSS_IPV6_EX, "ipv6-ex") \
_ (14, RTE_ETH_RSS_IPV6, "ipv6") \
/* ... 更多RSS类型 ... */
RSS处理流程:
数据包到达网卡
│
▼
网卡硬件计算RSS哈希
│
├── 根据源IP、目标IP、源端口、目标端口计算哈希值
└── 根据哈希值选择接收队列
│
▼
设置RSS相关标志和信息
│
├── 设置RTE_MBUF_F_RX_RSS_HASH标志
└── 将哈希值存储在mb->hash.rss中
│
▼
数据包分发到对应队列
│
▼
VPP提取RSS哈希信息
│
├── 提取mb->hash.rss:RSS哈希值
└── 可以使用哈希值进行流识别、负载均衡等
RSS哈希的使用场景:
- 负载均衡:将数据包分发到不同的接收队列,实现多线程负载均衡
- 流识别:使用哈希值识别数据流,确保同一流的数据包在同一队列处理
- 统计和监控:使用哈希值进行流量统计和分析
关键设计要点:
- 硬件计算:RSS哈希由网卡硬件计算,无需软件参与
- 队列分发:根据哈希值自动分发到不同队列,实现负载均衡
- 哈希存储:哈希值存储在mbuf中,供软件使用
通俗理解:
就像"快递分拣中心"的"自动分拣":
- 计算分拣码:根据"包裹信息"(IP和端口)计算"分拣码"(哈希值)
- 自动分发:根据"分拣码"将"包裹"分发到"不同的窗口"(队列)
- 负载均衡:通过"分发到不同窗口",实现"负载均衡"
- 流识别:同一个"寄件人-收件人"的"包裹"总是分发到同一个"窗口"(流识别)
10.6 硬件卸载标志的传递和使用
作用和实现原理:
硬件卸载标志从DPDK的mbuf传递到VPP的buffer,并在VPP的处理流程中使用。这些标志影响数据包的后续处理,例如跳过校验和验证、优化节点跳转等。标志的传递主要通过buffer的flags字段实现,这些标志会被复制到buffer模板中,然后应用到每个数据包。就像"快递分拣中心"的"标签传递","处理信息"(卸载标志)从"快递公司格式"(mbuf)传递到"分拣中心格式"(buffer),并在"后续处理"中使用。
标志传递流程:
步骤1:设备启动时设置buffer_flags
│
├── 检查设备能力(rxo)
├── 如果支持L4校验和卸载 → 设置VNET_BUFFER_F_L4_CHECKSUM_COMPUTED | VNET_BUFFER_F_L4_CHECKSUM_CORRECT
└── 存储到xd->buffer_flags
│
▼
步骤2:更新缓冲区模板
│
├── bt->flags = xd->buffer_flags(复制设备标志到模板)
└── 模板中的标志会被应用到所有数据包
│
▼
步骤3:复制模板到每个buffer
│
├── vlib_buffer_copy_template(b, &bt)
└── 每个buffer继承模板中的标志
│
▼
步骤4:根据ol_flags调整buffer标志
│
├── 如果L4校验和错误 → 清除VNET_BUFFER_F_L4_CHECKSUM_CORRECT标志
├── 如果LRO卸载 → 设置VNET_BUFFER_F_GSO标志
└── 其他标志处理...
│
▼
步骤5:后续节点使用buffer标志
│
├── 如果设置了VNET_BUFFER_F_L4_CHECKSUM_CORRECT → 跳过L4校验和验证
├── 如果设置了ETH_INPUT_FRAME_F_IP4_CKSUM_OK → 跳转到ip4-input-no-checksum
└── 其他优化...
buffer_flags设置 (src/plugins/dpdk/device/common.c:251):
c
/* 在设备启动时,设置buffer_flags */
xd->buffer_flags =
(VLIB_BUFFER_TOTAL_LENGTH_VALID | // 总长度有效标志
VLIB_BUFFER_EXT_HDR_VALID); // 外部头部有效标志
/* 如果设备支持TCP和UDP校验和卸载,设置L4校验和标志 */
if ((rxo & (RTE_ETH_RX_OFFLOAD_TCP_CKSUM | RTE_ETH_RX_OFFLOAD_UDP_CKSUM)) ==
(RTE_ETH_RX_OFFLOAD_TCP_CKSUM | RTE_ETH_RX_OFFLOAD_UDP_CKSUM))
{
xd->buffer_flags |=
(VNET_BUFFER_F_L4_CHECKSUM_COMPUTED | // L4校验和已计算
VNET_BUFFER_F_L4_CHECKSUM_CORRECT); // L4校验和正确
}
模板标志应用 (src/plugins/dpdk/device/node.c:384):
c
/* 在dpdk_device_input函数中,更新缓冲区模板 */
vlib_buffer_t *bt = &ptd->buffer_template;
/* 设置模板的标志字段(从设备buffer_flags复制) */
bt->flags = xd->buffer_flags;
/* 这样所有数据包在复制模板时,都会继承这些标志 */
标志在后续节点的使用 (src/plugins/dpdk/device/node.c:471):
c
/* 在ethernet-input节点的优化中使用IP校验和标志 */
if (PREDICT_TRUE (next_index == VNET_DEVICE_INPUT_NEXT_ETHERNET_INPUT))
{
/* ... 其他代码 ... */
/* 如果PMD支持IP4校验和校验,并且没有数据包被标记为IP校验和错误
* 则可以通知ethernet-input节点,使其可以直接将数据包发送到
* ip4-input-no-checksum节点,跳过软件校验和验证
*/
if (xd->flags & DPDK_DEVICE_FLAG_RX_IP4_CKSUM &&
(or_flags & RTE_MBUF_F_RX_IP_CKSUM_BAD) == 0)
f->flags |= ETH_INPUT_FRAME_F_IP4_CKSUM_OK; // 设置帧标志
}
LRO标志的使用 (src/plugins/dpdk/device/node.c:335):
c
/* 在dpdk_process_lro_offload函数中,设置GSO标志 */
static_always_inline void
dpdk_process_lro_offload (dpdk_device_t *xd, dpdk_per_thread_data_t *ptd,
uword n_rx_packets)
{
vlib_buffer_t *b0;
for (n = 0; n < n_rx_packets; n++)
{
b0 = vlib_buffer_from_rte_mbuf (ptd->mbufs[n]);
/* 如果数据包有LRO卸载标志,设置GSO相关标志和元数据 */
if (ptd->flags[n] & RTE_MBUF_F_RX_LRO)
{
b0->flags |= VNET_BUFFER_F_GSO; // 设置GSO标志
vnet_buffer2 (b0)->gso_size = ptd->mbufs[n]->tso_segsz; // GSO段大小
vnet_buffer2 (b0)->gso_l4_hdr_sz = dpdk_lro_find_l4_hdr_sz (b0); // L4头部大小
}
}
}
关键设计要点:
- 模板机制:通过模板机制,将设备级别的标志应用到所有数据包
- 标志继承:每个数据包通过复制模板,继承设备级别的标志
- 动态调整:根据实际的硬件卸载结果(ol_flags),动态调整buffer标志
- 性能优化:标志的传递和使用实现了性能优化,例如跳过校验和验证
VPP buffer标志定义 (src/vnet/buffer.h:49):
c
#define foreach_vnet_buffer_flag
_ (1, L4_CHECKSUM_COMPUTED, "l4-cksum-computed", 1) // L4校验和已计算
_ (2, L4_CHECKSUM_CORRECT, "l4-cksum-correct", 1) // L4校验和正确
_ (3, VLAN_2_DEEP, "vlan-2-deep", 1) // 双层VLAN
_ (4, VLAN_1_DEEP, "vlan-1-deep", 1) // 单层VLAN
_ (8, IS_IP4, "ip4", 1) // IPv4数据包
_ (9, IS_IP6, "ip6", 1) // IPv6数据包
_ (18, GSO, "gso", 0) // GSO标志
/* ... 更多标志 ... */
通俗理解:
就像"快递分拣中心"的"标签传递":
- 设置标准标签:根据"检查设备能力"(设备能力),设置"标准标签"(buffer_flags)
- 应用标签:使用"标准模板"为所有"包裹"贴上"标签"(复制模板)
- 调整标签:根据"实际检查结果"(ol_flags),调整"标签"(动态调整)
- 使用标签:后续环节根据"标签"进行"优化处理"(跳过检查、优化路由等)
10.7 本章总结
硬件卸载处理核心流程总结:
| 步骤 | 功能 | 标志/字段 | 作用 |
|---|---|---|---|
| 1. 标志提取 | dpdk_ol_flags_extract |
ol_flags → flags[] |
从mbuf提取硬件卸载标志 |
| 2. IP校验和 | IP校验和检查 | RTE_MBUF_F_RX_IP_CKSUM_BAD |
判断IP校验和是否正确,优化ethernet-input跳转 |
| 3. L4校验和 | L4校验和处理 | RTE_MBUF_F_RX_L4_CKSUM_BAD |
根据校验结果设置/清除VNET_BUFFER_F_L4_CHECKSUM_CORRECT |
| 4. VLAN处理 | VLAN标签识别 | RTE_MBUF_F_RX_VLAN |
识别VLAN标签,用于VLAN路由和过滤 |
| 5. RSS哈希 | 负载均衡哈希 | RTE_MBUF_F_RX_RSS_HASH |
使用哈希值进行负载均衡和流识别 |
| 6. LRO处理 | 大接收卸载 | RTE_MBUF_F_RX_LRO |
设置GSO标志,处理硬件合并的大数据包 |
| 7. 标志传递 | 标志应用到buffer | buffer_flags → bt->flags → b->flags |
将硬件卸载标志传递到VPP buffer |
关键性能优化技术:
- 硬件卸载:利用网卡硬件进行校验和验证、哈希计算等,减少CPU开销
- 标志优化:通过标志传递,实现跳过软件校验、优化节点跳转等
- 批量处理:通过OR结果快速判断是否有某些卸载标志,批量处理
- 模板机制:通过模板机制,将设备级别的标志应用到所有数据包
相关源码文件:
src/plugins/dpdk/device/node.c:152-dpdk_ol_flags_extract函数src/plugins/dpdk/device/node.c:409- L4校验和错误处理src/plugins/dpdk/device/node.c:324-dpdk_process_lro_offload函数src/plugins/dpdk/device/common.c:251-buffer_flags设置src/plugins/dpdk/device/dpdk_priv.h:127- 硬件卸载标志定义src/vnet/buffer.h:49- VPP buffer标志定义
第11章:多段数据包处理
本章概述:
第11章详细讲解DPDK插件中多段数据包(multi-segment packet)处理的核心机制。当数据包的大小超过单个缓冲区的容量时,需要使用多个缓冲区来存储数据包的不同部分。DPDK使用mbuf链(mbuf chain)来表示多段数据包,VPP使用buffer链(buffer chain)来表示。本章将详细讲解多段数据包的识别、缓冲区链的构建、标志的使用以及总长度的计算。
通俗理解:
就像"快递分拣中心"的"大包裹处理":
- 大包裹拆分:当"包裹"太大(超过单个容器容量)时,需要"拆分成多个小包裹"(多个缓冲区)
- 容器链接:使用"链接"(next_buffer)将"多个容器"连接起来,形成一个"大包裹"
- 总长度计算:计算"所有小包裹的总长度"(total_length),用于后续处理
11.1 多段数据包(Multi-Segment Packet)的识别
作用和实现原理:
多段数据包是指数据包的大小超过单个缓冲区容量,需要使用多个缓冲区来存储的情况。DPDK的mbuf结构支持通过next指针链接多个mbuf,形成mbuf链。VPP的buffer结构也支持通过next_buffer字段链接多个buffer,形成buffer链。多段数据包的识别主要通过检查mbuf的nb_segs字段(段数)来判断。就像"快递分拣中心"的"大包裹识别",通过"检查包裹数量"(nb_segs)判断是否是"大包裹"(多段数据包)。
多段数据包的结构:
单段数据包(Single-Segment Packet):
┌─────────────────────────────────────┐
│ mbuf[0] │
│ ┌───────────────────────────────┐ │
│ │ data (完整数据包) │ │
│ └───────────────────────────────┘ │
│ nb_segs = 1 │
│ next = NULL │
└─────────────────────────────────────┘
多段数据包(Multi-Segment Packet):
┌─────────────────────────────────────┐
│ mbuf[0] (第一段) │
│ ┌───────────────────────────────┐ │
│ │ data (数据包第一部分) │ │
│ └───────────────────────────────┘ │
│ nb_segs = 3 │
│ next ────────────────────────────┐ │
└─────────────────────────────────────┘ │
│
┌─────────────────────────────────────┐ │
│ mbuf[1] (第二段) │ │
│ ┌───────────────────────────────┐ │ │
│ │ data (数据包第二部分) │ │ │
│ └───────────────────────────────┘ │ │
│ nb_segs = 3 │ │
│ next ────────────────────────────┐ │ │
└─────────────────────────────────────┘ │ │
│ │
┌─────────────────────────────────────┐ │ │
│ mbuf[2] (第三段) │ │ │
│ ┌───────────────────────────────┐ │ │ │
│ │ data (数据包第三部分) │ │ │ │
│ └───────────────────────────────┘ │ │ │
│ nb_segs = 3 │ │ │
│ next = NULL │◄┘ │
└─────────────────────────────────────┘ │
│
mbuf链(mbuf chain)─────────────┘
DPDK mbuf多段结构:
c
/* DPDK mbuf结构中的多段相关字段 */
struct rte_mbuf {
/* ... 其他字段 ... */
uint16_t nb_segs; // 段数(segment count)
uint32_t pkt_len; // 数据包总长度(所有段的总长度)
uint16_t data_len; // 当前段的数据长度
struct rte_mbuf *next; // 指向下一个mbuf段的指针
/* ... 其他字段 ... */
};
多段数据包的识别 (src/plugins/dpdk/device/node.c:57):
c
/* 在dpdk_process_subseq_segs函数中,检查是否是多段数据包 */
if (mb->nb_segs < 2)
return 0; // 如果段数小于2,说明是单段数据包,直接返回
/* nb_segs字段的含义:
* - nb_segs == 1: 单段数据包,所有数据都在第一个mbuf中
* - nb_segs > 1: 多段数据包,数据分布在多个mbuf中
*/
设备多段支持标志 (src/plugins/dpdk/device/common.c:261):
c
/* 在设备启动时,检查设备是否支持多段数据包 */
dpdk_device_flag_set (xd, DPDK_DEVICE_FLAG_MAYBE_MULTISEG,
rxo & RTE_ETH_RX_OFFLOAD_SCATTER);
/* rxo是接收卸载能力,从rte_eth_dev_get_info获取
* 如果设备支持RTE_ETH_RX_OFFLOAD_SCATTER,则设置DPDK_DEVICE_FLAG_MAYBE_MULTISEG标志
* 这个标志用于决定是否调用多段处理函数
*/
多段数据包处理入口 (src/plugins/dpdk/device/node.c:401):
c
/* 在dpdk_device_input函数中,根据设备标志决定是否处理多段数据包 */
if (xd->flags & DPDK_DEVICE_FLAG_MAYBE_MULTISEG)
/* 设备可能支持多段数据包,传入maybe_multiseg=1 */
n_rx_bytes = dpdk_process_rx_burst (vm, ptd, n_rx_packets, 1, &or_flags);
else
/* 设备不支持多段数据包,传入maybe_multiseg=0,跳过多段处理 */
n_rx_bytes = dpdk_process_rx_burst (vm, ptd, n_rx_packets, 0, &or_flags);
多段数据包的使用场景:
- 大数据包:当数据包大小超过单个缓冲区容量(通常2048字节)时
- Jumbo Frame:支持Jumbo Frame(大于1500字节的以太网帧)时
- 零拷贝优化:某些驱动(如virtio)使用多段数据包实现零拷贝
关键设计要点:
- 段数检查 :通过
nb_segs字段快速判断是否是多段数据包 - 设备支持:只有支持scatter的设备才会产生多段数据包
- 条件处理:只有当设备可能支持多段时,才调用多段处理函数
通俗理解:
就像"快递分拣中心"的"大包裹识别":
- 检查包裹数量:通过"检查容器数量"(nb_segs)判断是否是"大包裹"(多段数据包)
- 设备能力:只有"支持大包裹"的"设备"(支持scatter)才会产生"大包裹"
- 条件处理:只有当"设备支持大包裹"时,才进行"大包裹处理"
11.2 dpdk_process_subseq_segs函数处理后续段
作用和实现原理:
dpdk_process_subseq_segs函数负责处理多段数据包的后续段(除第一段外的所有段)。它将DPDK的mbuf链转换为VPP的buffer链,设置每个段的元数据,并构建缓冲区链。函数遍历mbuf链的后续段,为每个段创建对应的buffer,设置数据偏移和长度,并通过next_buffer字段链接这些buffer。就像"快递分拣中心"的"大包裹处理",将"多个小包裹"(mbuf段)转换为"多个容器"(buffer段),并"链接起来"。
函数签名 (src/plugins/dpdk/device/node.c:47):
c
/**
* 处理多段数据包的后续段
*
* @param vm VPP主结构体指针
* @param b 第一个buffer指针(已经处理过的第一段)
* @param mb 第一个mbuf指针(已经处理过的第一段)
* @param bt 缓冲区模板指针
*
* @return 后续段的总长度(不包括第一段)
*/
static_always_inline uword
dpdk_process_subseq_segs (vlib_main_t * vm, // VPP主结构体
vlib_buffer_t * b, // 第一个buffer(第一段)
struct rte_mbuf *mb, // 第一个mbuf(第一段)
vlib_buffer_t * bt) // 缓冲区模板
函数完整实现 (src/plugins/dpdk/device/node.c:47):
c
static_always_inline uword
dpdk_process_subseq_segs (vlib_main_t * vm, vlib_buffer_t * b,
struct rte_mbuf *mb, vlib_buffer_t * bt)
{
u8 nb_seg = 1; // 当前段号(从1开始,因为第一段已经处理)
struct rte_mbuf *mb_seg = 0; // 当前处理的mbuf段指针
vlib_buffer_t *b_seg, *b_chain = 0; // 当前buffer段指针和链指针
mb_seg = mb->next; // 获取第一个后续段(第二段)
b_chain = b; // 链指针指向第一个buffer(用于链接)
/* 步骤1:检查是否是多段数据包 */
/* 如果段数小于2,说明是单段数据包,直接返回0 */
if (mb->nb_segs < 2)
return 0;
/* 步骤2:设置总长度相关标志和字段 */
/* VLIB_BUFFER_TOTAL_LENGTH_VALID标志表示total_length_not_including_first_buffer字段有效 */
b->flags |= VLIB_BUFFER_TOTAL_LENGTH_VALID;
/* 初始化后续段的总长度(不包括第一段) */
b->total_length_not_including_first_buffer = 0;
/* 步骤3:遍历后续段(从第二段开始) */
while (nb_seg < mb->nb_segs)
{
/* 步骤3.1:断言检查 */
/* 确保mb_seg不为空,如果为空说明mbuf链损坏 */
ASSERT (mb_seg != 0);
/* 步骤3.2:mbuf段转换为buffer段(零拷贝) */
/* 通过指针运算,从mbuf指针获取对应的buffer指针 */
b_seg = vlib_buffer_from_rte_mbuf (mb_seg);
/* 步骤3.3:复制缓冲区模板到当前段 */
/* 模板包含了所有数据包共有的信息(接口索引、错误码、标志等) */
vlib_buffer_copy_template (b_seg, bt);
/* 步骤3.4:设置当前段的数据偏移 */
/* 注释说明:某些驱动(如virtio)可能不会将数据包数据放在段的开始位置
* 所以不能假设b_seg->current_data == 0是正确的
*
* 计算过程:
* mb_seg->buf_addr + mb_seg->data_off: mbuf中数据的实际地址
* (void *) b_seg->data: buffer中数据区域的起始地址
* 两者相减得到数据在buffer中的偏移量
*/
b_seg->current_data =
(mb_seg->buf_addr + mb_seg->data_off) - (void *) b_seg->data;
/* 步骤3.5:设置当前段的数据长度 */
/* 从mbuf的data_len字段获取当前段的数据长度 */
b_seg->current_length = mb_seg->data_len;
/* 步骤3.6:累加后续段的总长度 */
/* total_length_not_including_first_buffer存储除第一段外的所有段的长度 */
b->total_length_not_including_first_buffer += mb_seg->data_len;
/* 步骤3.7:链接当前段到链中 */
/* 设置VLIB_BUFFER_NEXT_PRESENT标志,表示有后续段 */
b_chain->flags |= VLIB_BUFFER_NEXT_PRESENT;
/* 设置next_buffer字段,指向下一个buffer的索引 */
b_chain->next_buffer = vlib_get_buffer_index (vm, b_seg);
/* 步骤3.8:更新链指针和段指针 */
b_chain = b_seg; // 链指针移动到当前段(用于链接下一段)
mb_seg = mb_seg->next; // mbuf段指针移动到下一段
nb_seg++; // 段号递增
}
/* 步骤4:返回后续段的总长度 */
return b->total_length_not_including_first_buffer;
}
处理流程示例:
假设有一个3段的数据包(nb_segs = 3):
初始状态:
mb: 指向第一段mbuf
b: 指向第一段buffer(已处理)
mb_seg = mb->next: 指向第二段mbuf
b_chain = b: 指向第一段buffer
第1次循环(处理第二段):
1. b_seg = vlib_buffer_from_rte_mbuf(mb_seg) // 获取第二段buffer
2. vlib_buffer_copy_template(b_seg, bt) // 复制模板
3. 设置b_seg->current_data和current_length // 设置数据偏移和长度
4. b->total_length_not_including_first_buffer += mb_seg->data_len // 累加长度
5. b_chain->flags |= VLIB_BUFFER_NEXT_PRESENT // 设置标志
6. b_chain->next_buffer = b_seg的索引 // 链接第二段
7. b_chain = b_seg // 更新链指针
8. mb_seg = mb_seg->next // 移动到第三段
9. nb_seg = 2
第2次循环(处理第三段):
1. b_seg = vlib_buffer_from_rte_mbuf(mb_seg) // 获取第三段buffer
2. vlib_buffer_copy_template(b_seg, bt) // 复制模板
3. 设置b_seg->current_data和current_length // 设置数据偏移和长度
4. b->total_length_not_including_first_buffer += mb_seg->data_len // 累加长度
5. b_chain->flags |= VLIB_BUFFER_NEXT_PRESENT // 设置标志(第二段的标志)
6. b_chain->next_buffer = b_seg的索引 // 链接第三段(从第二段链接)
7. b_chain = b_seg // 更新链指针
8. mb_seg = mb_seg->next // 移动到NULL(结束)
9. nb_seg = 3
最终结果:
buffer[0] -> buffer[1] -> buffer[2] -> NULL
第一段 第二段 第三段
关键设计要点:
- 零拷贝转换:mbuf到buffer的转换通过指针运算实现,无需数据拷贝
- 模板复制:每个段都复制模板,确保所有段都有相同的元数据
- 数据偏移计算:根据mbuf的buf_addr和data_off计算buffer中的偏移
- 链式链接:通过next_buffer字段链接所有段,形成buffer链
通俗理解:
就像"快递分拣中心"的"大包裹处理":
- 遍历小包裹:从"第二个小包裹"开始,遍历"所有小包裹"(后续段)
- 转换容器:将每个"小包裹"(mbuf段)转换为"容器"(buffer段)
- 设置信息:为每个"容器"设置"标签"(模板)和"内容信息"(数据偏移和长度)
- 链接容器:使用"链接"(next_buffer)将"所有容器"连接起来
11.3 缓冲区链的构建
作用和实现原理:
缓冲区链(Buffer Chain)是VPP中表示多段数据包的数据结构。通过next_buffer字段,多个buffer被链接在一起,形成一个逻辑上连续的数据包。第一个buffer存储数据包的第一部分,后续buffer通过next_buffer字段链接,形成链式结构。就像"快递分拣中心"的"大包裹链接",使用"链接"(next_buffer)将"多个容器"(buffer)连接起来,形成一个"完整的大包裹"。
缓冲区链结构:
单段缓冲区(Single Buffer):
┌─────────────────────────────────────┐
│ buffer[0] │
│ ┌───────────────────────────────┐ │
│ │ flags: 0 │ │
│ │ next_buffer: 无效 │ │
│ │ current_data: ... │ │
│ │ current_length: ... │ │
│ │ data: [完整数据包] │ │
│ └───────────────────────────────┘ │
└─────────────────────────────────────┘
多段缓冲区链(Multi-Segment Buffer Chain):
┌─────────────────────────────────────┐
│ buffer[0] (第一段) │
│ ┌───────────────────────────────┐ │
│ │ flags: VLIB_BUFFER_NEXT_PRESENT│ │
│ │ next_buffer: index_of_buffer[1]│──┼──┐
│ │ current_data: ... │ │ │
│ │ current_length: 1500 │ │ │
│ │ total_length_not_including...: │ │ │
│ │ 1000 │ │ │
│ │ data: [数据包第一部分] │ │ │
│ └───────────────────────────────┘ │ │
└─────────────────────────────────────┘ │
│
┌─────────────────────────────────────┐ │
│ buffer[1] (第二段) │ │
│ ┌───────────────────────────────┐ │ │
│ │ flags: VLIB_BUFFER_NEXT_PRESENT│ │ │
│ │ next_buffer: index_of_buffer[2]│──┼──┼──┐
│ │ current_data: ... │ │ │ │
│ │ current_length: 500 │ │ │ │
│ │ data: [数据包第二部分] │ │ │ │
│ └───────────────────────────────┘ │ │ │
└─────────────────────────────────────┘ │ │
│ │
┌─────────────────────────────────────┐ │ │
│ buffer[2] (第三段) │ │ │
│ ┌───────────────────────────────┐ │ │ │
│ │ flags: 0 │ │ │ │
│ │ next_buffer: 无效 │◄─┘ │ │
│ │ current_data: ... │ │ │
│ │ current_length: 500 │ │ │
│ │ data: [数据包第三部分] │ │ │
│ └───────────────────────────────┘ │ │
└─────────────────────────────────────┘ │
│
buffer链(buffer chain)─────────────┘
缓冲区链构建代码 (src/plugins/dpdk/device/node.c:80):
c
/* 在dpdk_process_subseq_segs函数中,构建缓冲区链 */
while (nb_seg < mb->nb_segs)
{
/* ... 前面的代码:转换、复制模板、设置数据偏移和长度 ... */
/* 步骤1:设置VLIB_BUFFER_NEXT_PRESENT标志 */
/* 这个标志表示当前buffer有后续buffer */
b_chain->flags |= VLIB_BUFFER_NEXT_PRESENT;
/* 步骤2:设置next_buffer字段 */
/* next_buffer存储下一个buffer的索引(不是指针) */
/* vlib_get_buffer_index获取buffer的索引 */
b_chain->next_buffer = vlib_get_buffer_index (vm, b_seg);
/* 步骤3:更新链指针 */
/* b_chain指向当前处理的buffer,用于链接下一段 */
b_chain = b_seg;
/* ... 后续代码:移动到下一段 ... */
}
缓冲区链遍历:
c
/* 遍历缓冲区链的示例代码 */
vlib_buffer_t *b = vlib_get_buffer(vm, buffer_index); // 获取第一个buffer
/* 遍历整个链 */
while (b)
{
/* 处理当前buffer的数据 */
u8 *data = vlib_buffer_get_current(b);
u16 len = b->current_length;
/* ... 处理数据 ... */
/* 检查是否有后续buffer */
if (b->flags & VLIB_BUFFER_NEXT_PRESENT)
{
/* 获取下一个buffer */
b = vlib_get_buffer(vm, b->next_buffer);
}
else
{
/* 链的末尾,退出循环 */
b = NULL;
}
}
缓冲区链的访问函数 (src/vlib/buffer_funcs.h:366):
c
/**
* 获取缓冲区链中的下一个buffer
*
* @param vm VPP主结构体指针
* @param b 当前buffer指针
*
* @return 下一个buffer指针,如果没有则返回NULL
*/
always_inline vlib_buffer_t *
vlib_get_next_buffer (vlib_main_t * vm, vlib_buffer_t * b)
{
return (b->flags & VLIB_BUFFER_NEXT_PRESENT
? vlib_get_buffer (vm, b->next_buffer) : 0);
}
关键设计要点:
- 索引存储 :
next_buffer存储的是buffer索引,不是指针,这样可以跨线程使用 - 标志指示 :
VLIB_BUFFER_NEXT_PRESENT标志指示是否有后续buffer - 链式结构 :通过
next_buffer字段形成单向链表 - 零拷贝:缓冲区链的构建不需要数据拷贝,只是链接元数据
通俗理解:
就像"快递分拣中心"的"大包裹链接":
- 链接容器:使用"链接"(next_buffer)将"多个容器"(buffer)连接起来
- 标志指示:通过"标志"(VLIB_BUFFER_NEXT_PRESENT)指示"是否有下一个容器"
- 索引存储:存储"下一个容器的编号"(索引),而不是"容器本身"(指针)
- 链式结构:形成"单向链表",可以"顺序遍历"
11.4 VLIB_BUFFER_NEXT_PRESENT标志的使用
作用和实现原理:
VLIB_BUFFER_NEXT_PRESENT是VPP buffer的标志位,用于指示当前buffer是否有后续buffer(即是否是缓冲区链的一部分)。当设置了这个标志时,next_buffer字段有效,指向下一个buffer的索引。这个标志使得VPP可以快速判断是否需要遍历缓冲区链,而不需要检查next_buffer字段是否为有效值。就像"快递分拣中心"的"链接标志",通过"标志"快速判断"是否有下一个容器"。
标志定义 (src/vlib/buffer.h:74):
c
/* VPP buffer标志定义 */
#define foreach_vlib_buffer_flag
_( 0, IS_TRACED, 0) // 跟踪标志
_( 1, NEXT_PRESENT, "next-present") // 有后续buffer标志
_( 2, TOTAL_LENGTH_VALID, 0) // 总长度有效标志
_( 3, EXT_HDR_VALID, "ext-hdr-valid") // 外部头部有效标志
/* 标志值定义 */
enum {
VLIB_BUFFER_NEXT_PRESENT = (1 << 1), // 第1位,值为0x02
/* ... 其他标志 ... */
};
标志的设置 (src/plugins/dpdk/device/node.c:80):
c
/* 在dpdk_process_subseq_segs函数中,设置VLIB_BUFFER_NEXT_PRESENT标志 */
while (nb_seg < mb->nb_segs)
{
/* ... 前面的代码 ... */
/* 设置标志,表示当前buffer有后续buffer */
b_chain->flags |= VLIB_BUFFER_NEXT_PRESENT;
/* 设置next_buffer字段,指向下一个buffer的索引 */
b_chain->next_buffer = vlib_get_buffer_index (vm, b_seg);
/* ... 后续代码 ... */
}
标志的检查 (src/vlib/buffer_funcs.h:369):
c
/* 在vlib_get_next_buffer函数中,检查VLIB_BUFFER_NEXT_PRESENT标志 */
always_inline vlib_buffer_t *
vlib_get_next_buffer (vlib_main_t * vm, vlib_buffer_t * b)
{
/* 如果设置了VLIB_BUFFER_NEXT_PRESENT标志,返回下一个buffer */
/* 否则返回NULL(表示链的末尾) */
return (b->flags & VLIB_BUFFER_NEXT_PRESENT
? vlib_get_buffer (vm, b->next_buffer) : 0);
}
标志在总长度计算中的使用 (src/vlib/buffer_funcs.h:383):
c
/**
* 获取缓冲区链的总长度
*
* @param vm VPP主结构体指针
* @param b 第一个buffer指针
*
* @return 缓冲区链的总长度(所有段的总长度)
*/
always_inline uword
vlib_buffer_length_in_chain (vlib_main_t * vm, vlib_buffer_t * b)
{
uword len = b->current_length; // 第一段的长度
/* 快速路径:如果没有后续buffer,直接返回第一段的长度 */
if (PREDICT_TRUE ((b->flags & VLIB_BUFFER_NEXT_PRESENT) == 0))
return len;
/* 快速路径:如果总长度有效,直接使用缓存的总长度 */
if (PREDICT_TRUE (b->flags & VLIB_BUFFER_TOTAL_LENGTH_VALID))
return len + b->total_length_not_including_first_buffer;
/* 慢速路径:遍历整个链计算总长度 */
return vlib_buffer_length_in_chain_slow_path (vm, b);
}
标志的清除:
c
/* 当需要清除缓冲区链时,清除VLIB_BUFFER_NEXT_PRESENT标志 */
b->flags &= ~VLIB_BUFFER_NEXT_PRESENT; // 清除标志
b->next_buffer = 0; // 清除next_buffer字段
关键设计要点:
- 快速判断:通过检查标志位,可以快速判断是否有后续buffer,避免无效的内存访问
- 性能优化:标志检查是位操作,开销极小
- 与next_buffer配合:标志和next_buffer字段配合使用,标志指示next_buffer是否有效
通俗理解:
就像"快递分拣中心"的"链接标志":
- 快速判断:通过"标志"(VLIB_BUFFER_NEXT_PRESENT)快速判断"是否有下一个容器"
- 性能优化:检查"标志"比检查"链接"(next_buffer)更快
- 配合使用:当"标志"为真时,"链接"(next_buffer)才有效
11.5 总长度计算
作用和实现原理:
总长度计算是多段数据包处理的关键部分。由于数据包被分割成多个段,需要计算所有段的总长度。VPP使用total_length_not_including_first_buffer字段存储除第一段外的所有段的长度,总长度 = 第一段长度 + total_length_not_including_first_buffer。这样设计可以避免每次需要总长度时都遍历整个链,提高性能。就像"快递分拣中心"的"总重量计算",通过"记录除第一个容器外的所有容器重量"(total_length_not_including_first_buffer),可以快速计算"总重量"。
总长度字段定义 (src/vlib/buffer.h:191):
c
/* vlib_buffer_t结构体中的总长度字段 */
typedef struct
{
/* ... 其他字段 ... */
/** 除第一段外的所有段的总长度
* 只有当VLIB_BUFFER_TOTAL_LENGTH_VALID标志设置时才有效
*/
u32 total_length_not_including_first_buffer;
/* ... 其他字段 ... */
} vlib_buffer_t;
总长度字段的设置 (src/plugins/dpdk/device/node.c:60):
c
/* 在dpdk_process_subseq_segs函数中,设置总长度相关字段 */
/* 步骤1:设置VLIB_BUFFER_TOTAL_LENGTH_VALID标志 */
b->flags |= VLIB_BUFFER_TOTAL_LENGTH_VALID;
/* 步骤2:初始化后续段的总长度 */
b->total_length_not_including_first_buffer = 0;
/* 步骤3:在循环中累加后续段的长度 */
while (nb_seg < mb->nb_segs)
{
/* ... 前面的代码 ... */
/* 累加当前段的长度到总长度 */
b->total_length_not_including_first_buffer += mb_seg->data_len;
/* ... 后续代码 ... */
}
/* 步骤4:返回后续段的总长度 */
return b->total_length_not_including_first_buffer;
总长度的计算 (src/vlib/buffer_funcs.h:383):
c
/**
* 获取缓冲区链的总长度
*
* @param vm VPP主结构体指针
* @param b 第一个buffer指针
*
* @return 缓冲区链的总长度(所有段的总长度)
*/
always_inline uword
vlib_buffer_length_in_chain (vlib_main_t * vm, vlib_buffer_t * b)
{
uword len = b->current_length; // 第一段的长度
/* 快速路径1:如果没有后续buffer,直接返回第一段的长度 */
if (PREDICT_TRUE ((b->flags & VLIB_BUFFER_NEXT_PRESENT) == 0))
return len;
/* 快速路径2:如果总长度有效,直接使用缓存的总长度 */
/* 这是最常见的路径,避免了遍历整个链的开销 */
if (PREDICT_TRUE (b->flags & VLIB_BUFFER_TOTAL_LENGTH_VALID))
return len + b->total_length_not_including_first_buffer;
/* 慢速路径:遍历整个链计算总长度 */
/* 这种情况发生在总长度字段未设置或无效时 */
return vlib_buffer_length_in_chain_slow_path (vm, b);
}
慢速路径实现 (src/vlib/buffer.c:64):
c
/**
* 慢速路径:遍历整个缓冲区链计算总长度
*
* @param vm VPP主结构体指针
* @param b_first 第一个buffer指针
*
* @return 缓冲区链的总长度
*/
uword
vlib_buffer_length_in_chain_slow_path (vlib_main_t * vm,
vlib_buffer_t * b_first)
{
vlib_buffer_t *b = b_first;
uword l_first = b_first->current_length; // 第一段的长度
uword l = 0; // 后续段的总长度
/* 遍历后续段,累加长度 */
while (b->flags & VLIB_BUFFER_NEXT_PRESENT)
{
b = vlib_get_buffer (vm, b->next_buffer); // 获取下一个buffer
l += b->current_length; // 累加当前段的长度
}
/* 缓存计算结果到total_length_not_including_first_buffer字段 */
b_first->total_length_not_including_first_buffer = l;
/* 设置VLIB_BUFFER_TOTAL_LENGTH_VALID标志,表示总长度有效 */
b_first->flags |= VLIB_BUFFER_TOTAL_LENGTH_VALID;
/* 返回总长度 */
return l + l_first;
}
总长度计算示例:
假设有一个3段的数据包:
第一段:current_length = 1500
第二段:current_length = 500
第三段:current_length = 500
计算过程:
1. 在dpdk_process_subseq_segs中:
- 第一段:current_length = 1500(不累加到total_length_not_including_first_buffer)
- 第二段:total_length_not_including_first_buffer += 500 = 500
- 第三段:total_length_not_including_first_buffer += 500 = 1000
- 设置VLIB_BUFFER_TOTAL_LENGTH_VALID标志
2. 在vlib_buffer_length_in_chain中:
- len = b->current_length = 1500(第一段长度)
- 检查VLIB_BUFFER_NEXT_PRESENT标志:有后续段
- 检查VLIB_BUFFER_TOTAL_LENGTH_VALID标志:有效
- 返回:len + total_length_not_including_first_buffer = 1500 + 1000 = 2500
关键设计要点:
- 缓存总长度 :通过
total_length_not_including_first_buffer字段缓存总长度,避免重复计算 - 快速路径:如果总长度有效,直接使用缓存值,无需遍历链
- 慢速路径:如果总长度无效,遍历整个链计算,并缓存结果
- 性能优化:大多数情况下使用快速路径,性能开销极小
通俗理解:
就像"快递分拣中心"的"总重量计算":
- 记录重量:在"处理大包裹"时,"记录除第一个容器外的所有容器重量"(total_length_not_including_first_buffer)
- 快速计算:需要"总重量"时,只需"第一个容器重量" + "记录的总重量",无需"重新称重"
- 性能优化:通过"记录重量",避免"每次都要重新称重"(遍历链),提高效率
11.6 本章总结
多段数据包处理核心流程总结:
| 步骤 | 功能 | 关键字段/标志 | 作用 |
|---|---|---|---|
| 1. 识别 | 检查是否是多段数据包 | mb->nb_segs |
判断段数是否大于1 |
| 2. 初始化 | 设置总长度相关字段 | VLIB_BUFFER_TOTAL_LENGTH_VALID |
标记总长度字段有效 |
| 3. 遍历 | 遍历后续段 | mb->next |
获取下一个mbuf段 |
| 4. 转换 | mbuf段转换为buffer段 | vlib_buffer_from_rte_mbuf |
零拷贝转换 |
| 5. 模板复制 | 复制缓冲区模板 | vlib_buffer_copy_template |
设置公共元数据 |
| 6. 数据设置 | 设置数据偏移和长度 | current_data, current_length |
设置段的数据信息 |
| 7. 链接 | 链接buffer段 | VLIB_BUFFER_NEXT_PRESENT, next_buffer |
构建缓冲区链 |
| 8. 总长度 | 累加总长度 | total_length_not_including_first_buffer |
计算后续段总长度 |
关键性能优化技术:
- 零拷贝转换:mbuf到buffer的转换通过指针运算实现,无需数据拷贝
- 总长度缓存 :通过
total_length_not_including_first_buffer字段缓存总长度,避免重复计算 - 快速路径:如果总长度有效,直接使用缓存值,无需遍历链
- 标志优化 :通过
VLIB_BUFFER_NEXT_PRESENT标志快速判断是否有后续段
相关源码文件:
src/plugins/dpdk/device/node.c:47-dpdk_process_subseq_segs函数src/plugins/dpdk/device/node.c:401- 多段数据包处理入口src/vlib/buffer_funcs.h:366-vlib_get_next_buffer函数src/vlib/buffer_funcs.h:383-vlib_buffer_length_in_chain函数src/vlib/buffer.c:64-vlib_buffer_length_in_chain_slow_path函数src/vlib/buffer.h:74-VLIB_BUFFER_NEXT_PRESENT标志定义
第12章:流表处理(Flow Offload)
本章概述:
第12章详细讲解DPDK插件中流表处理(Flow Offload)的核心机制。Flow Offload是一种硬件加速技术,允许网卡硬件根据数据包的流特征(如源IP、目标IP、源端口、目标端口等)进行匹配,并执行预定义的动作(如标记、重定向到特定节点、调整buffer指针等)。DPDK使用Flow Director(FDIR)功能实现Flow Offload,VPP通过流表查找和处理实现相应的功能。本章将详细讲解Flow Offload的概念、FDIR标志的处理、流表查找机制以及流ID和buffer advance的处理。
通俗理解:
就像"快递分拣中心"的"智能分拣":
- 流表匹配:网卡硬件"自动识别包裹类型"(流特征),根据"预设规则"(流表)进行匹配
- 动作执行:匹配成功后,执行"预设动作"(标记、重定向、调整指针等)
- 快速路由:通过"硬件匹配",实现"快速路由",无需软件查找
12.1 Flow Offload的概念和作用
作用和实现原理:
Flow Offload是一种硬件加速技术,允许网卡硬件根据数据包的流特征进行匹配,并执行预定义的动作。当数据包到达网卡时,硬件会根据数据包的源IP、目标IP、源端口、目标端口等信息计算哈希值,然后在硬件流表中查找匹配的规则。如果找到匹配的规则,硬件会在mbuf中设置相应的标志(如RTE_MBUF_F_RX_FDIR),并将匹配的流ID存储在mbuf的hash.fdir.hi字段中。VPP通过检查这些标志和流ID,查找流表项,并执行相应的动作。就像"快递分拣中心"的"智能分拣系统",根据"包裹信息"(流特征)自动匹配"分拣规则"(流表),并执行"预设动作"(标记、重定向等)。
Flow Offload的优势:
- 硬件加速:流表匹配由网卡硬件完成,无需CPU参与,性能极高
- 快速路由:匹配成功后可以直接跳转到目标节点,跳过中间处理节点
- 精确匹配:支持基于五元组(源IP、目标IP、源端口、目标端口、协议)的精确匹配
- 灵活动作:支持多种动作,如标记、重定向、buffer调整等
Flow Offload的使用场景:
- 快速路径:将特定流的数据包直接路由到目标节点,跳过中间处理
- 流量标记:为特定流的数据包设置flow_id,用于后续的流量识别和处理
- 隧道处理:对于隧道数据包,可以跳过外层头部,直接处理内层数据
- 负载均衡:根据流特征将数据包分发到不同的处理节点
Flow Offload的流程:
步骤1:配置流表规则
│
├── 定义流特征(源IP、目标IP、源端口、目标端口等)
├── 定义动作(标记、重定向、buffer调整等)
└── 将规则安装到网卡硬件
│
▼
步骤2:数据包到达网卡
│
▼
步骤3:硬件流表匹配
│
├── 计算数据包的流特征哈希值
├── 在硬件流表中查找匹配的规则
└── 如果匹配成功:
├── 设置RTE_MBUF_F_RX_FDIR标志
├── 设置RTE_MBUF_F_RX_FDIR_ID标志(如果配置了标记)
└── 将流ID存储在mbuf->hash.fdir.hi中
│
▼
步骤4:VPP处理流表匹配结果
│
├── 检查RTE_MBUF_F_RX_FDIR标志
├── 如果设置了RTE_MBUF_F_RX_FDIR_ID,查找流表项
└── 执行流表项中定义的动作
通俗理解:
就像"快递分拣中心"的"智能分拣系统":
- 预设规则:提前设置"分拣规则"(流表),例如"来自北京的包裹"→"标记为A类"→"发送到A区"
- 自动匹配:当"包裹"到达时,"系统自动识别包裹信息"(流特征),并"匹配规则"(流表查找)
- 执行动作:匹配成功后,"系统自动执行预设动作"(标记、重定向等),无需人工干预
- 快速处理:通过"自动匹配和执行",实现"快速分拣",提高效率
12.2 FDIR(Flow Director)标志的处理
作用和实现原理:
FDIR(Flow Director)是DPDK提供的硬件流表匹配功能。当网卡硬件匹配到流表规则时,会在mbuf的ol_flags字段中设置相应的标志。VPP通过检查这些标志来判断数据包是否匹配了流表规则,并决定是否需要处理Flow Offload。就像"快递分拣中心"的"匹配标志",通过"检查标志"判断"包裹是否匹配了规则"。
FDIR相关标志定义 (src/plugins/dpdk/device/dpdk_priv.h:122):
c
/* DPDK FDIR相关标志 */
#define RTE_MBUF_F_RX_FDIR PKT_RX_FDIR // Flow Director匹配标志
#define RTE_MBUF_F_RX_FDIR_FLX PKT_RX_FDIR_FLX // Flow Director灵活匹配标志
#define RTE_MBUF_F_RX_FDIR_ID PKT_RX_FDIR_ID // Flow Director ID标志(表示有流ID)
FDIR标志的含义:
RTE_MBUF_F_RX_FDIR:表示数据包匹配了Flow Director规则,但可能没有配置标记(flow ID)RTE_MBUF_F_RX_FDIR_ID:表示数据包匹配了Flow Director规则,并且配置了标记(flow ID),流ID存储在mbuf->hash.fdir.hi中
FDIR标志的提取 (src/plugins/dpdk/device/node.c:199):
c
/* 在dpdk_process_rx_burst函数中,提取硬件卸载标志 */
or_flags |= dpdk_ol_flags_extract (mb, flags, 4);
/* dpdk_ol_flags_extract会提取每个mbuf的ol_flags,包括FDIR标志 */
/* or_flags是所有数据包标志的OR结果,用于快速判断是否有FDIR标志 */
FDIR标志的检查 (src/plugins/dpdk/device/node.c:426):
c
/* 在dpdk_device_input函数中,检查是否有FDIR标志 */
if (PREDICT_FALSE (or_flags & RTE_MBUF_F_RX_FDIR))
{
/* 至少有一个数据包匹配了Flow Director规则 */
/* 步骤1:初始化所有数据包的下一跳为默认值 */
/* 因为Flow Offload可能会为每个数据包设置不同的下一跳 */
for (n = 0; n < n_rx_packets; n++)
ptd->next[n] = next_index;
/* 步骤2:如果启用了Flow Offload,处理流表匹配结果 */
if (PREDICT_FALSE ((xd->flags & DPDK_DEVICE_FLAG_RX_FLOW_OFFLOAD) &&
(or_flags & RTE_MBUF_F_RX_FDIR)))
{
/* 处理Flow Offload:查找流表项,设置下一跳、flow_id、buffer_advance等 */
dpdk_process_flow_offload (xd, ptd, n_rx_packets);
}
/* 步骤3:将数据包入队到下一跳节点 */
/* 使用ptd->next数组,每个数据包可能有不同的下一跳 */
vlib_get_buffer_indices_with_offset (vm, (void **) ptd->mbufs,
ptd->buffers, n_rx_packets,
sizeof (struct rte_mbuf));
vlib_buffer_enqueue_to_next (vm, node, ptd->buffers, ptd->next,
n_rx_packets);
}
FDIR标志的处理流程:
接收数据包
│
▼
提取ol_flags标志
│
├── 检查or_flags & RTE_MBUF_F_RX_FDIR
└── 如果有FDIR标志:
├── 初始化所有数据包的下一跳为默认值
├── 如果启用了Flow Offload:
│ └── 调用dpdk_process_flow_offload处理流表匹配
└── 使用ptd->next数组入队数据包(每个数据包可能有不同的下一跳)
关键设计要点:
- 批量检查:通过OR结果快速判断是否有FDIR标志,避免逐个检查
- 条件处理:只有当设备启用了Flow Offload时,才处理流表匹配
- 每包下一跳 :使用
ptd->next数组,支持每个数据包有不同的下一跳
通俗理解:
就像"快递分拣中心"的"匹配标志检查":
- 批量检查:通过"汇总信息"(OR结果)快速判断"是否有匹配的包裹"(FDIR标志)
- 条件处理:只有当"启用了智能分拣"(Flow Offload)时,才进行"规则匹配处理"
- 个性化路由:每个"包裹"可能有"不同的目的地"(不同的下一跳),使用"数组"(ptd->next)存储
12.3 dpdk_process_flow_offload函数处理流表
作用和实现原理:
dpdk_process_flow_offload函数负责处理Flow Offload的流表匹配结果。它遍历所有接收到的数据包,检查每个数据包是否匹配了Flow Director规则(通过RTE_MBUF_F_RX_FDIR_ID标志判断),如果匹配,则从流表查找表中查找对应的流表项,并根据流表项中的配置执行相应的动作(设置下一跳、flow_id、buffer_advance等)。就像"快递分拣中心"的"规则匹配处理",根据"包裹的匹配信息"(流ID)查找"规则表"(流表),并执行"预设动作"。
函数签名 (src/plugins/dpdk/device/node.c:249):
c
/**
* 处理Flow Offload的流表匹配结果
*
* @param xd DPDK设备结构体指针
* @param ptd 线程数据指针(包含mbuf数组、flags数组、next数组等)
* @param n_rx_packets 接收到的数据包数量
*/
static_always_inline void
dpdk_process_flow_offload (dpdk_device_t * xd, // DPDK设备
dpdk_per_thread_data_t * ptd, // 线程数据
uword n_rx_packets) // 数据包数量
函数完整实现 (src/plugins/dpdk/device/node.c:249):
c
static_always_inline void
dpdk_process_flow_offload (dpdk_device_t * xd, dpdk_per_thread_data_t * ptd,
uword n_rx_packets)
{
uword n; // 循环计数器
dpdk_flow_lookup_entry_t *fle; // 流表查找项指针
vlib_buffer_t *b0; // buffer指针
/* TODO: 优化:使用预取和四重循环提高性能 */
/* 当前实现是单循环,可以优化为批量处理(每次处理4个数据包) */
/* 步骤1:遍历所有数据包 */
for (n = 0; n < n_rx_packets; n++)
{
/* 步骤1.1:检查当前数据包是否匹配了Flow Director规则并配置了流ID */
/* RTE_MBUF_F_RX_FDIR_ID标志表示数据包匹配了规则,并且有流ID */
/* 如果没有这个标志,说明数据包没有匹配规则或没有配置流ID,跳过处理 */
if ((ptd->flags[n] & RTE_MBUF_F_RX_FDIR_ID) == 0)
continue;
/* 步骤1.2:从流表查找表中查找流表项 */
/* mbuf->hash.fdir.hi存储了硬件匹配的流ID(Flow Director ID) */
/* 这个ID是在配置流表规则时分配的,用于索引流表查找表 */
/* xd->flow_lookup_entries是流表查找表的pool,每个设备有一个 */
fle = pool_elt_at_index (xd->flow_lookup_entries,
ptd->mbufs[n]->hash.fdir.hi);
/* 步骤1.3:设置下一跳节点(如果流表项中配置了) */
/* next_index字段存储了该流应该跳转到的节点索引 */
/* ~0(全1)表示未配置,使用默认下一跳 */
if (fle->next_index != (u16) ~ 0)
{
/* 设置该数据包的下一跳节点 */
ptd->next[n] = fle->next_index;
}
/* 步骤1.4:设置flow_id(如果流表项中配置了) */
/* flow_id用于后续的流量识别和处理,例如用于统计、监控等 */
/* ~0(全1)表示未配置,不设置flow_id */
if (fle->flow_id != ~0)
{
/* 获取buffer指针 */
b0 = vlib_buffer_from_rte_mbuf (ptd->mbufs[n]);
/* 设置flow_id */
b0->flow_id = fle->flow_id;
}
/* 步骤1.5:调整buffer指针(如果流表项中配置了) */
/* buffer_advance用于跳过某些头部,例如跳过隧道外层头部 */
/* ~0(全1)表示未配置,不调整buffer指针 */
if (fle->buffer_advance != ~0)
{
/* 获取buffer指针 */
b0 = vlib_buffer_from_rte_mbuf (ptd->mbufs[n]);
/* 调整buffer指针,跳过指定的字节数 */
vlib_buffer_advance (b0, fle->buffer_advance);
}
}
}
流表查找项结构 (src/plugins/dpdk/device/dpdk.h:90):
c
/**
* 流表查找项结构
* 存储在流表查找表中,用于快速查找流表规则
*/
typedef struct
{
u32 flow_id; // 流ID(用于流量识别,~0表示未配置)
u16 next_index; // 下一跳节点索引(~0表示使用默认下一跳)
i16 buffer_advance; // buffer指针调整量(~0表示不调整)
} dpdk_flow_lookup_entry_t;
流表查找表管理 (src/plugins/dpdk/device/dpdk.h:201):
c
/* 在dpdk_device_t结构体中 */
typedef struct
{
/* ... 其他字段 ... */
/* 流表相关字段 */
u32 supported_flow_actions; // 支持的流动作
dpdk_flow_entry_t *flow_entries; // 流表项pool
dpdk_flow_lookup_entry_t *flow_lookup_entries; // 流表查找表pool
u32 *parked_lookup_indexes; // 待回收的查找表索引
/* ... 其他字段 ... */
} dpdk_device_t;
处理流程示例:
假设有3个数据包,其中2个匹配了Flow Director规则:
数据包0:flags[0] & RTE_MBUF_F_RX_FDIR_ID = 0 → 跳过
数据包1:flags[1] & RTE_MBUF_F_RX_FDIR_ID != 0 → 处理
1. 从mbufs[1]->hash.fdir.hi获取流ID = 5
2. 查找flow_lookup_entries[5]
3. 如果fle->next_index != ~0,设置ptd->next[1] = fle->next_index
4. 如果fle->flow_id != ~0,设置b0->flow_id = fle->flow_id
5. 如果fle->buffer_advance != ~0,调用vlib_buffer_advance(b0, fle->buffer_advance)
数据包2:flags[2] & RTE_MBUF_F_RX_FDIR_ID != 0 → 处理
1. 从mbufs[2]->hash.fdir.hi获取流ID = 8
2. 查找flow_lookup_entries[8]
3. 执行相应的动作...
关键设计要点:
- 条件处理 :只有当数据包设置了
RTE_MBUF_F_RX_FDIR_ID标志时才处理 - 快速查找:使用流ID作为索引直接查找流表项,O(1)时间复杂度
- 灵活动作:支持设置下一跳、flow_id、buffer_advance等多种动作
- 性能优化:TODO注释表明可以优化为批量处理(四重循环)
通俗理解:
就像"快递分拣中心"的"规则匹配处理":
- 检查标志:检查"包裹"是否有"匹配标志"(RTE_MBUF_F_RX_FDIR_ID)
- 查找规则:根据"包裹的匹配ID"(流ID)查找"规则表"(流表查找表)
- 执行动作:根据"规则"执行"预设动作"(设置目的地、标记、调整指针等)
12.4 流表查找和下一跳选择
作用和实现原理:
流表查找是Flow Offload的核心机制。当数据包匹配了Flow Director规则时,硬件会将匹配的流ID存储在mbuf->hash.fdir.hi字段中。VPP使用这个流ID作为索引,在流表查找表中查找对应的流表项,获取配置的下一跳节点。如果流表项中配置了next_index,则使用该值作为数据包的下一跳;否则使用默认的下一跳。就像"快递分拣中心"的"规则查找",根据"包裹的匹配ID"查找"规则表",获取"预设的目的地"。
流ID的存储 (src/plugins/dpdk/device/node.c:264):
c
/* 在dpdk_process_flow_offload函数中,从mbuf获取流ID */
fle = pool_elt_at_index (xd->flow_lookup_entries,
ptd->mbufs[n]->hash.fdir.hi);
/* mbuf->hash.fdir.hi存储了硬件匹配的流ID */
/* 这个ID是在配置流表规则时分配的,范围通常是0到流表查找表的大小-1 */
流表查找过程:
数据包匹配Flow Director规则
│
▼
硬件设置流ID到mbuf->hash.fdir.hi
│
▼
VPP检查RTE_MBUF_F_RX_FDIR_ID标志
│
├── 如果设置了标志:
│ ├── 从mbuf->hash.fdir.hi获取流ID
│ ├── 使用流ID作为索引查找flow_lookup_entries[流ID]
│ └── 获取流表项fle
│
▼
检查fle->next_index
│
├── 如果fle->next_index != ~0:
│ └── 设置ptd->next[n] = fle->next_index(使用流表项中的下一跳)
└── 如果fle->next_index == ~0:
└── 使用默认的next_index(在dpdk_device_input中设置)
下一跳设置的代码 (src/plugins/dpdk/device/node.c:266):
c
/* 在dpdk_process_flow_offload函数中,设置下一跳节点 */
if (fle->next_index != (u16) ~ 0)
{
/* 流表项中配置了下一跳节点,使用该值 */
ptd->next[n] = fle->next_index;
}
/* 如果fle->next_index == ~0,表示未配置,使用默认的next_index */
/* 默认的next_index在dpdk_device_input函数中设置,可能是:
* - VNET_DEVICE_INPUT_NEXT_ETHERNET_INPUT(默认)
* - xd->per_interface_next_index(每接口重定向)
* - Feature Arc确定的下一跳
*/
流表项的配置 (src/plugins/dpdk/device/flow.c:803):
c
/* 在dpdk_flow_ops_fn函数中,配置流表项 */
if (flow->actions & VNET_FLOW_ACTION_REDIRECT_TO_NODE)
{
/* 如果流动作包含重定向到节点,设置next_index */
fle->next_index = flow->redirect_device_input_next_index;
}
else
{
/* 否则设置为~0,表示使用默认下一跳 */
fle->next_index = (u16) ~ 0;
}
下一跳节点的使用 (src/plugins/dpdk/device/node.c:443):
c
/* 在dpdk_device_input函数中,使用ptd->next数组入队数据包 */
if (PREDICT_FALSE (or_flags & RTE_MBUF_F_RX_FDIR))
{
/* ... 前面的代码:处理Flow Offload ... */
/* 将数据包入队到下一跳节点 */
/* 使用ptd->next数组,每个数据包可能有不同的下一跳 */
vlib_get_buffer_indices_with_offset (vm, (void **) ptd->mbufs,
ptd->buffers, n_rx_packets,
sizeof (struct rte_mbuf));
vlib_buffer_enqueue_to_next (vm, node, ptd->buffers, ptd->next,
n_rx_packets);
}
关键设计要点:
- O(1)查找:使用流ID作为索引直接查找流表项,时间复杂度为O(1)
- 灵活配置:支持为每个流配置不同的下一跳节点
- 默认值处理:如果流表项中未配置下一跳,使用默认的下一跳
- 每包下一跳 :使用
ptd->next数组,支持每个数据包有不同的下一跳
通俗理解:
就像"快递分拣中心"的"规则查找和目的地设置":
- 获取匹配ID:从"包裹"中获取"匹配ID"(流ID)
- 查找规则表:使用"匹配ID"作为"索引"查找"规则表"(流表查找表)
- 获取目的地:从"规则"中获取"预设的目的地"(next_index)
- 设置目的地:如果"规则"中配置了"目的地",使用该值;否则使用"默认目的地"
12.5 流ID和buffer advance的处理
作用和实现原理:
流ID和buffer advance是Flow Offload支持的两个重要动作。流ID(flow_id)用于为数据包设置标识符,用于后续的流量识别、统计、监控等。buffer advance用于调整buffer的数据指针,跳过某些头部(例如跳过隧道外层头部),直接处理内层数据。就像"快递分拣中心"的"标记和指针调整",为"包裹"设置"标记"(flow_id)用于"识别",调整"指针"(buffer_advance)用于"跳过外层包装"。
流ID的设置 (src/plugins/dpdk/device/node.c:269):
c
/* 在dpdk_process_flow_offload函数中,设置flow_id */
if (fle->flow_id != ~0)
{
/* 流表项中配置了flow_id,设置到buffer中 */
b0 = vlib_buffer_from_rte_mbuf (ptd->mbufs[n]);
b0->flow_id = fle->flow_id;
}
/* 如果fle->flow_id == ~0,表示未配置,不设置flow_id */
流ID的用途:
- 流量识别:用于识别和分类不同的数据流
- 统计监控:用于流量统计和监控
- 策略执行:用于执行基于流的策略(如QoS、ACL等)
- 调试追踪:用于数据包的调试和追踪
流ID的配置 (src/plugins/dpdk/device/flow.c:801):
c
/* 在dpdk_flow_ops_fn函数中,配置flow_id */
if (flow->actions & VNET_FLOW_ACTION_MARK)
{
/* 如果流动作包含标记,设置flow_id */
fle->flow_id = flow->mark_flow_id;
}
else
{
/* 否则设置为~0,表示未配置 */
fle->flow_id = ~0;
}
buffer advance的处理 (src/plugins/dpdk/device/node.c:275):
c
/* 在dpdk_process_flow_offload函数中,调整buffer指针 */
if (fle->buffer_advance != ~0)
{
/* 流表项中配置了buffer_advance,调整buffer指针 */
b0 = vlib_buffer_from_rte_mbuf (ptd->mbufs[n]);
vlib_buffer_advance (b0, fle->buffer_advance);
}
/* 如果fle->buffer_advance == ~0,表示未配置,不调整buffer指针 */
vlib_buffer_advance函数 (src/vlib/buffer.h:292):
c
/**
* 调整buffer的数据指针
*
* @param b buffer指针
* @param l 要跳过的字节数(正数表示向前移动,负数表示向后移动)
*/
always_inline void
vlib_buffer_advance (vlib_buffer_t * b, word l)
{
/* 断言:确保当前数据长度足够跳过 */
ASSERT (b->current_length >= l);
/* 调整数据指针:向前移动l字节 */
b->current_data += l;
/* 调整数据长度:减少l字节 */
b->current_length -= l;
/* 断言:如果是多段buffer,确保当前段长度满足最小要求 */
ASSERT ((b->flags & VLIB_BUFFER_NEXT_PRESENT) == 0 ||
b->current_length >= VLIB_BUFFER_MIN_CHAIN_SEG_SIZE);
}
buffer advance的使用场景:
- 隧道处理:跳过隧道外层头部(如VXLAN、GRE等),直接处理内层数据包
- 协议栈跳过:跳过某些协议层,直接处理上层协议
- 头部剥离:跳过某些头部,用于特殊处理
buffer advance的配置 (src/plugins/dpdk/device/flow.c:805):
c
/* 在dpdk_flow_ops_fn函数中,配置buffer_advance */
if (flow->actions & VNET_FLOW_ACTION_BUFFER_ADVANCE)
{
/* 如果流动作包含buffer advance,设置buffer_advance */
fle->buffer_advance = flow->buffer_advance;
}
else
{
/* 否则设置为~0,表示未配置 */
fle->buffer_advance = ~0;
}
buffer advance处理示例:
假设有一个VXLAN数据包:
外层:Ethernet + IP + UDP + VXLAN头部(共50字节)
内层:Ethernet + IP + TCP + 数据
处理前:
b->current_data = 0(指向外层Ethernet头部)
b->current_length = 1500(包含外层和内层)
配置buffer_advance = 50(跳过外层头部)
调用vlib_buffer_advance(b, 50)后:
b->current_data = 50(指向内层Ethernet头部)
b->current_length = 1450(只包含内层)
这样后续处理可以直接处理内层数据包,无需解析外层头部
关键设计要点:
- 条件设置:只有当流表项中配置了相应字段时才设置
- 零拷贝:buffer advance只是调整指针,不进行数据拷贝
- 灵活配置:支持为每个流配置不同的flow_id和buffer_advance
- 安全性检查:vlib_buffer_advance包含断言检查,确保操作安全
通俗理解:
就像"快递分拣中心"的"标记和指针调整":
- 设置标记(flow_id):为"包裹"设置"标记"(flow_id),用于"识别和分类"
- 调整指针(buffer_advance):调整"查看位置"(current_data),跳过"外层包装"(外层头部),直接查看"内层内容"(内层数据包)
- 零拷贝:只是"调整查看位置",不"重新包装",提高效率
12.6 本章总结
流表处理核心流程总结:
| 步骤 | 功能 | 关键字段/标志 | 作用 |
|---|---|---|---|
| 1. 标志检查 | 检查FDIR标志 | RTE_MBUF_F_RX_FDIR |
判断是否有数据包匹配了Flow Director规则 |
| 2. 流ID提取 | 从mbuf提取流ID | mbuf->hash.fdir.hi |
获取硬件匹配的流ID |
| 3. 流表查找 | 查找流表项 | flow_lookup_entries[流ID] |
根据流ID查找流表项 |
| 4. 下一跳设置 | 设置下一跳节点 | fle->next_index → ptd->next[n] |
根据流表项设置数据包的下一跳 |
| 5. 流ID设置 | 设置flow_id | fle->flow_id → b->flow_id |
为数据包设置流标识符 |
| 6. 指针调整 | 调整buffer指针 | fle->buffer_advance → vlib_buffer_advance |
跳过某些头部,直接处理内层数据 |
关键性能优化技术:
- 硬件加速:流表匹配由网卡硬件完成,无需CPU参与
- O(1)查找:使用流ID作为索引直接查找流表项,时间复杂度为O(1)
- 批量处理:可以优化为批量处理(四重循环),提高处理效率
- 零拷贝:buffer advance只是调整指针,不进行数据拷贝
相关源码文件:
src/plugins/dpdk/device/node.c:249-dpdk_process_flow_offload函数src/plugins/dpdk/device/node.c:426- Flow Offload处理入口src/plugins/dpdk/device/flow.c:728-dpdk_flow_ops_fn函数(流表管理)src/plugins/dpdk/device/dpdk.h:90-dpdk_flow_lookup_entry_t结构定义src/vlib/buffer.h:292-vlib_buffer_advance函数
第13章:LRO处理(Large Receive Offload)
本章概述:
第13章详细讲解DPDK插件中LRO(Large Receive Offload)处理的核心机制。LRO是一种硬件加速技术,允许网卡硬件将多个小的TCP分段合并成更大的数据包,减少软件需要处理的数据包数量,提高处理效率。当LRO启用时,网卡会检测到来自同一TCP连接的多個小分段,将它们合并成一个大包,并在mbuf中设置RTE_MBUF_F_RX_LRO标志。VPP通过检查这个标志,设置GSO(Generic Segmentation Offload)相关元数据,用于后续的分段处理。本章将详细讲解LRO的概念、处理流程、GSO标志的设置以及L4头部大小的计算。
通俗理解:
就像"快递分拣中心"的"包裹合并":
- LRO合并:网卡硬件"自动将多个小包裹"(TCP分段)"合并成一个大包裹"(合并后的数据包)
- GSO标记:为"大包裹"设置"GSO标记"(标志),表示"可以重新分段"
- 元数据设置:设置"包裹信息"(gso_size、gso_l4_hdr_sz等),用于"后续重新分段"
13.1 LRO的概念和作用
作用和实现原理:
LRO(Large Receive Offload)是一种网卡硬件加速技术,用于将多个小的TCP分段合并成更大的数据包。当TCP数据流被分割成多个小段时,LRO可以检测到这些段属于同一个TCP连接,并将它们合并成一个更大的数据包。这样可以减少软件需要处理的数据包数量,降低CPU开销,提高网络处理性能。就像"快递分拣中心"的"包裹合并",将"多个小包裹"(TCP分段)"合并成一个大包裹"(合并后的数据包),减少"处理次数"。
LRO的工作原理:
未启用LRO时:
TCP发送方:
Segment 1 (1460字节) → 网卡 → VPP处理
Segment 2 (1460字节) → 网卡 → VPP处理
Segment 3 (800字节) → 网卡 → VPP处理
总共:3个数据包需要处理
启用LRO后:
TCP发送方:
Segment 1 (1460字节) ┐
Segment 2 (1460字节) ├→ 网卡硬件合并 → 一个大包 (3720字节) → VPP处理
Segment 3 (800字节) ┘
总共:1个数据包需要处理
性能提升:
- 减少数据包数量:从3个减少到1个
- 降低CPU开销:减少中断次数和处理次数
- 提高处理效率:批量处理更大的数据包
LRO的优势:
- 减少数据包数量:将多个小分段合并成一个大包,减少数据包数量
- 降低CPU开销:减少中断次数和软件处理次数,降低CPU使用率
- 提高吞吐量:通过批量处理,提高网络处理吞吐量
- 保持兼容性:合并后的数据包仍然可以被重新分段,保持与下游系统的兼容性
LRO的配置 (src/plugins/dpdk/device/common.c:115):
c
/* 在设备启动时,如果启用了LRO,添加LRO卸载标志 */
if (xd->conf.enable_lro)
rxo |= RTE_ETH_RX_OFFLOAD_TCP_LRO; // 添加TCP LRO卸载标志
/* 在配置DPDK设备时,如果启用了LRO,设置最大LRO包大小 */
if (rxo & RTE_ETH_RX_OFFLOAD_TCP_LRO)
conf.rxmode.max_lro_pkt_size = xd->conf.max_lro_pkt_size; // 设置最大合并包大小
LRO的限制:
- 仅支持TCP:LRO主要用于TCP协议,不支持UDP等其他协议
- 合并限制 :合并后的数据包大小受限于
max_lro_pkt_size配置 - 需要重新分段:合并后的数据包可能需要重新分段才能发送到不支持大包的接口
通俗理解:
就像"快递分拣中心"的"包裹合并":
- 自动合并:网卡硬件"自动检测多个小包裹"(TCP分段)属于"同一个寄件人-收件人"(TCP连接)
- 合并成一个大包裹:将这些"小包裹"合并成"一个大包裹"(合并后的数据包)
- 减少处理次数:从"处理3个小包裹"变成"处理1个大包裹",减少"处理次数"
- 性能提升:通过"减少处理次数",降低"CPU使用率",提高"处理效率"
13.2 dpdk_process_lro_offload函数处理LRO
作用和实现原理:
dpdk_process_lro_offload函数负责处理LRO卸载的数据包。它遍历所有接收到的数据包,检查每个数据包是否设置了RTE_MBUF_F_RX_LRO标志(表示该数据包是LRO合并的结果)。如果设置了该标志,函数会为数据包设置GSO(Generic Segmentation Offload)相关标志和元数据,用于后续的分段处理。就像"快递分拣中心"的"合并包裹处理",检查"包裹"是否有"合并标记"(RTE_MBUF_F_RX_LRO),如果有,设置"可以重新分段标记"(GSO标志)。
函数签名 (src/plugins/dpdk/device/node.c:324):
c
/**
* 处理LRO卸载的数据包
*
* @param xd DPDK设备结构体指针
* @param ptd 线程数据指针(包含mbuf数组、flags数组等)
* @param n_rx_packets 接收到的数据包数量
*/
static_always_inline void
dpdk_process_lro_offload (dpdk_device_t * xd, // DPDK设备
dpdk_per_thread_data_t * ptd, // 线程数据
uword n_rx_packets) // 数据包数量
函数完整实现 (src/plugins/dpdk/device/node.c:324):
c
static_always_inline void
dpdk_process_lro_offload (dpdk_device_t *xd, dpdk_per_thread_data_t *ptd,
uword n_rx_packets)
{
uword n; // 循环计数器
vlib_buffer_t *b0; // buffer指针
/* 步骤1:遍历所有接收到的数据包 */
for (n = 0; n < n_rx_packets; n++)
{
/* 步骤1.1:mbuf转换为buffer(零拷贝) */
/* 通过指针运算,从mbuf指针获取对应的buffer指针 */
b0 = vlib_buffer_from_rte_mbuf (ptd->mbufs[n]);
/* 步骤1.2:检查当前数据包是否是LRO合并的结果 */
/* RTE_MBUF_F_RX_LRO标志表示该数据包是LRO合并的结果 */
if (ptd->flags[n] & RTE_MBUF_F_RX_LRO)
{
/* 步骤1.2.1:设置GSO标志 */
/* VNET_BUFFER_F_GSO标志表示该数据包是GSO数据包,可以重新分段 */
/* GSO(Generic Segmentation Offload)是通用的分段卸载技术 */
b0->flags |= VNET_BUFFER_F_GSO;
/* 步骤1.2.2:设置GSO段大小(payload大小) */
/* mbuf->tso_segsz存储了硬件计算的段大小(每个段的payload大小) */
/* 这个值会被存储到vnet_buffer2(b0)->gso_size中,用于后续分段 */
vnet_buffer2 (b0)->gso_size = ptd->mbufs[n]->tso_segsz;
/* 步骤1.2.3:计算并设置L4头部大小 */
/* gso_l4_hdr_sz用于分段时保持L4头部不变 */
/* 需要解析数据包头部,找到TCP头部并计算其大小(包括选项) */
vnet_buffer2 (b0)->gso_l4_hdr_sz = dpdk_lro_find_l4_hdr_sz (b0);
}
}
}
LRO处理入口 (src/plugins/dpdk/device/node.c:406):
c
/* 在dpdk_device_input函数中,检查是否有LRO卸载的数据包 */
if (PREDICT_FALSE ((or_flags & RTE_MBUF_F_RX_LRO)))
{
/* 至少有一个数据包是LRO合并的结果 */
/* 调用dpdk_process_lro_offload处理LRO卸载 */
dpdk_process_lro_offload (xd, ptd, n_rx_packets);
}
处理流程示例:
假设有3个数据包,其中2个是LRO合并的结果:
数据包0:flags[0] & RTE_MBUF_F_RX_LRO = 0 → 跳过,不是LRO包
数据包1:flags[1] & RTE_MBUF_F_RX_LRO != 0 → 处理
1. 设置b0->flags |= VNET_BUFFER_F_GSO
2. 设置gso_size = mbufs[1]->tso_segsz(例如1460)
3. 调用dpdk_lro_find_l4_hdr_sz计算L4头部大小(例如20字节)
数据包2:flags[2] & RTE_MBUF_F_RX_LRO != 0 → 处理
1. 设置b0->flags |= VNET_BUFFER_F_GSO
2. 设置gso_size = mbufs[2]->tso_segsz(例如1460)
3. 调用dpdk_lro_find_l4_hdr_sz计算L4头部大小(例如32字节,包含选项)
关键设计要点:
- 条件处理 :只有当数据包设置了
RTE_MBUF_F_RX_LRO标志时才处理 - GSO标志设置 :设置
VNET_BUFFER_F_GSO标志,表示数据包可以重新分段 - 元数据设置 :设置
gso_size和gso_l4_hdr_sz,用于后续的分段处理
通俗理解:
就像"快递分拣中心"的"合并包裹处理":
- 检查标记:检查"包裹"是否有"合并标记"(RTE_MBUF_F_RX_LRO)
- 设置分段标记:如果有,设置"可以重新分段标记"(VNET_BUFFER_F_GSO)
- 记录信息:记录"每个段的大小"(gso_size)和"头部大小"(gso_l4_hdr_sz),用于"后续重新分段"
13.3 GSO(Generic Segmentation Offload)标志的设置
作用和实现原理:
GSO(Generic Segmentation Offload)标志用于标识数据包是否可以重新分段。当数据包是LRO合并的结果时,需要设置VNET_BUFFER_F_GSO标志,表示该数据包可以重新分段成多个小段。这个标志会被后续的GSO节点或接口输出节点使用,如果目标接口不支持大包,会将数据包重新分段。就像"快递分拣中心"的"可重新分段标记",表示"大包裹"可以"重新拆分成小包裹"。
GSO标志定义 (src/vnet/buffer.h:67):
c
/* VPP buffer标志定义 */
#define foreach_vnet_buffer_flag
/* ... 其他标志 ... */
_ (18, GSO, "gso", 0) // GSO标志(第18位)
/* ... 其他标志 ... */
/* 标志值定义 */
enum {
VNET_BUFFER_F_GSO = (1 << 18), // 第18位,值为0x40000
/* ... 其他标志 ... */
};
GSO标志的设置 (src/plugins/dpdk/device/node.c:335):
c
/* 在dpdk_process_lro_offload函数中,设置GSO标志 */
if (ptd->flags[n] & RTE_MBUF_F_RX_LRO)
{
/* 设置GSO标志,表示该数据包是GSO数据包,可以重新分段 */
b0->flags |= VNET_BUFFER_F_GSO;
/* ... 设置其他GSO元数据 ... */
}
GSO标志的使用场景:
- 接口输出:在接口输出节点,如果目标接口不支持大包,检查GSO标志并重新分段
- GSO节点:专门的GSO节点可以处理GSO数据包,进行分段
- 隧道封装:在隧道封装前,可能需要将GSO数据包分段
GSO标志的检查(示例代码):
c
/* 在接口输出或GSO节点中,检查GSO标志 */
if (b->flags & VNET_BUFFER_F_GSO)
{
/* 数据包是GSO数据包,需要分段 */
u16 gso_size = vnet_buffer2(b)->gso_size; // 每个段的payload大小
u16 l4_hdr_sz = vnet_buffer2(b)->gso_l4_hdr_sz; // L4头部大小
/* 使用gso_size和l4_hdr_sz进行分段 */
/* ... 分段处理 ... */
}
关键设计要点:
- 标志指示 :
VNET_BUFFER_F_GSO标志指示数据包可以重新分段 - 与LRO配合:LRO合并后的数据包需要设置GSO标志,以便后续分段
- 灵活性:GSO标志使得数据包可以根据目标接口的能力进行灵活处理
通俗理解:
就像"快递分拣中心"的"可重新分段标记":
- 设置标记:为"合并后的大包裹"设置"可以重新分段标记"(VNET_BUFFER_F_GSO)
- 后续处理:如果"目标地址"不支持"大包裹",可以根据"标记"将"大包裹"重新拆分成"小包裹"
- 灵活性:通过"标记",实现"灵活处理",根据"目标能力"决定是否需要"重新分段"
13.4 L4头部大小的计算(dpdk_lro_find_l4_hdr_sz)
作用和实现原理:
dpdk_lro_find_l4_hdr_sz函数用于计算L4(TCP)头部的大小。由于TCP头部可能包含选项(如MSS、窗口缩放、时间戳等),头部大小不是固定的,需要根据TCP头部的data_offset字段计算。函数通过解析数据包的以太网头部、IP头部(IPv4或IPv6),找到TCP头部,然后根据TCP头部的data_offset字段计算头部大小(包括选项)。就像"快递分拣中心"的"包裹头部计算",需要"解析包裹标签"(协议头部),找到"TCP标签"(TCP头部),并计算"标签大小"(头部大小)。
函数签名 (src/plugins/dpdk/device/node.c:283):
c
/**
* 查找并计算L4(TCP)头部大小
*
* @param b buffer指针
*
* @return L4头部大小(字节数)
*/
static_always_inline u16
dpdk_lro_find_l4_hdr_sz (vlib_buffer_t *b)
函数完整实现 (src/plugins/dpdk/device/node.c:283):
c
static_always_inline u16
dpdk_lro_find_l4_hdr_sz (vlib_buffer_t *b)
{
u16 l4_hdr_sz = 0; // L4头部大小,初始化为0
u16 current_offset = 0; // 当前偏移量,用于跟踪解析位置
ethernet_header_t *e; // 以太网头部指针
tcp_header_t *tcp; // TCP头部指针
u8 *data = vlib_buffer_get_current (b); // 获取buffer中当前数据的指针
u16 ethertype; // 以太网类型
/* 步骤1:解析以太网头部 */
e = (void *) data; // 以太网头部在数据包的最开始
current_offset += sizeof (e[0]); // 增加以太网头部大小(14字节)
ethertype = clib_net_to_host_u16 (e->type); // 获取以太网类型(网络字节序转主机字节序)
/* 步骤2:处理VLAN标签(如果存在) */
/* 检查以太网帧是否包含VLAN标签 */
if (ethernet_frame_is_tagged (ethertype))
{
/* 有VLAN标签,解析VLAN头部 */
ethernet_vlan_header_t *vlan = (ethernet_vlan_header_t *) (e + 1);
ethertype = clib_net_to_host_u16 (vlan->type); // 获取内层以太网类型
current_offset += sizeof (*vlan); // 增加VLAN头部大小(4字节)
/* 检查是否有双层VLAN(Q-in-Q) */
if (ethertype == ETHERNET_TYPE_VLAN)
{
/* 有双层VLAN,再增加一个VLAN头部大小 */
vlan++;
current_offset += sizeof (*vlan); // 再增加4字节
ethertype = clib_net_to_host_u16 (vlan->type); // 获取最内层以太网类型
}
}
/* 步骤3:移动到IP头部位置 */
data += current_offset; // 数据指针移动到IP头部位置
/* 步骤4:根据IP版本解析IP头部并找到TCP头部 */
if (ethertype == ETHERNET_TYPE_IP4)
{
/* IPv4数据包 */
data += sizeof (ip4_header_t); // 跳过IPv4头部(20字节,无选项)
tcp = (void *) data; // TCP头部在IPv4头部之后
/* 计算TCP头部大小(包括选项) */
/* tcp_header_bytes函数根据TCP头部的data_offset字段计算头部大小 */
l4_hdr_sz = tcp_header_bytes (tcp);
}
else
{
/* IPv6数据包 */
/* FIXME: 当前实现未处理IPv6扩展头部,直接跳过IPv6基本头部 */
data += sizeof (ip6_header_t); // 跳过IPv6基本头部(40字节)
tcp = (void *) data; // TCP头部在IPv6头部之后(假设无扩展头部)
/* 计算TCP头部大小(包括选项) */
l4_hdr_sz = tcp_header_bytes (tcp);
}
/* 步骤5:返回L4头部大小 */
return l4_hdr_sz;
}
tcp_header_bytes函数 (src/vnet/tcp/tcp_packet.h:94):
c
/**
* 计算TCP头部大小(包括选项)
*
* @param t TCP头部指针
*
* @return TCP头部大小(字节数)
*/
always_inline int
tcp_header_bytes (tcp_header_t * t)
{
/* TCP头部的data_offset字段(4位)存储在data_offset_and_reserved字段的高4位 */
/* data_offset表示TCP头部长度(以32位字为单位) */
/* 因此头部大小 = data_offset * 4字节 */
return tcp_doff (t) * sizeof (u32);
}
/* tcp_doff宏定义:提取data_offset字段(高4位) */
#define tcp_doff(_th) ((_th)->data_offset_and_reserved >> 4)
TCP头部大小计算详解:
TCP头部结构:
┌─────────────────────────────────────────┐
│ 源端口 (16位) │
│ 目标端口 (16位) │
│ 序列号 (32位) │
│ 确认号 (32位) │
│ data_offset_and_reserved (8位) │
│ ├── data_offset (4位高) = 头部长度/4 │
│ └── reserved (4位低) │
│ flags (8位) │
│ 窗口大小 (16位) │
│ 校验和 (16位) │
│ 紧急指针 (16位) │
├─────────────────────────────────────────┤
│ 选项 (0-40字节,可选) │
│ - MSS (4字节) │
│ - 窗口缩放 (3字节) │
│ - 时间戳 (10字节) │
│ - SACK (可变) │
│ - 其他选项... │
└─────────────────────────────────────────┘
TCP头部大小 = data_offset * 4字节
- 最小:20字节(data_offset = 5,无选项)
- 最大:60字节(data_offset = 15,40字节选项)
处理流程示例:
假设有一个带VLAN标签的IPv4 TCP数据包:
数据包结构:
[Ethernet头部(14)] [VLAN标签(4)] [IPv4头部(20)] [TCP头部(32)] [数据...]
↑ ↑ ↑ ↑
data=0 current_offset=14 current_offset=18 current_offset=38
处理步骤:
1. 解析以太网头部:current_offset = 14
2. 检测到VLAN标签:current_offset = 18
3. 解析IPv4头部:current_offset = 38,data指向TCP头部
4. 调用tcp_header_bytes(tcp):
- 假设tcp->data_offset_and_reserved = 0x80(data_offset=8)
- 返回:8 * 4 = 32字节
5. 返回l4_hdr_sz = 32
关键设计要点:
- 逐层解析:从以太网头部开始,逐层解析到TCP头部
- VLAN处理:正确处理单层和双层VLAN标签
- IPv6限制:当前实现未处理IPv6扩展头部,这是已知的限制(FIXME)
- 动态计算:根据TCP头部的data_offset字段动态计算头部大小
通俗理解:
就像"快递分拣中心"的"标签大小计算":
- 逐层解析:从"最外层标签"(以太网头部)开始,逐层解析到"TCP标签"(TCP头部)
- VLAN处理:如果"包裹"有"VLAN标签",需要"跳过标签"才能看到"内层标签"
- 动态计算:根据"TCP标签"的"长度标记"(data_offset)计算"标签大小"(头部大小)
- 包含选项:TCP标签可能包含"额外信息"(选项),所以大小不是固定的
13.5 GSO相关元数据的设置
作用和实现原理:
GSO相关元数据用于存储分段所需的信息,包括每个段的payload大小(gso_size)和L4头部大小(gso_l4_hdr_sz)。这些元数据被存储在vnet_buffer_opaque2_t结构体中,通过vnet_buffer2(b)宏访问。当数据包需要重新分段时,这些元数据用于正确地分割数据包。就像"快递分拣中心"的"分段信息记录",记录"每个小包裹的大小"(gso_size)和"标签大小"(gso_l4_hdr_sz),用于"后续重新分段"。
GSO元数据结构 (src/vnet/buffer.h:477):
c
/* vnet_buffer_opaque2_t结构体中的GSO相关字段 */
typedef struct
{
/* ... 其他字段 ... */
/**
* GSO相关元数据
* 在GSO使能的接口接收GSO数据包时设置
* 需要一直保持到接口输出,以防出接口不支持GSO时需要分段
*/
struct
{
u16 gso_size; // L4 payload大小(每个段的payload大小)
u16 gso_l4_hdr_sz; // L4协议头部大小(TCP/UDP头部大小,包括选项)
i16 outer_l3_hdr_offset; // 外层L3头部偏移(用于隧道)
i16 outer_l4_hdr_offset; // 外层L4头部偏移(用于隧道)
};
/* ... 其他字段 ... */
} vnet_buffer_opaque2_t;
/* 访问宏 */
#define vnet_buffer2(b) ((vnet_buffer_opaque2_t *) (b)->opaque2)
GSO元数据的设置 (src/plugins/dpdk/device/node.c:336):
c
/* 在dpdk_process_lro_offload函数中,设置GSO元数据 */
if (ptd->flags[n] & RTE_MBUF_F_RX_LRO)
{
/* 设置GSO标志 */
b0->flags |= VNET_BUFFER_F_GSO;
/* 设置GSO段大小(payload大小) */
/* mbuf->tso_segsz存储了硬件计算的段大小(每个段的payload大小) */
/* 这个值通常等于MSS(Maximum Segment Size),例如1460字节 */
vnet_buffer2 (b0)->gso_size = ptd->mbufs[n]->tso_segsz;
/* 设置L4头部大小 */
/* 通过解析数据包头部,找到TCP头部并计算其大小(包括选项) */
vnet_buffer2 (b0)->gso_l4_hdr_sz = dpdk_lro_find_l4_hdr_sz (b0);
}
gso_size的含义:
c
/* gso_size是每个段的L4 payload大小(不包括L4头部) */
/* 例如:如果gso_size = 1460,表示每个段的payload为1460字节 */
/*
* 数据包结构(分段后):
* [L2头部] [L3头部] [L4头部(gso_l4_hdr_sz)] [Payload(gso_size)] ← 段1
* [L2头部] [L3头部] [L4头部(gso_l4_hdr_sz)] [Payload(gso_size)] ← 段2
* [L2头部] [L3头部] [L4头部(gso_l4_hdr_sz)] [Payload(剩余)] ← 段3(最后一段)
*/
GSO MTU大小的计算 (src/vnet/buffer.h:524):
c
/**
* 计算GSO MTU大小(包括所有头部)
*
* @param b buffer指针
*
* @return GSO MTU大小(字节数)
*/
#define gso_mtu_sz(b) (vnet_buffer2(b)->gso_size + // L4 payload大小
vnet_buffer2(b)->gso_l4_hdr_sz + // L4头部大小
vnet_buffer(b)->l4_hdr_offset - // L4头部偏移
vnet_buffer (b)->l3_hdr_offset) // L3头部偏移
/* 计算过程:
* gso_mtu_sz = gso_size + gso_l4_hdr_sz + (l4_hdr_offset - l3_hdr_offset)
* = payload大小 + L4头部大小 + (L3头部大小 + L2头部大小)
* = 整个段的MTU大小(从L2头部到L4 payload结束)
*/
GSO元数据的使用示例:
c
/* 在GSO分段节点中,使用GSO元数据进行分段 */
if (b->flags & VNET_BUFFER_F_GSO)
{
u16 gso_size = vnet_buffer2(b)->gso_size; // 每个段的payload大小(例如1460)
u16 l4_hdr_sz = vnet_buffer2(b)->gso_l4_hdr_sz; // L4头部大小(例如20或32)
u32 total_len = vlib_buffer_length_in_chain(vm, b); // 数据包总长度
/* 计算需要分段的段数 */
u32 payload_len = total_len - (l4_hdr_offset - current_data) - l4_hdr_sz;
u32 n_segments = (payload_len + gso_size - 1) / gso_size; // 向上取整
/* 为每个段分配buffer并复制数据 */
for (i = 0; i < n_segments; i++)
{
/* 复制L2+L3+L4头部 */
/* 复制gso_size字节的payload(最后一段可能更小) */
/* 更新TCP序列号和校验和 */
}
}
关键设计要点:
- 持久化存储:GSO元数据需要从接收一直保持到接口输出,以防出接口不支持GSO
- 缓存行优化:GSO元数据存储在第二个缓存行(opaque2),访问会有一定开销,但被大数据包分摊
- 分段依据:gso_size和gso_l4_hdr_sz是分段的关键依据
通俗理解:
就像"快递分拣中心"的"分段信息记录":
- 记录段大小(gso_size):记录"每个小包裹的payload大小"(例如1460字节)
- 记录头部大小(gso_l4_hdr_sz):记录"标签大小"(TCP头部大小,例如20或32字节)
- 持久化保存:这些信息需要"一直保存"到"最终发送",以防"目标地址"不支持"大包裹",需要"重新分段"
- 分段依据:后续可以根据这些信息"正确地重新分段"
13.6 本章总结
LRO处理核心流程总结:
| 步骤 | 功能 | 关键字段/标志 | 作用 |
|---|---|---|---|
| 1. 标志检查 | 检查LRO标志 | RTE_MBUF_F_RX_LRO |
判断是否有LRO合并的数据包 |
| 2. GSO标志设置 | 设置GSO标志 | VNET_BUFFER_F_GSO |
标识数据包可以重新分段 |
| 3. 段大小设置 | 设置GSO段大小 | gso_size |
存储每个段的payload大小 |
| 4. L4头部计算 | 计算L4头部大小 | dpdk_lro_find_l4_hdr_sz |
解析数据包并计算TCP头部大小 |
| 5. L4头部设置 | 设置L4头部大小 | gso_l4_hdr_sz |
存储L4头部大小(包括选项) |
关键性能优化技术:
- 硬件加速:LRO合并由网卡硬件完成,减少软件需要处理的数据包数量
- 元数据持久化:GSO元数据从接收一直保持到发送,支持灵活的分段处理
- 动态头部计算:根据TCP头部的data_offset字段动态计算头部大小,支持可变头部
相关源码文件:
src/plugins/dpdk/device/node.c:324-dpdk_process_lro_offload函数src/plugins/dpdk/device/node.c:283-dpdk_lro_find_l4_hdr_sz函数src/plugins/dpdk/device/node.c:406- LRO处理入口src/plugins/dpdk/device/common.c:115- LRO配置src/vnet/buffer.h:477- GSO元数据结构定义src/vnet/tcp/tcp_packet.h:94-tcp_header_bytes函数
第14章:数据包分发和下一跳选择
本章概述:
第14章详细讲解DPDK插件中数据包分发和下一跳选择的核心机制。数据包接收后,需要根据配置和特征选择下一个处理节点。VPP提供了多种机制来确定下一跳节点:默认下一跳、每接口重定向、Feature Arc处理、Flow Offload选择等。不同的选择机制适用于不同的场景,共同实现了灵活高效的数据包路由。本章将详细讲解这些机制的原理、实现和使用场景。
通俗理解:
就像"快递分拣中心"的"包裹路由":
- 默认路由:所有"包裹"默认发送到"以太网分拣区"(ethernet-input)
- 优化路由:如果"包裹"已经"检查通过"(IP校验和正确),可以直接发送到"快速通道"(ip4-input-no-checksum)
- 特殊路由:某些"包裹"根据"特殊规则"(Feature Arc、Flow Offload)发送到"特定区域"
- 灵活分发:支持"所有包裹同一个目的地"(单一下一跳)和"每个包裹不同目的地"(多下一跳)
14.1 下一跳节点的选择机制概述
作用和实现原理:
下一跳节点的选择机制是VPP数据包处理的核心。数据包从DPDK网卡接收后,需要根据配置、特征和规则选择下一个处理节点。VPP提供了多种选择机制,按优先级从高到低依次为:每接口重定向(per_interface_next_index)、Feature Arc处理、默认下一跳。此外,Flow Offload支持为每个数据包选择不同的下一跳。就像"快递分拣中心"的"包裹路由系统",根据"预设规则"和"包裹特征"选择"目标分拣区"。
下一跳选择机制的优先级:
优先级1:Flow Offload(每包选择)
│
├── 如果数据包匹配了Flow Director规则
│ └── 使用Flow Offload设置的下一跳(ptd->next数组)
│
▼
优先级2:每接口重定向(per_interface_next_index)
│
├── 如果接口配置了per_interface_next_index
│ └── 使用per_interface_next_index作为下一跳
│
▼
优先级3:Feature Arc处理
│
├── 如果接口配置了Feature Arc
│ └── Feature Arc确定下一跳
│
▼
优先级4:默认下一跳
│
└── 使用默认的ethernet-input节点
下一跳选择代码流程 (src/plugins/dpdk/device/node.c:350):
c
/* 步骤1:初始化默认下一跳 */
u32 next_index = VNET_DEVICE_INPUT_NEXT_ETHERNET_INPUT; // 默认:ethernet-input节点
/* 步骤2:检查每接口重定向(优先级最高,但如果Flow Offload启用则会被覆盖) */
if (PREDICT_FALSE (xd->per_interface_next_index != ~0))
next_index = xd->per_interface_next_index; // 使用每接口重定向
/* 步骤3:Feature Arc处理(如果接口配置了Feature Arc) */
if (PREDICT_FALSE (vnet_device_input_have_features (xd->sw_if_index)))
vnet_feature_start_device_input (xd->sw_if_index, &next_index, bt);
// Feature Arc可能会修改next_index
/* 步骤4:处理数据包并检查Flow Offload */
if (PREDICT_FALSE (or_flags & RTE_MBUF_F_RX_FDIR))
{
/* Flow Offload处理:为每个数据包设置下一跳 */
for (n = 0; n < n_rx_packets; n++)
ptd->next[n] = next_index; // 初始化为当前next_index
dpdk_process_flow_offload (xd, ptd, n_rx_packets); // 可能修改ptd->next数组
/* 使用ptd->next数组,每个数据包可能有不同的下一跳 */
}
else
{
/* 所有数据包使用同一个下一跳 */
/* 使用next_index作为所有数据包的下一跳 */
}
关键设计要点:
- 优先级明确:Flow Offload优先级最高,然后是每接口重定向、Feature Arc,最后是默认下一跳
- 灵活性:支持所有数据包同一下一跳(单一下一跳)和每个数据包不同下一跳(多下一跳)
- 性能优化:通过Feature Arc和优化标志,实现快速路径优化
通俗理解:
就像"快递分拣中心"的"包裹路由系统":
- 规则优先级:按照"特殊规则"→"接口规则"→"通用规则"的优先级选择"目标分拣区"
- 灵活性:支持"所有包裹同一个目的地"和"每个包裹不同目的地"
- 性能优化:通过"优化路径",减少"处理环节",提高"处理效率"
14.2 默认下一跳:ethernet-input节点
作用和实现原理:
默认下一跳是VNET_DEVICE_INPUT_NEXT_ETHERNET_INPUT,对应的节点是ethernet-input。这是所有设备输入节点的默认下一跳,用于处理以太网帧。ethernet-input节点会根据以太网类型(EtherType)将数据包分发到相应的处理节点(如ip4-input、ip6-input、mpls-input等)。就像"快递分拣中心"的"默认分拣区"(以太网分拣区),所有"包裹"默认先到这里,然后根据"包裹类型"(以太网类型)分发到"具体处理区"。
默认下一跳设置 (src/plugins/dpdk/device/node.c:350):
c
/* 在dpdk_device_input函数中,初始化默认下一跳 */
u32 next_index = VNET_DEVICE_INPUT_NEXT_ETHERNET_INPUT;
/* VNET_DEVICE_INPUT_NEXT_ETHERNET_INPUT是设备输入节点的默认下一跳 */
/* 对应的节点是ethernet-input,用于处理以太网帧 */
下一跳节点枚举定义 (src/vnet/devices/devices.h:22):
c
/**
* 设备输入节点的下一跳节点枚举
*/
typedef enum
{
VNET_DEVICE_INPUT_NEXT_IP4_NCS_INPUT, // ip4-input-no-checksum节点
VNET_DEVICE_INPUT_NEXT_IP4_INPUT, // ip4-input节点
VNET_DEVICE_INPUT_NEXT_IP6_INPUT, // ip6-input节点
VNET_DEVICE_INPUT_NEXT_MPLS_INPUT, // mpls-input节点
VNET_DEVICE_INPUT_NEXT_ETHERNET_INPUT, // ethernet-input节点(默认)
VNET_DEVICE_INPUT_NEXT_DROP, // error-drop节点
/* 隧道相关 */
VNET_DEVICE_INPUT_NEXT_IP4_DROP, // ip4-drop节点
VNET_DEVICE_INPUT_NEXT_IP6_DROP, // ip6-drop节点
VNET_DEVICE_INPUT_NEXT_PUNT, // punt-dispatch节点
VNET_DEVICE_INPUT_N_NEXT_NODES, // 下一跳节点总数
} vnet_device_input_next_t;
ethernet-input节点的作用:
ethernet-input节点是VPP以太网处理的核心节点,主要功能包括:
- 以太网类型识别:根据以太网帧的EtherType字段识别协议类型
- 数据包分发:将数据包分发到相应的处理节点(IP、IPv6、MPLS等)
- VLAN处理:处理VLAN标签
- MAC地址检查:检查目标MAC地址
- 优化处理:根据标志进行优化处理(如跳过IP校验和验证)
ethernet-input节点的分发逻辑(简化示例):
ethernet-input节点
│
├── EtherType == 0x0800 (IPv4)
│ └── → ip4-input或ip4-input-no-checksum
│
├── EtherType == 0x86DD (IPv6)
│ └── → ip6-input
│
├── EtherType == 0x8847/0x8848 (MPLS)
│ └── → mpls-input
│
└── 其他EtherType
└── → L2处理或其他节点
关键设计要点:
- 默认选择 :
ethernet-input是所有设备输入节点的默认下一跳 - 通用处理:适合大多数以太网数据包的处理
- 可优化:可以通过标志和配置进行优化(如跳过校验和验证)
通俗理解:
就像"快递分拣中心"的"默认分拣区":
- 默认路由:所有"包裹"默认先发送到"以太网分拣区"(ethernet-input)
- 类型识别:根据"包裹标签"(EtherType)识别"包裹类型"(IP、IPv6、MPLS等)
- 分发处理:将"包裹"分发到"相应的处理区"(ip4-input、ip6-input等)
14.3 ethernet-input的优化标志(ETH_INPUT_FRAME_F_IP4_CKSUM_OK)
作用和实现原理:
ETH_INPUT_FRAME_F_IP4_CKSUM_OK是传递给ethernet-input节点的帧标志,用于指示帧中的所有IPv4数据包的IP校验和都是正确的。当设置了这个标志时,ethernet-input节点可以将IPv4数据包直接发送到ip4-input-no-checksum节点,跳过IP校验和验证,提高处理性能。就像"快递分拣中心"的"快速通道标记",表示"包裹"已经"检查通过",可以直接进入"快速通道",无需"再次检查"。
标志定义 (src/vnet/ethernet/ethernet.h:54):
c
/* ethernet-input帧标志定义 */
#define ETH_INPUT_FRAME_F_SINGLE_SW_IF_IDX (1 << 0) // 帧中所有数据包共享同一个sw_if_index
#define ETH_INPUT_FRAME_F_IP4_CKSUM_OK (1 << 1) // 帧中所有IPv4数据包的IP校验和都是正确的
标志的设置 (src/plugins/dpdk/device/node.c:471):
c
/* 在dpdk_device_input函数中,设置ethernet-input优化标志 */
if (PREDICT_TRUE (next_index == VNET_DEVICE_INPUT_NEXT_ETHERNET_INPUT))
{
/* 获取下一帧的运行时结构 */
vlib_next_frame_t *nf;
vlib_frame_t *f;
ethernet_input_frame_t *ef;
nf = vlib_node_runtime_get_next_frame (vm, node, next_index);
f = vlib_get_frame (vm, nf->frame);
/* 设置帧标志:所有数据包共享同一个sw_if_index */
f->flags = ETH_INPUT_FRAME_F_SINGLE_SW_IF_IDX;
/* 设置帧的标量参数(sw_if_index和hw_if_index) */
ef = vlib_frame_scalar_args (f);
ef->sw_if_index = xd->sw_if_index;
ef->hw_if_index = xd->hw_if_index;
/* 如果PMD支持IP4校验和校验,并且没有数据包被标记为IP校验和错误
* 则可以通知ethernet-input节点,使其可以直接将IPv4数据包发送到
* ip4-input-no-checksum节点,跳过软件IP校验和验证
*/
if (xd->flags & DPDK_DEVICE_FLAG_RX_IP4_CKSUM && // 设备支持IP4校验和卸载
(or_flags & RTE_MBUF_F_RX_IP_CKSUM_BAD) == 0) // 没有IP校验和错误的数据包
f->flags |= ETH_INPUT_FRAME_F_IP4_CKSUM_OK; // 设置优化标志
vlib_frame_no_append (f);
}
标志在ethernet-input中的使用 (src/vnet/ethernet/node.c:1733):
c
/* 在ethernet_input_node函数中,检查标志 */
if (frame->flags & ETH_INPUT_FRAME_F_SINGLE_SW_IF_IDX)
{
ethernet_input_frame_t *ef = vlib_frame_scalar_args (frame);
/* 检查IP4校验和优化标志 */
int ip4_cksum_ok = (frame->flags & ETH_INPUT_FRAME_F_IP4_CKSUM_OK) != 0;
vnet_hw_interface_t *hi = vnet_get_hw_interface (vnm, ef->hw_if_index);
/* 传入ip4_cksum_ok标志,用于优化IPv4处理 */
eth_input_single_int (vm, node, hi, from, n_packets, ip4_cksum_ok);
}
优化效果 (src/vnet/ethernet/node.c:931):
c
/* 在eth_input_process_frame函数中,使用ip4_cksum_ok进行优化 */
next_ip4 = em->l3_next.input_next_ip4; // 默认:ip4-input节点
/* 如果设置了ip4_cksum_ok标志,且默认下一跳是ip4-input
* 则将下一跳改为ip4-input-no-checksum,跳过IP校验和验证
*/
if (next_ip4 == ETHERNET_INPUT_NEXT_IP4_INPUT && ip4_cksum_ok)
next_ip4 = ETHERNET_INPUT_NEXT_IP4_INPUT_NCS; // 使用ip4-input-no-checksum节点
性能优化效果:
- 跳过校验和验证 :
ip4-input-no-checksum节点不验证IP校验和,节省CPU周期 - 减少节点跳转 :直接从
ethernet-input跳转到ip4-input-no-checksum,减少中间处理 - 批量优化:标志适用于整个帧的所有数据包,实现批量优化
关键设计要点:
- 条件设置:只有当设备支持IP4校验和卸载且没有校验和错误时才设置
- 帧级别标志:标志适用于整个帧的所有数据包,不是单个数据包级别
- 性能优化:通过标志实现快速路径,跳过不必要的校验和验证
通俗理解:
就像"快递分拣中心"的"快速通道标记":
- 检查条件:只有当"检查设备支持自动检查"(设备支持IP4校验和卸载)且"所有包裹都检查通过"(没有校验和错误)时,才设置"快速通道标记"(ETH_INPUT_FRAME_F_IP4_CKSUM_OK)
- 快速通道:设置了"标记"的"包裹"可以直接进入"快速通道"(ip4-input-no-checksum),无需"再次检查"(跳过IP校验和验证)
- 批量优化:一个"标记"适用于整个"批次"(帧)的所有"包裹",实现批量优化
14.4 通过标志优化ethernet-input到ip4-input-no-checksum的跳转
作用和实现原理:
当设置了ETH_INPUT_FRAME_F_IP4_CKSUM_OK标志时,ethernet-input节点会将IPv4数据包直接发送到ip4-input-no-checksum节点,而不是ip4-input节点。ip4-input-no-checksum节点与ip4-input节点功能相同,但不验证IP校验和,因此性能更高。这个优化通过标志传递实现,避免了软件IP校验和验证的开销。就像"快递分拣中心"的"快速通道优化",根据"快速通道标记",直接将"包裹"发送到"快速通道"(ip4-input-no-checksum),跳过"检查环节"(IP校验和验证)。
优化跳转代码 (src/vnet/ethernet/node.c:931):
c
/* 在eth_input_process_frame函数中,根据ip4_cksum_ok标志优化下一跳 */
/* 步骤1:获取默认的IPv4下一跳 */
next_ip4 = em->l3_next.input_next_ip4; // 默认通常是ip4-input节点
/* 步骤2:如果设置了ip4_cksum_ok标志,且默认下一跳是ip4-input,则优化为ip4-input-no-checksum */
if (next_ip4 == ETHERNET_INPUT_NEXT_IP4_INPUT && ip4_cksum_ok)
{
/* 使用ip4-input-no-checksum节点,跳过IP校验和验证 */
next_ip4 = ETHERNET_INPUT_NEXT_IP4_INPUT_NCS;
}
/* 步骤3:后续处理中,IPv4数据包会使用优化后的next_ip4 */
ip4-input vs ip4-input-no-checksum:
c
/* ip4-input节点:验证IP校验和 */
VLIB_NODE_FN (ip4_input_node) (vlib_main_t * vm, vlib_node_runtime_t * node,
vlib_frame_t * frame)
{
/* 调用ip4_input_inline,verify_checksum参数为1(验证校验和) */
return ip4_input_inline (vm, node, frame, /* verify_checksum */ 1);
}
/* ip4-input-no-checksum节点:不验证IP校验和 */
VLIB_NODE_FN (ip4_input_no_checksum_node) (vlib_main_t * vm,
vlib_node_runtime_t * node,
vlib_frame_t * frame)
{
/* 调用ip4_input_inline,verify_checksum参数为0(不验证校验和) */
return ip4_input_inline (vm, node, frame, /* verify_checksum */ 0);
}
优化流程:
DPDK接收数据包
│
▼
检查硬件IP校验和
│
├── 设备支持IP4校验和卸载 AND 没有校验和错误
│ └── 设置ETH_INPUT_FRAME_F_IP4_CKSUM_OK标志
│
▼
发送到ethernet-input节点(带标志)
│
▼
ethernet-input检查标志
│
├── 如果设置了ETH_INPUT_FRAME_F_IP4_CKSUM_OK
│ └── 将IPv4数据包发送到ip4-input-no-checksum(跳过校验和验证)
└── 否则
└── 将IPv4数据包发送到ip4-input(验证校验和)
性能对比:
未优化路径:
dpdk-input → ethernet-input → ip4-input
↓
验证IP校验和(CPU开销)
优化路径:
dpdk-input → ethernet-input → ip4-input-no-checksum
↓
跳过IP校验和验证(零开销)
性能提升:
- 节省CPU周期:每个IPv4数据包节省一次IP校验和计算
- 提高吞吐量:减少处理时间,提高数据包处理速度
关键设计要点:
- 硬件加速:利用硬件IP校验和卸载,避免软件校验
- 标志传递:通过帧标志将优化信息传递给下一节点
- 条件优化:只有当硬件校验通过时才进行优化
- 安全性:硬件校验和错误的数据包仍然会被正确处理(发送到ip4-input验证)
通俗理解:
就像"快递分拣中心"的"快速通道优化":
- 硬件检查:网卡硬件"自动检查包裹完整性"(IP校验和),如果"检查通过"设置"快速通道标记"
- 快速通道:带有"标记"的"包裹"可以直接进入"快速通道"(ip4-input-no-checksum),跳过"再次检查"
- 性能提升:通过"跳过检查",减少"处理时间",提高"处理速度"
14.5 Feature Arc的处理(vnet_feature_start_device_input)
作用和实现原理:
Feature Arc(特性弧)是VPP的可扩展处理机制,允许在数据包处理流程中插入自定义的处理节点(feature)。vnet_feature_start_device_input函数用于启动device-input特性弧,查找接口配置的feature节点,并确定下一跳节点。如果接口配置了Feature Arc,该函数会修改next_index,将数据包路由到第一个feature节点。就像"快递分拣中心"的"特殊处理流程",根据"接口配置"(Feature Arc),将"包裹"路由到"特殊处理区"(feature节点)。
Feature Arc处理代码 (src/plugins/dpdk/device/node.c:398):
c
/* 在dpdk_device_input函数中,处理Feature Arc */
/* 步骤1:检查接口是否配置了Feature Arc */
if (PREDICT_FALSE (vnet_device_input_have_features (xd->sw_if_index)))
{
/* 接口配置了Feature Arc,启动特性弧处理 */
/* 该函数会查找接口配置的第一个feature节点,并修改next_index */
vnet_feature_start_device_input (xd->sw_if_index, &next_index, bt);
}
vnet_feature_start_device_input函数 (src/vnet/feature/feature.h:346):
c
/**
* 启动device-input特性弧处理
*
* @param sw_if_index 接口的软件索引
* @param next0 输入输出参数:下一跳节点索引(可能被修改)
* @param b0 buffer模板指针(用于设置feature arc信息)
*/
static_always_inline void
vnet_feature_start_device_input (u32 sw_if_index, // 接口索引
u32 *next0, // 下一跳节点索引(输入输出参数)
vlib_buffer_t *b0) // buffer模板
{
vnet_feature_main_t *fm = &feature_main;
vnet_feature_config_main_t *cm;
u8 feature_arc_index = fm->device_input_feature_arc_index; // device-input特性弧索引
cm = &fm->feature_config_mains[feature_arc_index];
/* 检查接口是否在特性弧的位图中(表示接口配置了feature) */
if (PREDICT_FALSE
(clib_bitmap_get
(fm->sw_if_index_has_features[feature_arc_index], sw_if_index)))
{
/* 接口配置了Feature Arc,设置buffer的feature arc信息 */
vnet_buffer (b0)->feature_arc_index = feature_arc_index; // 设置特性弧索引
b0->current_config_index =
vec_elt (cm->config_index_by_sw_if_index, sw_if_index); // 设置配置索引
/* 获取第一个feature节点的下一跳索引 */
vnet_get_config_data (&cm->config_main, &b0->current_config_index,
next0, /* # bytes of config data */ 0);
/* next0现在指向第一个feature节点的索引 */
}
}
vnet_device_input_have_features函数 (src/vnet/feature/feature.h:339):
c
/**
* 检查接口是否配置了device-input特性弧的feature
*
* @param sw_if_index 接口的软件索引
*
* @return 如果接口配置了feature返回非0,否则返回0
*/
static_always_inline int
vnet_device_input_have_features (u32 sw_if_index)
{
vnet_feature_main_t *fm = &feature_main;
return vnet_have_features (fm->device_input_feature_arc_index, sw_if_index);
}
Feature Arc处理流程:
检查接口是否配置了Feature Arc
│
├── 如果配置了:
│ ├── 设置buffer模板的feature_arc_index
│ ├── 设置buffer模板的current_config_index
│ └── 查找第一个feature节点,修改next_index
│
└── 如果未配置:
└── 保持next_index不变(使用默认或per_interface_next_index)
Feature Arc的使用场景:
- ACL处理:在数据包处理流程中插入ACL检查
- QoS处理:在数据包处理流程中插入QoS标记
- 统计监控:在数据包处理流程中插入统计节点
- 安全检测:在数据包处理流程中插入安全检测节点
关键设计要点:
- 条件处理:只有当接口配置了Feature Arc时才处理
- 模板设置:在buffer模板中设置feature arc信息,所有数据包继承
- 灵活扩展:通过Feature Arc机制实现处理流程的可扩展性
通俗理解:
就像"快递分拣中心"的"特殊处理流程":
- 检查配置:检查"接口"是否配置了"特殊处理流程"(Feature Arc)
- 设置信息:如果配置了,在"包裹模板"中设置"特殊处理流程信息"(feature_arc_index、current_config_index)
- 路由到第一个处理区:查找"第一个特殊处理区"(第一个feature节点),将"包裹"路由到那里
14.6 每接口下一跳索引(per_interface_next_index)重定向机制
作用和实现原理:
每接口下一跳索引(per_interface_next_index)重定向机制允许为每个接口配置一个固定的下一跳节点,覆盖默认的下一跳选择。这个机制用于特殊场景,例如直接将数据包发送到特定的处理节点(如IP处理节点),跳过ethernet-input节点。就像"快递分拣中心"的"接口特殊路由",为"特定窗口"(接口)配置"固定目的地"(per_interface_next_index),所有"包裹"都发送到那里。
per_interface_next_index的定义 (src/plugins/dpdk/device/dpdk.h:179):
c
/* 在dpdk_device_t结构体中 */
typedef struct
{
/* ... 其他字段 ... */
/* next node index if we decide to steal the rx graph arc */
u32 per_interface_next_index; // 每接口下一跳索引(~0表示未配置)
/* ... 其他字段 ... */
} dpdk_device_t;
per_interface_next_index的检查 (src/plugins/dpdk/device/node.c:393):
c
/* 在dpdk_device_input函数中,检查每接口重定向 */
/* 步骤1:初始化默认下一跳 */
u32 next_index = VNET_DEVICE_INPUT_NEXT_ETHERNET_INPUT; // 默认:ethernet-input
/* 步骤2:检查是否配置了每接口重定向 */
if (PREDICT_FALSE (xd->per_interface_next_index != ~0))
{
/* 接口配置了per_interface_next_index,使用该值覆盖默认下一跳 */
next_index = xd->per_interface_next_index;
}
/* 注意:per_interface_next_index的优先级高于Feature Arc
* 如果设置了per_interface_next_index,会跳过Feature Arc处理
*/
per_interface_next_index的初始化 (src/plugins/dpdk/device/init.c:376):
c
/* 在设备初始化时,将per_interface_next_index初始化为~0(未配置) */
xd->per_interface_next_index = ~0; // ~0表示未配置,使用默认下一跳
per_interface_next_index的设置 (src/plugins/dpdk/device/device.c:511):
c
/* 可以通过API或CLI设置per_interface_next_index */
/* 例如:将接口的下一跳设置为ip4-input节点 */
xd->per_interface_next_index = node_index; // node_index是目标节点的索引
使用场景:
- 直接IP处理:对于TUN接口,直接将数据包发送到IP处理节点,跳过以太网处理
- 特殊路由:为特定接口配置特殊的处理路径
- 性能优化:跳过不必要的处理节点,直接路由到目标节点
关键设计要点:
- 优先级高 :
per_interface_next_index的优先级高于Feature Arc和默认下一跳 - 接口级别:每个接口可以独立配置,不影响其他接口
- 条件检查 :只有当
per_interface_next_index != ~0时才使用
通俗理解:
就像"快递分拣中心"的"接口特殊路由":
- 接口配置:为"特定窗口"(接口)配置"固定目的地"(per_interface_next_index)
- 优先级高:如果配置了"固定目的地",所有"包裹"都发送到那里,忽略"默认目的地"
- 特殊用途:用于"特殊场景",例如"直接发送到IP处理区",跳过"以太网分拣区"
14.7 Flow Offload的每包下一跳选择(ptd->next数组)
作用和实现原理:
Flow Offload支持为每个数据包选择不同的下一跳节点。当数据包匹配了Flow Director规则时,可以在流表项中为每个流配置不同的下一跳节点。VPP使用ptd->next数组存储每个数据包的下一跳索引,支持批量处理多个数据包,每个数据包可能有不同的下一跳。就像"快递分拣中心"的"个性化路由",根据"包裹的匹配规则"(Flow Offload),为每个"包裹"设置"不同的目的地"(下一跳)。
ptd->next数组的使用 (src/plugins/dpdk/device/node.c:426):
c
/* 在dpdk_device_input函数中,处理Flow Offload的每包下一跳选择 */
if (PREDICT_FALSE (or_flags & RTE_MBUF_F_RX_FDIR))
{
/* 至少有一个数据包匹配了Flow Director规则 */
/* 步骤1:初始化所有数据包的下一跳为默认值 */
/* 因为Flow Offload可能会为每个数据包设置不同的下一跳 */
for (n = 0; n < n_rx_packets; n++)
ptd->next[n] = next_index; // 初始化为当前的next_index(可能是默认、per_interface或Feature Arc确定的)
/* 步骤2:如果启用了Flow Offload,处理流表匹配 */
if (PREDICT_FALSE ((xd->flags & DPDK_DEVICE_FLAG_RX_FLOW_OFFLOAD) &&
(or_flags & RTE_MBUF_F_RX_FDIR)))
{
/* 处理Flow Offload:查找流表项,为每个数据包设置下一跳 */
/* dpdk_process_flow_offload可能会修改ptd->next数组中的值 */
dpdk_process_flow_offload (xd, ptd, n_rx_packets);
}
/* 步骤3:使用ptd->next数组将数据包入队到不同的下一跳节点 */
vlib_get_buffer_indices_with_offset (vm, (void **) ptd->mbufs,
ptd->buffers, n_rx_packets,
sizeof (struct rte_mbuf));
vlib_buffer_enqueue_to_next (vm, node, ptd->buffers, ptd->next,
n_rx_packets);
}
dpdk_process_flow_offload中的下一跳设置 (src/plugins/dpdk/device/node.c:266):
c
/* 在dpdk_process_flow_offload函数中,为每个匹配的数据包设置下一跳 */
if (fle->next_index != (u16) ~ 0)
{
/* 流表项中配置了下一跳节点,使用该值 */
ptd->next[n] = fle->next_index; // 为第n个数据包设置下一跳
}
/* 如果fle->next_index == ~0,表示未配置,使用初始化的next_index */
每包下一跳选择示例:
假设有5个数据包,其中3个匹配了Flow Director规则:
初始化:
ptd->next[0] = next_index // 默认值
ptd->next[1] = next_index // 默认值
ptd->next[2] = next_index // 默认值
ptd->next[3] = next_index // 默认值
ptd->next[4] = next_index // 默认值
Flow Offload处理:
数据包0:未匹配 → ptd->next[0]保持不变 = next_index
数据包1:匹配流ID=5,fle->next_index = ip4-input → ptd->next[1] = ip4-input
数据包2:匹配流ID=8,fle->next_index = ip6-input → ptd->next[2] = ip6-input
数据包3:未匹配 → ptd->next[3]保持不变 = next_index
数据包4:匹配流ID=5,fle->next_index = ip4-input → ptd->next[4] = ip4-input
最终结果:
数据包0 → next_index
数据包1 → ip4-input
数据包2 → ip6-input
数据包3 → next_index
数据包4 → ip4-input
关键设计要点:
- 每包选择:每个数据包可以有独立的下一跳节点
- 初始化:所有数据包初始化为同一个next_index
- 流表覆盖:Flow Offload可以根据流表项为特定数据包设置不同的下一跳
- 批量处理 :使用
ptd->next数组支持批量处理多个数据包
通俗理解:
就像"快递分拣中心"的"个性化路由":
- 初始化:所有"包裹"初始设置为"同一个目的地"(next_index)
- 规则匹配:根据"包裹的匹配规则"(Flow Director),查找"规则表"(流表)
- 个性化设置:如果"规则"中配置了"特殊目的地"(next_index),为该"包裹"设置"特殊目的地"
- 批量处理:使用"数组"(ptd->next)存储每个"包裹"的"目的地",支持批量处理
14.8 数据包到下一节点的入队机制
作用和实现原理:
数据包到下一节点的入队机制负责将处理完的数据包发送到下一个处理节点。VPP提供了两种入队机制:单一下一跳入队(所有数据包使用同一个下一跳)和多下一跳入队(每个数据包可能有不同的下一跳)。单一下一跳入队使用vlib_get_new_next_frame和vlib_put_next_frame,多下一跳入队使用vlib_buffer_enqueue_to_next。就像"快递分拣中心"的"包裹发送",支持"所有包裹同一个目的地"和"每个包裹不同目的地"两种发送方式。
单一下一跳入队 (src/plugins/dpdk/device/node.c:450):
c
/* 在dpdk_device_input函数中,单一下一跳入队(else分支) */
else
{
u32 *to_next, n_left_to_next;
/* 步骤1:获取下一跳节点的frame指针 */
/* vlib_get_new_next_frame获取下一个节点的frame,用于存储数据包索引 */
vlib_get_new_next_frame (vm, node, next_index, to_next, n_left_to_next);
/* 步骤2:将mbuf转换为buffer索引,并复制到to_next数组中 */
vlib_get_buffer_indices_with_offset (vm, (void **) ptd->mbufs, to_next,
n_rx_packets,
sizeof (struct rte_mbuf));
/* 步骤3:如果下一跳是ethernet-input节点,进行特殊处理 */
if (PREDICT_TRUE (next_index == VNET_DEVICE_INPUT_NEXT_ETHERNET_INPUT))
{
/* 设置ethernet-input的帧标志和标量参数 */
vlib_next_frame_t *nf;
vlib_frame_t *f;
ethernet_input_frame_t *ef;
nf = vlib_node_runtime_get_next_frame (vm, node, next_index);
f = vlib_get_frame (vm, nf->frame);
f->flags = ETH_INPUT_FRAME_F_SINGLE_SW_IF_IDX;
ef = vlib_frame_scalar_args (f);
ef->sw_if_index = xd->sw_if_index;
ef->hw_if_index = xd->hw_if_index;
/* 设置IP4校验和优化标志(如果支持) */
if (xd->flags & DPDK_DEVICE_FLAG_RX_IP4_CKSUM &&
(or_flags & RTE_MBUF_F_RX_IP_CKSUM_BAD) == 0)
f->flags |= ETH_INPUT_FRAME_F_IP4_CKSUM_OK;
vlib_frame_no_append (f);
}
/* 步骤4:更新frame的剩余空间,并释放frame */
n_left_to_next -= n_rx_packets;
vlib_put_next_frame (vm, node, next_index, n_left_to_next);
single_next = 1; // 标记为单一下一跳
}
多下一跳入队 (src/plugins/dpdk/device/node.c:443):
c
/* 在dpdk_device_input函数中,多下一跳入队(Flow Offload分支) */
if (PREDICT_FALSE (or_flags & RTE_MBUF_F_RX_FDIR))
{
/* ... 前面的代码:Flow Offload处理 ... */
/* 步骤1:将mbuf转换为buffer索引 */
vlib_get_buffer_indices_with_offset (vm, (void **) ptd->mbufs,
ptd->buffers, n_rx_packets,
sizeof (struct rte_mbuf));
/* 步骤2:使用ptd->next数组将数据包入队到不同的下一跳节点 */
/* vlib_buffer_enqueue_to_next会根据ptd->next数组将数据包分发到不同的节点 */
vlib_buffer_enqueue_to_next (vm, node, ptd->buffers, ptd->next,
n_rx_packets);
}
vlib_buffer_enqueue_to_next函数 (src/vlib/buffer_funcs.c:93):
c
/**
* 将数据包入队到多个下一跳节点
*
* @param vm VPP主结构体指针
* @param node 当前节点运行时结构体指针
* @param buffers buffer索引数组
* @param nexts 下一跳节点索引数组(每个数据包一个)
* @param count 数据包数量
*/
static_always_inline void
vlib_buffer_enqueue_to_next_fn_inline (vlib_main_t *vm,
vlib_node_runtime_t *node,
u32 *buffers,
u32 *aux_data,
u16 *nexts,
uword count,
u8 maybe_aux)
{
/* 该函数会根据nexts数组将数据包分组,相同下一跳的数据包放在一起
* 然后为每个下一跳节点创建一个frame,并将数据包索引放入frame
* 最后将frame发送到对应的下一跳节点
*/
/* 实现细节:
* 1. 使用位图(bitmap)跟踪哪些数据包已经入队
* 2. 按照下一跳节点分组数据包
* 3. 为每个下一跳节点创建frame并入队数据包
* 4. 释放frame到下一跳节点
*/
}
入队机制对比:
| 特性 | 单一下一跳入队 | 多下一跳入队 |
|---|---|---|
| 使用场景 | 所有数据包使用同一个下一跳 | 每个数据包可能有不同的下一跳 |
| 函数 | vlib_get_new_next_frame + vlib_put_next_frame |
vlib_buffer_enqueue_to_next |
| 数据结构 | next_index(单个值) |
ptd->next(数组) |
| 性能 | 更高(直接入队) | 稍低(需要分组) |
| 灵活性 | 低(所有数据包相同) | 高(每个数据包可不同) |
关键设计要点:
- 条件选择:根据是否有Flow Offload选择不同的入队机制
- 性能优化:单一下一跳入队性能更高,优先使用
- 灵活性:多下一跳入队支持每个数据包不同的下一跳
通俗理解:
就像"快递分拣中心"的"包裹发送":
- 统一发送(单一下一跳):所有"包裹"发送到"同一个目的地",使用"统一发送方式"(vlib_get_new_next_frame),效率更高
- 个性化发送(多下一跳):每个"包裹"可能有"不同的目的地",使用"个性化发送方式"(vlib_buffer_enqueue_to_next),更灵活
- 自动选择:根据"是否有个性化路由"(Flow Offload)自动选择"发送方式"
14.9 单一下一跳vs多下一跳的处理(single_next标志)
作用和实现原理:
VPP使用single_next标志来区分单一下一跳和多下一跳的处理。当所有数据包使用同一个下一跳时,设置single_next = 1,使用单一下一跳入队机制;当每个数据包可能有不同的下一跳时,设置single_next = 0(或未设置),使用多下一跳入队机制。这个标志主要用于后续的数据包跟踪和调试。就像"快递分拣中心"的"发送方式标记",标记"所有包裹是否同一个目的地"。
single_next标志的设置 (src/plugins/dpdk/device/node.c:356):
c
/* 在dpdk_device_input函数中,声明single_next标志 */
int single_next = 0; // 初始化为0(多下一跳)
/* 在单一下一跳入队后,设置single_next = 1 */
else
{
/* ... 单一下一跳入队代码 ... */
single_next = 1; // 标记为单一下一跳
}
/* 注意:在Flow Offload分支(多下一跳)中,single_next保持为0 */
single_next标志的使用 (src/plugins/dpdk/device/node.c:484):
c
/* 在数据包跟踪处理中,使用single_next标志 */
if (PREDICT_FALSE ((n_trace = vlib_get_trace_count (vm, node))))
{
/* 如果是单一下一跳,需要重新获取buffer索引(因为之前没有存储) */
if (single_next)
vlib_get_buffer_indices_with_offset (vm, (void **) ptd->mbufs,
ptd->buffers, n_rx_packets,
sizeof (struct rte_mbuf));
/* ... 数据包跟踪处理 ... */
}
处理流程对比:
单一下一跳处理流程:
1. 初始化next_index = VNET_DEVICE_INPUT_NEXT_ETHERNET_INPUT
2. 检查per_interface_next_index(可能修改next_index)
3. 处理Feature Arc(可能修改next_index)
4. 处理数据包
5. 检查Flow Offload:
- 如果没有Flow Offload → 使用next_index(所有数据包相同)
- 使用vlib_get_new_next_frame获取frame
- 将所有数据包入队到同一个frame
- 设置single_next = 1
多下一跳处理流程:
1. 初始化next_index = VNET_DEVICE_INPUT_NEXT_ETHERNET_INPUT
2. 检查per_interface_next_index(可能修改next_index)
3. 处理Feature Arc(可能修改next_index)
4. 处理数据包
5. 检查Flow Offload:
- 如果有Flow Offload → 初始化ptd->next数组为next_index
- 调用dpdk_process_flow_offload(可能修改ptd->next数组)
- 使用vlib_buffer_enqueue_to_next,根据ptd->next数组分发
- single_next保持为0
关键设计要点:
- 标志区分 :使用
single_next标志区分两种处理模式 - 性能优化:单一下一跳模式性能更高,优先使用
- 灵活支持:多下一跳模式支持Flow Offload的每包路由
通俗理解:
就像"快递分拣中心"的"发送方式标记":
- 统一发送标记(single_next = 1):所有"包裹"发送到"同一个目的地",使用"统一发送方式"
- 个性化发送标记(single_next = 0):每个"包裹"可能有"不同的目的地",使用"个性化发送方式"
- 自动标记:根据"是否有个性化路由"自动设置"标记"
14.10 所有可能的下一跳节点
作用和实现原理:
设备输入节点支持多种下一跳节点,用于不同的处理场景。这些节点包括:错误处理节点(error-drop)、以太网处理节点(ethernet-input)、IP处理节点(ip4-input、ip4-input-no-checksum、ip6-input)、MPLS处理节点(mpls-input)等。不同的下一跳节点适用于不同的数据包类型和处理需求。就像"快递分拣中心"的"多个目标区域",根据"包裹类型"和"处理需求"选择"相应的区域"。
下一跳节点枚举定义 (src/vnet/devices/devices.h:22):
c
/**
* 设备输入节点的下一跳节点枚举
*/
typedef enum
{
/* IP处理节点 */
VNET_DEVICE_INPUT_NEXT_IP4_NCS_INPUT, // ip4-input-no-checksum节点(跳过IP校验和验证)
VNET_DEVICE_INPUT_NEXT_IP4_INPUT, // ip4-input节点(验证IP校验和)
VNET_DEVICE_INPUT_NEXT_IP6_INPUT, // ip6-input节点(IPv6处理)
/* MPLS处理节点 */
VNET_DEVICE_INPUT_NEXT_MPLS_INPUT, // mpls-input节点(MPLS处理)
/* 以太网处理节点(默认) */
VNET_DEVICE_INPUT_NEXT_ETHERNET_INPUT, // ethernet-input节点(以太网帧处理,默认下一跳)
/* 错误处理节点 */
VNET_DEVICE_INPUT_NEXT_DROP, // error-drop节点(丢弃数据包)
/* 隧道相关节点 */
VNET_DEVICE_INPUT_NEXT_IP4_DROP, // ip4-drop节点(IPv4丢弃)
VNET_DEVICE_INPUT_NEXT_IP6_DROP, // ip6-drop节点(IPv6丢弃)
VNET_DEVICE_INPUT_NEXT_PUNT, // punt-dispatch节点(发送到控制平面)
VNET_DEVICE_INPUT_N_NEXT_NODES, // 下一跳节点总数
} vnet_device_input_next_t;
下一跳节点名称映射 (src/vnet/devices/devices.h:39):
c
#define VNET_DEVICE_INPUT_NEXT_NODES
{
[VNET_DEVICE_INPUT_NEXT_DROP] = "error-drop", // 错误丢弃
[VNET_DEVICE_INPUT_NEXT_ETHERNET_INPUT] = "ethernet-input", // 以太网输入(默认)
[VNET_DEVICE_INPUT_NEXT_IP4_NCS_INPUT] = "ip4-input-no-checksum", // IPv4输入(无校验和)
[VNET_DEVICE_INPUT_NEXT_IP4_INPUT] = "ip4-input", // IPv4输入
[VNET_DEVICE_INPUT_NEXT_IP6_INPUT] = "ip6-input", // IPv6输入
[VNET_DEVICE_INPUT_NEXT_MPLS_INPUT] = "mpls-input", // MPLS输入
[VNET_DEVICE_INPUT_NEXT_IP4_DROP] = "ip4-drop", // IPv4丢弃
[VNET_DEVICE_INPUT_NEXT_IP6_DROP] = "ip6-drop", // IPv6丢弃
[VNET_DEVICE_INPUT_NEXT_PUNT] = "punt-dispatch", // 发送到控制平面
}
各节点的使用场景:
1. error-drop节点:
c
/* 用途:丢弃错误的数据包 */
/* 使用场景:数据包处理错误、不符合要求的数据包 */
VNET_DEVICE_INPUT_NEXT_DROP → "error-drop"
2. ethernet-input节点(默认):
c
/* 用途:处理以太网帧,根据EtherType分发到相应处理节点 */
/* 使用场景:大多数以太网数据包的标准处理路径 */
VNET_DEVICE_INPUT_NEXT_ETHERNET_INPUT → "ethernet-input"
3. ip4-input-no-checksum节点:
c
/* 用途:处理IPv4数据包,但不验证IP校验和(性能优化) */
/* 使用场景:硬件已经验证了IP校验和,可以跳过软件验证 */
VNET_DEVICE_INPUT_NEXT_IP4_NCS_INPUT → "ip4-input-no-checksum"
4. ip4-input节点:
c
/* 用途:处理IPv4数据包,并验证IP校验和 */
/* 使用场景:需要软件验证IP校验和的IPv4数据包 */
VNET_DEVICE_INPUT_NEXT_IP4_INPUT → "ip4-input"
5. ip6-input节点:
c
/* 用途:处理IPv6数据包 */
/* 使用场景:IPv6数据包的直接处理(跳过ethernet-input) */
VNET_DEVICE_INPUT_NEXT_IP6_INPUT → "ip6-input"
6. mpls-input节点:
c
/* 用途:处理MPLS数据包 */
/* 使用场景:MPLS数据包的直接处理(跳过ethernet-input) */
VNET_DEVICE_INPUT_NEXT_MPLS_INPUT → "mpls-input"
7. punt-dispatch节点:
c
/* 用途:将数据包发送到控制平面 */
/* 使用场景:需要控制平面处理的数据包(如ICMP、ARP等) */
VNET_DEVICE_INPUT_NEXT_PUNT → "punt-dispatch"
节点选择策略总结:
| 下一跳节点 | 使用场景 | 性能 | 特点 |
|---|---|---|---|
| ethernet-input | 默认,大多数以太网数据包 | 标准 | 通用处理,根据EtherType分发 |
| ip4-input-no-checksum | IP校验和已由硬件验证 | 高 | 跳过IP校验和验证,性能优化 |
| ip4-input | 需要软件验证IP校验和 | 标准 | 完整的IP校验和验证 |
| ip6-input | IPv6数据包直接处理 | 标准 | 跳过ethernet-input,直接IP处理 |
| mpls-input | MPLS数据包直接处理 | 标准 | 跳过ethernet-input,直接MPLS处理 |
| error-drop | 错误数据包 | N/A | 丢弃数据包 |
| punt-dispatch | 控制平面处理 | 低 | 发送到控制平面,性能开销大 |
关键设计要点:
- 默认选择 :
ethernet-input是默认下一跳,适合大多数场景 - 性能优化 :
ip4-input-no-checksum提供性能优化路径 - 灵活路由:支持直接路由到IP、IPv6、MPLS等节点,跳过中间处理
- 错误处理:支持错误丢弃和控制平面处理
通俗理解:
就像"快递分拣中心"的"多个目标区域":
- 默认区域(ethernet-input):大多数"包裹"先到这里,根据"类型"分发
- 快速通道(ip4-input-no-checksum):已经"检查通过"的"包裹"可以直接进入,无需"再次检查"
- 专用区域(ip6-input、mpls-input):特定类型的"包裹"可以直接进入,跳过"默认区域"
- 错误处理(error-drop):有问题的"包裹"会被丢弃
- 特殊处理(punt-dispatch):需要"人工处理"的"包裹"发送到"控制中心"
14.11 本章总结
数据包分发和下一跳选择核心流程总结:
| 步骤 | 机制 | 优先级 | 作用 |
|---|---|---|---|
| 1. 初始化 | 默认下一跳 | 最低 | 初始化为ethernet-input节点 |
| 2. 每接口重定向 | per_interface_next_index | 高 | 接口级别的下一跳覆盖 |
| 3. Feature Arc | vnet_feature_start_device_input | 中 | 特性弧确定的下一跳 |
| 4. Flow Offload | dpdk_process_flow_offload | 最高 | 每包下一跳选择(可能不同) |
| 5. 入队 | vlib_get_new_next_frame或vlib_buffer_enqueue_to_next | - | 将数据包发送到下一跳节点 |
关键性能优化技术:
- 快速路径优化 :通过
ETH_INPUT_FRAME_F_IP4_CKSUM_OK标志,直接跳转到ip4-input-no-checksum - 批量处理:单一下一跳入队支持批量处理,性能更高
- 硬件加速:利用硬件校验和卸载,跳过软件验证
相关源码文件:
src/plugins/dpdk/device/node.c:350- 默认下一跳设置src/plugins/dpdk/device/node.c:393- 每接口下一跳检查src/plugins/dpdk/device/node.c:398- Feature Arc处理src/plugins/dpdk/device/node.c:455- ethernet-input特殊处理src/plugins/dpdk/device/node.c:426- Flow Offload处理src/plugins/dpdk/device/node.c:443- 多下一跳入队src/plugins/dpdk/device/node.c:450- 单一下一跳入队src/vnet/devices/devices.h:22- 下一跳节点枚举定义src/vnet/feature/feature.h:346-vnet_feature_start_device_input函数src/vlib/buffer_funcs.c:93-vlib_buffer_enqueue_to_next函数
第15章:dpdk-output节点核心处理
本章概述:
第15章详细讲解DPDK插件中dpdk-output节点的核心处理机制。dpdk-output节点是VPP数据包发送路径上的关键节点,负责将数据包从VPP的数据结构转换为DPDK的mbuf结构,并通过DPDK的发送接口将数据包发送到网卡。本章将详细讲解节点的注册、主要处理函数、发送队列选择机制,以及Input和Output节点的协同工作。就像"快递发送中心"的"发送节点",负责将"已处理的包裹"(数据包)发送到"物理运输设备"(网卡)。
通俗理解:
就像"快递发送中心"的"发送节点":
- 节点注册:注册"发送节点"到"分拣系统"(VLIB图),使得"包裹"可以路由到这里
- 发送处理:将"已处理的包裹"(vlib_buffer)转换为"运输格式"(mbuf),然后"发送"到"物理设备"(网卡)
- 队列选择:根据"包裹信息"(数据包元数据)选择"发送队列"(TX queue)
- 协同工作:与"接收节点"(dpdk-input)协同工作,完成"包裹的完整处理流程"
15.1 dpdk-output节点的注册和类型
作用和实现原理:
dpdk-output节点是DPDK设备类的TX(发送)函数节点。当DPDK设备添加到VPP时,VPP会根据设备类注册信息自动创建对应的TX节点。节点类型是VLIB_NODE_TYPE_INTERNAL,标志是VLIB_NODE_FLAG_IS_OUTPUT,表示这是一个输出节点。节点名称格式为<interface-name>-tx,例如dpdk-0-tx。就像"快递发送中心"的"发送节点注册",为"每个发送窗口"(接口)注册"发送节点"到"分拣系统"。
设备类注册 (src/plugins/dpdk/device/device.c:726):
c
/**
* DPDK设备类定义
* 包含DPDK设备的所有操作函数和回调
*/
VNET_DEVICE_CLASS (dpdk_device_class) = {
.name = "dpdk", // 设备类名称
.tx_function_n_errors = DPDK_TX_FUNC_N_ERROR, // TX函数错误数量
.tx_function_error_strings = dpdk_tx_func_error_strings, // TX函数错误字符串
.format_device_name = format_dpdk_device_name, // 设备名称格式化函数
.format_device = format_dpdk_device, // 设备格式化函数
.format_tx_trace = format_dpdk_tx_trace, // TX跟踪格式化函数
.clear_counters = dpdk_clear_hw_interface_counters, // 清除计数器函数
.admin_up_down_function = dpdk_interface_admin_up_down, // 管理上/下函数
.subif_add_del_function = dpdk_subif_add_del_function, // 子接口添加/删除函数
.rx_redirect_to_node = dpdk_set_interface_next_node, // RX重定向到节点函数
.mac_addr_change_function = dpdk_set_mac_address, // MAC地址更改函数
.mac_addr_add_del_function = dpdk_add_del_mac_address, // MAC地址添加/删除函数
.format_flow = format_dpdk_flow, // 流格式化函数
.flow_ops_function = dpdk_flow_ops_fn, // 流操作函数
.set_rss_queues_function = dpdk_interface_set_rss_queues, // 设置RSS队列函数
.rx_mode_change_function = dpdk_interface_rx_mode_change, // RX模式更改函数
};
节点注册过程 (src/vnet/interface.c:989):
c
/* 当硬件接口添加时,VPP会自动创建TX节点 */
vlib_node_registration_t r;
vnet_interface_output_runtime_t rt = {
.hw_if_index = hw_index, // 硬件接口索引
.sw_if_index = hw->sw_if_index, // 软件接口索引
.dev_instance = hw->dev_instance, // 设备实例索引
.is_deleted = 0, // 是否已删除标志
};
clib_memset (&r, 0, sizeof (r));
r.type = VLIB_NODE_TYPE_INTERNAL; // 节点类型:内部节点
r.runtime_data = &rt; // 运行时数据
r.runtime_data_bytes = sizeof (rt); // 运行时数据大小
r.scalar_size = sizeof (vnet_hw_if_tx_frame_t); // 标量参数大小
r.vector_size = sizeof (u32); // 向量参数大小(buffer索引)
r.flags = VLIB_NODE_FLAG_IS_OUTPUT; // 节点标志:输出节点
/* 使用设备类的TX函数注册信息 */
if (dev_class->tx_fn_registrations)
{
r.function = 0;
r.node_fn_registrations = dev_class->tx_fn_registrations;
}
else
r.function = dev_class->tx_function;
/* 注册节点,节点名称为"<interface-name>-tx" */
hw->tx_node_index = vlib_register_node (vm, &r, "%v-tx", hw->name);
VNET_DEVICE_CLASS_TX_FN宏 (src/vnet/interface.h:340):
c
/**
* 定义设备类TX函数的宏
* 该宏用于定义多架构支持的TX函数
*/
#define VNET_DEVICE_CLASS_TX_FN(devclass) \
uword CLIB_MARCH_SFX (devclass##_tx_fn) ( \
vlib_main_t *, vlib_node_runtime_t *, vlib_frame_t *); \
static vlib_node_fn_registration_t CLIB_MARCH_SFX ( \
devclass##_tx_fn_registration) = { \
.function = &CLIB_MARCH_SFX (devclass##_tx_fn), \
}; \
static void __clib_constructor CLIB_MARCH_SFX ( \
devclass##_tx_fn_multiarch_register) (void) \
{ \
extern vnet_device_class_t devclass; \
vlib_node_fn_registration_t *r; \
r = &CLIB_MARCH_SFX (devclass##_tx_fn_registration); \
r->march_variant = CLIB_MARCH_SFX (CLIB_MARCH_VARIANT_TYPE); \
r->next_registration = devclass.tx_fn_registrations; \
devclass.tx_fn_registrations = r; \
} \
uword CLIB_MARCH_SFX (devclass##_tx_fn)
节点类型和标志:
- VLIB_NODE_TYPE_INTERNAL:内部节点类型,表示这是VPP内部的节点
- VLIB_NODE_FLAG_IS_OUTPUT:输出节点标志,表示这是输出节点,数据包会离开VPP
关键设计要点:
- 自动注册:节点在硬件接口添加时自动注册,无需手动创建
- 多架构支持 :通过
VNET_DEVICE_CLASS_TX_FN宏支持多架构优化版本 - 运行时数据:节点运行时数据包含设备实例索引,用于查找对应的DPDK设备
通俗理解:
就像"快递发送中心"的"发送节点注册":
- 自动注册:当"新窗口"(硬件接口)添加时,自动为"窗口"注册"发送节点"
- 节点标识:每个"发送节点"有"唯一名称"(例如dpdk-0-tx),用于"分拣系统"识别
- 设备关联:节点与"物理设备"(DPDK设备)关联,通过"设备实例索引"查找设备信息
15.2 节点的主要处理函数(VNET_DEVICE_CLASS_TX_FN)
作用和实现原理:
VNET_DEVICE_CLASS_TX_FN是dpdk-output节点的主要处理函数。该函数接收包含数据包索引的frame,将buffer索引转换为mbuf指针,验证和设置mbuf字段,处理硬件卸载,然后通过rte_eth_tx_burst发送数据包。函数使用批量处理和预取优化,支持多段数据包和硬件卸载处理。就像"快递发送中心"的"发送处理函数",将"包裹索引"转换为"实际包裹",检查"包裹完整性",然后"发送"到"运输设备"。
函数签名 (src/plugins/dpdk/device/device.c:272):
c
/**
* DPDK设备类TX函数
*
* @param vm VPP主结构体指针
* @param node 节点运行时结构体指针
* @param f 包含数据包索引的frame
*
* @return 成功发送的数据包数量
*/
VNET_DEVICE_CLASS_TX_FN (dpdk_device_class) (vlib_main_t * vm,
vlib_node_runtime_t * node,
vlib_frame_t * f)
函数完整实现 (src/plugins/dpdk/device/device.c:272):
c
VNET_DEVICE_CLASS_TX_FN (dpdk_device_class) (vlib_main_t * vm,
vlib_node_runtime_t * node,
vlib_frame_t * f)
{
dpdk_main_t *dm = &dpdk_main;
/* 步骤1:获取设备信息 */
vnet_interface_output_runtime_t *rd = (void *) node->runtime_data;
dpdk_device_t *xd = vec_elt_at_index (dm->devices, rd->dev_instance);
/* 步骤2:获取帧的标量参数(队列ID和共享队列标志) */
vnet_hw_if_tx_frame_t *tf = vlib_frame_scalar_args (f);
u32 n_packets = f->n_vectors; // 数据包数量
u32 n_left;
u32 thread_index = vm->thread_index; // 当前线程索引
int queue_id = tf->queue_id; // 发送队列ID
u8 is_shared = tf->shared_queue; // 是否为共享队列
u32 tx_pkts = 0; // 成功发送的数据包数量
dpdk_per_thread_data_t *ptd = vec_elt_at_index (dm->per_thread_data,
thread_index);
struct rte_mbuf **mb; // mbuf指针数组
vlib_buffer_t *b[4]; // buffer指针数组(用于批量处理)
ASSERT (n_packets <= VLIB_FRAME_SIZE);
/* 步骤3:将buffer索引转换为mbuf指针 */
/* vlib_get_buffers_with_offset通过buffer索引获取buffer指针
* 然后通过负偏移量获取mbuf指针(因为mbuf在buffer前面) */
vlib_get_buffers_with_offset (vm, vlib_frame_vector_args (f),
(void **) ptd->mbufs, n_packets,
-(i32) sizeof (struct rte_mbuf));
n_left = n_packets;
mb = ptd->mbufs;
/* 步骤4:批量处理数据包(使用预取优化) */
#if (CLIB_N_PREFETCHES >= 8)
/* 如果支持8个预取槽位,使用8重循环 */
while (n_left >= 8)
{
u32 or_flags;
/* 预取后续数据包 */
dpdk_prefetch_buffer (vm, mb[4]);
dpdk_prefetch_buffer (vm, mb[5]);
dpdk_prefetch_buffer (vm, mb[6]);
dpdk_prefetch_buffer (vm, mb[7]);
/* 获取当前数据包的buffer指针 */
b[0] = vlib_buffer_from_rte_mbuf (mb[0]);
b[1] = vlib_buffer_from_rte_mbuf (mb[1]);
b[2] = vlib_buffer_from_rte_mbuf (mb[2]);
b[3] = vlib_buffer_from_rte_mbuf (mb[3]);
/* 检查是否有多段数据包 */
or_flags = b[0]->flags | b[1]->flags | b[2]->flags | b[3]->flags;
if (or_flags & VLIB_BUFFER_NEXT_PRESENT)
{
/* 有多段数据包,验证多段mbuf */
dpdk_validate_rte_mbuf (vm, b[0], 1);
dpdk_validate_rte_mbuf (vm, b[1], 1);
dpdk_validate_rte_mbuf (vm, b[2], 1);
dpdk_validate_rte_mbuf (vm, b[3], 1);
}
else
{
/* 单段数据包,验证单段mbuf */
dpdk_validate_rte_mbuf (vm, b[0], 0);
dpdk_validate_rte_mbuf (vm, b[1], 0);
dpdk_validate_rte_mbuf (vm, b[2], 0);
dpdk_validate_rte_mbuf (vm, b[3], 0);
}
/* 处理硬件卸载(如果启用) */
if (PREDICT_FALSE ((xd->flags & DPDK_DEVICE_FLAG_TX_OFFLOAD) &&
(or_flags & VNET_BUFFER_F_OFFLOAD)))
{
dpdk_buffer_tx_offload (xd, b[0], mb[0]);
dpdk_buffer_tx_offload (xd, b[1], mb[1]);
dpdk_buffer_tx_offload (xd, b[2], mb[2]);
dpdk_buffer_tx_offload (xd, b[3], mb[3]);
}
/* 数据包跟踪(如果启用) */
if (PREDICT_FALSE (node->flags & VLIB_NODE_FLAG_TRACE))
{
if (b[0]->flags & VLIB_BUFFER_IS_TRACED)
dpdk_tx_trace_buffer (dm, node, xd, queue_id, b[0]);
if (b[1]->flags & VLIB_BUFFER_IS_TRACED)
dpdk_tx_trace_buffer (dm, node, xd, queue_id, b[1]);
if (b[2]->flags & VLIB_BUFFER_IS_TRACED)
dpdk_tx_trace_buffer (dm, node, xd, queue_id, b[2]);
if (b[3]->flags & VLIB_BUFFER_IS_TRACED)
dpdk_tx_trace_buffer (dm, node, xd, queue_id, b[3]);
}
mb += 4;
n_left -= 4;
}
#elif (CLIB_N_PREFETCHES >= 4)
/* 如果支持4个预取槽位,使用4重循环 */
while (n_left >= 4)
{
/* 类似的批量处理逻辑 */
/* ... */
}
#endif
/* 步骤5:处理剩余的数据包(单个处理) */
while (n_left > 0)
{
b[0] = vlib_buffer_from_rte_mbuf (mb[0]);
/* 验证mbuf(假设可能是多段) */
dpdk_validate_rte_mbuf (vm, b[0], 1);
/* 处理硬件卸载(如果启用) */
dpdk_buffer_tx_offload (xd, b[0], mb[0]);
/* 数据包跟踪(如果启用) */
if (PREDICT_FALSE (node->flags & VLIB_NODE_FLAG_TRACE))
if (b[0]->flags & VLIB_BUFFER_IS_TRACED)
dpdk_tx_trace_buffer (dm, node, xd, queue_id, b[0]);
mb++;
n_left--;
}
/* 步骤6:批量发送数据包 */
tx_pkts = n_packets = mb - ptd->mbufs;
n_left = tx_burst_vector_internal (vm, xd, ptd->mbufs, n_packets, queue_id,
is_shared);
/* 步骤7:处理发送失败的数据包 */
{
/* 如果有未发送的数据包,更新统计并释放mbuf */
if (PREDICT_FALSE (n_left))
{
tx_pkts -= n_left;
vlib_simple_counter_main_t *cm;
vnet_main_t *vnm = vnet_get_main ();
/* 更新发送错误计数器 */
cm = vec_elt_at_index (vnm->interface_main.sw_if_counters,
VNET_INTERFACE_COUNTER_TX_ERROR);
vlib_increment_simple_counter (cm, thread_index, xd->sw_if_index,
n_left);
/* 记录错误 */
vlib_error_count (vm, node->node_index, DPDK_TX_FUNC_ERROR_PKT_DROP,
n_left);
/* 释放未发送的mbuf */
while (n_left--)
rte_pktmbuf_free (ptd->mbufs[n_packets - n_left - 1]);
}
}
return tx_pkts; // 返回成功发送的数据包数量
}
处理流程总结:
接收frame(包含buffer索引)
│
▼
获取设备信息和队列信息
│
▼
将buffer索引转换为mbuf指针
│
▼
批量处理数据包(使用预取优化)
│
├── 验证mbuf(单段或多段)
├── 处理硬件卸载(如果启用)
└── 数据包跟踪(如果启用)
│
▼
批量发送数据包(tx_burst_vector_internal)
│
├── 成功发送 → 更新统计
└── 发送失败 → 更新错误统计,释放mbuf
关键设计要点:
- 批量处理:使用批量处理循环(4重或8重),提高处理效率
- 预取优化:使用预取指令提前加载数据,减少内存访问延迟
- 条件处理:根据硬件卸载支持和数据包特征进行条件处理
- 错误处理:发送失败的数据包会被释放并更新错误统计
通俗理解:
就像"快递发送中心"的"发送处理函数":
- 接收包裹索引:接收"包裹索引列表"(frame中的buffer索引)
- 转换为实际包裹:将"包裹索引"转换为"实际包裹指针"(mbuf指针)
- 批量检查和处理:批量检查"包裹完整性"(验证mbuf),设置"运输信息"(硬件卸载)
- 批量发送:将"包裹"批量发送到"运输设备"(网卡)
- 错误处理:如果"发送失败",更新"错误统计"并"释放包裹"
15.3 发送队列的选择机制
作用和实现原理:
发送队列的选择机制决定了数据包应该发送到哪个TX队列。队列ID存储在frame的标量参数中(vnet_hw_if_tx_frame_t->queue_id),通常由interface-output节点或feature节点根据数据包的元数据和负载均衡策略选择。DPDK支持每个设备有多个TX队列,用于负载均衡和性能优化。就像"快递发送中心"的"队列选择",根据"包裹信息"和"负载均衡策略"选择"发送队列"。
队列ID的获取 (src/plugins/dpdk/device/device.c:279):
c
/* 在dpdk-output节点的主处理函数中,获取队列ID */
vnet_hw_if_tx_frame_t *tf = vlib_frame_scalar_args (f);
int queue_id = tf->queue_id; // 队列ID(由上游节点设置)
u8 is_shared = tf->shared_queue; // 是否为共享队列标志
vnet_hw_if_tx_frame_t结构 (src/vnet/interface.h):
c
/**
* 硬件接口TX帧的标量参数结构
*/
typedef struct
{
u32 queue_id; // 发送队列ID
u8 shared_queue; // 是否为共享队列(0=独占,1=共享)
} vnet_hw_if_tx_frame_t;
队列选择过程:
队列ID通常由interface-output节点或feature节点选择,选择策略包括:
- 基于线程的队列分配:每个线程绑定到特定的队列
- 基于流的负载均衡:根据数据包的流特征(如源IP、目标IP、源端口、目标端口)计算哈希值选择队列
- 基于接口配置:接口可以配置使用特定的队列
队列使用 (src/plugins/dpdk/device/device.c:423):
c
/* 在tx_burst_vector_internal函数中,使用队列ID发送数据包 */
n_left = tx_burst_vector_internal (vm, xd, ptd->mbufs, n_packets, queue_id,
is_shared);
tx_burst_vector_internal函数 (src/plugins/dpdk/device/device.c:156):
c
/**
* 批量发送数据包到指定的TX队列
*
* @param vm VPP主结构体指针
* @param xd DPDK设备结构体指针
* @param mb mbuf指针数组
* @param n_left 要发送的数据包数量
* @param queue_id 队列ID
* @param is_shared 是否为共享队列
*
* @return 未发送的数据包数量(0表示全部发送成功)
*/
static_always_inline u32
tx_burst_vector_internal (vlib_main_t *vm, dpdk_device_t *xd,
struct rte_mbuf **mb, u32 n_left, int queue_id,
u8 is_shared)
{
dpdk_tx_queue_t *txq;
u32 n_retry;
int n_sent = 0;
n_retry = 16; // 最大重试次数
txq = vec_elt_at_index (xd->tx_queues, queue_id); // 获取队列结构
do
{
/* 如果是共享队列,需要加锁 */
if (is_shared)
clib_spinlock_lock (&txq->lock);
/* 批量发送数据包 */
n_sent = rte_eth_tx_burst (xd->port_id, queue_id, mb, n_left);
/* 释放锁 */
if (is_shared)
clib_spinlock_unlock (&txq->lock);
n_retry--;
n_left -= n_sent; // 减去已发送的数量
mb += n_sent; // 移动mbuf指针
}
while (n_sent && n_left && (n_retry > 0)); // 继续发送直到全部发送或重试次数用完
return n_left; // 返回未发送的数量
}
队列类型:
- 独占队列 (
is_shared = 0):队列只能由一个线程访问,无需加锁 - 共享队列 (
is_shared = 1):队列可以由多个线程访问,需要加锁保护
关键设计要点:
- 上游选择:队列ID由上游节点(interface-output)选择,dpdk-output节点直接使用
- 负载均衡:多个队列支持负载均衡,提高发送性能
- 线程安全:共享队列使用锁保护,独占队列无需锁
通俗理解:
就像"快递发送中心"的"队列选择":
- 上游选择:由"上游节点"(interface-output)根据"包裹信息"和"负载均衡策略"选择"发送队列"
- 队列使用:dpdk-output节点直接使用选择的"队列ID",将"包裹"发送到"对应队列"
- 线程安全:如果"队列"是"共享的"(多个线程使用),需要"加锁"保护;如果是"独占的"(单个线程使用),无需加锁
15.4 节点在VLIB图中的位置
作用和实现原理:
dpdk-output节点在VPP的VLIB图中位于数据包发送路径的末端。数据包从feature节点经过interface-output节点,最终到达dpdk-output节点,然后通过DPDK发送到网卡。节点通过VLIB_NODE_FLAG_IS_OUTPUT标志标识为输出节点,表示数据包会离开VPP。就像"快递发送中心"在"分拣系统"中的位置,位于"发送路径"的"末端",是"包裹"离开"分拣系统"的"出口"。
VLIB图中的位置:
数据包处理流程:
┌─────────────┐
│ Feature │ ← 各种feature节点(ACL、NAT、QoS等)
│ Nodes │
└──────┬──────┘
│
▼
┌─────────────┐
│ interface- │ ← interface-output节点
│ output │ 选择发送队列,准备发送
└──────┬──────┘
│
▼
┌─────────────┐
│ dpdk-output │ ← dpdk-output节点(输出节点)
│ (TX) │ 转换mbuf,发送到网卡
└──────┬──────┘
│
▼
┌─────────────┐
│ DPDK │ ← DPDK PMD,发送到物理网卡
│ PMD │
└─────────────┘
节点注册 (src/vnet/interface.c:1013):
c
/* 注册TX节点 */
hw->tx_node_index = vlib_register_node (vm, &r, "%v-tx", hw->name);
/* 节点名称格式:<interface-name>-tx,例如:dpdk-0-tx */
/* 添加下一跳节点:error-drop */
vlib_node_add_named_next_with_slot (vm, hw->tx_node_index,
"error-drop",
VNET_INTERFACE_TX_NEXT_DROP);
/* 注册output节点 */
hw->output_node_index = vlib_register_node (vm, &r, "%v-output", hw->name);
/* 节点名称格式:<interface-name>-output,例如:dpdk-0-output */
/* 连接output节点到TX节点 */
vlib_node_add_next_with_slot (vm, hw->output_node_index,
hw->tx_node_index,
VNET_INTERFACE_OUTPUT_NEXT_TX);
节点关系:
- interface-output节点:位于dpdk-output节点之前,负责选择发送队列和准备发送
- dpdk-output节点(TX节点):实际的发送节点,负责将数据包发送到网卡
- error-drop节点:错误处理节点,用于丢弃发送失败的数据包
节点标志:
- VLIB_NODE_FLAG_IS_OUTPUT:标识为输出节点,数据包会离开VPP
- VLIB_NODE_TYPE_INTERNAL:内部节点类型
关键设计要点:
- 末端节点:dpdk-output节点位于发送路径的末端
- 自动连接:节点由VPP自动注册和连接,无需手动配置
- 错误处理:节点可以连接到error-drop节点处理错误
通俗理解:
就像"快递发送中心"在"分拣系统"中的位置:
- 末端位置:位于"发送路径"的"末端",是"包裹"离开"分拣系统"的"最后一步"
- 自动连接:由"系统"自动注册和连接,无需"人工配置"
- 错误处理:如果"发送失败",可以将"包裹"路由到"错误处理节点"(error-drop)
15.5 Input和Output的协同工作
作用和实现原理:
Input和Output节点协同工作,完成数据包的完整处理流程。Input节点(dpdk-input)负责从网卡接收数据包,转换为VPP的数据结构,并路由到处理节点。Output节点(dpdk-output)负责将处理后的数据包转换回DPDK的mbuf结构,并发送到网卡。两者共享相同的设备结构(dpdk_device_t)和每线程数据(dpdk_per_thread_data_t),使用相同的mbuf pool和缓冲区管理机制。就像"快递分拣中心"的"接收窗口"和"发送窗口"协同工作,完成"包裹"的"接收"和"发送"流程。
协同工作流程:
接收路径(Input):
┌─────────────┐
│ DPDK │ ← 从网卡接收数据包
│ PMD │
└──────┬──────┘
│
▼
┌─────────────┐
│ dpdk-input │ ← 转换为vlib_buffer,设置元数据
│ (RX) │
└──────┬──────┘
│
▼
┌─────────────┐
│ Feature │ ← 各种处理(路由、ACL、NAT等)
│ Nodes │
└──────┬──────┘
│
▼
┌─────────────┐
│ interface- │ ← 准备发送
│ output │
└──────┬──────┘
│
▼
┌─────────────┐
│ dpdk-output │ ← 转换为mbuf,发送到网卡
│ (TX) │
└──────┬──────┘
│
▼
┌─────────────┐
│ DPDK │ ← 发送到网卡
│ PMD │
└─────────────┘
共享的数据结构:
-
设备结构 (
dpdk_device_t):c/* Input和Output共享同一个设备结构 */ dpdk_device_t *xd = vec_elt_at_index (dm->devices, rd->dev_instance); -
每线程数据 (
dpdk_per_thread_data_t):c/* Input和Output使用同一个每线程数据 */ dpdk_per_thread_data_t *ptd = vec_elt_at_index (dm->per_thread_data, thread_index); -
缓冲区管理:
- Input:从mbuf pool分配缓冲区,转换为vlib_buffer
- Output:将vlib_buffer转换回mbuf,释放到mbuf pool
数据转换:
Input路径(mbuf → vlib_buffer):
c
/* 在dpdk-input节点中 */
/* mbuf在buffer前面,通过负偏移量获取buffer指针 */
vlib_buffer_t *b = vlib_buffer_from_rte_mbuf (mbuf);
/* 设置buffer元数据 */
vlib_buffer_copy_template (b, bt);
Output路径(vlib_buffer → mbuf):
c
/* 在dpdk-output节点中 */
/* buffer在mbuf后面,通过正偏移量获取mbuf指针 */
struct rte_mbuf *mb = rte_mbuf_from_vlib_buffer (buffer);
/* 验证和设置mbuf字段 */
dpdk_validate_rte_mbuf (vm, buffer, maybe_multiseg);
关键设计要点:
- 零拷贝转换:mbuf和vlib_buffer之间的转换是零拷贝的,通过指针运算实现
- 共享资源:Input和Output共享设备结构和每线程数据,减少内存开销
- 对称设计:Input和Output的处理流程是对称的,便于理解和维护
通俗理解:
就像"快递分拣中心"的"接收窗口"和"发送窗口"协同工作:
- 接收窗口(Input):接收"外部包裹"(从网卡),转换为"内部格式"(vlib_buffer),发送到"分拣区"(feature节点)
- 发送窗口(Output):接收"已处理包裹"(vlib_buffer),转换为"外部格式"(mbuf),发送到"外部"(网卡)
- 共享资源:使用"同一个窗口系统"(设备结构)和"同一个工作区"(每线程数据),减少"资源开销"
- 对称设计:接收和发送的"处理流程"是对称的,便于"管理和维护"
15.6 本章总结
dpdk-output节点核心处理流程总结:
| 步骤 | 功能 | 关键函数/机制 | 作用 |
|---|---|---|---|
| 1. 节点注册 | 注册TX节点到VLIB图 | vlib_register_node |
创建输出节点 |
| 2. 获取参数 | 获取设备信息和队列ID | vnet_interface_output_runtime_t |
确定发送目标 |
| 3. 转换指针 | buffer索引转换为mbuf指针 | vlib_get_buffers_with_offset |
准备发送数据 |
| 4. 批量处理 | 验证mbuf和处理硬件卸载 | dpdk_validate_rte_mbuf、dpdk_buffer_tx_offload |
准备发送 |
| 5. 批量发送 | 发送数据包到网卡 | tx_burst_vector_internal、rte_eth_tx_burst |
实际发送 |
| 6. 错误处理 | 处理发送失败的数据包 | 错误统计、mbuf释放 | 错误处理 |
关键性能优化技术:
- 批量处理:使用批量处理循环(4重或8重),提高处理效率
- 预取优化:使用预取指令提前加载数据,减少内存访问延迟
- 零拷贝转换:mbuf和vlib_buffer之间的转换是零拷贝的
- 硬件卸载:利用硬件加速处理校验和、TSO等
相关源码文件:
src/plugins/dpdk/device/device.c:272-VNET_DEVICE_CLASS_TX_FN函数src/plugins/dpdk/device/device.c:156-tx_burst_vector_internal函数src/plugins/dpdk/device/device.c:114-dpdk_validate_rte_mbuf函数src/plugins/dpdk/device/device.c:196-dpdk_buffer_tx_offload函数src/plugins/dpdk/device/device.c:726- 设备类注册src/vnet/interface.c:989- 节点注册src/vnet/interface.h:340-VNET_DEVICE_CLASS_TX_FN宏定义
第16章:数据包发送处理
本章概述:
第16章详细讲解DPDK插件中数据包发送处理的核心机制。数据包发送是VPP数据路径的重要环节,涉及vlib_buffer到mbuf的转换、mbuf验证、批量发送优化、队列锁定机制等多个方面。本章将详细讲解tx_burst_vector_internal函数的实现、rte_eth_tx_burst的调用机制、vlib_buffer到mbuf的转换过程,以及发送队列锁定和批量发送优化技术。就像"快递发送中心"的"发送处理",将"内部包裹格式"(vlib_buffer)转换为"运输格式"(mbuf),然后"批量发送"到"运输设备"(网卡)。
通俗理解:
就像"快递发送中心"的"发送处理":
- 格式转换:将"内部包裹格式"(vlib_buffer)转换为"运输格式"(mbuf)
- 验证处理:检查"包裹完整性"(验证mbuf字段),确保"运输信息"正确
- 批量发送:将多个"包裹"批量发送到"运输设备"(网卡),提高效率
- 队列管理:通过"队列锁定"机制,确保"多线程发送"的"安全性"
16.1 tx_burst_vector_internal函数详解
作用和实现原理:
tx_burst_vector_internal函数是DPDK插件中数据包发送的核心函数。它负责将mbuf数组批量发送到指定的TX队列。函数处理共享队列的锁定、调用DPDK的rte_eth_tx_burst发送数据包,并支持重试机制处理队列满的情况。函数返回未发送的数据包数量(0表示全部发送成功)。就像"快递发送中心"的"批量发送函数",将"包裹数组"批量发送到"指定队列",处理"队列满"的情况。
函数签名 (src/plugins/dpdk/device/device.c:155):
c
/**
* 批量发送数据包到指定的TX队列
*
* @param vm VPP主结构体指针
* @param xd DPDK设备结构体指针
* @param mb mbuf指针数组
* @param n_left 要发送的数据包数量
* @param queue_id 队列ID
* @param is_shared 是否为共享队列(0=独占,1=共享)
*
* @return 未发送的数据包数量(0表示全部发送成功)
*/
static_always_inline u32
tx_burst_vector_internal (vlib_main_t *vm, dpdk_device_t *xd,
struct rte_mbuf **mb, u32 n_left, int queue_id,
u8 is_shared)
函数完整实现 (src/plugins/dpdk/device/device.c:156):
c
static_always_inline u32
tx_burst_vector_internal (vlib_main_t *vm, dpdk_device_t *xd,
struct rte_mbuf **mb, u32 n_left, int queue_id,
u8 is_shared)
{
dpdk_tx_queue_t *txq; // TX队列结构体指针
u32 n_retry; // 重试次数计数器
int n_sent = 0; // 本次发送的数据包数量
n_retry = 16; // 最大重试次数:16次
txq = vec_elt_at_index (xd->tx_queues, queue_id); // 获取TX队列结构
do
{
/* 步骤1:如果是共享队列,需要加锁保护 */
/* 共享队列可能被多个线程同时访问,需要互斥保护 */
if (is_shared)
clib_spinlock_lock (&txq->lock);
/* 步骤2:调用DPDK的批量发送函数 */
/* rte_eth_tx_burst是DPDK的批量发送接口,返回实际发送的数据包数量 */
/* 如果队列满,可能只发送部分数据包 */
n_sent = rte_eth_tx_burst (xd->port_id, queue_id, mb, n_left);
/* 步骤3:释放锁(如果是共享队列) */
if (is_shared)
clib_spinlock_unlock (&txq->lock);
/* 步骤4:更新状态 */
n_retry--; // 减少重试次数
n_left -= n_sent; // 减去已发送的数量,得到剩余数量
mb += n_sent; // 移动mbuf指针,指向未发送的数据包
}
while (n_sent && n_left && (n_retry > 0));
/* 循环条件:
* - n_sent > 0:本次有数据包发送成功
* - n_left > 0:还有数据包未发送
* - n_retry > 0:还有重试次数
*
* 如果队列满(n_sent < n_left),会继续重试
* 如果队列完全满(n_sent == 0),也会继续重试(最多16次)
*/
return n_left; // 返回未发送的数据包数量(0表示全部发送成功)
}
处理流程:
初始化:n_retry = 16, n_left = 要发送的数量
│
▼
循环(最多16次):
│
├── 如果是共享队列 → 加锁
│
├── 调用rte_eth_tx_burst发送
│ ├── 成功发送n_sent个数据包
│ └── 队列满时,n_sent < n_left
│
├── 如果是共享队列 → 解锁
│
├── 更新:n_left -= n_sent, mb += n_sent
│
└── 继续循环,直到:
├── 全部发送成功(n_left == 0)→ 退出
├── 队列完全满且无进展(n_sent == 0)→ 继续重试
└── 重试次数用完(n_retry == 0)→ 退出
│
▼
返回n_left(未发送的数量)
重试机制的作用:
- 处理队列满:当TX队列满时,DPDK可能只发送部分数据包,重试机制可以继续发送剩余的数据包
- 等待队列释放:在重试过程中,网卡可能会释放队列空间,允许继续发送
- 限制重试次数:最多重试16次,避免无限循环
关键设计要点:
- 共享队列锁定:共享队列使用自旋锁保护,独占队列无需锁
- 批量发送:使用DPDK的批量发送接口,提高发送效率
- 重试机制:支持重试,处理队列满的情况
- 返回值语义:返回未发送的数量,0表示全部发送成功
通俗理解:
就像"快递发送中心"的"批量发送函数":
- 队列选择:根据"队列ID"找到"目标队列"(TX队列)
- 加锁保护:如果"队列"是"共享的"(多个线程使用),需要"加锁"保护
- 批量发送:调用"运输接口"(rte_eth_tx_burst)批量发送"包裹"(数据包)
- 重试机制:如果"队列满"(只发送了部分包裹),继续重试,直到"全部发送"或"重试次数用完"
- 返回结果:返回"未发送的包裹数量"(0表示全部发送成功)
16.2 rte_eth_tx_burst调用和批量发送
作用和实现原理:
rte_eth_tx_burst是DPDK提供的批量发送接口,用于将多个mbuf批量发送到网卡的TX队列。该函数是DPDK PMD(Poll Mode Driver)的核心接口,由网卡驱动实现。函数返回实际发送的数据包数量,如果队列满,可能返回小于请求的数量。批量发送可以减少系统调用次数,提高发送效率。就像"快递发送中心"的"运输接口",批量将"包裹"发送到"运输设备"(网卡)。
函数调用 (src/plugins/dpdk/device/device.c:173):
c
/* 调用DPDK的批量发送函数 */
n_sent = rte_eth_tx_burst (xd->port_id, // 端口ID
queue_id, // 队列ID
mb, // mbuf指针数组
n_left); // 要发送的数据包数量
/* 返回值n_sent:实际发送的数据包数量
* - n_sent == n_left:全部发送成功
* - 0 < n_sent < n_left:部分发送(队列满)
* - n_sent == 0:队列完全满,无法发送
*/
rte_eth_tx_burst函数原型(DPDK API):
c
/**
* DPDK批量发送函数
*
* @param port_id 端口ID
* @param queue_id 队列ID
* @param tx_pkts mbuf指针数组
* @param nb_pkts 要发送的数据包数量
*
* @return 实际发送的数据包数量(可能小于nb_pkts)
*/
static inline uint16_t
rte_eth_tx_burst (uint16_t port_id, uint16_t queue_id,
struct rte_mbuf **tx_pkts, uint16_t nb_pkts);
批量发送的优势:
- 减少系统调用:批量发送可以减少系统调用次数,降低开销
- 提高吞吐量:网卡可以一次处理多个数据包,提高吞吐量
- 降低延迟:批量处理可以减少上下文切换,降低延迟
- 利用硬件特性:现代网卡支持批量处理,批量发送可以更好地利用硬件特性
批量发送示例:
c
/* 假设要发送10个数据包 */
struct rte_mbuf *mbufs[10];
u32 n_packets = 10;
/* 批量发送 */
u16 n_sent = rte_eth_tx_burst (port_id, queue_id, mbufs, n_packets);
/* 处理结果 */
if (n_sent == n_packets)
{
/* 全部发送成功 */
}
else if (n_sent > 0)
{
/* 部分发送(队列满) */
/* 剩余数据包:mbufs[n_sent .. n_packets-1] */
/* 需要重试发送剩余的数据包 */
}
else
{
/* 队列完全满,无法发送 */
/* 需要重试 */
}
队列满的处理:
当TX队列满时,rte_eth_tx_burst可能只发送部分数据包。VPP通过重试机制处理这种情况:
c
do
{
n_sent = rte_eth_tx_burst (xd->port_id, queue_id, mb, n_left);
n_left -= n_sent; // 减去已发送的数量
mb += n_sent; // 移动指针,指向未发送的数据包
}
while (n_sent && n_left && (n_retry > 0));
关键设计要点:
- 批量接口:使用DPDK的批量发送接口,提高发送效率
- 返回值处理:正确处理返回值,区分全部成功、部分成功和完全失败
- 重试机制:通过重试机制处理队列满的情况
- 指针移动:正确移动mbuf指针,处理部分发送的情况
通俗理解:
就像"快递发送中心"的"运输接口":
- 批量发送:一次发送多个"包裹"(数据包),而不是逐个发送
- 效率提升:减少"运输次数"(系统调用),提高"运输效率"
- 队列处理:如果"运输队列满"(只发送了部分包裹),返回"已发送数量",剩余包裹需要"重试发送"
- 返回值:返回"实际发送的包裹数量",用于判断是否需要"重试"
16.3 vlib_buffer到mbuf的转换
作用和实现原理:
vlib_buffer到mbuf的转换是数据包发送前的关键步骤。由于mbuf和vlib_buffer在内存中是连续存储的(mbuf在buffer前面),转换通过指针运算实现,是零拷贝的。dpdk_validate_rte_mbuf函数负责验证和设置mbuf的字段,确保mbuf字段正确,特别是对于多段数据包。就像"快递发送中心"的"格式转换",将"内部包裹格式"(vlib_buffer)转换为"运输格式"(mbuf),确保"运输信息"正确。
内存布局 (src/plugins/dpdk/buffer.h:19):
c
/* mbuf和vlib_buffer在内存中的布局 */
/*
* 内存布局:
* [mbuf结构] [vlib_buffer结构] [数据区域]
* ↑ ↑
* mbuf指针 buffer指针
*
* buffer指针 = mbuf指针 + sizeof(struct rte_mbuf)
* mbuf指针 = buffer指针 - sizeof(struct rte_mbuf)
*/
/* 从vlib_buffer获取mbuf指针 */
#define rte_mbuf_from_vlib_buffer(x) (((struct rte_mbuf *)x) - 1)
/* 从mbuf获取vlib_buffer指针 */
#define vlib_buffer_from_rte_mbuf(x) ((vlib_buffer_t *)(x+1))
dpdk_validate_rte_mbuf函数 (src/plugins/dpdk/device/device.c:114):
c
/**
* 验证和设置mbuf字段
*
* @param vm VPP主结构体指针
* @param b vlib_buffer指针
* @param maybe_multiseg 是否可能是多段数据包(1=可能,0=不可能)
*/
static_always_inline void
dpdk_validate_rte_mbuf (vlib_main_t * vm, vlib_buffer_t * b,
int maybe_multiseg)
{
struct rte_mbuf *mb, *first_mb, *last_mb;
/* 步骤1:获取mbuf指针(零拷贝转换) */
last_mb = first_mb = mb = rte_mbuf_from_vlib_buffer (b);
/* 步骤2:如果buffer来自非DPDK源,需要初始化mbuf头部 */
/* VLIB_BUFFER_EXT_HDR_VALID标志表示mbuf头部已经初始化 */
if (PREDICT_FALSE ((b->flags & VLIB_BUFFER_EXT_HDR_VALID) == 0))
{
/* mbuf头部未初始化,调用rte_pktmbuf_reset初始化 */
rte_pktmbuf_reset (mb);
}
/* 步骤3:设置第一段的字段 */
first_mb->nb_segs = 1; // 段数初始化为1
mb->data_len = b->current_length; // 数据长度 = buffer当前长度
mb->pkt_len = maybe_multiseg ? // 包总长度
vlib_buffer_length_in_chain (vm, b) : // 多段:链总长度
b->current_length; // 单段:当前长度
mb->data_off = VLIB_BUFFER_PRE_DATA_SIZE + b->current_data; // 数据偏移
/* 步骤4:处理多段数据包(如果maybe_multiseg=1且有多段) */
while (maybe_multiseg && (b->flags & VLIB_BUFFER_NEXT_PRESENT))
{
/* 获取下一个buffer段 */
b = vlib_get_buffer (vm, b->next_buffer);
mb = rte_mbuf_from_vlib_buffer (b);
/* 如果mbuf头部未初始化,初始化它 */
if (PREDICT_FALSE ((b->flags & VLIB_BUFFER_EXT_HDR_VALID) == 0))
rte_pktmbuf_reset (mb);
/* 链接mbuf段 */
last_mb->next = mb; // 链接上一个段的next指针
last_mb = mb; // 更新last_mb指针
/* 设置当前段的字段 */
mb->data_len = b->current_length; // 数据长度
mb->pkt_len = b->current_length; // 包长度(对于后续段,等于data_len)
mb->data_off = VLIB_BUFFER_PRE_DATA_SIZE + b->current_data; // 数据偏移
/* 更新第一段的段数 */
first_mb->nb_segs++;
/* 如果buffer的引用计数>1,需要设置mbuf的pool */
/* 这用于正确释放多引用计数的buffer */
if (PREDICT_FALSE (b->ref_count > 1))
mb->pool = dpdk_no_cache_mempool_by_buffer_pool_index[b->buffer_pool_index];
}
}
转换过程:
单段数据包:
vlib_buffer (当前段)
│
├── current_length = 1500
├── current_data = 0
└── flags (无VLIB_BUFFER_NEXT_PRESENT)
│
▼
mbuf (第一段)
│
├── nb_segs = 1
├── data_len = 1500
├── pkt_len = 1500
├── data_off = VLIB_BUFFER_PRE_DATA_SIZE + 0
└── next = NULL
多段数据包:
vlib_buffer链 (3段)
│
├── 段1:current_length = 1500, flags |= VLIB_BUFFER_NEXT_PRESENT
├── 段2:current_length = 500
└── 段3:current_length = 300
│
▼
mbuf链 (3段)
│
├── 段1:
│ ├── nb_segs = 3
│ ├── data_len = 1500
│ ├── pkt_len = 2300 (总长度)
│ ├── data_off = ...
│ └── next → 段2
│
├── 段2:
│ ├── data_len = 500
│ ├── pkt_len = 500
│ ├── data_off = ...
│ └── next → 段3
│
└── 段3:
├── data_len = 300
├── pkt_len = 300
├── data_off = ...
└── next = NULL
关键字段说明:
- nb_segs:数据包的段数,第一段存储总段数
- data_len:当前段的数据长度
- pkt_len:第一段存储总长度,后续段存储当前段长度
- data_off:数据在buffer中的偏移量
- next:指向下一个mbuf段的指针(多段数据包)
关键设计要点:
- 零拷贝转换:通过指针运算实现,无需数据拷贝
- 多段支持:正确处理多段数据包的链接
- 条件初始化:只有当mbuf头部未初始化时才初始化
- 引用计数处理:正确处理多引用计数的buffer
通俗理解:
就像"快递发送中心"的"格式转换":
- 零拷贝转换:通过"指针运算"直接获取"运输格式"(mbuf),无需"重新包装"(数据拷贝)
- 设置运输信息:设置"包裹信息"(nb_segs、data_len、pkt_len等),确保"运输设备"(网卡)能正确识别
- 多段处理:如果是"多个容器"(多段数据包),需要"链接容器"(设置next指针),形成"容器链"
- 验证处理:确保"运输信息"正确,特别是"多段包裹"的"链接信息"
16.4 发送队列锁定机制
作用和实现原理:
发送队列锁定机制用于保护共享队列的并发访问。当多个线程需要访问同一个TX队列时,需要使用锁来保护队列,防止并发冲突。DPDK支持独占队列(每个线程使用独立的队列)和共享队列(多个线程共享一个队列)。独占队列无需锁,共享队列需要锁保护。就像"快递发送中心"的"队列保护",当"多个窗口"(线程)使用"同一个队列"时,需要"加锁"保护,防止"并发冲突"。
队列类型:
-
独占队列 (
is_shared = 0):- 每个线程使用独立的队列
- 无需加锁,性能更高
- 适用于多队列网卡
-
共享队列 (
is_shared = 1):- 多个线程共享一个队列
- 需要加锁保护
- 适用于单队列网卡或队列数量少于线程数
锁定机制代码 (src/plugins/dpdk/device/device.c:169):
c
/* 在tx_burst_vector_internal函数中 */
if (is_shared)
{
/* 如果是共享队列,需要加锁 */
clib_spinlock_lock (&txq->lock);
}
/* 发送数据包 */
n_sent = rte_eth_tx_burst (xd->port_id, queue_id, mb, n_left);
if (is_shared)
{
/* 发送完成后,释放锁 */
clib_spinlock_unlock (&txq->lock);
}
clib_spinlock_lock/unlock:
c
/**
* 自旋锁加锁
* 如果锁已被其他线程持有,当前线程会自旋等待
*/
clib_spinlock_lock (&txq->lock);
/**
* 自旋锁解锁
* 释放锁,允许其他等待的线程获取锁
*/
clib_spinlock_unlock (&txq->lock);
队列锁的定义 (src/plugins/dpdk/device/dpdk.h):
c
/* 在dpdk_tx_queue_t结构体中 */
typedef struct
{
/* ... 其他字段 ... */
clib_spinlock_t lock; // 队列锁(用于共享队列)
/* ... 其他字段 ... */
} dpdk_tx_queue_t;
锁定范围:
锁的保护范围只包括rte_eth_tx_burst调用,不包括mbuf准备和验证等操作。这样可以最小化锁的持有时间,减少锁竞争:
c
/* 锁外操作:mbuf准备和验证(无锁,可并行) */
dpdk_validate_rte_mbuf (vm, b[0], maybe_multiseg);
dpdk_buffer_tx_offload (xd, b[0], mb[0]);
/* 加锁:发送操作(需要锁保护) */
if (is_shared)
clib_spinlock_lock (&txq->lock);
n_sent = rte_eth_tx_burst (xd->port_id, queue_id, mb, n_left);
if (is_shared)
clib_spinlock_unlock (&txq->lock);
/* 锁外操作:后续处理(无锁,可并行) */
性能考虑:
- 最小化锁持有时间:只在必要时持有锁,减少锁竞争
- 优先使用独占队列:如果可能,使用独占队列避免锁
- 自旋锁vs互斥锁:使用自旋锁,避免上下文切换开销
关键设计要点:
- 条件锁定 :只有当
is_shared=1时才加锁 - 最小化范围:锁的保护范围只包括发送操作
- 自旋锁:使用自旋锁,避免上下文切换
通俗理解:
就像"快递发送中心"的"队列保护":
- 独占队列:每个"窗口"(线程)使用"独立队列",无需"加锁",效率更高
- 共享队列:多个"窗口"使用"同一个队列",需要"加锁"保护,防止"并发冲突"
- 最小化锁时间:只在"发送包裹"时"加锁","准备包裹"等操作在"锁外"进行,减少"等待时间"
- 自旋锁:使用"自旋锁"(等待时不切换线程),避免"上下文切换"开销
16.5 批量发送优化
作用和实现原理:
批量发送优化是提高数据包发送性能的关键技术。通过批量处理多个数据包、使用预取指令、最小化锁持有时间等技术,可以显著提高发送吞吐量。VPP在dpdk-output节点中使用批量处理循环、预取优化等技术实现批量发送优化。就像"快递发送中心"的"批量发送优化",通过"批量处理"、"预先准备"等技术提高"发送效率"。
批量处理循环 (src/plugins/dpdk/device/device.c:301):
c
/* 8重循环(如果支持8个预取槽位) */
#if (CLIB_N_PREFETCHES >= 8)
while (n_left >= 8)
{
/* 预取后续数据包 */
dpdk_prefetch_buffer (vm, mb[4]);
dpdk_prefetch_buffer (vm, mb[5]);
dpdk_prefetch_buffer (vm, mb[6]);
dpdk_prefetch_buffer (vm, mb[7]);
/* 处理当前数据包 */
b[0] = vlib_buffer_from_rte_mbuf (mb[0]);
b[1] = vlib_buffer_from_rte_mbuf (mb[1]);
b[2] = vlib_buffer_from_rte_mbuf (mb[2]);
b[3] = vlib_buffer_from_rte_mbuf (mb[3]);
/* 验证和处理 */
/* ... */
mb += 4;
n_left -= 4;
}
#endif
预取优化 (src/plugins/dpdk/device/device.c:187):
c
/**
* 预取buffer数据
* 提前加载数据到CPU缓存,减少内存访问延迟
*/
static_always_inline __clib_unused void
dpdk_prefetch_buffer (vlib_main_t * vm, struct rte_mbuf *mb)
{
vlib_buffer_t *b = vlib_buffer_from_rte_mbuf (mb);
CLIB_PREFETCH (mb, sizeof (struct rte_mbuf), STORE); // 预取mbuf
clib_prefetch_load (b); // 预取buffer
}
批量发送的优势:
- 减少函数调用开销:批量处理减少函数调用次数
- 提高CPU利用率:批量处理提高CPU流水线效率
- 减少内存访问延迟:预取优化减少内存访问延迟
- 提高缓存命中率:批量处理提高缓存命中率
优化技术总结:
| 优化技术 | 实现方式 | 作用 |
|---|---|---|
| 批量处理 | 8重或4重循环 | 减少循环开销,提高CPU利用率 |
| 预取优化 | CLIB_PREFETCH指令 | 提前加载数据到缓存,减少延迟 |
| 最小化锁时间 | 只在发送时加锁 | 减少锁竞争,提高并发性能 |
| 零拷贝转换 | 指针运算 | 避免数据拷贝,提高性能 |
| 条件处理 | PREDICT_TRUE/FALSE | 优化分支预测,提高执行效率 |
性能对比:
未优化:
逐个处理数据包 → 多次函数调用 → 高开销
优化后:
批量处理(8个一组)→ 减少函数调用 → 低开销
预取后续数据 → 减少内存延迟 → 提高吞吐量
最小化锁时间 → 减少锁竞争 → 提高并发性能
关键设计要点:
- 批量处理:使用8重或4重循环批量处理数据包
- 预取优化:提前预取后续数据包,减少内存访问延迟
- 条件编译:根据预取槽位数量选择循环大小
- 性能平衡:在代码复杂度和性能之间找到平衡
通俗理解:
就像"快递发送中心"的"批量发送优化":
- 批量处理:一次处理多个"包裹"(8个或4个一组),而不是逐个处理,减少"处理次数"
- 预先准备:在处理当前"包裹"时,提前"准备"后续"包裹"(预取),减少"等待时间"
- 最小化锁时间:只在"实际发送"时"加锁",其他操作在"锁外"进行,减少"等待时间"
- 提高效率:通过"批量处理"和"预先准备",显著提高"发送效率"
16.6 本章总结
数据包发送处理核心流程总结:
| 步骤 | 功能 | 关键函数/机制 | 作用 |
|---|---|---|---|
| 1. 获取mbuf指针 | buffer索引转换为mbuf指针 | vlib_get_buffers_with_offset |
准备发送数据 |
| 2. 批量处理 | 批量验证和处理mbuf | 批量循环、预取优化 | 提高处理效率 |
| 3. 验证mbuf | 验证和设置mbuf字段 | dpdk_validate_rte_mbuf |
确保mbuf正确 |
| 4. 硬件卸载 | 处理硬件卸载标志 | dpdk_buffer_tx_offload |
设置硬件卸载 |
| 5. 批量发送 | 批量发送到网卡 | tx_burst_vector_internal、rte_eth_tx_burst |
实际发送 |
| 6. 重试处理 | 处理发送失败的数据包 | 重试机制 | 处理队列满 |
关键性能优化技术:
- 批量处理:使用8重或4重循环批量处理数据包,减少循环开销
- 预取优化:使用预取指令提前加载数据,减少内存访问延迟
- 零拷贝转换:mbuf和vlib_buffer之间的转换是零拷贝的
- 最小化锁时间:只在发送时加锁,减少锁竞争
- 重试机制:通过重试机制处理队列满的情况
相关源码文件:
src/plugins/dpdk/device/device.c:156-tx_burst_vector_internal函数src/plugins/dpdk/device/device.c:114-dpdk_validate_rte_mbuf函数src/plugins/dpdk/device/device.c:187-dpdk_prefetch_buffer函数src/plugins/dpdk/device/device.c:272- TX函数主入口src/plugins/dpdk/buffer.h:19- mbuf和buffer转换宏
第17章:硬件卸载处理(发送侧)
本章概述:
第17章详细讲解DPDK插件中发送侧硬件卸载处理的核心机制。硬件卸载是提高网络处理性能的关键技术,允许网卡硬件执行原本需要CPU完成的任务,如校验和计算、数据包分段、隧道封装等。VPP通过dpdk_buffer_tx_offload函数将VPP的卸载标志转换为DPDK的mbuf卸载标志,并设置相应的头部长度信息。本章将详细讲解各种硬件卸载类型的处理,包括IP校验和卸载、L4校验和卸载、TSO(TCP Segmentation Offload)、VXLAN隧道卸载等。就像"快递发送中心"的"自动处理系统",将"处理任务"(校验和计算、分段等)交给"自动化设备"(网卡硬件),提高"处理效率"。
通俗理解:
就像"快递发送中心"的"自动处理系统":
- 硬件卸载:将"处理任务"(校验和计算、分段等)交给"自动化设备"(网卡硬件),减少"人工处理"(CPU处理)
- 标志转换:将"内部标记"(VPP卸载标志)转换为"设备能理解的标记"(DPDK mbuf标志)
- 信息设置:设置"处理所需的信息"(头部长度等),让"设备"能正确"处理包裹"
- 性能提升:通过"硬件处理",显著提高"处理效率"和"吞吐量"
17.1 dpdk_buffer_tx_offload函数详解
作用和实现原理:
dpdk_buffer_tx_offload函数负责将VPP的硬件卸载标志转换为DPDK的mbuf卸载标志,并设置相应的头部长度信息。该函数检查buffer中的卸载标志和GSO标志,提取卸载标志,转换为DPDK的ol_flags,并计算和设置各种头部长度(l2_len、l3_len、l4_len、outer_l2_len、outer_l3_len等)。就像"快递发送中心"的"标记转换系统",将"内部标记"(VPP卸载标志)转换为"设备标记"(DPDK标志),并设置"处理信息"(头部长度)。
函数签名 (src/plugins/dpdk/device/device.c:195):
c
/**
* 设置TX硬件卸载标志和头部长度
*
* @param xd DPDK设备结构体指针
* @param b vlib_buffer指针
* @param mb rte_mbuf指针
*/
static_always_inline void
dpdk_buffer_tx_offload (dpdk_device_t * xd, vlib_buffer_t * b,
struct rte_mbuf *mb)
函数完整实现 (src/plugins/dpdk/device/device.c:195):
c
static_always_inline void
dpdk_buffer_tx_offload (dpdk_device_t * xd, vlib_buffer_t * b,
struct rte_mbuf *mb)
{
int is_ip4 = b->flags & VNET_BUFFER_F_IS_IP4; // 是否为IPv4数据包
u32 tso = b->flags & VNET_BUFFER_F_GSO, max_pkt_len; // GSO(TSO)标志
u32 ip_cksum, tcp_cksum, udp_cksum, outer_hdr_len = 0; // 校验和卸载标志
u32 outer_ip_cksum, vxlan_tunnel; // 外层校验和和VXLAN隧道标志
u64 ol_flags; // DPDK mbuf卸载标志
vnet_buffer_oflags_t oflags = 0; // VPP卸载标志
/* 步骤1:快速路径检查 - 如果没有卸载需求,直接返回 */
/* 检查是否有卸载标志(VNET_BUFFER_F_OFFLOAD)或GSO标志 */
if (PREDICT_TRUE (((b->flags & VNET_BUFFER_F_OFFLOAD) | tso) == 0))
return; // 没有卸载需求,直接返回
/* 步骤2:提取VPP卸载标志 */
oflags = vnet_buffer (b)->oflags;
ip_cksum = oflags & VNET_BUFFER_OFFLOAD_F_IP_CKSUM; // IP校验和卸载
tcp_cksum = oflags & VNET_BUFFER_OFFLOAD_F_TCP_CKSUM; // TCP校验和卸载
udp_cksum = oflags & VNET_BUFFER_OFFLOAD_F_UDP_CKSUM; // UDP校验和卸载
outer_ip_cksum = oflags & VNET_BUFFER_OFFLOAD_F_OUTER_IP_CKSUM; // 外层IP校验和卸载
vxlan_tunnel = oflags & VNET_BUFFER_OFFLOAD_F_TNL_VXLAN; // VXLAN隧道卸载
/* 步骤3:设置基本的DPDK卸载标志 */
ol_flags = is_ip4 ? RTE_MBUF_F_TX_IPV4 : RTE_MBUF_F_TX_IPV6; // IP版本标志
ol_flags |= ip_cksum ? RTE_MBUF_F_TX_IP_CKSUM : 0; // IP校验和卸载标志
ol_flags |= tcp_cksum ? RTE_MBUF_F_TX_TCP_CKSUM : 0; // TCP校验和卸载标志
ol_flags |= udp_cksum ? RTE_MBUF_F_TX_UDP_CKSUM : 0; // UDP校验和卸载标志
/* 步骤4:处理VXLAN隧道卸载 */
if (vxlan_tunnel)
{
/* 设置外层IP版本和校验和标志 */
ol_flags |= outer_ip_cksum ?
RTE_MBUF_F_TX_OUTER_IPV4 | RTE_MBUF_F_TX_OUTER_IP_CKSUM :
RTE_MBUF_F_TX_OUTER_IPV6;
ol_flags |= RTE_MBUF_F_TX_TUNNEL_VXLAN; // VXLAN隧道标志
/* 计算VXLAN隧道的头部长度 */
/* l2_len:内层L2头部长度(从外层L4结束到内层L3开始) */
mb->l2_len =
vnet_buffer (b)->l3_hdr_offset - vnet_buffer2 (b)->outer_l4_hdr_offset;
/* l3_len:内层L3头部长度(从内层L3开始到内层L4开始) */
mb->l3_len =
vnet_buffer (b)->l4_hdr_offset - vnet_buffer (b)->l3_hdr_offset;
/* outer_l2_len:外层L2头部长度(从数据包开始到外层L3开始) */
mb->outer_l2_len =
vnet_buffer2 (b)->outer_l3_hdr_offset - b->current_data;
/* outer_l3_len:外层L3头部长度(从外层L3开始到外层L4开始) */
mb->outer_l3_len = vnet_buffer2 (b)->outer_l4_hdr_offset -
vnet_buffer2 (b)->outer_l3_hdr_offset;
outer_hdr_len = mb->outer_l2_len + mb->outer_l3_len; // 外层头部总长度
}
else
{
/* 非隧道数据包:只计算内层头部长度 */
/* l2_len:L2头部长度(从数据包开始到L3开始) */
mb->l2_len = vnet_buffer (b)->l3_hdr_offset - b->current_data;
/* l3_len:L3头部长度(从L3开始到L4开始) */
mb->l3_len =
vnet_buffer (b)->l4_hdr_offset - vnet_buffer (b)->l3_hdr_offset;
mb->outer_l2_len = 0; // 无外层头部
mb->outer_l3_len = 0; // 无外层头部
}
/* 步骤5:处理TSO(TCP Segmentation Offload) */
if (tso)
{
/* 设置L4头部长度(用于TSO分段) */
mb->l4_len = vnet_buffer2 (b)->gso_l4_hdr_sz;
/* 设置TSO段大小(每个段的payload大小) */
mb->tso_segsz = vnet_buffer2 (b)->gso_size;
/* 计算最大包长度(用于判断是否需要TSO) */
max_pkt_len =
outer_hdr_len + mb->l2_len + mb->l3_len + mb->l4_len + mb->tso_segsz;
/* 如果数据包长度大于最大包长度,说明需要TSO分段 */
if (mb->tso_segsz != 0 && mb->pkt_len > max_pkt_len)
{
/* 根据L4协议类型设置TSO标志 */
ol_flags |=
(tcp_cksum ? RTE_MBUF_F_TX_TCP_SEG : RTE_MBUF_F_TX_UDP_SEG);
}
}
/* 步骤6:设置mbuf的卸载标志 */
mb->ol_flags |= ol_flags;
/* 步骤7:Intel网卡的特殊处理 */
/* Intel网卡需要额外的校验和标志准备 */
if (xd->flags & DPDK_DEVICE_FLAG_INTEL_PHDR_CKSUM)
rte_net_intel_cksum_flags_prepare (mb, ol_flags);
}
处理流程:
检查是否有卸载需求
│
├── 无卸载需求 → 直接返回
└── 有卸载需求 → 继续处理
│
▼
提取VPP卸载标志
│
├── IP校验和卸载标志
├── TCP/UDP校验和卸载标志
├── 外层IP校验和卸载标志
└── VXLAN隧道卸载标志
│
▼
设置DPDK基本卸载标志
│
├── IP版本标志(IPv4/IPv6)
├── IP校验和卸载标志
└── L4校验和卸载标志
│
▼
处理VXLAN隧道
│
├── 如果有隧道 → 设置外层头部长度和隧道标志
└── 如果无隧道 → 只设置内层头部长度
│
▼
处理TSO
│
├── 如果有GSO标志 → 设置L4头部长度和段大小
└── 如果数据包需要分段 → 设置TSO标志
│
▼
设置mbuf卸载标志
│
└── 应用Intel网卡特殊处理(如果需要)
关键设计要点:
- 快速路径:如果没有卸载需求,直接返回,避免不必要的处理
- 标志转换:将VPP的卸载标志转换为DPDK的mbuf标志
- 头部长度计算:正确计算各种头部长度,用于硬件卸载处理
- 条件处理:根据数据包类型和特征进行条件处理
通俗理解:
就像"快递发送中心"的"标记转换系统":
- 快速检查:如果"包裹"不需要"特殊处理"(无卸载需求),直接跳过
- 提取标记:提取"内部标记"(VPP卸载标志),了解"需要哪些处理"
- 转换标记:将"内部标记"转换为"设备能理解的标记"(DPDK标志)
- 设置信息:设置"处理所需的信息"(头部长度等),让"设备"能正确"处理"
- 特殊处理:对于"特殊设备"(Intel网卡),进行"额外处理"
17.2 TX硬件卸载标志的设置
作用和实现原理:
TX硬件卸载标志存储在mbuf的ol_flags字段中,用于指示网卡硬件需要执行的卸载操作。VPP通过检查buffer中的卸载标志和GSO标志,将VPP的卸载标志转换为DPDK的mbuf卸载标志。标志的设置是条件性的,只有当相应的卸载需求存在时才设置。就像"快递发送中心"的"处理标记",在"包裹"上设置"标记",指示"自动化设备"需要执行的"处理操作"。
VPP卸载标志定义 (src/vnet/buffer.h:110):
c
/**
* VPP硬件卸载标志枚举
*/
#define foreach_vnet_buffer_offload_flag
_ (0, IP_CKSUM, "offload-ip-cksum", 1) // IP校验和卸载
_ (1, TCP_CKSUM, "offload-tcp-cksum", 1) // TCP校验和卸载
_ (2, UDP_CKSUM, "offload-udp-cksum", 1) // UDP校验和卸载
_ (3, OUTER_IP_CKSUM, "offload-outer-ip-cksum", 1) // 外层IP校验和卸载
_ (4, OUTER_UDP_CKSUM, "offload-outer-udp-cksum", 1) // 外层UDP校验和卸载
_ (5, TNL_VXLAN, "offload-vxlan-tunnel", 1) // VXLAN隧道卸载
_ (6, TNL_IPIP, "offload-ipip-tunnel", 1) // IPIP隧道卸载
typedef enum
{
#define _(bit, name, s, v) VNET_BUFFER_OFFLOAD_F_##name = (1 << bit),
foreach_vnet_buffer_offload_flag
#undef _
} vnet_buffer_oflags_t;
DPDK mbuf卸载标志设置 (src/plugins/dpdk/device/device.c:217):
c
/* 在dpdk_buffer_tx_offload函数中,设置DPDK mbuf卸载标志 */
ol_flags = is_ip4 ? RTE_MBUF_F_TX_IPV4 : RTE_MBUF_F_TX_IPV6; // IP版本标志
ol_flags |= ip_cksum ? RTE_MBUF_F_TX_IP_CKSUM : 0; // IP校验和卸载
ol_flags |= tcp_cksum ? RTE_MBUF_F_TX_TCP_CKSUM : 0; // TCP校验和卸载
ol_flags |= udp_cksum ? RTE_MBUF_F_TX_UDP_CKSUM : 0; // UDP校验和卸载
标志映射关系:
| VPP标志 | DPDK标志 | 说明 |
|---|---|---|
VNET_BUFFER_OFFLOAD_F_IP_CKSUM |
RTE_MBUF_F_TX_IP_CKSUM |
IP校验和卸载 |
VNET_BUFFER_OFFLOAD_F_TCP_CKSUM |
RTE_MBUF_F_TX_TCP_CKSUM |
TCP校验和卸载 |
VNET_BUFFER_OFFLOAD_F_UDP_CKSUM |
RTE_MBUF_F_TX_UDP_CKSUM |
UDP校验和卸载 |
VNET_BUFFER_OFFLOAD_F_OUTER_IP_CKSUM |
RTE_MBUF_F_TX_OUTER_IP_CKSUM |
外层IP校验和卸载 |
VNET_BUFFER_F_GSO |
RTE_MBUF_F_TX_TCP_SEG 或 RTE_MBUF_F_TX_UDP_SEG |
TSO分段卸载 |
VNET_BUFFER_OFFLOAD_F_TNL_VXLAN |
RTE_MBUF_F_TX_TUNNEL_VXLAN |
VXLAN隧道卸载 |
标志的应用 (src/plugins/dpdk/device/device.c:259):
c
/* 将ol_flags设置到mbuf的ol_flags字段 */
mb->ol_flags |= ol_flags;
卸载标志的检查 (src/plugins/dpdk/device/device.c:333):
c
/* 在TX函数中,检查是否需要处理硬件卸载 */
if (PREDICT_FALSE ((xd->flags & DPDK_DEVICE_FLAG_TX_OFFLOAD) &&
(or_flags & VNET_BUFFER_F_OFFLOAD)))
{
/* 设备支持TX卸载,且数据包有卸载需求 */
dpdk_buffer_tx_offload (xd, b[0], mb[0]);
/* ... 批量处理其他数据包 ... */
}
关键设计要点:
- 条件设置:只有当相应的卸载需求存在时才设置标志
- 标志映射:VPP标志需要正确映射到DPDK标志
- 批量处理:在批量处理循环中处理卸载标志,提高效率
通俗理解:
就像"快递发送中心"的"处理标记":
- 标记提取:从"包裹"中提取"内部标记"(VPP卸载标志),了解"需要哪些处理"
- 标记转换:将"内部标记"转换为"设备能理解的标记"(DPDK标志)
- 标记应用:将"设备标记"应用到"包裹"上(mb->ol_flags),指示"设备"需要执行的"处理操作"
17.3 IP校验和卸载
作用和实现原理:
IP校验和卸载允许网卡硬件计算和验证IPv4数据包的IP头部校验和。当设置了VNET_BUFFER_OFFLOAD_F_IP_CKSUM标志时,VPP会将该标志转换为RTE_MBUF_F_TX_IP_CKSUM,并设置相应的头部长度信息。网卡硬件会根据这些信息计算IP头部的校验和,并将其写入IP头部的checksum字段。就像"快递发送中心"的"自动校验系统",让"自动化设备"(网卡)自动计算和填写"包裹校验码"(IP校验和)。
IP校验和卸载标志设置 (src/plugins/dpdk/device/device.c:211):
c
/* 在dpdk_buffer_tx_offload函数中,提取IP校验和卸载标志 */
oflags = vnet_buffer (b)->oflags;
ip_cksum = oflags & VNET_BUFFER_OFFLOAD_F_IP_CKSUM; // 检查是否有IP校验和卸载需求
/* 设置DPDK mbuf标志 */
ol_flags = is_ip4 ? RTE_MBUF_F_TX_IPV4 : RTE_MBUF_F_TX_IPV6; // IP版本标志
ol_flags |= ip_cksum ? RTE_MBUF_F_TX_IP_CKSUM : 0; // IP校验和卸载标志
IP校验和卸载的工作原理:
数据包准备发送
│
├── 检查是否有VNET_BUFFER_OFFLOAD_F_IP_CKSUM标志
│
▼
设置RTE_MBUF_F_TX_IP_CKSUM标志
│
├── 设置IP版本标志(RTE_MBUF_F_TX_IPV4)
├── 设置IP校验和卸载标志(RTE_MBUF_F_TX_IP_CKSUM)
└── 设置头部长度信息(l2_len、l3_len)
│
▼
发送到网卡
│
▼
网卡硬件处理
│
├── 根据l2_len和l3_len定位IP头部
├── 计算IP头部校验和
└── 将校验和写入IP头部的checksum字段
头部长度的作用:
网卡硬件需要知道IP头部的位置才能计算校验和。通过设置l2_len和l3_len,网卡可以正确定位IP头部:
c
/* 在非隧道数据包中 */
mb->l2_len = vnet_buffer (b)->l3_hdr_offset - b->current_data; // L2头部长度
mb->l3_len = vnet_buffer (b)->l4_hdr_offset - vnet_buffer (b)->l3_hdr_offset; // L3头部长度
/* 网卡硬件使用这些信息:
* - IP头部位置 = data + l2_len
* - IP头部长度 = l3_len
* - 校验和计算范围 = IP头部(不包括checksum字段本身)
*/
IP校验和卸载的优势:
- 降低CPU开销:校验和计算由硬件完成,不需要CPU参与
- 提高吞吐量:硬件计算比软件计算快得多
- 保持正确性:硬件计算保证校验和正确
关键设计要点:
- 仅支持IPv4:IP校验和卸载仅适用于IPv4数据包(IPv6没有IP层校验和)
- 头部长度设置:需要正确设置l2_len和l3_len,让硬件定位IP头部
- 标志组合:IP校验和卸载通常与其他卸载标志组合使用
通俗理解:
就像"快递发送中心"的"自动校验系统":
- 标记识别:检查"包裹"是否有"需要自动校验"的标记(VNET_BUFFER_OFFLOAD_F_IP_CKSUM)
- 设置设备标记:将"标记"转换为"设备能理解的标记"(RTE_MBUF_F_TX_IP_CKSUM)
- 提供位置信息:设置"校验码位置"(l2_len、l3_len),让"设备"知道"在哪里计算校验码"
- 自动计算:设备自动计算"校验码"(IP校验和),并填写到"指定位置"(IP头部checksum字段)
17.4 L4校验和卸载
作用和实现原理:
L4校验和卸载允许网卡硬件计算和验证TCP或UDP数据包的校验和。当设置了VNET_BUFFER_OFFLOAD_F_TCP_CKSUM或VNET_BUFFER_OFFLOAD_F_UDP_CKSUM标志时,VPP会将这些标志转换为相应的DPDK标志,并设置头部长度信息。网卡硬件会根据这些信息计算L4头部的校验和,包括伪头部校验和。就像"快递发送中心"的"自动校验系统",让"自动化设备"(网卡)自动计算和填写"L4层校验码"(TCP/UDP校验和)。
L4校验和卸载标志设置 (src/plugins/dpdk/device/device.c:212):
c
/* 在dpdk_buffer_tx_offload函数中,提取L4校验和卸载标志 */
oflags = vnet_buffer (b)->oflags;
tcp_cksum = oflags & VNET_BUFFER_OFFLOAD_F_TCP_CKSUM; // TCP校验和卸载标志
udp_cksum = oflags & VNET_BUFFER_OFFLOAD_F_UDP_CKSUM; // UDP校验和卸载标志
/* 设置DPDK mbuf标志 */
ol_flags |= tcp_cksum ? RTE_MBUF_F_TX_TCP_CKSUM : 0; // TCP校验和卸载标志
ol_flags |= udp_cksum ? RTE_MBUF_F_TX_UDP_CKSUM : 0; // UDP校验和卸载标志
L4校验和卸载的工作原理:
数据包准备发送
│
├── 检查是否有L4校验和卸载标志
│ ├── VNET_BUFFER_OFFLOAD_F_TCP_CKSUM → RTE_MBUF_F_TX_TCP_CKSUM
│ └── VNET_BUFFER_OFFLOAD_F_UDP_CKSUM → RTE_MBUF_F_TX_UDP_CKSUM
│
▼
设置DPDK标志和头部长度
│
├── 设置L4校验和卸载标志
├── 设置头部长度信息(l2_len、l3_len、l4_len)
└── 设置IP版本标志(IPv4/IPv6)
│
▼
发送到网卡
│
▼
网卡硬件处理
│
├── 根据头部长度定位L4头部
├── 计算伪头部校验和(IP头部信息)
├── 计算L4头部和数据的校验和
└── 将校验和写入L4头部的checksum字段
头部长度的作用:
网卡硬件需要知道L4头部的位置和长度才能计算校验和:
c
/* 在非隧道数据包中 */
mb->l2_len = vnet_buffer (b)->l3_hdr_offset - b->current_data; // L2头部长度
mb->l3_len = vnet_buffer (b)->l4_hdr_offset - vnet_buffer (b)->l3_hdr_offset; // L3头部长度
/* l4_len在TSO时设置,用于TSO分段 */
/* 网卡硬件使用这些信息:
* - L4头部位置 = data + l2_len + l3_len
* - L4头部长度 = l4_len(如果设置了)
* - 伪头部校验和 = IP头部信息(源IP、目标IP、协议、长度等)
* - L4校验和 = 伪头部 + L4头部 + L4数据
*/
TCP vs UDP校验和:
c
/* TCP校验和卸载 */
if (tcp_cksum)
ol_flags |= RTE_MBUF_F_TX_TCP_CKSUM;
/* UDP校验和卸载 */
if (udp_cksum)
ol_flags |= RTE_MBUF_F_TX_UDP_CKSUM;
/* 注意:TCP和UDP不能同时设置,因为它们是互斥的 */
L4校验和卸载的优势:
- 降低CPU开销:校验和计算由硬件完成,不需要CPU参与
- 提高吞吐量:硬件计算比软件计算快得多
- 支持大数据包:硬件可以高效处理大数据包的校验和计算
关键设计要点:
- 协议区分:TCP和UDP使用不同的标志,需要正确区分
- 头部长度设置:需要正确设置头部长度,让硬件定位L4头部
- 伪头部处理:L4校验和包括伪头部,硬件需要IP头部信息
通俗理解:
就像"快递发送中心"的"自动校验系统":
- 协议识别:识别"包裹类型"(TCP或UDP),设置相应的"校验标记"
- 设置设备标记:将"标记"转换为"设备能理解的标记"(RTE_MBUF_F_TX_TCP_CKSUM或RTE_MBUF_F_TX_UDP_CKSUM)
- 提供位置信息:设置"校验码位置"(l2_len、l3_len等),让"设备"知道"在哪里计算校验码"
- 自动计算:设备自动计算"校验码"(包括伪头部校验和),并填写到"指定位置"(TCP/UDP头部checksum字段)
17.5 TSO(TCP Segmentation Offload)处理
作用和实现原理:
TSO(TCP Segmentation Offload)允许网卡硬件将大的TCP/UDP数据包分割成多个符合MTU要求的小段。当设置了VNET_BUFFER_F_GSO标志时,VPP会设置TSO相关的标志和参数(l4_len、tso_segsz),让网卡硬件进行分段。TSO可以减少CPU分段开销,提高发送性能。就像"快递发送中心"的"自动分段系统",让"自动化设备"(网卡)自动将"大包裹"分割成"符合运输要求的小包裹"。
TSO标志检查 (src/plugins/dpdk/device/device.c:200):
c
/* 在dpdk_buffer_tx_offload函数中,检查GSO(TSO)标志 */
u32 tso = b->flags & VNET_BUFFER_F_GSO; // GSO标志,用于TSO
/* 快速路径检查 */
if (PREDICT_TRUE (((b->flags & VNET_BUFFER_F_OFFLOAD) | tso) == 0))
return; // 没有卸载需求(包括TSO),直接返回
TSO处理代码 (src/plugins/dpdk/device/device.c:247):
c
/* 在dpdk_buffer_tx_offload函数中,处理TSO */
if (tso)
{
/* 步骤1:设置L4头部长度 */
/* 用于TSO分段时保持L4头部不变 */
mb->l4_len = vnet_buffer2 (b)->gso_l4_hdr_sz;
/* 步骤2:设置TSO段大小(每个段的payload大小,不包括头部) */
mb->tso_segsz = vnet_buffer2 (b)->gso_size;
/* 步骤3:计算最大包长度(用于判断是否需要TSO) */
/* max_pkt_len = 外层头部 + L2头部 + L3头部 + L4头部 + TSO段大小 */
max_pkt_len =
outer_hdr_len + mb->l2_len + mb->l3_len + mb->l4_len + mb->tso_segsz;
/* 步骤4:如果数据包长度大于最大包长度,说明需要TSO分段 */
if (mb->tso_segsz != 0 && mb->pkt_len > max_pkt_len)
{
/* 根据L4协议类型设置TSO标志 */
ol_flags |=
(tcp_cksum ? RTE_MBUF_F_TX_TCP_SEG : RTE_MBUF_F_TX_UDP_SEG);
}
}
TSO工作原理:
数据包准备发送(带有GSO标志)
│
├── 检查是否有VNET_BUFFER_F_GSO标志
│
▼
设置TSO参数
│
├── l4_len = gso_l4_hdr_sz(L4头部长度)
├── tso_segsz = gso_size(每个段的payload大小)
└── 计算max_pkt_len(最大包长度)
│
▼
判断是否需要TSO
│
├── 如果pkt_len > max_pkt_len → 需要TSO分段
│ └── 设置RTE_MBUF_F_TX_TCP_SEG或RTE_MBUF_F_TX_UDP_SEG
└── 如果pkt_len <= max_pkt_len → 不需要TSO分段
│
▼
发送到网卡
│
▼
网卡硬件处理(如果设置了TSO标志)
│
├── 将大包分割成多个小段
├── 每个段的payload大小 = tso_segsz
├── 复制L2+L3+L4头部到每个段
├── 更新每个段的IP长度字段
├── 更新每个段的TCP序列号和校验和
└── 发送所有段
TSO参数说明:
- l4_len:L4头部长度,用于TSO分段时保持L4头部不变
- tso_segsz:每个段的payload大小(不包括头部),通常等于MSS
- max_pkt_len:最大包长度,如果数据包长度超过此值,需要TSO分段
TSO分段示例:
假设有一个9000字节的TCP数据包,MSS=1460:
原始数据包:
[L2头部(14)] [IP头部(20)] [TCP头部(20)] [数据(8946)]
总长度 = 9000字节
TSO参数:
l2_len = 14
l3_len = 20
l4_len = 20
tso_segsz = 1460
max_pkt_len = 14 + 20 + 20 + 1460 = 1514
网卡硬件分段:
段1:[L2(14)] [IP(20)] [TCP(20)] [数据(1460)] → 1514字节
段2:[L2(14)] [IP(20)] [TCP(20)] [数据(1460)] → 1514字节
...
段6:[L2(14)] [IP(20)] [TCP(20)] [数据(1146)] → 1200字节
每个段:
- 头部相同(L2+L3+L4)
- IP长度字段不同
- TCP序列号不同
- TCP校验和不同(硬件计算)
关键设计要点:
- 条件判断:只有当数据包长度超过max_pkt_len时才设置TSO标志
- 协议区分:TCP和UDP使用不同的TSO标志
- 参数设置:需要正确设置l4_len和tso_segsz
通俗理解:
就像"快递发送中心"的"自动分段系统":
- 识别大包裹:检查"包裹"是否有"需要分段"的标记(VNET_BUFFER_F_GSO)
- 设置分段参数:设置"每个小包裹的大小"(tso_segsz)和"标签大小"(l4_len)
- 判断是否需要分段:如果"包裹"超过"最大运输尺寸"(max_pkt_len),需要分段
- 自动分段:设备自动将"大包裹"分割成"符合运输要求的小包裹",并更新"每个小包裹的信息"(序列号、校验和等)
17.6 VXLAN隧道卸载
作用和实现原理:
VXLAN隧道卸载允许网卡硬件处理VXLAN隧道的封装和校验和计算。当设置了VNET_BUFFER_OFFLOAD_F_TNL_VXLAN标志时,VPP会设置VXLAN隧道相关的标志和头部长度信息(outer_l2_len、outer_l3_len等)。网卡硬件可以根据这些信息正确处理VXLAN隧道数据包的封装和校验和。就像"快递发送中心"的"自动封装系统",让"自动化设备"(网卡)自动处理"外层包装"(VXLAN隧道)的"封装和校验"。
VXLAN隧道卸载标志设置 (src/plugins/dpdk/device/device.c:215):
c
/* 在dpdk_buffer_tx_offload函数中,提取VXLAN隧道标志 */
oflags = vnet_buffer (b)->oflags;
vxlan_tunnel = oflags & VNET_BUFFER_OFFLOAD_F_TNL_VXLAN; // VXLAN隧道标志
outer_ip_cksum = oflags & VNET_BUFFER_OFFLOAD_F_OUTER_IP_CKSUM; // 外层IP校验和标志
VXLAN隧道处理代码 (src/plugins/dpdk/device/device.c:222):
c
/* 在dpdk_buffer_tx_offload函数中,处理VXLAN隧道 */
if (vxlan_tunnel)
{
/* 步骤1:设置外层IP版本和校验和标志 */
ol_flags |= outer_ip_cksum ?
RTE_MBUF_F_TX_OUTER_IPV4 | RTE_MBUF_F_TX_OUTER_IP_CKSUM :
RTE_MBUF_F_TX_OUTER_IPV6;
ol_flags |= RTE_MBUF_F_TX_TUNNEL_VXLAN; // VXLAN隧道标志
/* 步骤2:计算内层头部长度 */
/* l2_len:内层L2头部长度(从外层L4结束到内层L3开始) */
mb->l2_len =
vnet_buffer (b)->l3_hdr_offset - vnet_buffer2 (b)->outer_l4_hdr_offset;
/* l3_len:内层L3头部长度(从内层L3开始到内层L4开始) */
mb->l3_len =
vnet_buffer (b)->l4_hdr_offset - vnet_buffer (b)->l3_hdr_offset;
/* 步骤3:计算外层头部长度 */
/* outer_l2_len:外层L2头部长度(从数据包开始到外层L3开始) */
mb->outer_l2_len =
vnet_buffer2 (b)->outer_l3_hdr_offset - b->current_data;
/* outer_l3_len:外层L3头部长度(从外层L3开始到外层L4开始) */
mb->outer_l3_len = vnet_buffer2 (b)->outer_l4_hdr_offset -
vnet_buffer2 (b)->outer_l3_hdr_offset;
outer_hdr_len = mb->outer_l2_len + mb->outer_l3_len; // 外层头部总长度
}
VXLAN数据包结构:
VXLAN数据包结构:
┌─────────────────────────────────────────┐
│ 外层L2头部(Ethernet) │ ← outer_l2_len
├─────────────────────────────────────────┤
│ 外层L3头部(IP) │ ← outer_l3_len
├─────────────────────────────────────────┤
│ 外层L4头部(UDP + VXLAN) │
├─────────────────────────────────────────┤
│ 内层L2头部(Ethernet) │ ← l2_len
├─────────────────────────────────────────┤
│ 内层L3头部(IP) │ ← l3_len
├─────────────────────────────────────────┤
│ 内层L4头部(TCP/UDP) │ ← l4_len
├─────────────────────────────────────────┤
│ 内层数据 │
└─────────────────────────────────────────┘
头部长度计算详解:
c
/* 外层头部长度 */
outer_l2_len = outer_l3_hdr_offset - current_data
/* 从数据包开始到外层L3头部开始的距离 */
/* 包括:Ethernet头部 + VLAN标签(如果有) */
outer_l3_len = outer_l4_hdr_offset - outer_l3_hdr_offset
/* 从外层L3头部开始到外层L4头部开始的距离 */
/* 包括:IP头部 */
/* 内层头部长度 */
l2_len = l3_hdr_offset - outer_l4_hdr_offset
/* 从外层L4头部结束到内层L3头部开始的距离 */
/* 包括:VXLAN头部 + 内层Ethernet头部 */
l3_len = l4_hdr_offset - l3_hdr_offset
/* 从内层L3头部开始到内层L4头部开始的距离 */
/* 包括:内层IP头部 */
VXLAN隧道卸载的优势:
- 硬件加速:隧道封装和校验和计算由硬件完成
- 降低CPU开销:减少CPU处理隧道数据包的开销
- 提高吞吐量:硬件处理比软件处理快得多
关键设计要点:
- 头部长度区分:需要区分外层头部和内层头部长度
- 校验和卸载:外层IP校验和需要单独处理
- TSO支持:VXLAN隧道卸载可以与TSO组合使用
通俗理解:
就像"快递发送中心"的"自动封装系统":
- 识别隧道包裹:检查"包裹"是否有"需要隧道封装"的标记(VNET_BUFFER_OFFLOAD_F_TNL_VXLAN)
- 设置设备标记:将"标记"转换为"设备能理解的标记"(RTE_MBUF_F_TX_TUNNEL_VXLAN)
- 提供位置信息:设置"外层包装"和"内层包裹"的位置信息(outer_l2_len、outer_l3_len、l2_len、l3_len等)
- 自动封装:设备自动处理"外层包装"的"封装和校验",包括"外层校验码"的计算
17.7 头部长度计算(l2_len、l3_len、l4_len)
作用和实现原理:
头部长度信息是硬件卸载的关键,网卡硬件需要知道各个协议层头部的位置和长度才能正确执行卸载操作。VPP通过计算和设置l2_len、l3_len、l4_len、outer_l2_len、outer_l3_len等字段,为网卡硬件提供头部位置信息。这些长度基于buffer中的头部偏移量计算。就像"快递发送中心"的"位置标记系统",为"自动化设备"标记"各个部分的位置",让设备知道"在哪里处理"。
头部长度字段说明:
- l2_len:L2(以太网)头部长度
- l3_len:L3(IP)头部长度
- l4_len:L4(TCP/UDP)头部长度(主要用于TSO)
- outer_l2_len:外层L2头部长度(隧道)
- outer_l3_len:外层L3头部长度(隧道)
非隧道数据包的头部长度计算 (src/plugins/dpdk/device/device.c:240):
c
/* 在dpdk_buffer_tx_offload函数中,非隧道数据包的头部长度计算 */
else
{
/* l2_len:L2头部长度(从数据包开始到L3头部开始) */
mb->l2_len = vnet_buffer (b)->l3_hdr_offset - b->current_data;
/* 包括:Ethernet头部 + VLAN标签(如果有) */
/* l3_len:L3头部长度(从L3头部开始到L4头部开始) */
mb->l3_len =
vnet_buffer (b)->l4_hdr_offset - vnet_buffer (b)->l3_hdr_offset;
/* 包括:IP头部 */
/* 无外层头部 */
mb->outer_l2_len = 0;
mb->outer_l3_len = 0;
}
VXLAN隧道数据包的头部长度计算 (src/plugins/dpdk/device/device.c:228):
c
/* 在dpdk_buffer_tx_offload函数中,VXLAN隧道数据包的头部长度计算 */
if (vxlan_tunnel)
{
/* 内层头部长度 */
/* l2_len:内层L2头部长度(从外层L4结束到内层L3开始) */
mb->l2_len =
vnet_buffer (b)->l3_hdr_offset - vnet_buffer2 (b)->outer_l4_hdr_offset;
/* 包括:VXLAN头部 + 内层Ethernet头部 */
/* l3_len:内层L3头部长度(从内层L3开始到内层L4开始) */
mb->l3_len =
vnet_buffer (b)->l4_hdr_offset - vnet_buffer (b)->l3_hdr_offset;
/* 包括:内层IP头部 */
/* 外层头部长度 */
/* outer_l2_len:外层L2头部长度(从数据包开始到外层L3开始) */
mb->outer_l2_len =
vnet_buffer2 (b)->outer_l3_hdr_offset - b->current_data;
/* 包括:外层Ethernet头部 + VLAN标签(如果有) */
/* outer_l3_len:外层L3头部长度(从外层L3开始到外层L4开始) */
mb->outer_l3_len = vnet_buffer2 (b)->outer_l4_hdr_offset -
vnet_buffer2 (b)->outer_l3_hdr_offset;
/* 包括:外层IP头部 */
outer_hdr_len = mb->outer_l2_len + mb->outer_l3_len;
}
L4头部长度的设置 (src/plugins/dpdk/device/device.c:249):
c
/* 在TSO处理中,设置L4头部长度 */
if (tso)
{
/* l4_len:L4头部长度(用于TSO分段) */
mb->l4_len = vnet_buffer2 (b)->gso_l4_hdr_sz;
/* 包括:TCP/UDP头部(包括选项) */
}
头部偏移量的来源:
头部偏移量存储在buffer的元数据中:
c
/* 在vnet_buffer_opaque_t结构体中 */
i16 l2_hdr_offset; // L2头部偏移量(从buffer->data开始)
i16 l3_hdr_offset; // L3头部偏移量(从buffer->data开始)
i16 l4_hdr_offset; // L4头部偏移量(从buffer->data开始)
/* 在vnet_buffer_opaque2_t结构体中(隧道) */
i16 outer_l3_hdr_offset; // 外层L3头部偏移量
i16 outer_l4_hdr_offset; // 外层L4头部偏移量
头部长度计算示例:
非隧道IPv4 TCP数据包:
数据包结构:
[Ethernet(14)] [IP(20)] [TCP(32)] [Data...]
↑ ↑ ↑
current_data l3_hdr l4_hdr
计算:
l2_len = l3_hdr_offset - current_data = 14
l3_len = l4_hdr_offset - l3_hdr_offset = 20
l4_len = 32(TSO时设置)
VXLAN隧道IPv4 TCP数据包:
数据包结构:
[Outer Ethernet(14)] [Outer IP(20)] [UDP(8)] [VXLAN(8)] [Inner Ethernet(14)] [Inner IP(20)] [TCP(32)] [Data...]
↑ ↑ ↑ ↑ ↑ ↑ ↑
current_data outer_l3_hdr outer_l4_hdr l3_hdr_offset l4_hdr_offset
计算:
outer_l2_len = outer_l3_hdr_offset - current_data = 14
outer_l3_len = outer_l4_hdr_offset - outer_l3_hdr_offset = 20
l2_len = l3_hdr_offset - outer_l4_hdr_offset = 8 + 14 = 22(VXLAN + Inner Ethernet)
l3_len = l4_hdr_offset - l3_hdr_offset = 20
l4_len = 32(TSO时设置)
关键设计要点:
- 偏移量基础:头部长度基于buffer中的头部偏移量计算
- 隧道区分:隧道数据包需要区分外层和内层头部长度
- L4长度特殊:L4头部长度主要在TSO时设置,用于分段
通俗理解:
就像"快递发送中心"的"位置标记系统":
- 标记位置:为"包裹"的"各个部分"标记"位置"(头部偏移量)
- 计算长度:根据"位置标记"计算"各个部分的长度"(l2_len、l3_len等)
- 提供信息:将"长度信息"提供给"自动化设备",让设备知道"在哪里处理"
- 隧道区分:对于"双层包裹"(隧道),需要区分"外层"和"内层"的位置和长度
17.8 本章总结
TX硬件卸载处理核心流程总结:
| 步骤 | 功能 | 关键字段/标志 | 作用 |
|---|---|---|---|
| 1. 检查卸载需求 | 快速路径检查 | VNET_BUFFER_F_OFFLOAD、VNET_BUFFER_F_GSO |
判断是否有卸载需求 |
| 2. 提取VPP标志 | 提取卸载标志 | vnet_buffer(b)->oflags |
获取VPP卸载标志 |
| 3. 设置基本标志 | 设置IP版本和校验和标志 | RTE_MBUF_F_TX_IPV4/IPV6、RTE_MBUF_F_TX_IP_CKSUM等 |
转换为DPDK标志 |
| 4. 处理隧道 | 设置隧道标志和头部长度 | RTE_MBUF_F_TX_TUNNEL_VXLAN、outer_l2_len等 |
处理隧道卸载 |
| 5. 处理TSO | 设置TSO标志和参数 | RTE_MBUF_F_TX_TCP_SEG、l4_len、tso_segsz |
处理分段卸载 |
| 6. 计算头部长度 | 计算各种头部长度 | l2_len、l3_len、l4_len等 |
为硬件提供位置信息 |
| 7. 设置mbuf标志 | 应用标志到mbuf | mb->ol_flags |
指示硬件执行卸载 |
关键性能优化技术:
- 快速路径:如果没有卸载需求,直接返回,避免不必要的处理
- 硬件加速:利用硬件执行校验和计算、分段等操作,降低CPU开销
- 批量处理:在批量处理循环中处理卸载标志,提高效率
- 条件处理:根据数据包类型和特征进行条件处理
相关源码文件:
src/plugins/dpdk/device/device.c:196-dpdk_buffer_tx_offload函数src/plugins/dpdk/device/device.c:333- TX卸载处理入口src/vnet/buffer.h:110- VPP卸载标志定义src/plugins/dpdk/device/dpdk_priv.h:148- DPDK mbuf标志定义
第18章:发送队列管理
本章概述:
第18章详细讲解DPDK插件中发送队列管理的核心机制。发送队列是数据包发送的关键资源,需要正确分配、配置和管理。VPP支持多队列发送,可以实现负载均衡和提高并发性能。队列与线程的绑定关系决定了数据包的发送路径,共享队列和独占队列的选择影响性能和线程安全。本章将详细讲解发送队列的分配、队列与线程的绑定、共享队列和独占队列的区别、队列锁定机制以及发送失败处理。就像"快递发送中心"的"发送队列管理",为"每个窗口"(线程)分配"发送队列",支持"独占队列"和"共享队列",确保"发送安全"和"高效"。
通俗理解:
就像"快递发送中心"的"发送队列管理":
- 队列分配:为"每个发送窗口"分配"发送队列",用于"排队发送包裹"
- 线程绑定:将"队列"绑定到"特定窗口"(线程),确定"哪些窗口可以使用哪些队列"
- 共享与独占:支持"独占队列"(一个窗口专用)和"共享队列"(多个窗口共用)
- 队列锁定:对于"共享队列",使用"锁"保护,确保"并发安全"
- 失败处理:处理"发送失败"的情况,更新"统计信息"并"释放资源"
18.1 发送队列的分配和配置
作用和实现原理:
发送队列的分配和配置是DPDK设备初始化的重要步骤。DPDK设备需要分配TX队列数组,为每个队列调用DPDK的rte_eth_tx_queue_setup创建队列,并初始化队列锁。队列配置包括队列数量、队列大小(描述符数量)等。就像"快递发送中心"的"队列分配",为"每个发送窗口"分配"发送队列",设置"队列大小"等参数。
队列结构定义 (src/plugins/dpdk/device/dpdk.h:106):
c
/**
* DPDK TX队列结构
*/
typedef struct
{
CLIB_CACHE_LINE_ALIGN_MARK (cacheline0); // 缓存行对齐标记
clib_spinlock_t lock; // 队列锁(用于共享队列)
u32 queue_index; // 队列索引(VPP内部索引)
} dpdk_tx_queue_t;
队列分配代码 (src/plugins/dpdk/device/common.c:206):
c
/* 在dpdk_device_setup函数中,分配和配置TX队列 */
/* 步骤1:分配TX队列数组 */
vec_validate_aligned (xd->tx_queues, xd->conf.n_tx_queues - 1,
CLIB_CACHE_LINE_BYTES);
/* 步骤2:为每个队列调用DPDK的队列设置函数 */
for (j = 0; j < xd->conf.n_tx_queues; j++)
{
/* 调用DPDK的TX队列设置函数 */
/* 参数:
* - port_id: 端口ID
* - queue_id: 队列ID(0到n_tx_queues-1)
* - n_tx_desc: 队列大小(描述符数量)
* - socket_id: NUMA节点ID(用于内存分配)
* - conf: 队列配置(NULL表示使用默认配置)
*/
rv = rte_eth_tx_queue_setup (xd->port_id, j, xd->conf.n_tx_desc,
xd->cpu_socket, 0);
/* 如果失败,尝试使用任意NUMA节点 */
if (rv < 0)
rv = rte_eth_tx_queue_setup (xd->port_id, j, xd->conf.n_tx_desc,
SOCKET_ID_ANY, 0);
if (rv < 0)
dpdk_device_error (xd, "rte_eth_tx_queue_setup", rv);
/* 初始化队列锁 */
clib_spinlock_init (&vec_elt (xd->tx_queues, j).lock);
}
队列注册 (src/plugins/dpdk/device/init.c:571):
c
/* 在设备初始化时,注册TX队列到VPP */
for (q = 0; q < xd->conf.n_tx_queues; q++)
{
dpdk_tx_queue_t *txq = vec_elt_at_index (xd->tx_queues, q);
/* 注册TX队列到VPP,获取队列索引 */
txq->queue_index =
vnet_hw_if_register_tx_queue (vnm, xd->hw_if_index, q);
}
vnet_hw_if_register_tx_queue函数 (src/vnet/interface/tx_queue.c:35):
c
/**
* 注册TX队列到VPP
*
* @param vnm VPP主结构体指针
* @param hw_if_index 硬件接口索引
* @param queue_id 队列ID
*
* @return 队列索引(VPP内部索引)
*/
u32
vnet_hw_if_register_tx_queue (vnet_main_t *vnm, u32 hw_if_index, u32 queue_id)
{
vnet_interface_main_t *im = &vnm->interface_main;
vnet_hw_interface_t *hi = vnet_get_hw_interface (vnm, hw_if_index);
vnet_hw_if_tx_queue_t *txq;
u64 key = tx_queue_key (hw_if_index, queue_id); // 生成唯一键
u32 queue_index;
/* 检查队列是否已经注册 */
if (hash_get_mem (im->txq_index_by_hw_if_index_and_queue_id, &key))
clib_panic ("Trying to register already registered queue id (%u) in the "
"interface %v\n",
queue_id, hi->name);
/* 分配队列结构 */
pool_get_zero (im->hw_if_tx_queues, txq);
queue_index = txq - im->hw_if_tx_queues;
/* 添加到接口的队列索引列表 */
vec_add1 (hi->tx_queue_indices, queue_index);
/* 添加到哈希表,用于快速查找 */
hash_set_mem_alloc (&im->txq_index_by_hw_if_index_and_queue_id, &key,
queue_index);
/* 设置队列信息 */
txq->hw_if_index = hw_if_index;
txq->queue_id = queue_id;
log_debug ("register: interface %v queue-id %u", hi->name, queue_id);
return queue_index;
}
vnet_hw_if_tx_queue_t结构 (src/vnet/interface.h:632):
c
/**
* VPP硬件接口TX队列结构
*/
typedef struct
{
u8 shared_queue : 1; // 是否为共享队列标志
u32 hw_if_index; // 硬件接口索引
u32 queue_id; // 队列ID(硬件队列ID)
clib_bitmap_t *threads; // 使用该队列的线程位图
} vnet_hw_if_tx_queue_t;
关键设计要点:
- 缓存行对齐:队列结构使用缓存行对齐,避免false sharing
- 双重索引:队列有队列ID(硬件ID)和队列索引(VPP内部索引)
- 哈希查找:使用哈希表快速查找队列索引
通俗理解:
就像"快递发送中心"的"队列分配":
- 分配队列:为"每个发送窗口"分配"发送队列"(dpdk_tx_queue_t)
- 配置队列:调用"设备接口"(rte_eth_tx_queue_setup)配置"队列大小"等参数
- 注册队列:将"队列"注册到"系统"(VPP),获取"系统索引"(queue_index)
- 初始化锁:为"队列"初始化"锁"(用于共享队列的保护)
18.2 队列与线程的绑定关系
作用和实现原理:
队列与线程的绑定关系决定了哪些线程可以使用哪些队列。VPP通过vnet_hw_if_tx_queue_assign_thread函数将队列绑定到线程,维护一个线程位图。绑定关系用于interface-output节点选择发送队列。就像"快递发送中心"的"队列绑定",将"队列"绑定到"特定窗口"(线程),确定"哪些窗口可以使用哪些队列"。
队列与线程的绑定 (src/plugins/dpdk/device/init.c:578):
c
/* 在设备初始化时,将队列绑定到线程 */
/* 默认策略:轮询分配,线程ID对队列数取模 */
for (q = 0; q < tm->n_vlib_mains; q++) // 遍历所有线程
{
/* 选择队列:线程ID对队列数取模 */
u32 qi = xd->tx_queues[q % xd->conf.n_tx_queues].queue_index;
/* 将队列绑定到线程 */
vnet_hw_if_tx_queue_assign_thread (vnm, qi, q);
}
vnet_hw_if_tx_queue_assign_thread函数 (src/vnet/interface/tx_queue.c:108):
c
/**
* 将TX队列绑定到线程
*
* @param vnm VPP主结构体指针
* @param queue_index 队列索引(VPP内部索引)
* @param thread_index 线程索引
*/
void
vnet_hw_if_tx_queue_assign_thread (vnet_main_t *vnm, u32 queue_index,
u32 thread_index)
{
vnet_hw_if_tx_queue_t *txq = vnet_hw_if_get_tx_queue (vnm, queue_index);
vnet_hw_interface_t *hi = vnet_get_hw_interface (vnm, txq->hw_if_index);
/* 在线程位图中设置该线程 */
txq->threads = clib_bitmap_set (txq->threads, thread_index, 1);
/* 如果多个线程使用该队列,标记为共享队列 */
if (clib_bitmap_count_set_bits (txq->threads) > 1)
txq->shared_queue = 1;
log_debug (
"assign_thread: interface %v queue-id %u thread %u queue-shared %s",
hi->name, txq->queue_id, thread_index,
(txq->shared_queue == 1 ? "yes" : "no"));
}
绑定策略示例:
假设有4个线程和2个队列:
线程0 → 队列0 (0 % 2 = 0)
线程1 → 队列1 (1 % 2 = 1)
线程2 → 队列0 (2 % 2 = 0) → 队列0成为共享队列
线程3 → 队列1 (3 % 2 = 1) → 队列1成为共享队列
最终结果:
队列0:线程位图 = [0, 2],shared_queue = 1(共享)
队列1:线程位图 = [1, 3],shared_queue = 1(共享)
假设有2个线程和4个队列:
线程0 → 队列0 (0 % 4 = 0)
线程1 → 队列1 (1 % 4 = 1)
最终结果:
队列0:线程位图 = [0],shared_queue = 0(独占)
队列1:线程位图 = [1],shared_queue = 0(独占)
队列2:未绑定任何线程
队列3:未绑定任何线程
绑定关系的使用:
interface-output节点使用绑定关系选择发送队列:
c
/* 在interface-output节点中,根据线程选择队列 */
/* 使用哈希函数计算数据包的流哈希值 */
u32 hash = vnet_flow_hash (b, ...);
/* 根据哈希值选择队列 */
u32 queue_id = lookup_table[hash % n_queues];
/* 选择的队列必须是当前线程绑定的队列之一 */
关键设计要点:
- 轮询分配:默认使用轮询策略分配队列到线程
- 动态绑定:绑定关系可以在运行时修改
- 位图管理:使用位图高效管理线程绑定关系
通俗理解:
就像"快递发送中心"的"队列绑定":
- 绑定策略:采用"轮询分配"策略,将"队列"按顺序绑定到"线程"(线程ID对队列数取模)
- 位图记录:使用"位图"记录"哪些线程使用该队列"
- 共享判断:如果"多个线程"使用"同一个队列",标记为"共享队列"
- 灵活使用:interface-output节点根据"绑定关系"选择"发送队列"
18.3 共享队列和独占队列
作用和实现原理:
共享队列和独占队列是VPP支持多线程发送的两种模式。独占队列(shared_queue = 0)只能由一个线程访问,无需加锁,性能更高。共享队列(shared_queue = 1)可以由多个线程访问,需要加锁保护,性能稍低但更灵活。队列的共享状态是根据线程绑定关系动态确定的。就像"快递发送中心"的"队列类型","独占队列"是"一个窗口专用","共享队列"是"多个窗口共用"。
共享队列的判断 (src/vnet/interface/tx_queue.c:115):
c
/* 在vnet_hw_if_tx_queue_assign_thread函数中,判断是否为共享队列 */
/* 如果多个线程使用该队列,标记为共享队列 */
if (clib_bitmap_count_set_bits (txq->threads) > 1)
txq->shared_queue = 1;
独占队列的判断 (src/vnet/interface/tx_queue.c:130):
c
/* 在vnet_hw_if_tx_queue_unassign_thread函数中,判断是否为独占队列 */
/* 如果只有一个线程使用该队列,标记为独占队列 */
if (clib_bitmap_count_set_bits (txq->threads) < 2)
txq->shared_queue = 0;
共享队列vs独占队列对比:
| 特性 | 独占队列 | 共享队列 |
|---|---|---|
| 线程数量 | 1个线程 | 多个线程 |
| 锁保护 | 无需锁 | 需要锁 |
| 性能 | 更高(无锁竞争) | 稍低(有锁竞争) |
| 灵活性 | 低(固定线程) | 高(多线程可用) |
| 使用场景 | 队列数 >= 线程数 | 队列数 < 线程数 |
共享队列的使用 (src/plugins/dpdk/device/device.c:284):
c
/* 在TX函数中,获取共享队列标志 */
vnet_hw_if_tx_frame_t *tf = vlib_frame_scalar_args (f);
u8 is_shared = tf->shared_queue; // 从frame的标量参数中获取
/* 在tx_burst_vector_internal中使用 */
if (is_shared)
clib_spinlock_lock (&txq->lock); // 共享队列需要加锁
独占队列的使用:
c
/* 独占队列无需加锁,直接发送 */
n_sent = rte_eth_tx_burst (xd->port_id, queue_id, mb, n_left);
/* 无需锁保护 */
队列共享状态的变化:
初始状态:
队列0:threads = [],shared_queue = 0(独占,但未绑定)
绑定线程0:
队列0:threads = [0],shared_queue = 0(独占)
绑定线程1:
队列0:threads = [0, 1],shared_queue = 1(共享)
取消绑定线程0:
队列0:threads = [1],shared_queue = 0(独占)
关键设计要点:
- 动态判断:队列的共享状态是根据线程绑定关系动态判断的
- 性能优化:独占队列无需锁,性能更高
- 灵活支持:支持队列数少于线程数的情况(共享队列)
通俗理解:
就像"快递发送中心"的"队列类型":
- 独占队列:一个"窗口"(线程)专用,无需"加锁",效率更高
- 共享队列:多个"窗口"共用,需要"加锁"保护,确保"并发安全"
- 动态判断:根据"绑定关系"动态判断是"独占"还是"共享"
- 性能权衡:在"性能"和"灵活性"之间找到平衡
18.4 队列锁定机制
作用和实现原理:
队列锁定机制用于保护共享队列的并发访问。当多个线程需要访问同一个队列时,需要使用自旋锁保护队列,防止并发冲突。DPDK插件在tx_burst_vector_internal函数中使用队列锁,只在发送时持有锁,最小化锁的持有时间。就像"快递发送中心"的"队列锁定",当"多个窗口"使用"同一个队列"时,需要"加锁"保护,确保"并发安全"。
队列锁的定义 (src/plugins/dpdk/device/dpdk.h:109):
c
/* 在dpdk_tx_queue_t结构体中 */
typedef struct
{
CLIB_CACHE_LINE_ALIGN_MARK (cacheline0);
clib_spinlock_t lock; // 队列锁(用于共享队列)
u32 queue_index;
} dpdk_tx_queue_t;
队列锁的初始化 (src/plugins/dpdk/device/common.c:220):
c
/* 在dpdk_device_setup函数中,初始化队列锁 */
for (j = 0; j < xd->conf.n_tx_queues; j++)
{
/* ... 队列设置 ... */
/* 初始化队列锁 */
clib_spinlock_init (&vec_elt (xd->tx_queues, j).lock);
}
队列锁的使用 (src/plugins/dpdk/device/device.c:169):
c
/* 在tx_burst_vector_internal函数中,使用队列锁 */
static_always_inline u32
tx_burst_vector_internal (vlib_main_t *vm, dpdk_device_t *xd,
struct rte_mbuf **mb, u32 n_left, int queue_id,
u8 is_shared)
{
dpdk_tx_queue_t *txq;
u32 n_retry;
int n_sent = 0;
n_retry = 16;
txq = vec_elt_at_index (xd->tx_queues, queue_id);
do
{
/* 如果是共享队列,需要加锁 */
if (is_shared)
clib_spinlock_lock (&txq->lock);
/* 发送数据包 */
n_sent = rte_eth_tx_burst (xd->port_id, queue_id, mb, n_left);
/* 释放锁 */
if (is_shared)
clib_spinlock_unlock (&txq->lock);
/* ... 更新状态 ... */
}
while (n_sent && n_left && (n_retry > 0));
return n_left;
}
clib_spinlock_lock/unlock:
c
/**
* 自旋锁加锁
* 如果锁已被其他线程持有,当前线程会自旋等待
*/
clib_spinlock_lock (&txq->lock);
/**
* 自旋锁解锁
* 释放锁,允许其他等待的线程获取锁
*/
clib_spinlock_unlock (&txq->lock);
锁的保护范围:
锁的保护范围只包括rte_eth_tx_burst调用,不包括mbuf准备和验证等操作。这样可以最小化锁的持有时间,减少锁竞争:
锁外操作(无锁,可并行):
├── mbuf验证和设置
├── 硬件卸载处理
└── buffer索引转换
加锁操作(需要锁保护):
└── rte_eth_tx_burst调用
锁外操作(无锁,可并行):
├── 错误处理
└── 统计更新
性能考虑:
- 最小化锁时间:只在必要时持有锁,减少锁竞争
- 自旋锁:使用自旋锁,避免上下文切换开销
- 缓存行对齐:队列结构使用缓存行对齐,避免false sharing
关键设计要点:
- 条件锁定 :只有当
is_shared=1时才加锁 - 最小化范围:锁的保护范围只包括发送操作
- 自旋锁:使用自旋锁,避免上下文切换
通俗理解:
就像"快递发送中心"的"队列锁定":
- 条件锁定:只有当"队列是共享的"(is_shared=1)时才"加锁"
- 最小化锁时间:只在"实际发送"时"加锁","准备包裹"等操作在"锁外"进行
- 自旋锁:使用"自旋锁"(等待时不切换线程),避免"上下文切换"开销
- 并发安全:通过"加锁"确保"多个窗口"同时使用"同一个队列"时的"安全性"
18.5 发送失败处理
作用和实现原理:
发送失败处理是数据包发送路径的重要环节。当rte_eth_tx_burst无法发送所有数据包时(队列满),未发送的数据包需要被处理。VPP会更新错误统计、记录错误计数,并释放未发送的mbuf。这样可以确保系统稳定运行,避免资源泄漏。就像"快递发送中心"的"发送失败处理",当"队列满"无法发送"包裹"时,更新"错误统计","释放包裹"。
发送失败检查 (src/plugins/dpdk/device/device.c:423):
c
/* 在TX函数中,检查发送结果 */
tx_pkts = n_packets = mb - ptd->mbufs;
n_left = tx_burst_vector_internal (vm, xd, ptd->mbufs, n_packets, queue_id,
is_shared);
/* n_left:未发送的数据包数量
* - n_left == 0:全部发送成功
* - n_left > 0:有数据包发送失败
*/
发送失败处理代码 (src/plugins/dpdk/device/device.c:426):
c
/* 处理发送失败的数据包 */
{
/* 如果有未发送的数据包(n_left > 0) */
if (PREDICT_FALSE (n_left))
{
/* 步骤1:更新成功发送的数量 */
tx_pkts -= n_left; // 成功发送的数量 = 总数 - 未发送的数量
/* 步骤2:更新发送错误统计计数器 */
vlib_simple_counter_main_t *cm;
vnet_main_t *vnm = vnet_get_main ();
cm = vec_elt_at_index (vnm->interface_main.sw_if_counters,
VNET_INTERFACE_COUNTER_TX_ERROR);
vlib_increment_simple_counter (cm, thread_index, xd->sw_if_index,
n_left);
/* 步骤3:记录错误计数 */
vlib_error_count (vm, node->node_index, DPDK_TX_FUNC_ERROR_PKT_DROP,
n_left);
/* 步骤4:释放未发送的mbuf */
while (n_left--)
{
/* 释放mbuf,将其返回到mempool */
rte_pktmbuf_free (ptd->mbufs[n_packets - n_left - 1]);
}
}
}
发送失败的原因:
- 队列满:TX队列已满,无法接收更多数据包
- 设备故障:网卡设备故障或处于错误状态
- 资源不足:系统资源不足
重试机制:
tx_burst_vector_internal函数包含重试机制,尝试多次发送:
c
do
{
n_sent = rte_eth_tx_burst (xd->port_id, queue_id, mb, n_left);
n_left -= n_sent;
mb += n_sent;
n_retry--;
}
while (n_sent && n_left && (n_retry > 0));
/* 如果队列满,会继续重试,直到全部发送或重试次数用完 */
错误统计更新:
c
/* 更新接口级别的发送错误计数器 */
vlib_increment_simple_counter (cm, thread_index, xd->sw_if_index, n_left);
/* 记录节点级别的错误计数 */
vlib_error_count (vm, node->node_index, DPDK_TX_FUNC_ERROR_PKT_DROP, n_left);
mbuf释放:
c
/* 释放未发送的mbuf */
rte_pktmbuf_free (ptd->mbufs[n_packets - n_left - 1]);
/* 将mbuf返回到mempool,供后续使用 */
关键设计要点:
- 错误统计:更新接口和节点的错误统计
- 资源释放:释放未发送的mbuf,避免资源泄漏
- 重试机制:通过重试机制尝试发送剩余数据包
- 失败容忍:系统应该能够容忍发送失败,继续运行
通俗理解:
就像"快递发送中心"的"发送失败处理":
- 检查结果:检查"发送结果",判断是否有"未发送的包裹"(n_left > 0)
- 更新统计:更新"错误统计"(发送错误计数器),记录"失败数量"
- 释放资源:将"未发送的包裹"(mbuf)"释放"回"资源池"(mempool)
- 重试机制:在发送过程中,如果"队列满",会"重试"发送,直到"全部发送"或"重试次数用完"
18.6 本章总结
发送队列管理核心流程总结:
| 步骤 | 功能 | 关键函数/机制 | 作用 |
|---|---|---|---|
| 1. 队列分配 | 分配TX队列数组 | vec_validate_aligned |
分配队列结构 |
| 2. 队列配置 | 配置DPDK队列 | rte_eth_tx_queue_setup |
创建DPDK队列 |
| 3. 锁初始化 | 初始化队列锁 | clib_spinlock_init |
准备锁保护 |
| 4. 队列注册 | 注册队列到VPP | vnet_hw_if_register_tx_queue |
获取队列索引 |
| 5. 线程绑定 | 绑定队列到线程 | vnet_hw_if_tx_queue_assign_thread |
建立绑定关系 |
| 6. 共享判断 | 判断队列类型 | 线程位图计数 | 确定共享/独占 |
| 7. 发送使用 | 使用队列发送 | tx_burst_vector_internal |
实际发送 |
| 8. 失败处理 | 处理发送失败 | 错误统计、mbuf释放 | 错误处理 |
关键设计要点:
- 缓存行对齐:队列结构使用缓存行对齐,避免false sharing
- 动态绑定:队列与线程的绑定关系可以在运行时修改
- 条件锁定:只有当队列是共享的时才加锁
- 最小化锁时间:只在发送时持有锁,减少锁竞争
- 错误处理:正确处理发送失败,更新统计并释放资源
相关源码文件:
src/plugins/dpdk/device/init.c:571- TX队列分配和线程绑定src/plugins/dpdk/device/common.c:206- TX队列配置和锁初始化src/plugins/dpdk/device/device.c:156- 队列锁定机制src/plugins/dpdk/device/device.c:426- 发送失败处理src/vnet/interface/tx_queue.c:35- 队列注册src/vnet/interface/tx_queue.c:108- 线程绑定src/plugins/dpdk/device/dpdk.h:106- 队列结构定义src/vnet/interface.h:632- VPP队列结构定义