DPDK — 数据平面开发技术

目录

DPDK 完全内核旁路技术实现

DPDK 技术分为基本技术和优化技术两类。其中,前者指标准的 DPDK 数据平面开发包和 I/O 转发实现技术。

DPDK 实现原理

在这里插入图片描述

  • 内核协议栈(左边):网卡 -> 驱动 -> 协议栈 -> Socket 接口 -> 业务。
  • DPDK 基于 UIO(User Space I/O)的内核旁路(右边):网卡 -> DPDK 轮询模式-> DPDK 基础库 -> 业务。

NOTE:说 DPDK 依赖网卡设备不如说 DPDK 依赖的是网卡设备对应的驱动程序。支持 DPDK 的 NIC Kernel Driver 可以转换为 UIO Driver 模式。由此,如有需要,DPDK 实际上是可以在虚拟机上使用的,前提是网卡设备通过 Passthrough 的方式给到虚拟机。所以,该场景中 SR-IOV 网卡会是一个不错的选择。

DPDK 架构

在这里插入图片描述

在最底部的内核态(Linux Kernel),DPDK 拥有两个模块:KNI 与 IGB_UIO。而 DPDK 的上层用户态由很多库组成,主要包括核心部件库(Core Libraries)、平台相关模块(Platform)、网卡轮询模式驱动模块(PMD-Natives&Virtual)、QoS 库、报文转发分类算法(Classify)等几大类,用户应用程序可以使用这些库进行二次开发。下面我们逐一介绍这些组件的功能和作用。

UIO,DPDK 的基石

传统的收发数据包方式,首先网卡通过中断方式通知内核协议栈对数据包进行处理,内核协议栈先会对数据包进行合法性进行必要的校验,然后判断数据包目标是否为本机的 Socket,满足条件则会将数据包拷贝一份向上递交到用户态 Socket 来处理。不仅处理路径冗长,还需要从内核到应用层的一次拷贝过程。

为了使得网卡驱动(e.g. PMD Driver)运行在用户态,实现内核旁路。Linux 提供了 UIO(User Space I/O)机制。使用 UIO 可以通过 read() 感知中断,通过 mmap() 实现和网卡设备的通讯。

简单来说,UIO 是用户态的一种 I/O 技术,DPDK 能够绕过内核协议栈,提供了用户态 PMD Driver 的支持,根本上是得益于 UIO 技术。DPDK 架构在 Linux 内核中安装了 IGB_UIO(igb_uio.ko 和 kni.ko.IGB_UIO)模块,以此借助 UIO 技术来截获中断,并重设中断回调行为,从而绕过内核协议栈后续的处理流程。并且 IGB_UIO 会在内核初始化的过程中将网卡硬件寄存器映射到用户态。

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

在这里插入图片描述

此外,DPDK 还在用户态实现了一套精巧的内存池技术,内核态和用户态之间的的内存交互不进行拷贝,只做控制权转移。这样,当收发数据包时,就减少了内存拷贝的开销。

PMD,DPDK 的核心优化

我们知道,Linux 内核在收包时有两种方式可供选择,一种是中断方式,另外一种是轮询方式。

从哲学的角度来说,中断是外界强加给你的信号,你必须被动应对,而轮询则是你主动地处理事情。前者最大的影响就是打断你当前工作的连续性,而后者则不会,事务的安排自在掌握。

中断对性能的影响有多大?在 x86 体系结构中,一次中断处理需要将 CPU 的状态寄存器保存到堆栈,并运行中断服务程序,最后再将保存的状态寄存器信息从堆栈中恢复。整个过程需要至少 300 个处理器时钟周期。

轮询对性能的提升有多大?网卡收到报文后,可以借助 DDIO(Direct Data I/O)技术直接将报文保存到 CPU 的 Cache 中,或者保存到内存中(没有 DDIO 技术的情况下),并设置报文到达的标志位。应用程序则可以周期性地轮询报文到达的标志位,检测是否有新报文需要处理。整个过程中完全没有中断处理过程,因此应用程序的网络报文处理能力得以极大提升。

故此,想要 CPU 执行始终高效,就必然需要一个内核线程去主动 Poll(轮询)网卡,而这种行为与当前的内核协议栈是不相容的,即便当前内核协议栈可以使用 NAPI 中断+轮询的方式,但依旧没有根本上解决问题。除非再重新实现一套全新的内核协议栈,显然这并不现实,但幸运的是,我们可以在用户态实现这一点。

针对 Intel 网卡,DPDK 实现了基于轮询方式的 PMD(Poll Mode Drivers)网卡驱动。该驱动由用户态的 API 以及 PMD Driver 构成,内核态的 UIO Driver 屏蔽了网卡发出的中断信号,然后由用户态的 PMD Driver 采用主动轮询的方式。除了链路状态通知仍必须采用中断方式以外,均使用无中断方式直接操作网卡设备的接收和发送队列。

PMD Driver 从网卡上接收到数据包后,会直接通过 DMA 方式传输到预分配的内存中,同时更新无锁环形队列中的数据包指针,不断轮询的应用程序很快就能感知收到数据包,并在预分配的内存地址上直接处理数据包,这个过程非常简洁。

PMD 极大提升了网卡 I/O 性能。此外,PMD 还同时支持物理和虚拟两种网络接口,支持 Intel、Cisco、Broadcom、Mellanox、Chelsio 等整个行业生态系统的网卡设备,以及支持基于 KVM、VMware、 Xen 等虚拟化网络接口。PMD 实现了 Intel 1GbE、10GbE 和 40GbE 网卡下基于轮询收发包。

UIO+PMD,前者旁路了内核,后者主动轮询避免了硬中断,DPDK 从而可以在用户态进行收发包的处理。带来了零拷贝(Zero Copy)、无系统调用(System call)的优化。同时,还避免了软中断的异步处理,也减少了上下文切换带来的 Cache Miss。

值得注意的是,运行在 PMD 的 Core 会处于用户态 CPU 100% 的状态,如下图:
在这里插入图片描述
由于,网络空闲时 CPU 会长期处于空转状态,带来了电力能耗的问题。所以,DPDK 引入了 Interrupt DPDK(中断 DPDK)模式。Interrupt DPDK 的原理和 NAPI 很像,就是 PMD 在没数据包需要处理时自动进入睡眠,改为中断通知,接收到收包中断信号后,激活主动轮询。这就是所谓的链路状态中断通知。并且 Interrupt DPDK 还可以和其他进程共享一个 CPU Core,但 DPDK 进程仍具有更高的调度优先级。

在这里插入图片描述

IGB_UIO

虽然 PMD 是在用户态实现的驱动程序,但实际上还是会依赖于内核提供的策略。其中 UIO 内核模块,是内核提供的用户态驱动框架,而 igb_uio 是 DPDK 用于与 UIO 交互的内核模块,通过 igb_uio 来 bind 指定的 PCI 网卡设备到 DPDK 使用。

igb_uio 内核模块主要功能之一就是用于注册一个 PCI 设备。实际上这是由 DPDK 提供个一个 Python 脚本 dpdk-devbind 来完成的,当执行 dpdk-devbind 来 bind 网卡时,会通过 sysfs 与内核交互,让内核使用指定的驱动程序来匹配网卡。具体的行为是向文件 /sys/bus/pci/devices/(pci id)/driver_override 写入指定驱动的名称,或者向 /sys/bus/pci/drivers/igb_uio(驱动名称)/new_id 写入要 bind 的网卡设备的 PCI ID。前者是配置设备,让其选择驱动;后者是是配置驱动,让其支持新的 PCI 设备。按照内核的文档 https://www.kernel.org/doc/Documentation/ABI/testing/sysfs-bus-pci 中提到,这两个动作都会促使驱动程序 bind 新的网卡设备。

dpdk-devbind 具体的步骤如下

  1. 获取脚本执行参数指定的网卡(e.g. eth1)设备的 PCI 信息。实际是执行指令 lspci–Dvmmn 查看,主要关注 Slot、Vendor 以及 Device 信息。
Slot: 0000:06:00.1
Class: 0200
Vendor: 8086
Device: 1521
SVendor: 15d9
SDevice: 1521
Rev: 01
  1. unbind 网卡设备之前的 igb 模块,将 Step 1 中获取到的 eth1 对应的 Slot 信息 0000:06:00.1 值写入 igb 驱动的 unbind 文件。e.g. echo 0000:06:00.1 > /sys/bus/pci/drivers/igb/unbind
  2. bind 网卡设备到新的 igb_uio 模块,将 eth1 的 Vendor 和 Device ID 信息写入 igb_uio 驱动的 new_id 文件。e.g. echo 0x8086 0x1521 > /sys/bus/pci/drivers/igb_uio/new_id

igb_uio 内核模块的另一个主要功能就是让用于态的 PMD 驱动程序得以与 UIO 进行交互

  1. 调用 igbuio_setup_bars,设置 uio_info 的 uio_mem 和 uio_port。
  2. 设置 uio_info 的其他成员。
  3. 调用 uio_register_device,注册 UIO 设备。
  4. 打开 UIO 设备并注册中断。
  5. 调用 uio_event_notify,将注册的 UIO 设备的 “内存空间” 映射到用户态的应用空间。其 mmap 的函数为 uio_mmap。至此,UIO 就可以让 PMD 驱动程序在用户态应用层访问设备的大部分资源了。
  6. 应用层 UIO 初始化。同时,DPDK 还需要把 PCI 设备的 BAR 映射到应用层。在 pci_uio_map_resource 函数中会调用 pci_uio_map_resource_by_index 做资源映射。
  7. 在 PMD 驱动程序中,DPDK 应用程序,会调用 rte_eth_rx_burst 读取数据报文。如果网卡接收 Buffer 的描述符表示已经完成一个报文的接收(e.g. 有 E1000_RXD_STAT_DD 标志),则 rte_mbuf_raw_alloc 一个 mbuf 进行处理。
  8. 对应 RTC 模型的 DPDK 应用程序来说,就是不断的调用 rte_eth_rx_burst 去询问网卡是否有新的报文。如果有,就取走所有的报文或达到参数 nb_pkts 的上限。然后进行报文处理,处理完毕,再次循环。

KNI

KNI(Kernel NIC Interface,内核网卡接口),是 DPDK 允许用户态和内核态交换报文的解决方案,模拟了一个虚拟的网口,提供 DPDK 应用程序和 Linux 内核之间通讯没接。即 KNI 接口允许报文从用户态接收后转发到 Linux 内核协议栈中去。

虽然 DPDK 的高速转发性能很出色,但是也有自己的一些缺点,比如没有标准协议栈就是其中之一,当然也可能当时设计时就将没有将协议栈考虑进去,毕竟协议栈需要将报文转发处理,可能会使处理报文的能力大大降低。

在这里插入图片描述

上图是 KNI 的 mbuf 的使用流程,也可以看出报文的流向,因为报文在代码中其实就是一个个内存指针。其中 rx_q 右边是用户态,左边是内核态。最后通过调用 netif_rx 将报文送入 Linux 内核协议栈,这其中需要将 DPDK 的 mbuf 转换成标准的 skb_buf 结构体。当 Linux 内核向 KNI 端口发送报文时,调用回调函数 kni_net_tx,然后报文经过转换之后发送到端口上。

核心部件库

核心部件库(Core Libraries)是 DPDK 面向用户态协议栈应用程序员开发的模块。

  • EAL(Environment Abstraction Layer,环境抽象层):对 DPDK 的运行环境(e.g. Linux 操作系统)进行初始化,包括:HugePage 内存分配、内存/缓冲区/队列分配、原子性无锁操作、NUMA 亲和性、CPU 绑定等,并通过 UIO 或 VFIO 技术将 PCI/PCIe 设备地址映射到用户态,方便了用户态的 DPDK 应用程序调用。同时为应用程序提供了一个通用接口,隐藏了其与底层库以及设备打交道的相关细节。

  • MALLOC(堆内存管理组件):为 DPDK 应用程序提供从 HugePage 内分配堆内存的接口。当需要为 SKB(Socket Buffer,本质是若干个数据包的缓存区)分配大量的小块内存时(如:分配用于存储 Buffer descriptor table 中每个表项指针的内存)可以调用该接口。由于堆内存是从 HugePage 内存分配的,所以可以减少 TLB 缺页。

NOTE:堆,是由开发人员主动分配和释放的存储空间, 若开发人员不释放,则程序结束时由 OS 回收,分配方式类似于链表;与堆不同,栈,是由操作系统自动分配和释放的存储空间 ,用于存放函数的参数值、局部变量等,其操作方式类似于数据结构中的栈。

  • MBUF(网络报文缓存块管理组件):为 DPDK 应用程序提供创建和释放用于存储数据报文信息的缓存块的接口。提供了两种类型的 MBUF,一种用于存储一般信息,一种用于存储实际的报文数据。这些 MBUF 存储在一个内存池中。

  • MEMPOOL(内存池管理组件):为 DPDK 应用程序和其它组件提供分配内存池的接口,内存池是一个由固定大小的多个内存块组成的内存容器,可用于存储不同的对像实体,如:数据报文缓存块等。内存池由内存池的名称(一个字符串)进行唯一标识,它由一个 Ring 缓冲区和一组本地缓存队列组成,每个 CPU Core 优先从自身的缓存队列中分配内存块,当本地缓存队列减少到一定程度时,开始从内存环缓冲区中申请内存块来进行补充。

  • RING(环缓冲区管理组件):为 DPDK 应用程序和其它组件提供一个无锁的多生产者多消费者 FIFO 队列。

NOTE:DPDK 基于 Linux 内核的无锁环形缓冲 kfifo 实现了一套自己的无锁机制。支持单生产者入列/单消费者出列和多生产者入列/多消费者出列操作,在数据传输的时候,降低性能的同时还能保证数据的同步。

  • TIMER(定时器组件):提供一些异步周期执行的接口(也可以只执行一次),可以指定某个函数在规定时间内的异步执行,就像 LIBC 中的 timer 定时器。但是这里的定时器需要 DPDK 应用程序在主循环中周期内调用 rte_timer_manage 来使能定时器,使用起来不那么方便。TIMER 的时间参考来自 EAL 层提供的时间接口。

核心部件库对应的 DPDK 核心组件实现

在这里插入图片描述

  • RTE:Run-Time Environment
  • EAL:Environment Abstraction Layer
  • PMD:Poll-Mode Driver

Memory Manager(librte_malloc,堆内存管理器):提供一组 API,用于从 HugePages 内存创建的 memzones 中分配内存。

Ring Manager(librte_ring,环形队列管理器):在一个大小有限的页表中,Ring 数据结构提供了一个无锁的多生产者-多消费者 FIFO API。相较于无锁队列,它有一些的优势,如:更容易实现,适应于大容量操作,而且速度更快。 Ring 在 Memory Pool Manager 中被使用,而且 Ring 还用于不同 CPU Core 之间或是 Processor 上处理单元之间的通信。

Memory Pool Manager(librte_mempool,内存池管理器):内存池管理器负责分配的内存中的 Pool 对象。Pool 由名称唯一标识,并使用一个 Ring 来存储空闲对象。它提供了其他一些可选的服务,例如:每个 CPU Core 的对象缓存和对齐方式帮助,以确保将填充的对象在所有内存通道上得到均匀分布。

Network Packet Buffer Management(librte_mbuf,网络报文缓冲管理):提供了创建和销毁数据报文缓冲区的能力。DPDK 应用程序中可以使用这些缓存区来存储消息以及报文数据。

Timer Manager(librte_timer,定时器管理):为 DPDK 应用程序的执行单元提供了定时服务,为函数异步执行提供支持。定时器可以设置周期调用或只调用一次。DPDK 应用程序可以使用 EAL 提供的接口获取高精度时钟,并且能在每个核上根据需要进行初始化。

平台相关模块

平台相关模块(Platform)包括 KNI、POWER(能耗管理)以及 IVSHMEM 接口。

  • KNI:主要通过 Linux 内核中的 kni.ko 模块将数据报文从用户态传递给内核态协议栈处理,以便常规的用户进程(e.g. Container)可以使用 Linux 内核协议栈传统的 Socket 接口对相关报文进行处理。

  • POWER:提供了一些 API,让 DPDK 应用程序可以根据收包速率动态调整 CPU 频率或让 CPU 进入不同的休眠状态。

  • IVSHMEM:模块提供了虚拟机与虚拟机之间,或者虚拟机与主机之间的零拷贝共享内存机制。当 DPDK 应用程序运行时,IVSHMEM 模块会调用 Core Libraries 的 API,把几个 HugePage 内存映射为一个 IVSHMEM 设备池,并通过参数传递给 QEMU,这样,就实现了虚拟机之间的零拷贝内存共享。

几个关键 API 的使用举例

  • Buffer Manager API:通过预先从 EAL 上分配固定大小的多个内存对象,避免了在运行过程中动态进行内存分配和回收,以此来提高效率,用于数据包 Buffer 的管理。
  • Queue/Ring Manager API:以高效的方式实现了无锁的 FIFO 环形队列,适用于一个生产者多个消费者、一个消费者多个生产者模型。支持批量无锁操作,可避免锁冲突导致的等待。
  • Packet Flow Classification API:通过 Intel SSE 基于多元组的方式实现了高效的 HASH 算法,以便快速对数据包进行分类处理。该 API 一般用于路由查找过程中的最长前缀匹配。此外,安全产品场景中,可以根据 DataFlow 五元组来标记不同的用户。

在这里插入图片描述

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章