1.什么是dpdk
DPDK(Data Plane Development Kit)是一个由多家公司(如6WIND、Intel等)开发的开源项目,主要基于Linux系统运行。它是一组用于快速数据包处理的函数库与驱动集合,可以极大提高数据处理性能和吞吐量,进而提高数据平面应用程序的工作效率。
DPDK主要包含以下关键组件和特性:
- 用户空间驱动程序:提供了一组用户空间的网络设备驱动程序,如网卡驱动程序和虚拟设备驱动程序,用于实现对网络设备的直接控制和数据包的收发。
- 数据平面库:提供了一组高性能的数据平面库,包括数据包接收和发送库(PMD, Poll Mode Drivers)、数据包处理库(Packet Framework)等。这些库提供了优化的数据包处理算法和数据结构,以及多核并发处理的支持,以实现高吞吐量和低延迟的数据包处理。
- 内存管理:提供了高效的内存管理机制,包括大页内存、内存池和环形缓冲区等,以减少内存分配和释放的开销,并提高数据访问的效率。
- 网络协议栈绕过:DPDK可以绕过操作系统内核的网络协议栈,直接访问网卡和网络设备,以减少数据包处理的延迟和开销。
DPDK广泛应用于各种需要高性能数据包处理的场景,如防火墙、负载均衡器、入侵检测系统等。在这些场景中,DPDK能够帮助开发者实现高吞吐量、低延迟的数据处理,满足各种严苛的性能需求。
此外,DPDK还提供了环境抽象层(EAL, Environment Abstraction Layer),主要负责对计算机底层资源(如硬件和内存空间)的访问,并对提供给用户的接口实施了实现细节的封装。这使得DPDK能够扩展到任何处理器上使用,并且提供了对Linux和FreeBSD等操作系统的支持。
总之,DPDK是一个强大而灵活的工具集,为开发者提供了在Intel架构处理器上进行高性能数据包处理的解决方案。
2.dpdk的特性
2.1 pmd
在DPDK(Data Plane Development Kit)中,PMD(Poll Mode Driver)是DPDK架构的核心组件。PMD是特定网络设备的操作系统层抽象,允许应用程序直接与底层硬件交互,绕过传统操作系统网络堆栈。具体来说,PMD具有以下特点和功能:
- 设备特定驱动程序:PMD是设备特定的驱动程序,直接与网络设备的寄存器和内存映射区域交互。它们负责处理设备初始化、数据传输和中断处理。
- 轮询模式:PMD使用轮询模式(Poll Mode)来接收数据包,这意味着驱动程序会不断地检查网络设备是否有新的数据包到达,而不是等待操作系统内核的中断通知。这种方式可以显著提高数据包接收的效率和性能。
- 自定义网络行为:PMD允许应用程序根据特定需求定制网络行为,例如流量整形或自定义协议处理。这为用户提供了更大的灵活性和控制力。
- 支持多种网络接口:PMD同时支持物理和虚拟两种网络接口,包括Intel、Cisco、Broadcom、Mellanox、Chelsio等整个行业生态系统的网卡设备,以及基于KVM、VMware、Xen等虚拟化网络接口。
通过使用PMD,DPDK应用程序可以实现高性能的数据包处理,从而满足各种网络应用的性能需求
2.2 UIO
UIO(Userspace I/O)是运行在用户空间的I/O技术。在Linux系统中,一般的驱动设备都是运行在内核空间,而在用户空间则通过应用程序调用。然而,UIO则是将驱动的很少一部分运行在内核空间,而在用户空间实现驱动的绝大多数功能。使用UIO可以避免设备的驱动程序需要随着内核的更新而更新的问题。
UIO是Linux内核提供的用户态驱动框架,能够在用户空间运行设备驱动,内核中只需要编写和维护一小块内核模块。这使得驱动程序中的bug不会使内核崩溃,并且可以在不重新编译内核的情况下更新驱动程序。UIO的工作原理主要是通过一个设备文件和几个sysfs属性文件来访问UIO设备,其中/dev/uioX用于访问卡的地址空间,并且可以在/dev/uioX上使用select()来等待中断。
然而,UIO也存在一些不足,如不支持DMA(不受IOMMU的保护)、中断支持有限以及需要Root权限运行等。但总的来说,UIO提供了一种在用户空间实现设备驱动的方法,为开发者提供了更大的灵活性和便利性。
2.3 NUMA
在传统的对称多处理器(SMP)系统中,每个处理器都可以直接访问系统中的共享内存,内存访问延迟是均匀的。但随着系统规模的扩大和内存需求的增加,传统的SMP架构可能无法满足性能需求。NUMA系统将处理器和内存划分为不同的区域,称为节点。每个节点包含一组处理器和与之关联的本地内存。每个处理器可以直接访问本地节点的内存,但对于访问其他节点上的内存,则需要通过系统总线或互连网络进行通信。这种处理器与内存的非均匀访问方式导致了内存访问延迟的不同。
NUMA架构的设计背后的思想是将计算任务划分为可以在特定节点上执行的子任务。这样可以最小化远程内存访问,提高内存访问效率和整体系统性能。NUMA系统通常用于需要高性能和可伸缩性的应用程序,例如大型数据库、科学计算和虚拟化。
以上信息仅供参考,如需更多关于NUMA的信息,建议咨询计算机专业人士或查阅计算机专业书籍。
3.dpdk的使用步骤
3.1 配置运行环境
我们在linux环境下,输入如下操作
export RTE_SDK=“dpdk所在路径”
export RTE_TARGET=x86_64-native-linux-gcc
3.2 配置运行环境
随后我们来到dpdk的usertools目录下面,运行dpdk-setup.sh这个脚本
依次执行如下操作
43(加载uio模块)
44(加载vfio模块)
45(加载kni模块)
46(设置巨页,我们设置为512即可)
47(设置巨页,512)
49(需要先把对应的网卡down掉,然后输入对应网卡对应的pic地址)
4.dpdk的基本函数api
4.1函数rte_eal_init()
dpdk其实相对于我们的应用程序来说,可以把他看做成一个框架来使用,dpdk为我们的提供了头文件和库。我们使用其为我们提供的特定api来实现dpdk的使用。
我们先来介绍一下一些基本的函数说明.
int rte_eal_init(int argc, char *argv[]);
上面函数用来对dpdk的运行环境进行初始化,其中两个参数一般是用来做一些运行选项。如下
char *argv[] = {"your_program_name", "-l", "1-3", "--pci-whitelist=0000:01:00.0", NULL};
int argc = sizeof(argv) / sizeof(argv[0]) - 1; // 注意减去程序名称
int ret = rte_eal_init(argc, argv);
1.在这个例子中,
-l
选项用于指定DPDK应用程序应该使用的CPU核心。在这个例子中,1-3
表示使用CPU的1、2、3号核心。
2.
--pci-whitelist
选项用于指定DPDK应该绑定的PCI设备。在这个例子中,0000:01:00.0
是PCI设备的地址。
rte_eal_init()
函数的主要功能和作用包括:
- 初始化 EAL 环境:这个函数会设置和初始化 EAL 所需的各种参数和资源,例如内存池、内存映射、中断处理等。
- 参数解析:该函数会解析传递给 DPDK 应用程序的命令行参数,这些参数通常用于指定 EAL 的行为,如内存分配方式、使用的 CPU 核心、PCI 设备等。
- 硬件资源检测:在初始化过程中,
rte_eal_init()
可能会检测并枚举可用的硬件资源,如 PCI 设备、内存等。 - 内存分配:根据用户指定的参数或默认设置,该函数会分配和初始化必要的内存资源,如大页内存(Huge Pages)。
- CPU 核心绑定:该函数可以将 DPDK 应用程序的线程绑定到特定的 CPU 核心上,以减少线程迁移和上下文切换的开销。
- PCI 设备配置:如果应用程序需要与 PCI 设备(如网卡)进行交互,
rte_eal_init()
可能会配置这些设备,使它们可以被 DPDK 应用程序访问。 - 日志和调试:该函数还可能设置和初始化日志和调试功能,以便在出现问题时可以进行有效的故障排查。
4.2 函数rte_pktmbuf_pool_create()
函数rte_pktmbuf_pool_create
是DPDK(Data Plane Development Kit)中用于创建一个pktmbuf内存池的函数。pktmbuf是DPDK中用于处理网络数据包的内存缓冲区结构。
struct rte_mempool *rte_pktmbuf_pool_create(const char *name,
unsigned n,
unsigned cache_size,
uint16_t priv_size,
uint16_t data_room_size,
int socket_id);
name
:内存池的名称,用于标识和调试。n
:要创建的元素个数,即内存池中预分配mbuf的数量。这决定了内存池的最大容量。cache_size
:预先缓存的元素数量,用于加速mbuf的分配操作。如果设置为0,则不使用缓存机制。priv_size
:每个mbuf数据结构中priv_size
字段的大小,即需要用户自定义的私有数据空间大小。data_room_size
:mbuf buffer的大小,即每个mbuf能够承载数据的大小。这通常根据数据包的大小和数量来确定。socket_id
:NUMA(Non-Uniform Memory Access)节点编号。用于指定内存池应分配到的NUMA节点。这有助于优化内存访问性能。
函数返回一个指向新创建的rte_mempool
结构的指针,如果创建失败则返回NULL。
4.3 函数 rte_eth_dev_count_avail
函数rte_eth_dev_count_avail
是DPDK(Data Plane Development Kit)中的一个函数,用于获取当前系统中可用的以太网设备(网卡)的数量。这些设备必须已经通过DPDK绑定(或绑定到DPDK驱动程序)才能被认为是可用的。
#include <rte_ethdev.h>
uint16_t rte_eth_dev_count_avail(void);
1.返回一个无符号16位整数(
uint16_t
),表示当前系统中可用的以太网设备的数量。
4.4 函数 rte_eth_dev_configure
函数rte_eth_dev_configure
是DPDK(Data Plane Development Kit)中用于配置以太网设备(网卡)的函数。这个函数允许你设置以太网设备的参数,如接收队列和发送队列的数量,以及其他网络设备的属性。
int rte_eth_dev_configure(uint16_t port_id, uint16_t nb_rx_queues, uint16_t nb_tx_queues, const struct rte_eth_conf *eth_conf);
port_id
:要配置的以太网设备(网卡)的端口号。nb_rx_queues
:接收队列的数量。这是DPDK为接收数据包而创建的队列数量。nb_tx_queues
:发送队列的数量。这是DPDK为发送数据包而创建的队列数量。eth_conf
:指向一个rte_eth_conf
结构体的指针,该结构体包含了要配置的以太网设备的各种属性和参数信息。- 函数返回一个整数,表示配置操作的结果。如果配置成功,则返回0;否则,返回一个负的错误码。
4.5 函数rte_eth_rx_queue_setup
函数rte_eth_rx_queue_setup
是DPDK(Data Plane Development Kit)中用于设置以太网设备接收队列的函数。在DPDK中,接收队列是用于从网络接收数据包的通道,它们由DPDK库管理,并允许应用程序高效地接收和处理数据包。
int rte_eth_rx_queue_setup(uint16_t port_id, uint16_t rx_queue_id, uint16_t nb_rx_desc, unsigned int socket_id, const struct rte_eth_rxconf *rx_conf, struct rte_mempool *mb_pool);
port_id
:要配置的以太网设备的端口号。rx_queue_id
:要设置的接收队列的编号。在多个接收队列的场景中,这个编号用于区分不同的队列。nb_rx_desc
:指定此接收队列中使用的接收描述符(mbuf)的数量。这通常是2的幂次方,以提高内存访问效率。socket_id
:指定内存分配器在哪个NUMA(Non-Uniform Memory Access)节点上进行内存分配。这有助于优化内存访问性能。rx_conf
:指向一个rte_eth_rxconf
结构体的指针,该结构体包含了接收队列的配置选项,如RSS(Receive Side Scaling)哈希函数、数据包校验和卸载等。mb_pool
:指向一个DPDK内存池(mempool)的指针,用于从该内存池中分配mbuf来存储接收到的数据包。
4.6 函数rte_eth_dev_start
函数 rte_eth_dev_start()
是DPDK(Data Plane Development Kit)中用于启动以太网设备的函数。这个函数将启动之前通过 rte_eth_dev_configure()
和 rte_eth_rx_queue_setup()
等函数配置好的以太网设备,并使其能够开始接收和发送数据包。
int rte_eth_dev_start(uint16_t port_id);
port_id
:要启动的以太网设备的端口号。
4.7 函数rte_eth_rx_burst
rte_eth_rx_burst
函数是DPDK(Data Plane Development Kit)库中的一个重要函数,它用于从指定的以太网设备的接收队列中批量读取数据包。这个函数对于提高网络数据包的处理效率和性能非常关键。
uint16_t rte_eth_rx_burst(uint16_t port_id, uint16_t queue_id, struct rte_mbuf **rx_pkts, uint16_t nb_pkts);
port_id
:指定要读取的以太网设备端口号。queue_id
:指定要读取的接收队列编号。rx_pkts
:这是一个指向rte_mbuf*
类型数组的指针,用于存储从接收队列中读取到的数据包。每个元素都是一个指向rte_mbuf
结构体的指针,该结构体描述了数据包在内存中的布局。nb_pkts
:指定最多可以读取的数据包数量。这是函数尝试从接收队列中读取的最大数据包数量。
5.代码示例
下面代码,使用dpdk截取网卡数据,并直接解析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
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;
const int num_tx_queues = 0;
//配置网口, 设置0网口,设置其接收和发送队列。
rte_eth_dev_configure(global_portid, num_rx_queues, num_tx_queues, &port_conf_default);
//设置接收队列,因为这里只有一个接收队列,所以他的编号id是0
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");
}
//创建一个内存池
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) {
//读取udp数据包中的数据部分,直接解读
struct rte_udp_hdr *udphdr = (struct rte_udp_hdr *)(iphdr + 1);
printf("udp : %s\n", (char*)(udphdr+1));
}
}
}
printf("hello dpdk\n");
}