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
相关文章
|
4月前
|
存储 缓存 网络协议
dpdk课程学习之练习笔记二(arp, udp协议api测试)
dpdk课程学习之练习笔记二(arp, udp协议api测试)
63 0
|
3月前
【DPDK 】dpdk测试发udp包
【DPDK 】dpdk测试发udp包
|
3月前
|
网络协议
netmap: UDP 协议栈的实现
netmap: UDP 协议栈的实现
|
3月前
|
缓存 小程序 网络协议
DPDK UDP小程序使用记录
DPDK UDP小程序使用记录
35 0
|
4月前
|
缓存 网络协议 网络架构
什么是协议栈? 用户态协议栈设计(udp协议栈)
什么是协议栈? 用户态协议栈设计(udp协议栈)
|
5月前
|
存储 缓存 网络协议
2.8 基于DPDK的UDP用户态协议栈实现
2.8 基于DPDK的UDP用户态协议栈实现
113 0
|
21天前
|
域名解析 网络协议 关系型数据库
tcp和udp的区别是什么
TCP和UDP是互联网协议中的传输层协议。TCP是面向连接的,通过三次握手建立可靠连接,提供数据顺序和可靠性保证,适用于HTTP、FTP等需要保证数据完整性的应用。UDP则是无连接的,数据报独立发送,传输速度快但不保证可靠性,常用于实时通信、流媒体和DNS解析等对速度要求高的场景。根据应用需求选择合适的协议至关重要。
tcp和udp的区别是什么
|
25天前
|
网络协议 网络性能优化
认识TCP和UDP的区别
重排机制:由于UDP数据包可能因网络原因而发生乱序,因此在应用层需要对接收到的数据包进行排序。
25 4
|
26天前
|
网络协议 网络性能优化
网络面试题:TCP和UDP的区别
网络面试题:TCP和UDP的区别
22 0
|
1月前
|
网络协议 Python
Python网络编程实现TCP和UDP连接
Python网络编程实现TCP和UDP连接
24 0