DPDK:UDP 协议栈的实现

简介: DPDK:UDP 协议栈的实现

云技术的出现代表着网络功能虚拟化(NFV)共享硬件成为趋势,NFV的定义是通过标准的服务器、标准交换机实现各种传统的或新的网络功能。越来越多的网络设备基础架构开始逐步向基于通用处理器平台的方向发展。NFV 使得网络在变得更加可控制和成本更低的同时,也需要支持大规模用户或应用程序的性能需求,以及具备对大规模数据的处理能力。

传统的基于内核的报文处理方式通过中断将报文分发至内核,当服务器处理大规模报文时会产生频繁的中断,造成大量的性能开销。当内核协议栈处理报文完毕后,还涉及到将报文一次拷贝到用户层的操作。以上问题成为服务器处理大规模报文的阻碍。

高性能网络报文处理框架的理念是服务器上软件的方式进行优化,利用CPU多核对网络负载处理、依靠软件的方式旁路内核、通过轮询屏蔽中断,在用户态处理网络报文和业务。目前运行在用户态的高性能网络报文处理框架中,Intel 的 DPDK (数据平面开发套件,Data Plane Development Kit) 最早开源投入商用,应用也最为广泛,除此以外还有 wind、Netmap、PF_Ring、NBA、Snap 等一系列高性能报文处理框架。

DPDK 技术采用数据层与控制层分离的设计,在用户空间处理数据包、管理内存、调度处理器,而内核仅负责部分控制指令的处理。

1、DPDK 原理

DPDK 舍弃了内核中断,提供全用户态的驱动,拥有高效的内存管理机制,报文直接通过直接内存存取(DMA, direct memory access)传输到用户态处理,减少内存拷贝次数。

DPDK 旁路原理


左边:传统方式,数据从网卡 -> 驱动 -> 协议栈 -> socket 接口 -> 业务

右边:DPDK 方式,基于 UIO 旁路数据。数据从网卡 -> DPDK轮询模式-> DPDK基础库 -> 业务

DPDK 特点

  • UIO:用户态驱动 IO,将报文拷贝到用户空间
  • hugepage:巨页,通过大内存页提高内存使用效率。
  • cpu affinity:将线程绑定到 cpu 上,这样在线程执行过程中,就不会被随意调度,一方面减少了线程间的频繁切换带来的开销,另一方面避免了 CPU 缓存的局部失效性,增加了 CPU 缓存的命中率。
  • zero copy:减少数据拷贝次数

1.1、用户态驱动 IO

为减少中断开销,DPDK 抛弃了传统的内核中断,采用轮询模式驱动 (poll mode driver, PMD) 的方式直接操作网卡的接收和发送队列,将报文直接拷贝到用户空间,不再经过内核协议栈。

DPDK 的用户态 IO(user space I/O, UIO)驱动技术为 PMD 提供了支持。其主要功能是拦截中断,并重设中断回调行为,从而绕过内核协议栈的后续处理流程。

UIO 技术使得内核空间与用户空间的内存交互不用进行拷贝,而是只做控制权转移,减少了报文的拷贝过程。即具有零拷贝,无系统调用的好处,同步处理也减少了上下文切换带来的 cache miss。从而中断与拷贝中节省的资源和时延,有效地运行在报文处理流程中,提高了报文的处理、转发效率。

UIO 设备的实现机制其实是对用户空间暴露文件接口,比如当注册一个 UIO 设备 uioX,就会出现文件 /dev/uioX,对该文件的读写就是对设备内存的读写。除此之外,对设备的控制还可以通过 /sys/class/uio 下的各个文件的读写来完成。


1.2、内存池管理

Linux 使用 4kB 大小的分页来管理内存,而 DPDK 使用 2MB 或 1GB 的巨页来管理内存,一个页表缓存 TLB 表项可以指向更大的内存区域,从而减少了 TLB miss。

DPDK 对网络报文的内存操作对象是 Mbuf,Mbuf 存储在内存池 Mempool 中。内存池中的内存从巨页中提前分配,并且同时预先分配好指定大小的 Mbuf 对象。内存池采用双环形缓冲区来管理网络报文的生命处理周期,当一个网络报文被网卡接收后,DPDK 在 Mbuf 的环形缓冲中创建一个 Mbuf 对象。对网络报文的所有操作都集中在 Mbuf 对象中,仅在必要时对实际网络报文进行访问。也就是说,内核空间和用户空间的内存交互不进行拷贝,只做控制权转移。

2、DPDK 启动设置

环境监测

# 1、查询网卡信息,并检查是否能 ping 通
 # 2、查看系统是否支持多队列网卡
 cat /proc/interrupts | grep eth0

dpdk 启动设置

# 1、设置 dpdk 环境变量
 export RTE_SDK=/home/king/share/dpdk/dpdk-stable-19.08.2
 export RTE_TARGET=x86_64-native-linux-gcc
 # 2、执行usertools的启动设置
 ./usertools/dpdk-setup.sh
 # 选择 43,Insert IGB UIO module,选择网卡为 vmxnet3 会加载此模块
 # 选择 44,Insert VFIO module,选择网卡为 e1000 会加载此模块
 # 选择 45,Insert KNI module,写回内核
 # 选择 46,Setup hugepage,512
 # 选择 47,Setup hugepage,512
 # 选择 49,绑定 igb_uio 模块
 # 1、先宕掉 eth0 网卡
 sudo ifconfig eth0 down 
 # 2、绑定 pci


3、DPDK:UDP 协议栈

这里实现一个简单的 udp 协议栈,对网卡传递来的 udp 数据包做 echo 处理。

3.1、代码实现

dpdk 环境初始化

int main(int argc, char *argv[]) {
     // 1、eal 初始化
     if (rte_eal_init(argc, argv) < 0) {
         rte_exit(EXIT_FAILURE, "Error\n");
     }
     // 2、创建 mbuf 内存池
     // 内存池分配 MBUF_NUMBER 个 mbuf,mbuf 大小默认固定为 2048(data_room_size指定),牺牲内存空间提高吞吐量 
     struct rte_mempool *mbuf_pool = rte_pktmbuf_pool_create("mbufpool", MBUF_NUMBER, 0, 0, RTE_MBUF_DEFAULT_BUF_SIZE, rte_socket_id());
     if (!mbuf_pool) {
         rte_exit(EXIT_FAILURE, "mbuf Error\n");
     }
     // 3、配置网卡设备
     // 设置接收和发送队列数量
     uint16_t nb_rx_queues = 1;
     uint16_t nb_tx_queues = 1;
     // 创建网卡配置文件
     const struct rte_eth_conf port_conf_default = {
         .rxmode = {.max_rx_pkt_len = RTE_ETHER_MAX_LEN }
     };
     // 配置网卡设备
     rte_eth_dev_configure(gDpdkPortId, nb_rx_queues, nb_tx_queues, &port_conf_default);
     // 创建网卡接收队列
     rte_eth_rx_queue_setup(gDpdkPortId, 0, 128, rte_eth_dev_socket_id(gDpdkPortId), NULL, mbuf_pool);
     // 创建网卡发送队列
     rte_eth_tx_queue_setup(gDpdkPortId, 0, 1024, rte_eth_dev_socket_id(gDpdkPortId), NULL);
     // 启动网卡设备
     rte_eth_dev_start(gDpdkPortId);
     ...
 }

封装 udp 数据包

static struct rte_mbuf *alloc_udp_pkt(struct rte_mempool *pool, uint8_t *data, uint16_t length) {
     // 内存池分配一块固定大小的 mbuf,2048
     struct rte_mbuf *mbuf = rte_pktmbuf_alloc(pool);
     if (!mbuf) {
         rte_exit(EXIT_FAILURE, "rte_pktmbuf_alloc error\n");
     }
     // mbuf 包的长度:udp 包的长度 + ip 首部 + 以太网帧首部
     mbuf->pkt_len = length + sizeof(struct rte_ipv4_hdr) + sizeof(struct rte_ether_hdr);
     // mbuf 包的数据长度
     mbuf->data_len = length + sizeof(struct rte_ipv4_hdr) + sizeof(struct rte_ether_hdr);
     uint8_t *msg = rte_pktmbuf_mtod(mbuf, uint8_t*);
     // ether_hdr
     struct rte_ether_hdr *eth = (struct rte_ether_hdr *)msg;
     rte_memcpy(eth->s_addr.addr_bytes, gSrcMac, RTE_ETHER_ADDR_LEN);
     rte_memcpy(eth->d_addr.addr_bytes, gDstMac, RTE_ETHER_ADDR_LEN);
     eth->ether_type = htons(RTE_ETHER_TYPE_IPV4);
     // iphdr
     struct rte_ipv4_hdr *ip = (struct rte_ipv4_hdr *)(msg + sizeof(struct rte_ether_hdr));
     ip->version_ihl = 0x45;
     ip->type_of_service = 0;
     ip->total_length = htons(length + sizeof(struct rte_ipv4_hdr));
     ip->packet_id = 0;
     ip->fragment_offset = 0;
     ip->time_to_live = 64;
     ip->next_proto_id = IPPROTO_UDP;
     ip->src_addr = gSrcIp;
     ip->dst_addr = gDstIp;
     ip->hdr_checksum = 0;
     ip->hdr_checksum = rte_ipv4_cksum(ip);
     // udphdr
     struct rte_udp_hdr *udp = (struct rte_udp_hdr *)(msg + sizeof(struct rte_ether_hdr) + sizeof(struct rte_ipv4_hdr));
     udp->src_port = gSrcPort;
     udp->dst_port = gDstPort;
     udp->dgram_len = htons(length);
     rte_memcpy((uint8_t*)(udp+1), data, length-sizeof(struct rte_udp_hdr));
     udp->dgram_cksum = 0;
     udp->dgram_cksum = rte_ipv4_udptcp_cksum(ip, udp);
     return mbuf;
 }

从以太网 uio 网卡接收 udp 数据包,并转发回去。dpdk 的接收发送数据操作 burst 是内存操作,不存在 io 的阻塞问题。

int main(int argc, char *argv[]) {
     ...
     // 业务逻辑:echo udp 数据包
     while (1) {
         unsigned num_recvd = 0;
         unsigned i = 0;
          // 逻辑 2:从以太网网卡读取数据,若是 udp 数据,则执行 echo 操作
         // 使用 mbuf 操作
         struct rte_mbuf *mbufs[MBUF_SIZE];
         // 从网卡接收队列接收数据:直接操作内存
         num_recvd = rte_eth_rx_burst(gDpdkPortId, 0, mbufs, MBUF_SIZE);
         if (num_recvd > MBUF_SIZE) {
             rte_exit(EXIT_FAILURE, "rte_eth_rx_burst Error\n");
         }
         for (i = 0; i < num_recvd; i++) {
             // 取出以太网帧头(返回 mbuf 结构体)
             struct rte_ether_hdr *ehdr = rte_pktmbuf_mtod(mbufs[i], struct rte_ether_hdr *);
             // 非 ip 数据包,丢弃处理
             if (ehdr->ether_type != rte_cpu_to_be_16(RTE_ETHER_TYPE_IPV4)) {
                 continue;
             }
             // 取出 ip 首部(返回 mbuf 结构体偏移位置)
             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 首部(iphdr + 1,偏移一个 ip 首部,即返回 udp 首部)
                 struct rte_udp_hdr* udphdr = (struct rte_udp_hdr*)(iphdr + 1);  
                 // 返回 udp 包的长度,避免长度为 0
                 uint16_t length = ntohs(udphdr->dgram_len);
                 *((char*) udphdr + length) = '\0';
                 // 打印数据包的源 ip 和目的 ip
                 struct in_addr addr;
                 addr.s_addr = iphdr->src_addr;
                 printf("src: %s:%d, ", inet_ntoa(addr), ntohs(udphdr->src_port));
                 addr.s_addr = iphdr->dst_addr;
                 printf("dst: %s:%d, %s\n", inet_ntoa(addr), ntohs(udphdr->dst_port), (char *)(udphdr + 1));
                 // 转发 udp 数据包,实现 echo
                 rte_memcpy(gSrcMac, ehdr->d_addr.addr_bytes, RTE_ETHER_ADDR_LEN);
                 rte_memcpy(gDstMac, ehdr->s_addr.addr_bytes, RTE_ETHER_ADDR_LEN);
                 rte_memcpy(&gSrcIp, &iphdr->dst_addr, sizeof(uint32_t));
                 rte_memcpy(&gDstIp, &iphdr->src_addr, sizeof(uint32_t));
                 rte_memcpy(&gSrcPort, &udphdr->dst_port, sizeof(uint16_t));
                 rte_memcpy(&gDstPort, &udphdr->src_port, sizeof(uint16_t));
                 // 封装 udp 数据包,udphdr+1 返回 udp 数据包的数据
                 struct rte_mbuf *mbuf = alloc_udp_pkt(mbuf_pool, (uint8_t*)(udphdr + 1), length);
                 // 发送 mbuf 数据
                 rte_eth_tx_burst(gDpdkPortId, 0, &mbuf, 1); 
             } 
         }
     }
 }

在测试 udp echo 时,由于自定义的协议栈没有实现 arp 协议的处理方法,需要对网卡设置静态 arp,才能进行 echo 测试。

3.2、设置静态 arp

# 以管理员的身份运行cmd
 # 1、寻找要绑定主机的 ip 和 mac
 arp -a
 # 2、查看要进行 arp 绑定的网卡 idx 编号
 netsh i i show in
 # 3、arp 绑定:netsh -c i i add neighbors idx IP MAC
 netsh -c i i add neighbors 10 192.168.0.104 00-0c-29-18-ef-9d
 # arp 解除绑定:netsh -c i i delete neighbors idx
 netsh -c i i delete neighbors 10

查看要进行 arp 绑定的网卡编号的方法如下:


若代码中实现 KNI,则无需配置静态 arp。

4、DPDK:KNI

在 udp 协议栈的基础上实现功能,其他类型数据包通过 KNI(内核网卡接口,Kernel NIC Interface)交给内核协议栈进行处理,并将内核协议栈处理后的结果返回给以太网网卡,发送出去。


这里存在两条数据流向

  • 从以太网网卡读取数据,若是 udp 数据,则执行 echo 操作;否则,转发到内核协议栈处理
  • 从内核协议栈读取数据,转发到以太网网卡接口,发送数据

4.1、代码实现

// 网卡配置接口
 static int g_config_network_if(uint16_t port_id, uint8_t if_up) {
     if (!rte_eth_dev_is_valid_port(port_id)) {
         return -EINVAL;
     }
     int ret = 0;
     if (if_up) {
         rte_eth_dev_stop(port_id);
         ret = rte_eth_dev_start(port_id);
     } 
     else {
         rte_eth_dev_stop(port_id);
     }
     if (ret < 0) {
         printf("Failed to start port : %d\n", port_id);
     }
     return 0;
 }
 int main(int argc, char *argv[]) {
     ...
     // 初始化 kni
     if (rte_kni_init(gDpdkPortId) == -1) {
         rte_exit(EXIT_FAILURE, "kni init failed\n");
     }
     // 定义 kni 配置
     struct rte_kni_conf conf;
     memset(&conf, 0, sizeof(conf));
     // 定义 kni 网卡名字,通过 ifconfig -a 命令查看 
     snprintf(conf.name, RTE_KNI_NAMESIZE, "vEth%d", gDpdkPortId);
     conf.group_id = gDpdkPortId;
     conf.mbuf_size = RTE_MBUF_DEFAULT_BUF_SIZE;
     // 获取以太网网卡 mac 地址
     rte_eth_macaddr_get(gDpdkPortId, (struct rte_ether_addr*)conf.mac_addr);
     // 获取以太网 mtu
     rte_eth_dev_get_mtu(gDpdkPortId, &conf.mtu);
     // 定义 kni 操作
     struct rte_kni_ops ops;
     memset(&ops, 0, sizeof(ops));
     ops.port_id = gDpdkPortId;
     ops.config_network_if = g_config_network_if;
     // 创建 kni 网卡接口,通过 mmap 映射到内存
     global_kni = rte_kni_alloc(mbuf_pool, &conf, &ops);
     ...
     // 业务逻辑:接收发送数据
     while (1) {
         // 逻辑 1:从内核协议栈读取数据,转发到以太网网卡接口,发送数据
         struct rte_mbuf *kni_burst[MBUF_SIZE];
         // 读取 kni 返回的内核协议栈读取的数据
         num_recvd = rte_kni_rx_burst(global_kni, kni_burst, MBUF_SIZE);
         if (num_recvd > MBUF_SIZE) {
             rte_exit(EXIT_FAILURE, "rte_kni_rx_burst Error\n");
         }
         // 向以太网网卡发送读取到的 kni 的数据
         unsigned nb_tx = rte_eth_tx_burst(gDpdkPortId, 0, kni_burst, num_recvd);
         // 未处理完的数据包的处理方式,可以再次转发,这里选择直接丢弃
         if (nb_tx < num_recvd) {
             // 将未转发的数据包释放掉
             for (i = nb_tx; i < num_recvd; i++) {
                 rte_pktmbuf_free(kni_burst[i]);
                 kni_burst[i] = NULL;
             }
         }
         ...
         // 逻辑2:从以太网网卡读取数据,判断数据类型
         // 若是 udp 数据,则执行 echo 操作;否则,转发到内核协议栈处理
         for (i = 0;i < num_recvd;i ++) {
             ...
              // 其他类型的数据发送到 kni,交由内核协议栈处理(向内核写数据)
             else {
                 rte_kni_tx_burst(global_kni, &mbufs[i], 1);
             }
         }
         // 处理 kni 请求
         rte_kni_handle_request(global_kni);
     }
 }

4.2、程序测试

首先,为保证 kni 可以运行,需要开启向内核写入数据

echo 1 > /sys/devices/virtual/net/lo/carrier

为避免干扰,解除静态 arp 绑定

netsh -c i i delete neighbors 10

启动程序,ifconfig -a 可以查看 kni 网卡信息,名字为 vEth0

vEth0     Link encap:Ethernet  HWaddr 00:0c:29:18:ef:9d  
           BROADCAST MULTICAST  MTU:1500  Metric:1
           RX packets:2 errors:0 dropped:2 overruns:0 frame:0
           TX packets:0 errors:0 dropped:0 overruns:0 carrier:0
           collisions:0 txqueuelen:1000 
           RX bytes:120 (120.0 B)  TX bytes:0 (0.0 B)

为 vEth0 配置 ip 地址

ifconfig vEth0 192.168.0.105 up

捕获发送到 vEth0 (内核协议栈)的数据

tshark -i vEth0 icmp

远端执行 ping 命令,捕获到 kni 的 icmp 数据包,说明其他协议成功交给内核处理,并返回。

192.168.0.106 → 192.168.0.105 ICMP 74 Echo (ping) request  id=0x0001, seq=23/5888, ttl=64
 192.168.0.105 → 192.168.0.106 ICMP 74 Echo (ping) reply    id=0x0001, seq=23/5888, ttl=6
相关文章
|
2月前
|
存储 网络协议 算法
UDP 协议和 TCP 协议
本文介绍了UDP和TCP协议的基本结构与特性。UDP协议具有简单的报文结构,包括报头和载荷,报头由源端口、目的端口、报文长度和校验和组成。UDP使用CRC校验和来检测传输错误。相比之下,TCP协议提供更可靠的传输服务,其结构复杂,包含序列号、确认序号和标志位等字段。TCP通过确认应答和超时重传来保证数据传输的可靠性,并采用三次握手建立连接,四次挥手断开连接,确保通信的稳定性和完整性。
83 1
UDP 协议和 TCP 协议
|
2月前
|
网络协议
UDP 协议
UDP 协议
109 58
|
19天前
|
网络协议 网络性能优化 C#
C# 一分钟浅谈:UDP 与 TCP 协议区别
【10月更文挑战第8天】在网络编程中,传输层协议的选择对应用程序的性能和可靠性至关重要。本文介绍了 TCP 和 UDP 两种常用协议的基础概念、区别及应用场景,并通过 C# 代码示例详细说明了如何处理常见的问题和易错点。TCP 适用于需要可靠传输和顺序保证的场景,而 UDP 适用于对延迟敏感且可以容忍一定数据丢失的实时应用。
25 1
|
28天前
|
网络协议 算法 数据格式
【TCP/IP】UDP协议数据格式和报文格式
【TCP/IP】UDP协议数据格式和报文格式
81 3
|
1月前
|
存储 网络协议 算法
更深层次理解传输层两协议【UDP | TCP】【UDP 缓冲区 | TCP 8种策略 | 三次握手四次挥手】
UDP和TCP各有所长,UDP以其低延迟、轻量级的特点适用于对实时性要求极高的应用,而TCP凭借其强大的错误检测、流量控制和拥塞控制机制,确保了数据的可靠传输,适用于文件传输、网页浏览等场景。理解它们的工作原理,特别是UDP的缓冲区管理和TCP的8种策略,对于优化网络应用的性能、确保数据的高效和可靠传输至关重要。开发者在选择传输层协议时,应根据实际需求权衡利弊,合理利用这两项关键技术。
52 5
|
1月前
|
JavaScript 安全 Java
谈谈UDP、HTTP、SSL、TLS协议在java中的实际应用
下面我将详细介绍UDP、HTTP、SSL、TLS协议及其工作原理,并提供Java代码示例(由于Deno是一个基于Node.js的运行时,Java代码无法直接在Deno中运行,但可以通过理解Java示例来类比Deno中的实现)。
57 1
|
2月前
|
监控 网络协议 网络性能优化
如何办理支持UDP协议的网络
在当今网络环境中,UDP(用户数据报协议)因传输速度快、延迟低而广泛应用于在线游戏、视频流媒体、VoIP等实时服务。本文详细介绍了办理支持UDP协议网络的方法,包括了解UDP应用场景、选择合适的ISP及网络套餐、购买支持UDP的设备并进行优化设置,以及解决常见问题的策略,帮助用户确保网络稳定性和速度满足实际需求。
|
2月前
|
网络协议
UDP协议在网络通信中的独特应用与优势
UDP(用户数据报协议)作为关键的传输层协议,在网络通信中展现出独特优势。本文探讨UDP的无连接性及低开销特性,使其在实时性要求高的场景如视频流、在线游戏中表现优异;其不保证可靠交付的特性赋予应用程序自定义传输策略的灵活性;面向报文的高效处理能力及短小的包头设计进一步提升了数据传输效率。总之,UDP适用于高速、实时性强且对可靠性要求不高的应用场景,为网络通信提供了多样化的选择。
|
2月前
|
网络协议 视频直播 C语言
C语言 网络编程(三)UDP 协议
UDP(用户数据报协议)是一种无需建立连接的通信协议,适用于高效率的数据传输,但不保证数据的可靠性。其特点是无连接、尽力交付且面向报文,具备较高的实时性。UDP广泛应用于视频会议、实时多媒体通信、直播及DNS查询等场景,并被许多即时通讯软件和服务(如MSN/QQ/Skype、流媒体、VoIP等)采用进行实时数据传输。UDP报文由首部和数据部分组成,首部包含源端口、目的端口、长度和校验和字段。相比TCP,UDP具有更高的传输效率和更低的资源消耗。
|
3月前
|
域名解析 网络协议 Linux
在Linux中,我们都知道,dns采用了tcp协议,又采用了udp协议,什么时候采用tcp协议?什么 时候采用udp协议?为什么要这么设计?
在Linux中,我们都知道,dns采用了tcp协议,又采用了udp协议,什么时候采用tcp协议?什么 时候采用udp协议?为什么要这么设计?