DPDK是什么
DPDK(Data Plane Development Kit),是一种用户空间数据平面开发工具包,它的主要目标是为开发人员提供一个创建高性能数据平面应用程序的平台。DPDK开发者能够用C语言、汇编与新型Intel架构的linux内核进行交互,使得数据平面应用程序在高度优化的环境中运行。
背景
随着芯片技术与高速网络接口技术的一日千里式发展,报文吞吐需要处理10Gbps 端口处理能力,世面上大量的 25G、 40G 甚至100G 高速端口已经出现,对于这些高吞吐量的网卡和应用场景往往需要专用的硬件加速器或者专门的网络处理器才能处理数据包的转发,传统的Linux协议栈在处理高流量的网络报文时往往会成为性能瓶颈(频繁的上下文切换和内存拷贝)。2009 年开始,以 Venky Venkastraen,Walter Gilmore,Mike Lynch为核心的 Intel 团队,开始了可行性研究, 希望借助纯软件技术+x86通用服务器(而不是专门的硬件)来实现对高速网络的支持,很快他们取得了一定的技术 突破,设计了运行在 Linux 用户态的网卡程序架构。随后,Intel与 6wind 进行了更进一步的合作,共同交付了早期的 DPDK 软件开发包。2011年开始, 6wind、Windriver、Tieto、Radisys 先后宣布了对 Intel DPDK 的商业服务 支持。Intel 起初只是将 DPDK以源代码方式分享给少量客户,作为评估IA 平台和硬件性能的软件服务模块, 随着时间推移与行业的大幅度接受, 2013年Intel 将 DPDK 这一软件以 BSD 开源方式,分享在Intel的网站上,供开发者免费下载。
项目架构
这部分参考自:About - DPDK
Data Plane Development Kit (DPDK) is a Linux Foundation project that consists of libraries to accelerate packet processing workloads running on a wide variety of CPU architectures.
Project Architecture
At its core, DPDK consists of a set of libraries and drivers that support fast packet processing across a variety of hardware and software environments. This framework includes an Environment Abstraction Layer (EAL) that abstracts hardware details from the applications, allowing developers to write portable code that runs efficiently on different platforms, including x86, ARM, and PowerPC processors.
EAL (Environment Abstraction Layer): Provides a basic interface between DPDK applications and the underlying hardware, abstracting away specifics of the operating system and hardware differences.
Memory Management: Includes hugepage support, memory pools, and buffer management, essential for efficient packet processing.
Poll Mode Drivers (PMDs): These are optimized drivers for various network interfaces, bypassing the kernel’s network stack to reduce latency and increase throughput.
Ring Buffers: Utilized for efficient queueing mechanisms, allowing high-speed inter-process communication.
APIs for Packet Processing: Offers a set of functions and libraries for packet manipulation, including header parsing, packet classification, and packet forwarding.
Crypto and Security: Provides libraries and drivers to support cryptographic operations and secure communication.
Eventdev and Timers: For event-driven programming and time management functionalities, aiding in scheduling and execution of tasks in a timely manner.
- Ethernet: 对层二的包进行处理,比如MAC地址的获取和处理等。
- IP Fragementation: 在IP层面对IP包进行分片和重组(IP Reassembly)。在网络传输中,不同的网络链路和设备(如路由器、交换机)可能对数据包的大小有不同的限制,这个限制称为 MTU(Maximum Transmission Unit,最大传输单元)。如果一个数据包的大小超过了某个链路的 MTU,就需要将这个数据包分成更小的片段,以便通过该链路传输。
- TCP Segmentation:在TCP层面对数据包进行分片和重组。TCP提供安全的传输机制,仅仅使用IP Fragementation是不够的。
- Flow Offload:是指DPDK将数据流的处理从 CPU 转移到硬件设备(如网卡、智能网卡、硬件加速器等)上,以提高数据包处理的性能和效率,硬件设备通常具有专门的处理单元,可以高效地执行特定任务,如数据包分类、转发、加密/解密等。
- Tunnels:DPDK 提供了一系列的库和 API,用于支持隧道协议的封装和解封装。GRE(Generic Routing Encapsulation),VXLAN(Virtual Extensible LAN),IPsec(Internet Protocol Security)。
- Classification: 在 DPDK中,"Classification"(分类)指的是对网络流量进行分类的过程。分类是网络数据包处理中的一个关键步骤,通过识别和区分不同类型的流量,可以应用不同的处理策略,如转发、过滤、限速、优先级处理等。DPDK 提供了一些库和 API,用于支持高性能的流量分类操作。流量分类是指根据数据包的特征(如源 IP 地址、目的 IP 地址、端口号、协议类型等)对数据包进行分类,以便应用不同的处理策略。
- DPDK 提供了 rte_flow API,用于定义和管理流量分类规则。rte_flow API 支持多种匹配条件和处理操作,可以用于实现复杂的流量分类策略。
- DPDK 提供了 rte_acl 库,用于实现基于访问控制列表的流量分类。rte_acl 库支持多字段匹配和高效的规则查找,可以用于防火墙、访问控制等场景。
- DPDK 提供了 rte_lpm 库,用于实现最长前缀匹配的流量分类。rte_lpm 库通常用于路由查找和 IP 地址匹配。
- Distribution:在 DPDK(Data Plane Development Kit)中,"Distribution"(分发)通常指的是将网络流量分发到多个处理单元(如 CPU 核心、队列、端口等)进行并行处理的过程。分发机制在高性能网络应用中非常重要,因为它可以有效地利用多核处理器的计算能力,提高数据包处理的吞吐量和效率。
- RSS(Receive Side Scaling),RSS 是一种硬件加速的分发机制,用于将接收到的数据包分发到多个接收队列。RSS 通常基于数据包的头部字段(如源 IP 地址、目的 IP 地址、端口号等)进行哈希计算,以实现流量的均匀分发。
- Flow Director 是一种高级的硬件加速分发机制,可以根据用户定义的流规则将数据包分发到特定的队列或 CPU 核心。Flow Director 支持更复杂的流分类和分发策略。
- 软件分发,DPDK 提供了一些软件分发库,如 rte_distributor 和 rte_ring,用于在软件层面实现数据包的分发和负载均衡。
- QoS:在 DPDK(Data Plane Development Kit)中,QoS(Quality of Service,服务质量)是指对网络流量进行管理和控制,以确保不同类型的流量能够获得所需的带宽、延迟、抖动和丢包率等服务质量。
- DPDK 提供了 rte_flow API,用于定义和管理流量分类规则。
- DPDK 提供了 rte_sched 库,用于实现流量整形和带宽管理。
- DPDK 提供了 rte_meter 库,用于实现带宽管理和流量计量。
- Crypto:DPDK 提供了一些库和 API,用于支持高性能的加密和解密操作,特别是在需要高吞吐量和低延迟的网络应用中。DPDK 提供了 rte_cryptodev 库,用于抽象和管理加密设备以及加密和解密相关的API。
- Baseband:在 DPDK(Data Plane Development Kit)中,"baseband" 通常指的是基带处理(Baseband Processing),这是无线通信系统中的一个关键部分。DPDK 提供了高性能的数据包处理库,可以用于接收和发送基带信号的数据包。这些库包括 rte_ethdev、rte_mbuf 等。
- Compression: DPDK 提供了一些库和 API,用于支持高性能的数据压缩和解压缩操作,特别是在需要高吞吐量和低延迟的网络应用中。DPDK 提供了 rte_compressdev 库,用于抽象和管理压缩设备和压缩和解压缩操作的 API。
Libraries
By leveraging DPDK libraries, developers can create optimized packet processing paths, manage timers for executing functions asynchronously, and utilize a wide range of drivers and libraries tailored for fast packet processing.
- librte_eal – Environment Abstraction Layer: Provides the foundational API for DPDK, facilitating access to hardware resources such as memory, timers, and logs.
- librte_mempool – Memory Pool Manager: Manages memory pools for efficient and speedy packet handling.
- librte_ring – Ring Buffer Manager: Implements lock-free FIFO queues, enabling high-speed communication between various DPDK components.
- librte_mbuf – Packet Buffer Management: Handles packet buffers, which are crucial for packet transmission and reception.
- librte_ethdev – Ethernet Device API: Offers an API for configuring and querying Ethernet devices. It supports various operations, including sending and receiving packets.
- librte_net – Network Helper Library: Provides helper APIs for dealing with network protocols.
- librte_ip_frag – IP Fragmentation and Reassembly: Handles fragmentation and reassembly of IP packets, supporting both IPv4 and IPv6.
- librte_kni – Kernel Network Interface: Facilitates communication between DPDK applications and the Linux kernel networking stack, primarily used for debugging or interfacing with existing Linux network services.
工作原理
DPDK的工作原理主要基于以下几个关键概念:绕过内核、轮询和用户态驱动。
绕过内核
首先,DPDK应用程序运行在操作系统的用户空间,利用自身提供的数据面库进行收发包处理,绕过了Linux内核态协议栈,以提升报文处理效率。这是由于在传统的网络设备上,如路由器、交换机等,需要在瞬间进行大量的报文收发,因此常常能够看到专门的NP(Network Process)处理器,有的用FPGA,有的用ASIC。这些专用器件通过内置的硬件电路(或通过编程形成的)来实现快速的数据包处理。绕过内核能够避免程序的上下文切换以及数据在内核态和用户态之间的拷贝,能够提高数据的处理效率。
轮询
其次,DPDK实现了轮询技术,轮询是一种主动检查的机制,应用程序不断地检查网络接口(NIC)是否有新的数据包到达(例如:循环调用函数rte_eth_rx_burst来检查是否有新的网络数据包),而不是依赖于中断通知。通过轮询,应用程序可以在用户态直接访问网络数据包,从而减少上下文切换和中断处理的开销,提高数据包处理的性能。这是因为中断上下文切换会消耗大量的CPU时间,而轮询可以避免这种消耗,从而提高数据包处理的效率。
轮询的优点:
- 低延迟:由于轮询机制避免了中断处理的开销,数据包处理的延迟显著降低。
- 高吞吐量:轮询机制使得CPU核心可以专注于数据包处理,提高了数据包处理的吞吐量。
- 可预测性:轮询机制提供了更稳定和可预测的性能,因为它避免了中断引起的不可预测的延迟。
轮询的缺点:
- CPU占用高:轮询机制会导致CPU核心一直处于忙碌状态,即使没有数据包需要处理。这可能会导致高功耗和资源浪费。也就是一般的DPDK的应用的CPU使用率都是100%。
- 复杂性:实现高效的轮询机制需要仔细的设计和优化,增加了开发的复杂性。
用户态驱动
最后,DPDK采用了用户态驱动。用户态驱动是指网卡的驱动程序(PMD,Poll Mode Driver)运行在用户态而不是内核态,用户态的驱动程序可以通过DMA或者MMIO的方式直接将网卡的数据写入到内存或者将内存中的数据直接写入到网卡。不同的网卡对应不同的PMD,下面是一些常见的PMD:
- ixgbe:用于Intel 82599 10GbE网卡的驱动程序。
- i40e:用于Intel X710/XL710 40GbE网卡的驱动程序。
- net_ice:用于处理Intel E810系列网卡的数据包。
- mlx4 和 mlx5:用于Mellanox网卡的驱动程序。
- virtio:用于虚拟化环境中的Virtio网卡的驱动程序。
这些驱动程序都位于DPDK源码的<DPDK_ROOT>/drivers/net目录中。
特别的,在使用这些PMD之前,需要将网卡从内核态解绑,并绑定到vfio-pci这个内核态驱动上。虽然DPDK的用户态驱动程序(PMD)在用户态运行,但它们仍然需要某种机制来安全地访问和管理硬件资源,这就是vfio-pci内核态驱动程序的作用所在。vfio-pci的作用:
- 设备管理和隔离
vfio-pci 提供了一种安全的方式来管理和隔离PCI设备。它使用IOMMU(Input-Output Memory Management Unit)来确保设备只能访问分配给它的内存区域,从而防止设备之间的内存访问冲突。
通过 vfio-pci,可以将设备从内核态驱动程序中解绑,并将其分配给用户态应用程序。这种隔离机制确保了设备的安全性和稳定性。
- 直接内存访问(DMA)
vfio-pci 允许用户态应用程序直接访问设备的寄存器和内存,通过内存映射I/O(MMIO)和直接内存访问(DMA)来实现高效的数据传输。
这种直接访问机制减少了内核态和用户态之间的数据拷贝和上下文切换,从而提高了数据包处理的性能。
- 中断重映射
vfio-pci 提供了中断重映射功能,使得用户态应用程序可以处理设备中断。这对于某些高性能应用程序来说是非常重要的,尽管DPDK通常使用轮询机制来避免中断开销。
可以简单理解成,vfio-pci用于设备的管理(管理面),而PMD用于实际的网络数据包的处理(数据面)。
工作模式
DPDK主要采用了两种工作模式:轮询模式和混杂模式。
轮询模式
轮询模式相对于非轮询模式。轮询模式(Polling Mode)是最常用的数据包处理模式。然而,非轮询模式(Non-Polling Mode)也可以用于某些特定场景。非轮询模式通常依赖于中断机制来处理数据包,而不是主动轮询接收队列。轮询模式下,主函数一直循环调用rte_eth_rx_burst来判断是否有新的网络数据包,
for (;;) {
rx_pkt_nb = rte_eth_rx_burst(config->port, config->queue, bufs, RTE_MBUF_SIZE);
if (unlikely(rx_pkt_nb == 0)) {
continue;
}
//...
}
而在非轮询模式下,通过注册中断处理程序来被动调用rte_eth_rx_burst,
// 启用中断
rte_eth_dev_rx_intr_enable(portid, 0);
// 注册中断处理程序
rte_intr_callback_register(rte_eth_dev_socket_id(portid), eth_rx_intr_handler, (void *)(uintptr_t)portid);
...
static void
eth_rx_intr_handler(void *param) {
uint16_t port = (uint16_t)(uintptr_t)param;
struct rte_mbuf *bufs[BURST_SIZE];
uint16_t nb_rx;
nb_rx = rte_eth_rx_burst(port, 0, bufs, BURST_SIZE);
if (nb_rx > 0) {
for (int i = 0; i < nb_rx; i++) {
rte_pktmbuf_free(bufs[i]);
}
}
}
总结:
- 轮询模式:通过主动检查接收队列来处理数据包,提供高吞吐量和低延迟,但会导致高CPU占用。
- 非轮询模式:通过中断机制来处理数据包,减少CPU占用,但可能会增加数据包处理的延迟。
混杂模式
混杂模式是一种网络接口配置模式,在这种模式下,网卡会接收所有经过它的数据包,而不仅仅是发给它的或广播的数据包。
配置方式:
- 调用函数rte_eth_promiscuous_enable(port_id);来启动网卡的混杂模式。
- 调用函数rte_eth_promiscuous_disable(port_id);来关闭网卡的混杂模式。
- 调用函数rte_eth_promiscuous_get来检查网口的模式。
大多数网卡默认情况都是关闭混杂模式的,开启混杂模式的优缺点,
优点
- 网络监控:混杂模式非常适合用于网络监控和分析工具,因为它可以捕获网络中的所有流量
- 调试和测试:在调试和测试网络应用程序时,混杂模式可以帮助捕获所有数据包,便于分析和排查问题。
缺点
- 安全性:混杂模式可能会带来安全风险,因为它允许捕获所有经过网卡的数据包,包括不属于本机的数据包。
- 性能开销:接收和处理所有数据包可能会增加系统的负载,影响性能。
分发机制
DPDK的分发机制主要包含两个方面:多队列和RSS(Receive Side Scaling,接收方扩展)。
多队列
在多队列模式下,每个核被指定处理一个或多个队列。这样,不同的核就可以操作不同的队列,实现了并行处理,提高了数据处理效率。以Intel E810 100Gbps网卡为例,其最大支持的rx队列数为256,每个队列的最大深度为4096。队列可以想象成数组,里面的元素是网络数据包的描述符(descriptor),主要包含的是网络数据包在mempool中的地址,也就是指针。所以简单来说队列就是指针数组。当我们调用rte_eth_rx_burst的时候,就会从指定的队列中获取数据包,处理完数据后会调用rte_pktmbuf_free_bulk来释放队列中的数据包。
RSS
队列是如何增加元素的呢?假设网卡启用了256个队列,当某个数据包到达到网卡的时候,网卡将这个数据包放到哪个队列中呢?这要涉及到网卡的RSS了。在RSS模式下,数据包会根据预设的哈希函数进行分类,然后分发到不同的处理器队列中进行处理。这种方式能够有效地实现负载均衡,提高系统的吞吐量。下面代码用于配置RSS功能,
...
if (rx_queues > 1) {
port_conf.rxmode.mq_mode = RTE_ETH_MQ_RX_RSS; // Multiqueue mode
port_conf.rx_adv_conf.rss_conf.rss_key = NULL; // Use default RSS key
port_conf.rx_adv_conf.rss_conf.rss_hf =
dev_info.flow_type_rss_offloads; // Use all device supported RSS hash functions
//port_conf.rx_adv_conf.rss_conf.rss_hf = RTE_ETH_RSS_PROTO_MASK & dev_info.flow_type_rss_offloads;
}
数据平面处理方式
DPDK数据平面的处理方式包含RTC和Pipleline。
RTC
Run-to-completion(RTC)模式是另外一种数据处理方式,一个core会处理数据包生命周期中的所有事情。例如,core1(收包)->core1(处理)->core1(转发),core2(收包)->core2(处理)->core2(转发)等等。可以通过增加core的数量来scaling以提高系统的整体性能,每个core在处理数据包的地位上是平等的,工作负载也是一样的。这种模式的主要优点是简单直观,易于实现。但要注意的是,如果core的处理能力不一样(例如频率不一样),则要防止能力低的core可能会出现瓶颈。
Pipleline
DPDK的Pipeline模式是一种数据处理方式,主要应用于数据平面处理,如网络设备的快速转发等。它通过一系列的处理阶段(pipeline stages)来处理数据包,每个阶段可以执行特定的操作,如分类、转发、修改等,不同的阶段由不同的core来处理。例如,core1(收包)->core2(处理)->core3(转发)。数据在不同的core中进行流动,所有的core都可以操作同一个网络数据包,数据包存放到环形缓冲区中(环形队列),所有core都可以访问此环形队列。由于每个阶段所消耗的CPU资源是不一样的,所以可以为不同的阶段配置不同的CPU数量。
然而,虽然Pipeline模式可以提高数据处理效率,但其代码逻辑复杂,可能会引入潜在的内存安全风险,因此需要大量的测试以确保其稳定性和安全性。
DPDK数据包处理步骤
DPDK数据处理方式主要包括以下几个步骤:初始化,接收数据,数据包处理,数据包发送,清理资源,
初始化
在应用程序启动时,DPDK会初始化所有需要的硬件设备、线程和内存池等资源,并为每个CPU核心分配一个独立的运行环境。
// Init DPDK env
int ret = rte_eal_init(argc, argv);
// Create mempool
struct rte_mempool *mbuf_pool = rte_pktmbuf_pool_create("MBUF_POOL", 8192, 250, 0, RTE_MBUF_DEFAULT_BUF_SIZE, rte_socket_id());
// Setup rx queues
ret_val = rte_eth_dev_configure(port_id, rx_queues, 0, &port_conf);
// Configure descriptors
ret_val = rte_eth_rx_queue_setup(port_id, i, rx_descs, rte_eth_dev_socket_id(port_id), NULL, mbuf_pools[i]);
// Launch worker thread
retval = rte_eal_remote_launch((lcore_function_t *)start_worker_thread, config, core_id);
接收数据
当一个数据包到达网卡时,DPDK会将其缓存在一个环形缓冲区中(mempool中),并通过一定的分类规则(RSS)将数据包的描述符(地址)存放到网卡的接收队列中,应用程序通过调用rte_eth_rx_burst可以获取到数据包。
nb_rx = rte_eth_rx_burst(port, 0, bufs, BURST_SIZE);
数据包处理
应用程序从队列中取出待处理的数据包后,根据应用程序需求处理数据包,例如特征(MAC,IP,Port,Protocol等)提取,修改IP,数据存储等。
数据包发送
完成处理后,如果要发送数据,DPDK会将结果重新打包成网络报文,并通过网卡发送回网络中。调用rte_eth_tx_burst批量发送数据包。
nb_tx = rte_eth_tx_burst(port, 0, bufs, nb_rx);
回收资源
当应用程序不再需要某些资源时,DPDK会将它们释放回内存池或者硬件设备中,以便其他线程或者进程使用。例如调用函数rte_pktmbuf_free(bufs[i]);来释放mempool中的资源,
rte_pktmbuf_free(bufs[i]);
DPDK思维导图
DPDK收发包相关
收包
一台服务器可能有多个网卡,接入到PCIe通道,每个网卡可能有一个或多个物理网口,即port,这些网口共用网卡的处理芯片。对于Intel E810 100Gbps网卡为例,单个port的接收和发送速率为100Gbps,但是两个网卡的速率最多为160Gbps左右。一个port可以有多个接收/发送队列,例如 E810 100Gbps网卡,一个port最多可以支持256个接收队列,当网卡接收到数据包时会将数据包按照RSS负载均衡算法分配给这些队列,不同的线程可以从这些队列中获取数据并处理,这样的目的是可以充分发挥多核CPU的性能。一个接收/发送队列可以设置一定数量的队列描述符,用于存放数据包的metadata。例如,E810 100Gbps网卡,最多支持4096个Rx描述符,即可以同时允许4096个数据包在网卡中等待应用程序处理,如果此时还有新的数据包到达网口,这些数据包只能被drop,就是丢包的意思。描述符中存放的是数据包在内存中的地址,而不是数据包本身。
以DPDK收包为例,首先会调用rte_pktmbuf_pool_create函数为不同的port创建内存池,内存池中可以存放mbuf对象,即数据包对象,
struct rte_mempool *mbuf_pool = rte_pktmbuf_pool_create(
strbuf, NUM_MBUFS_DEFAULT, MBUF_CACHE_SIZE, 0, RTE_MBUF_DEFAULT_BUF_SIZE, socket_id);
然后调用rte_eth_dev_configure设置队列数量,
ret_val = rte_eth_dev_configure(port_id, rx_queues, 0, &port_conf);
接着设置每个队列的描述符数量以及关联mbuf_pool,
for (uint16_t i = 0; i < rx_queues; i++) {
ret_val = rte_eth_rx_queue_setup(port_id, i, rx_descs, rte_eth_dev_socket_id(port_id), NULL,
mbuf_pool);
if (ret_val) {
RTE_LOG(ERR, NTR,
"Failed to configure descriptors for port %d with rx queue %d, error: %s\n",
port_id, i, rte_strerror(-ret_val));
return ret_val;
}
}
最后启动port,
ret_val = rte_eth_dev_start(port_id);
启动port后,可以调用rte_eth_rx_burst来轮询(PMD)获取一定数量的数据包,
rx_pkt_nb = rte_eth_rx_burst(config->port, config->queue, bufs, WORKER_RTE_MBUF_SIZE);
数据包以这个结构struct rte_mbuf *bufs[WORKER_RTE_MBUF_SIZE];来存放。
处理完数据包以后,调用函数rte_pktmbuf_free_bulk来释放相关资源,应该包含清空rx queue中的描述符,清空mbuf_pool中的数据等,
rte_pktmbuf_free_bulk(bufs, rx_pkt_nb);
发包
同上面一样,发包一样需要初始化的步骤,只是发包的时候需要调用下面的函数来初始化发包队列,
ret = rte_eth_tx_queue_setup(portid, 0, TX_RING_SIZE, rte_eth_dev_socket_id(portid), NULL);
注意:如果是多个发送队列,需要循环调用上面的函数,第二个参数表示发包队列id。
处理完数据包以后再调用下面函数发送数据包,
nb_tx = rte_eth_tx_burst(port, 0, bufs, nb_rx);
其中,第二个参数是发包队列id。
DPDK在收报的时候可以通过RSS来平均分配接收队列,但是在发包的时候,却没有类似机制。虽然DPDK没有内置的发送端负载均衡机制,但你可以通过以下几种方法来实现发送数据包的负载均衡:
- 基于流的负载均衡:根据数据包的流信息(如源IP、目标IP、源端口、目标端口等)计算哈希值,并将数据包分配到不同的发送队列。
- 轮询调度:使用轮询调度算法(Round-Robin)将数据包分配到不同的发送队列。
- 自定义调度算法:根据具体需求实现自定义的调度算法,将数据包分配到不同的发送队列。
DPDK的应用场景
网络功能虚拟化(NFV)
- 虚拟路由器(vRouter):使用DPDK实现高性能的虚拟路由器,能够处理大量的网络流量。
- 虚拟交换机(vSwitch):使用DPDK实现虚拟交换机,如Open vSwitch(OVS)的DPDK加速版本,提供高性能的虚拟网络功能。
- 虚拟防火墙(vFirewall):使用DPDK实现虚拟防火墙,提供高效的包过滤和安全功能。
- 虚拟专用网(vVPN):使用DPDK实现虚拟专用网,提供高性能的加密和解密功能。
数据包处理和分析
- 数据包捕获和分析工具:使用DPDK实现高性能的数据包捕获和分析工具,如Wireshark的高性能版本。
- 入侵检测系统(IDS):使用DPDK实现入侵检测系统,能够实时分析网络流量并检测异常行为。
- DDoS防护系统:使用DPDK实现DDoS防护系统,能够高效地检测和防御分布式拒绝服务攻击。
- 数据面驱动组件:使用VPP时,可以将DPDK作为高效的数据面驱动组件。
高性能网络服务
- 负载均衡器:使用DPDK实现高性能的负载均衡器,能够高效地分发网络流量。
- 内容分发网络(CDN):使用DPDK实现内容分发网络,提供高效的内容缓存和分发功能。
- DNS服务器:使用DPDK实现高性能的DNS服务器,能够快速解析域名请求。
Ubuntu22.04 安装DKDK
#!/bin/bash
DPDK_VER=24.07
function logdate { date "+%Y-%m-%d %H:%M:%S"; }
function info { echo "$(logdate) [INFO] $*"; }
function warn { echo "$(logdate) [WARN] $*"; }
function error { echo "$(logdate) [ERROR] $*"; exit 1; }
function check_os() {
os_name=$(grep 'NAME="Ubuntu"' < /etc/os-release)
os_ver=$(grep 'VERSION_ID="22.04"' < /etc/os-release)
[[ -n "${os_name}" && -n "${os_ver}" ]] || error "Only Ubuntu 22.04 is supported"
}
function install_dpdk() {
info "Installing DPDK..."
core_count=$(cat /proc/cpuinfo| grep "processor"| wc -l)
info "Core count: ${core_count}"
sudo rm -rf dpdk-${DPDK_VER}.tar.xz dpdk-${DPDK_VER} && \
sudo apt-get install -y wget cmake libnuma-dev pkg-config meson python3-pip python3-pyelftools ninja-build libpcap-dev && \
wget https://fast.dpdk.org/rel/dpdk-${DPDK_VER}.tar.xz && \
tar -xvf dpdk-${DPDK_VER}.tar.xz && \
cd dpdk-${DPDK_VER} && \
sudo meson build -Dmax_lcores=${core_count} && \
cd build && \
sudo ninja && \
sudo ninja install && \
sudo ldconfig && \
# Test DPDK installation, succeed if you see some "hello from core XXX" after executing dpdk-helloworld
sudo meson configure -Dexamples=helloworld && \
sudo ninja && \
sudo ./examples/dpdk-helloworld
}
function main() {
check_os
install_dpdk
}
main
实现简单的L2 Forward
架构
代码
#include <rte_eal.h>
#include <rte_ethdev.h>
#include <rte_mbuf.h>
#include <signal.h>
#include <unistd.h>
#define NB_PORTS 2
#define RX_DESC_NUM 4096
#define TX_DESC_NUM 4096
#define NUM_MBUFS_DEFAULT (2 * RX_DESC_NUM)
#define MBUF_CACHE_SIZE 512
#define BURST_SIZE 4096
uint8_t is_stop = 0;
static void port_init(uint16_t port_id, struct rte_mempool *mbuf_pool);
static int start_worker(const uint16_t *port_id);
static void signal_handler(int sig);
static const struct rte_eth_conf port_conf_default = {.rxmode = {
.mq_mode = RTE_ETH_MQ_RX_NONE,
.max_lro_pkt_size = RTE_ETHER_MAX_LEN,
}};
static void port_init(uint16_t port_id, struct rte_mempool *mbuf_pool) {
struct rte_eth_conf port_conf = port_conf_default;
const uint16_t rx_queues = 1;
const uint16_t tx_queues = 1;
int retval = 0;
// Check port_id
if (port_id >= rte_eth_dev_count_avail()) {
rte_exit(EXIT_FAILURE, "Port id %d is invalid.\n", port_id);
}
// Configure Rx number and Tx number
retval = rte_eth_dev_configure(port_id, rx_queues, tx_queues, &port_conf);
if (retval != 0) {
rte_exit(EXIT_FAILURE, "Failed to config port: %d\n", port_id);
}
// Configure Rx queues
for (uint16_t i = 0; i < rx_queues; i++) {
retval = rte_eth_rx_queue_setup(port_id, i, RX_DESC_NUM, rte_eth_dev_socket_id(port_id),
NULL, mbuf_pool);
if (retval < 0) {
rte_exit(EXIT_FAILURE, "Failed to config Rx queue: %d for port: %d\n", i, port_id);
}
}
// Configure Tx queues
for (uint16_t i = 0; i < tx_queues; i++) {
retval =
rte_eth_tx_queue_setup(port_id, i, TX_DESC_NUM, rte_eth_dev_socket_id(port_id), NULL);
if (retval < 0) {
rte_exit(EXIT_FAILURE, "Failed to config Tx queue: %d for port: %d\n", i, port_id);
}
}
// Enable eth promiscuous
rte_eth_promiscuous_enable(port_id);
// Start port
retval = rte_eth_dev_start(port_id);
if (retval < 0) {
rte_exit(EXIT_FAILURE, "Failed to start port %d\n", port_id);
}
// Print port information
struct rte_ether_addr addr;
rte_eth_macaddr_get(port_id, &addr);
printf("Initialized port successfully, id: %u, MAC=%02" PRIx8 ":%02" PRIx8 ":%02" PRIx8
":%02" PRIx8 ":%02" PRIx8 ":%02" PRIx8 "\n",
(unsigned)port_id, addr.addr_bytes[0], addr.addr_bytes[1], addr.addr_bytes[2],
addr.addr_bytes[3], addr.addr_bytes[4], addr.addr_bytes[5]);
}
static int start_worker(const uint16_t *port_id) {
struct rte_mbuf *bufs[BURST_SIZE];
const uint16_t queue_id = 0; // Only use 1 queue
const uint16_t rx_port_id = *port_id;
const uint16_t tx_port_id = (*port_id + 1) % 2;
uint16_t nb_rx = 0;
uint16_t nb_tx = 0;
printf("Starting worker, lcore id: %d, port id: %d, target port id: %d\n", rte_lcore_id(),
rx_port_id, tx_port_id);
while (1) {
if (unlikely(is_stop)) {
printf("lcore: %d is stopped\n", rte_lcore_id());
break;
}
// Receive packets
nb_rx = rte_eth_rx_burst(rx_port_id, queue_id, bufs, BURST_SIZE);
if (unlikely(nb_rx == 0)) {
continue;
}
// Forward packets to target port
nb_tx = rte_eth_tx_burst(tx_port_id, queue_id, bufs, nb_rx);
if (nb_tx < nb_rx) {
// Delete the packets that cannot forward
for (uint16_t i = nb_tx; i < nb_rx; i++) {
rte_pktmbuf_free(bufs[i]);
}
}
}
}
static void signal_handler(int sig) {
printf("Caught signal: %d\n", sig);
is_stop = 1;
}
int main(int argc, char *argv[]) {
signal(SIGINT, signal_handler);
signal(SIGTERM, signal_handler);
struct rte_mempool *mbuf_pool;
unsigned nb_ports;
// Hard code lcore id, lcore 1 for port 0; lcore 2 for port 1
uint16_t core_id[] = {1, 2};
// Hard code port id, will use 2 ports, id 0 and id 1
uint16_t port_id[] = {0, 1};
// Init EAL
if (rte_eal_init(argc, argv) < 0) {
rte_exit(EXIT_FAILURE, "Error with EAL initialization\n");
}
// Get and check port count
nb_ports = rte_eth_dev_count_avail();
if (nb_ports < NB_PORTS) {
rte_exit(EXIT_FAILURE, "Error: number of ports must be at least 2\n");
}
for (uint16_t i = 0; i < NB_PORTS; i++) {
// Create membuf
char buf_name[64];
sprintf(buf_name, "MBUF_POOL_PORT_%d", i);
mbuf_pool = rte_pktmbuf_pool_create(buf_name, NUM_MBUFS_DEFAULT, MBUF_CACHE_SIZE, 0,
RTE_MBUF_DEFAULT_BUF_SIZE, rte_socket_id());
if (mbuf_pool == NULL) {
rte_exit(EXIT_FAILURE, "Cannot create mbuf pool\n");
}
// Init port
port_init(port_id[i], mbuf_pool);
// Launch worker
if (rte_eal_remote_launch((lcore_function_t *)start_worker, &port_id[i], core_id[i])) {
rte_exit(EXIT_FAILURE, "Failed to launch worker process on core %d\n", (i + 1));
}
}
while (!is_stop) {
usleep(1000000);
}
printf("main core : %d is stopped\n", rte_get_main_lcore());
return 0;
}