目录
一、UDP接收
#include <stdio.h>
#include <rte_eal.h> // 包含DPDK的Environment Abstraction Layer(EAL)函数
#include <rte_ethdev.h> // 包含以太网设备操作相关的函数和结构定义
#include <arpa/inet.h> // 包含用于网络地址转换的函数
int global_portid = 0; // 全局变量,用于指定操作的网络端口
#define NUM_MBUFS 4096 // 定义MBUF池的大小
#define BURST_SIZE 128 // 定义每次接收数据包的最大数量
static const struct rte_eth_conf port_conf_default = {
.rxmode = { .max_rx_pkt_len = RTE_ETHER_MAX_LEN }
}; // 定义端口配置的默认参数,包括最大接收包长度
static int ustack_init_port(struct rte_mempool *mbuf_pool) {
uint16_t nb_sys_ports = rte_eth_dev_count_avail(); // 获取可用的端口数量
if (nb_sys_ports == 0) {
rte_exit(EXIT_FAILURE, "No Supported eth found\n"); // 如果没有端口可用,退出程序
}
const int num_rx_queues = 1; // 接收队列数量设置为1
const int num_tx_queues = 0; // 发送队列数量设置为0
rte_eth_dev_configure(global_portid, num_rx_queues, num_tx_queues, &port_conf_default); // 配置端口
if (rte_eth_rx_queue_setup(global_portid, 0, 128, rte_eth_dev_socket_id(global_portid), NULL, mbuf_pool) < 0) {
rte_exit(EXIT_FAILURE, "Could not setup RX queue\n"); // 设置接收队列
}
if (rte_eth_dev_start(global_portid) < 0) {
rte_exit(EXIT_FAILURE, "Could not start\n"); // 启动端口
}
return 0;
}
int main(int argc, char *argv[]) {
if (rte_eal_init(argc, argv) < 0) {
rte_exit(EXIT_FAILURE, "Error with EAL init\n"); // 初始化EAL
}
struct rte_mempool *mbuf_pool = rte_pktmbuf_pool_create("mbuf pool", NUM_MBUFS, 0, 0, RTE_MBUF_DEFAULT_BUF_SIZE, rte_socket_id());
if (mbuf_pool == NULL) {
rte_exit(EXIT_FAILURE, "Could not create mbuf pool\n"); // 创建MBUF池
}
ustack_init_port(mbuf_pool); // 初始化端口
while (1) {
struct rte_mbuf *mbufs[BURST_SIZE] = {0}; // 接收缓冲区数组
uint16_t num_recvd = rte_eth_rx_burst(global_portid, 0, mbufs, BURST_SIZE); // 从网络端口接收数据包
if (num_recvd > BURST_SIZE) {
rte_exit(EXIT_FAILURE, "Error receiving from eth\n"); // 接收错误检查
}
int i = 0;
for (i = 0; i < num_recvd; i++) {
struct rte_ether_hdr *ethhdr = rte_pktmbuf_mtod(mbufs[i], struct rte_ether_hdr *); // 获取以太网头部
if (ethhdr->ether_type != rte_cpu_to_be_16(RTE_ETHER_TYPE_IPV4)) {
continue; // 只处理IPv4数据包
}
struct rte_ipv4_hdr *iphdr = rte_pktmbuf_mtod_offset(mbufs[i], struct rte_ipv4_hdr *, sizeof(struct rte_ether_hdr)); // 获取IP头部
if (iphdr->next_proto_id == IPPROTO_UDP) { // 检查是否为UDP数据包
struct rte_udp_hdr *udphdr = (struct rte_udp_hdr *)(iphdr + 1); // 获取UDP头部
printf("udp : %s\n", (char*)(udphdr+1)); // 输出UDP数据内容
}
}
}
printf("hello dpdk\n"); // 程序启动提示信息
}
1、初始化环境
这里主要负责初始化DPDK的环境抽象层(EAL)。这是使用DPDK开发网络应用的第一步,因为EAL负责管理CPU核心、内存资源以及PCI设备的访问权限等。
int main(int argc, char *argv[]) {
if (rte_eal_init(argc, argv) < 0) {
rte_exit(EXIT_FAILURE, "Error with EAL init\n");
}
...
}
"rte_eal_init"
函数根据传入的参数初始化EAL。如果初始化失败,程序会打印错误信息并退出。
2、创建内存池
网络应用通常需要处理大量的数据包。为了提高性能,DPDK使用内存池来管理这些数据包,避免频繁的内存分配和释放。
struct rte_mempool *mbuf_pool = rte_pktmbuf_pool_create("mbuf pool", NUM_MBUFS, 0, 0, RTE_MBUF_DEFAULT_BUF_SIZE, rte_socket_id());
if (mbuf_pool == NULL) {
rte_exit(EXIT_FAILURE, "Could not create mbuf pool\n");
}
创建了一个名为"mbuf pool"的内存池,用于存储4096个数据包缓冲区(MBUF)。如果内存池创建失败,程序将打印错误信息并退出。
rte_pktmbuf_pool_create()
在代码中创建了一个名为 "mbuf pool" 的内存池,其中包含 NUM_MBUFS 个数据包缓冲区,每个CPU核心的缓存大小设置为0(使用默认设置),每个mbuf没有额外的私有空间,数据区大小设置为默认值,最后,内存池被分配在执行代码的CPU对应的NUMA节点上,六个输入参数这里详细解释下:
1)name
- 类型:
const char *
- 描述: 内存池的名称。这个名字在调试时很有用,因为它可以帮助识别不同的内存池。
2) n
- 类型:
unsigned n
- 描述: 内存池中mbuf的数量。这个数量定义了池中可以存储的最大数据包数。
3)cache_size
- 类型:
unsigned cache_size
- 描述: 每个CPU核心的缓存大小。DPDK使用本地缓存来减少对全局数据结构的争用和访问,从而提高性能。设置为0时,将使用默认缓存大小。
4)priv_size
- 类型:
uint16_t priv_size
- 描述: 每个mbuf的私有数据区大小。这是用户可以用于存储额外信息(例如,自定义控制信息)的空间。
5)data_room_size
- 类型:
uint16_t data_room_size
- 描述: 每个mbuf中的数据区大小。这应该足够大,以存储最大的预期数据包。
RTE_MBUF_DEFAULT_BUF_SIZE
通常用作这个参数,它定义了一个默认的缓冲区大小,足以容纳常见的网络数据包。
6)socket_id
- 类型:
int socket_id
- 描述: 分配内存池的NUMA(非统一内存访问)节点。通常使用
rte_socket_id()
来获取当前执行代码的CPU所在的NUMA节点。这有助于优化内存访问性能,确保内存和CPU之间的局部性最大化。
3、网络端口配置
一旦内存池创建完成,下一步是配置网络端口。这包括设置接收和发送队列的数量、最大接收包长度等。
函数 ustack_init_port() 负责初始化和配置指定的网络端口。这个函数是网络应用的关键部分,因为它设置了网络设备的基本运行参数,包括端口的接收和发送队列。
调用rte_eth_dev_configure来设置端口参数,以及调用rte_eth_rx_queue_setup来设置接收队列。
1)检查可用端口数量
uint16_t nb_sys_ports = rte_eth_dev_count_avail();
if (nb_sys_ports == 0) {
rte_exit(EXIT_FAILURE, "No Supported eth found\n");
}
首先,函数调用 rte_eth_dev_count_avail() 来获取系统中可用的以太网端口数量。如果没有可用的端口,函数会调用 rte_exit(),打印错误信息并退出程序。这是一个基本的错误检查步骤,确保了程序有可操作的网络设备。
2)配置网络端口
const int num_rx_queues = 1;
const int num_tx_queues = 0;
rte_eth_dev_configure(global_portid, num_rx_queues, num_tx_queues, &port_conf_default);
在确定有可用端口后,函数配置这些端口。这里,设置接收队列数量为1,发送队列数量为0(只关注接收)。然后,使用 rte_eth_dev_configure() 函数和之前定义的端口配置 port_conf_default 来配置全局变量 global_portid 指定的端口。
rte_eth_dev_configure
是一个核心函数,在DPDK应用程序中用于配置网络设备(即网卡)。这个函数设置了网络端口的基本操作参数,包括接收和发送队列的数量以及其他端口相关的配置。
各参数的意义如下:
-
port_id
:- 类型:
uint16_t
- 说明:指定要配置的以太网设备的端口号。DPDK在系统启动时会枚举所有支持的网络设备,并为每个设备分配一个唯一的端口号。
- 类型:
-
nb_rx_queue
:- 类型:
uint16_t
- 说明:为指定的端口配置的接收队列数量。这个值决定了可以并行处理多少流量,通常与处理器核心数相关。每个接收队列可以绑定到特定的处理器核心上,以平衡负载和优化性能。
- 类型:
-
nb_tx_queue
:- 类型:
uint16_t
- 说明:为指定的端口配置的发送队列数量。同接收队列类似,发送队列的数量可以根据发送流量的需求和处理器核心数来设置。
- 类型:
-
conf
:- 类型:
const struct rte_eth_conf *
- 说明:指向
rte_eth_conf
结构的指针,该结构包含了更详细的网络端口配置信息。这个结构体允许你设置各种网络设备的高级特性,如下所示:struct rte_eth_conf { struct { struct { uint64_t offloads; // 定义接收时的一些特定操作,如校验和计算、分散/聚集等 uint32_t max_rx_pkt_len; // 可接收的最大数据包长度 uint16_t split_hdr_size; // 如果使用分散/聚集,定义头部分割的大小 uint8_t header_split; // 是否启用头部分割功能 uint8_t hw_ip_checksum; // 是否启用硬件IP校验和 uint8_t hw_vlan_filter; // 是否启用硬件VLAN过滤 uint8_t hw_vlan_strip; // 是否在接收时自动剥离VLAN标签 uint8_t hw_vlan_extend; // 是否启用VLAN扩展处理 uint8_t jumbo_frame; // 是否启用巨帧处理 uint8_t hw_strip_crc; // 是否剥离接收数据包的CRC uint8_t enable_scatter; // 是否启用散布聚集接收 uint8_t enable_lro; // 是否启用大接收脱载(LRO) } rxmode; struct { uint64_t offloads; // 定义发送时的一些特定操作,如校验和计算等 } txmode; } rx_adv_conf, tx_adv_conf; uint64_t link_speeds; // 定义支持的链路速度 struct rte_eth_rss_conf rss_conf; // 定义接收端扩展功能,如RSS(接收侧缩放) };
static const struct rte_eth_conf port_conf_default = { .rxmode = {.max_rx_pkt_len = RTE_ETHER_MAX_LEN} };
代码开头,这里只明确设置了 rte_eth_conf 结构体中的 max_rx_pkt_len 字段。其他的配置参数采用的是结构体初始化时的默认值,这意味着除非特别指定,否则这些参数将被初始化为零或默认状态。在DPDK中,结构体的未显式初始化字段通常会被设置为零或对应类型的默认值。通过设定 .max_rx_pkt_len = RTE_ETHER_MAX_LEN,配置网络端口准备接收标准以太网帧的最大长度的数据包。这是基本配置,用于确保端口能处理标准大小的以太网帧。
- 类型:
3)设置接收队列
if (rte_eth_rx_queue_setup(global_portid, 0, 128, rte_eth_dev_socket_id(global_portid), NULL, mbuf_pool) < 0) {
rte_exit(EXIT_FAILURE, "Could not setup RX queue\n");
}
接下来,函数为端口设置接收队列。这里它指定队列索引为0,描述符数量为128,使用从 rte_eth_dev_socket_id() 获取的NUMA节点。内存池 mbuf_pool 被用作接收数据包的存储。如果队列设置失败,程序将打印错误信息并退出。
rte_eth_rx_queue_setup 是DPDK库中用来配置网络设备的接收队列的函数。下面是每个参数的详细说明:
-
uint16_t port_id
- 说明: 网络端口的标识符。这是要配置的网络设备的端口号。
- 用途: 用于指定要设置接收队列的具体网络端口。
-
uint16_t rx_queue_id
- 说明: 接收队列的索引号。
- 用途: 端口可以有多个接收队列,此参数用于指定哪一个接收队列将被设置。
-
uint16_t nb_rx_desc
- 说明: 接收队列中描述符的数量。
- 用途: 描述符是用于指定网络数据包内存位置的数据结构,此参数设置队列中可用的描述符数量,影响队列处理数据包的能力。
-
unsigned int socket_id
- 说明: 指定接收队列绑定的NUMA节点的socket ID。
- 用途: 对于多socket系统,绑定接收队列到具体的NUMA节点可以优化内存访问性能。
-
*const struct rte_eth_rxconf rx_conf
- 说明: 指向接收队列配置结构的指针。
- 用途: 该结构包含了一系列接收选项和配置,如接收模式和阈值等,用于细致调整接收行为和性能。
-
*struct rte_mempool mb_pool
- 说明: 指向内存池的指针。
- 用途: 此内存池用于分配接收数据包的内存缓冲区。设置正确的内存池是保证网络数据包正确接收的关键。
rx_conf
被设置为 NULL
,表示使用默认的接收队列配置。通常,你可能需要根据特定应用的需求来调整这个配置,以达到最佳的接收性能。
问题:为什么用这个rte_eth_dev_socket_id(global_portid)获取id不是直接global_portid?
在 rte_eth_dev_socket_id(global_portid)
函数的使用中,我们需要区分两个重要的概念:端口标识符(port_id
)和套接字标识符(socket_id
)。这两个标识符在DPDK中服务于不同的目的。
端口标识符 (port_id
)
- 描述:
port_id
是用来标识网络端口的编号。在DPDK中,每个物理或虚拟的网络接口被分配一个唯一的编号,称为port_id
。这个编号用于配置和管理特定的网络端口。 - 用途:用来指定哪一个网络端口将接受配置指令,比如接收队列和发送队列的设置、启动和停止端口等。
套接字标识符 (socket_id
)
- 描述:
socket_id
指的是网络端口绑定的NUMA(Non-Uniform Memory Access)节点的套接字标识符。NUMA是一种内存设计,用于多处理器系统中,其中内存访问时间取决于内存的物理位置和访问该内存的处理器。 - 用途:在配置DPDK网络端口时,将接收队列等绑定到具体的NUMA节点可以优化内存访问的性能。
socket_id
用于指示数据结构和缓冲区应该在哪个NUMA节点上分配,以最小化跨节点的内存访问延迟。
使用 rte_eth_dev_socket_id(global_portid)
当调用 rte_eth_dev_socket_id(global_portid)
:
- 这个函数返回与指定端口 (
global_portid
) 绑定的NUMA节点的套接字ID。 - 它是根据物理设备的位置和系统的NUMA架构自动确定的。
- 在设置接收队列的内存池时,使用正确的
socket_id
确保了内存分配的效率和性能最优化,特别是在处理高速网络流量时。
因此,即使你已经有了端口号 (global_portid
),你仍然需要使用 rte_eth_dev_socket_id
函数来获取对应端口的NUMA节点信息,以便正确地配置内存和队列,保证数据处理的性能。
4)启动端口
if (rte_eth_dev_start(global_portid) < 0) {
rte_exit(EXIT_FAILURE, "Could not start\n");
}
4、处理数据包
1)接收数据包
在无限循环中,使用 rte_eth_rx_burst 函数从指定的网络端口(global_portid)批量接收数据包。这个函数的功能是尝试从设备的接收队列中获取最多 BURST_SIZE(定义为128)个数据包。
uint16_t num_recvd = rte_eth_rx_burst(global_portid, 0, mbufs, BURST_SIZE);
global_portid
:接收数据的端口号。0
:指定从哪个接收队列获取数据包。mbufs
:一个指向struct rte_mbuf *
数组的指针,用于存放接收到的数据包。BURST_SIZE
:此次调用尝试接收的最大数据包数量。
返回实际接收到的数据包数量。如果返回的数量超出了请求的数量,程序将输出错误信息并退出。
2)处理数据包
for (i = 0; i < num_recvd; i++) {
struct rte_ether_hdr *ethhdr = rte_pktmbuf_mtod(mbufs[i], struct rte_ether_hdr *);
if (ethhdr->ether_type != rte_cpu_to_be_16(RTE_ETHER_TYPE_IPV4)) {
continue; // 忽略非IPv4类型的数据包
}
...
}
rte_pktmbuf_mtod(mbufs[i], struct rte_ether_hdr *)
:将mbuf
数据包转换为指向其数据开始位置的指针(此例中为以太网头部)。ethhdr->ether_type
:检查以太网帧的类型。如果不是IPv4数据包(以太网类型不等于RTE_ETHER_TYPE_IPV4
),则跳过当前循环迭代。- rte_cpu_to_be_16():
- 功能: 将CPU表示的16位数转换为大端格式。
- 用途: 用于网络协议中的字节序转换。
对于是IPv4的数据包,程序进一步检查IP协议头,特别是协议类型是否为UDP:
struct rte_ipv4_hdr *iphdr = rte_pktmbuf_mtod_offset(mbufs[i], struct rte_ipv4_hdr *, sizeof(struct rte_ether_hdr));
if (iphdr->next_proto_id == IPPROTO_UDP) {
struct rte_udp_hdr *udphdr = (struct rte_udp_hdr *)(iphdr + 1);
printf("udp : %s\n", (char*)(udphdr+1)); // 打印UDP数据内容
}
rte_pktmbuf_mtod_offset
:从数据包的指定偏移量处获取数据,这里是跳过以太网头部后的IP头部。iphdr->next_proto_id
:检查IP数据包的下一个协议是否为UDP。udphdr
:指向UDP头部的指针。(char*)(udphdr+1)
:指向UDP数据内容的指针,这里+1意味着跳过UDP头部直接指向数据部分。
二、发送UDP数据包
在接收UDP包的基础上增加了发送UDP包的代码逻辑,完整代码如下:
#include <stdio.h>
#include <rte_eal.h>
#include <rte_ethdev.h>
#include <arpa/inet.h>
int global_portid = 0;
#define NUM_MBUFS 4096
#define BURST_SIZE 128
#define ENABLE_SEND 1
#if ENABLE_SEND
uint8_t global_smac[RTE_ETHER_ADDR_LEN];
uint8_t global_dmac[RTE_ETHER_ADDR_LEN];
uint32_t global_sip;
uint32_t global_dip;
uint16_t global_sport;
uint16_t global_dport;
#endif
static const struct rte_eth_conf port_conf_default = {
.rxmode = { .max_rx_pkt_len = RTE_ETHER_MAX_LEN }
};
static int ustack_init_port(struct rte_mempool *mbuf_pool) {
//number
uint16_t nb_sys_ports = rte_eth_dev_count_avail();
if (nb_sys_ports == 0) {
rte_exit(EXIT_FAILURE, "No Supported eth found\n");
}
struct rte_eth_dev_info dev_info;
rte_eth_dev_info_get(global_portid, &dev_info);
// eth0,
const int num_rx_queues = 1;
#if ENABLE_SEND
const int num_tx_queues = 1;
#else
const int num_tx_queues = 0;
#endif
rte_eth_dev_configure(global_portid, num_rx_queues, num_tx_queues, &port_conf_default);
if (rte_eth_rx_queue_setup(global_portid, 0, 128, rte_eth_dev_socket_id(global_portid), NULL, mbuf_pool) < 0) {
rte_exit(EXIT_FAILURE, "Could not setup RX queue\n");
}
#if ENABLE_SEND
struct rte_eth_txconf txq_conf = dev_info.default_txconf;
txq_conf.offloads = port_conf_default.rxmode.offloads;
if (rte_eth_tx_queue_setup(global_portid, 0, 512, rte_eth_dev_socket_id(global_portid), &txq_conf) < 0) {
rte_exit(EXIT_FAILURE, "Could not setup TX queue\n");
}
#endif
if (rte_eth_dev_start(global_portid) < 0) {
rte_exit(EXIT_FAILURE, "Could not start\n");
}
return 0;
}
// msg
static int ustack_encode_udp_pkt(uint8_t *msg, uint8_t *data, uint16_t total_len) {
//1 ether header
struct rte_ether_hdr *eth = (struct rte_ether_hdr *)msg;
rte_memcpy(eth->d_addr.addr_bytes, global_dmac, RTE_ETHER_ADDR_LEN);
rte_memcpy(eth->s_addr.addr_bytes, global_smac, RTE_ETHER_ADDR_LEN);
eth->ether_type = htons(RTE_ETHER_TYPE_IPV4);
//1 ip header
struct rte_ipv4_hdr *ip = (struct rte_ipv4_hdr*)(eth + 1); //msg + sizeof(struct rte_ether_hdr);
ip->version_ihl = 0x45;
ip->type_of_service = 0;
ip->total_length = htons(total_len - sizeof(struct rte_ether_hdr));
ip->packet_id = 0;
ip->fragment_offset = 0;
ip->time_to_live = 64;
ip->next_proto_id = IPPROTO_UDP;
ip->src_addr = global_sip;
ip->dst_addr = global_dip;
ip->hdr_checksum = 0;
ip->hdr_checksum = rte_ipv4_cksum(ip);
//1 udp header
struct rte_udp_hdr *udp = (struct rte_udp_hdr *)(ip + 1);
udp->src_port = global_sport;
udp->dst_port = global_dport;
uint16_t udplen = total_len - sizeof(struct rte_ether_hdr) - sizeof(struct rte_ipv4_hdr);
udp->dgram_len = htons(udplen);
rte_memcpy((uint8_t*)(udp+1), data, udplen);
udp->dgram_cksum = 0;
udp->dgram_cksum = rte_ipv4_udptcp_cksum(ip, udp);
return 0;
}
int main(int argc, char *argv[]) {
if (rte_eal_init(argc, argv) < 0) {
rte_exit(EXIT_FAILURE, "Error with EAL init\n");
}
struct rte_mempool *mbuf_pool = rte_pktmbuf_pool_create("mbuf pool", NUM_MBUFS, 0, 0, RTE_MBUF_DEFAULT_BUF_SIZE, rte_socket_id());
if (mbuf_pool == NULL) {
rte_exit(EXIT_FAILURE, "Could not create mbuf pool\n");
}
ustack_init_port(mbuf_pool);
while (1) {
struct rte_mbuf *mbufs[BURST_SIZE] = {0};
uint16_t num_recvd = rte_eth_rx_burst(global_portid, 0, mbufs, BURST_SIZE);
if (num_recvd > BURST_SIZE) {
rte_exit(EXIT_FAILURE, "Error receiving from eth\n");
}
int i = 0;
for (i = 0;i < num_recvd;i ++) {
struct rte_ether_hdr *ethhdr = rte_pktmbuf_mtod(mbufs[i], struct rte_ether_hdr *);
if (ethhdr->ether_type != rte_cpu_to_be_16(RTE_ETHER_TYPE_IPV4)) {
continue;
}
struct rte_ipv4_hdr *iphdr = rte_pktmbuf_mtod_offset(mbufs[i], struct rte_ipv4_hdr *, sizeof(struct rte_ether_hdr));
if (iphdr->next_proto_id == IPPROTO_UDP) {
struct rte_udp_hdr *udphdr = (struct rte_udp_hdr *)(iphdr + 1);
#if ENABLE_SEND
rte_memcpy(global_smac, ethhdr->d_addr.addr_bytes, RTE_ETHER_ADDR_LEN);
rte_memcpy(global_dmac, ethhdr->s_addr.addr_bytes, RTE_ETHER_ADDR_LEN);
rte_memcpy(&global_sip, &iphdr->dst_addr, sizeof(uint32_t));
rte_memcpy(&global_dip, &iphdr->src_addr, sizeof(uint32_t));
rte_memcpy(&global_sport, &udphdr->dst_port, sizeof(uint32_t));
rte_memcpy(&global_dport, &udphdr->src_port, sizeof(uint32_t));
struct in_addr addr;
addr.s_addr = iphdr->src_addr;
printf("sip %s:%d --> ", inet_ntoa(addr), ntohs(udphdr->src_port));
addr.s_addr = iphdr->dst_addr;
printf("dip %s:%d --> ", inet_ntoa(addr), ntohs(udphdr->dst_port));
uint16_t length = ntohs(udphdr->dgram_len);
uint16_t total_len = length + sizeof(struct rte_ipv4_hdr) + sizeof(struct rte_ether_hdr);
struct rte_mbuf *mbuf = rte_pktmbuf_alloc(mbuf_pool);
if (!mbuf) {
rte_exit(EXIT_FAILURE, "Error rte_pktmbuf_alloc\n");
}
mbuf->pkt_len = total_len;
mbuf->data_len = total_len;
uint8_t *msg = rte_pktmbuf_mtod(mbuf, uint8_t *);
ustack_encode_udp_pkt(msg, (uint8_t*)(udphdr+1), total_len);
rte_eth_tx_burst(global_portid, 0, &mbuf, 1);
#endif
printf("udp : %s\n", (char*)(udphdr+1));
} else if (iphdr->next_proto_id == IPPROTO_TCP) {
struct rte_tcp_hdr *tcphdr = (struct rte_tcp_hdr *)(iphdr + 1);
struct in_addr addr;
addr.s_addr = iphdr->src_addr;
printf("sip %s:%d --> ", inet_ntoa(addr), ntohs(tcphdr->src_port));
addr.s_addr = iphdr->dst_addr;
printf("dip %s:%d \n", inet_ntoa(addr), ntohs(tcphdr->dst_port));
}
}
}
printf("hello dpdk\n");
}
1、定义全局变量
#define ENABLE_SEND 1
#if ENABLE_SEND
uint8_t global_smac[RTE_ETHER_ADDR_LEN];
uint8_t global_dmac[RTE_ETHER_ADDR_LEN];
uint32_t global_sip;
uint32_t global_dip;
uint16_t global_sport;
uint16_t global_dport;
#endif
定义了用于发送的全局变量,包括源和目的MAC地址、IP地址以及端口号。
ENABLE_SEND宏用于控制是否启用发送功能。
2、网络端口配置增加发送队列
#if ENABLE_SEND
const int num_tx_queues = 1;
#else
const int num_tx_queues = 0;
#endif
rte_eth_dev_configure(global_portid, num_rx_queues, num_tx_queues, &port_conf_default);
在ustack_init_port()函数中增加配置发送队列
3、设置发送队列
#if ENABLE_SEND
struct rte_eth_txconf txq_conf = dev_info.default_txconf;
txq_conf.offloads = port_conf_default.rxmode.offloads;
if (rte_eth_tx_queue_setup(global_portid, 0, 512, rte_eth_dev_socket_id(global_portid), &txq_conf) < 0) {
rte_exit(EXIT_FAILURE, "Could not setup TX queue\n");
}
#endif
设置发送队列,使用从设备信息中获得的默认发送队列配置(dev_info.default_txconf)。这里还设置了队列的offload特性,以增强数据包处理性能。
rte_eth_txconf 结构体在 DPDK 中用于详细配置以太网设备的发送队列。这个结构体允许用户对网络端口的发送操作进行精细控制,包括如何处理网络流量、如何利用硬件加速功能等。这个结构体中的一些关键字段:
-
tx_thresh:这些阈值用于控制发送队列的缓冲行为,例如何时开始处理缓冲中的包,这可以帮助优化延迟和吞吐量。
pthresh
:预取阈值。决定了在队列描述符被提交到硬件之前,描述符必须被预取到CPU缓存的最小数量。这是一个性能优化参数,可以减少因等待数据而导致的处理延迟。hthresh
:主机阈值。这是一个触发硬件开始处理包的描述符数量的阈值,与pthresh
结合使用,可以更高效地管理描述符。wthresh
:写回阈值。这个参数控制数据包被发送到网络之前,多少描述符会被写回到内存。
-
tx_free_thresh
- 控制何时释放一个发送描述符。如果设置为0,DPDK会在硬件确认数据包已发送后立即释放描述符。如果设置一个大于1的值,可以延迟描述符的释放,这在某些情况下可以提高性能。
-
tx_rs_thresh
- 设置在写回阶段必须达到的发送描述符数量,之后才会设置RS(Report Status)位。这个功能有助于管理和监控发送队列的状态。
-
txq_flags
- 提供对队列行为的低级控制,包括关闭或启用特定的硬件offload功能和其他高级配置。
-
offloads
- 这是DPDK 17.11及更高版本中新增的字段,用于设置各种发送队列的硬件加速功能,如TCP/UDP校验和计算、分片、多队列散列等。这些offload功能可以显著提高性能,因为它们将计算负担从CPU转移至网络硬件。
4、 编码UDP数据包
static int ustack_encode_udp_pkt(uint8_t *msg, uint8_t *data, uint16_t total_len) {
...
udp->dgram_cksum = rte_ipv4_udptcp_cksum(ip, udp);
return 0;
}
这个函数负责构建一个完整的UDP数据包,包括以太网头、IP头和UDP头,并计算IP和UDP的校验和。
当用户接收并处理完数据包后得到新的用户数据需要发送,此时我们只需要逆向操作接收数据包的过程即可。
一个UDP数据帧组成结构如图所示,在用户数据上添加UDP头,在此基础上再添加IP头,最后再添加以太网头,一个UDP数据帧就组装完毕,就可直接通过网卡发送。
1)构建以太网头部 (Ethernet Header)
struct rte_ether_hdr *eth = (struct rte_ether_hdr *)msg;
rte_memcpy(eth->d_addr.addr_bytes, global_dmac, RTE_ETHER_ADDR_LEN);
rte_memcpy(eth->s_addr.addr_bytes, global_smac, RTE_ETHER_ADDR_LEN);
eth->ether_type = htons(RTE_ETHER_TYPE_IPV4);
struct rte_ether_hdr *eth = (struct rte_ether_hdr *)msg;
定义了一个指向消息缓冲区起始位置的以太网头部结构指针。- 使用
rte_memcpy
函数将全局定义的目的MAC地址(global_dmac
)和源MAC地址(global_smac
)复制到以太网头部结构中。 - 设置
eth->ether_type
为IPv4,表示随后的数据是IPv4数据包。
2)构建IP头部 (IP Header)
struct rte_ipv4_hdr *ip = (struct rte_ipv4_hdr*)(eth + 1);
ip->version_ihl = 0x45; // IPv4, header length = 5 words (20 bytes)
ip->type_of_service = 0;
ip->total_length = htons(total_len - sizeof(struct rte_ether_hdr));
ip->packet_id = 0;
ip->fragment_offset = 0;
ip->time_to_live = 64;
ip->next_proto_id = IPPROTO_UDP; // Next protocol is UDP
ip->src_addr = global_sip;
ip->dst_addr = global_dip;
struct rte_ipv4_hdr *ip = (struct rte_ipv4_hdr*)(eth + 1);
指向以太网头部后的位置,即IP头部的开始位置。ip->version_ihl
设置IP版本号为4和头部长度(IHL)为5(即20字节)。0x45-
在IP协议的头部结构中,
version_ihl
字段是一个 8 位的字段,由两部分组成:- Version (版本号):占用高 4 位,指定 IP 协议的版本。对于 IPv4 来说,这个值固定为 4。
- IHL (Internet Header Length):占用低 4 位,表示 IP 头部的长度,以 32 位字为单位(每个字长 4 字节)。这个长度包括了所有的选项(如果有的话)。
- 0x4:代表 IPv4。
- 0x5:表示 IP 头部长度为 5 个 32 位字,即 5 x 4 = 20 字节。为什么是以4字节为最小单位?
- 内存对齐:许多计算机系统更有效地处理对齐在4字节边界上的数据。将IP头部的长度设为4字节的倍数有助于这种高效处理。
- 简单性和灵活性:以32位为单位允许头部长度容易计算并简化处理逻辑,因为网络通常在这种边界上操作字节。
-
ip->total_length
设置为整个数据包的长度减去以太网头部的长度。ip->time_to_live
设置生存时间。ip->next_proto_id
设置下一个协议为UDP。ip->src_addr
和ip->dst_addr
设置为全局定义的源和目的IP地址。
3)计算IP头部校验和
ip->hdr_checksum = 0; // Clear checksum field for calculation
ip->hdr_checksum = rte_ipv4_cksum(ip);
- 在计算校验和前,先将
ip->hdr_checksum
设置为0。 - 使用
rte_ipv4_cksum
函数计算校验和。
在IP通信中,校验和是一个重要的特性,用于确保IP头部数据在传输过程中的完整性。这是一种错误检测机制,可以帮助接收端检测在传输过程中可能发生的数据损坏。校验的几个关键点:
1.错误检测机制
- 目的:IP头部的校验和主要用于检测数据在传输过程中由于网络噪音、硬件错误等原因引起的任何错误或损坏。
- 方法:通过对IP头部的所有16位字进行加和,然后取反得到的结果就是校验和。接收端将对接收到的IP头部进行同样的计算,并将计算结果与接收到的校验和字段进行比较。
- 效果:如果计算结果与校验和匹配,则认为头部没有错误;如果不匹配,则说明数据在传输过程中可能被破坏,接收端通常会丢弃这样的数据包。
2.如何计算校验和
- 计算校验和的标准步骤是首先将校验和字段设置为0,然后将头部中的每个16位整数相加,包括首部长度和版本、服务类型、总长度等字段。
- 计算得到的总和再进行端转换(如果需要),并取一次反码(按位取反)得到最终的校验和。
- 在发送数据包前,将这个校验和值填入IP头部的校验和字段中。
3.重要性
- 可靠性:尽管IP协议本身是不可靠的,通过校验和提供了一种基本的数据完整性验证方式,增加了数据传输的可靠性。
- 简易性:这种校验和计算方法简单,易于在硬件上实现,对处理器的计算负担较小。
- 快速错误反馈:可以在数据包到达终点之前的任何一点检测到错误,从而减少不必要的数据传输。
4.局限性
- 校验和只能检测到错误,不能纠正错误。它也可能不会捕捉到所有的错误,尤其是在出现多个错误时,这些错误可能相互抵消。
- 校验和不涉及数据负载部分,只针对IP头部,因此不保证整个数据包的完整性。
在DPDK这样的高性能网络处理框架中,虽然可能有硬件支持直接计算校验和,但在软件层面上仍然需要正确实现这一机制,以确保在没有硬件支持的情况下也能保证数据的正确性。这就是为什么在构建网络数据包时需要计算并设置正确的IP头部校验和。
4)构建UDP头部 (UDP Header)
struct rte_udp_hdr *udp = (struct rte_udp_hdr *)(ip + 1);
udp->src_port = global_sport;
udp->dst_port = global_dport;
uint16_t udplen = total_len - sizeof(struct rte_ether_hdr) - sizeof(struct rte_ipv4_hdr);
udp->dgram_len = htons(udplen);
struct rte_udp_hdr *udp = (struct rte_udp_hdr *)(ip + 1);
指向IP头部后的位置,即UDP头部的开始位置。- 设置UDP源端口和目的端口。
udp->dgram_len
设置为除去以太网头和IP头后的数据长度。
5)计算UDP校验和
rte_memcpy((uint8_t*)(udp+1), data, udplen);
udp->dgram_cksum = 0; // Clear checksum field for calculation
udp->dgram_cksum = rte_ipv4_udptcp_cksum(ip, udp);
- 将数据复制到UDP数据区。
- 清除校验和字段,然后使用
rte_ipv4_udptcp_cksum
函数计算UDP的校验和。
5、在主循环中处理和发送数据包
#if ENABLE_SEND
rte_memcpy(global_smac, ethhdr->d_addr.addr_bytes, RTE_ETHER_ADDR_LEN);
rte_memcpy(global_dmac, ethhdr->s_addr.addr_bytes, RTE_ETHER_ADDR_LEN);
...
struct rte_mbuf *mbuf = rte_pktmbuf_alloc(mbuf_pool);
if (!mbuf) {
rte_exit(EXIT_FAILURE, "Error rte_pktmbuf_alloc\n");
}
mbuf->pkt_len = total_len;
mbuf->data_len = total_len;
uint8_t *msg = rte_pktmbuf_mtod(mbuf, uint8_t *);
ustack_encode_udp_pkt(msg, (uint8_t*)(udphdr+1), total_len);
rte_eth_tx_burst(global_portid, 0, &mbuf, 1);
#endif
在接收处理循环中,代码首先复制接收到的数据包中的源和目的MAC地址、IP地址和端口号到全局变量。然后分配一个新的rte_mbuf缓冲区,调用ustack_encode_udp_pkt函数填充数据包内容,最后通过rte_eth_tx_burst函数发送出去。