Table of Contents
Data Plane Development Kit (DPDK) 是一个由 Intel 推动的高性能网络包处理库,通过在用户态处理 I/O 的方式加速处理网络包,在这篇文章中简单的介绍了 virtio, vhost-net, vhost-user 以及如何将 dpdk 与 qemu 打通。
基本原理
当我们使用 qemu 在机器上运行虚拟机的时候,如下图所示,一共有 5 个部分需要我们注意,分别是 host 的 kernel space 和 user space,guest 的 kernel space 和 user space,qemu 是运行在 host user space 上的。
Virtio
Virtio 是一种半虚拟化技术,指 guest 知道自己是一个虚拟机,相对的是全虚拟化,指 guest 并不知道自己是虚拟机。全虚拟化中 qemu 需要截获全部的收包/发包请求并转化成与真正与物理 NIC 的请求,这样做的代价是非常大的且实现起来比较复杂, virtio 这种半虚拟化的方案在性能和维护的易用性上有很大的提升,所以是目前比较主流的一种 I/O 方案。(注意:这里只是以 NIC 为例,事实上 virtio 是对 I/O 请求的一种抽象,virtio 可以接管任何 I/O)
Virtio 在客户操作系统内核中安装前端驱动(front-end driver)和在虚拟机系统中实现后端驱动(back-end device)的方式。前后端驱动通过 virtqueue 直接通信,这就绕过了经过操作系统内核模块的过程,达到提高 I/O 性能的目的。在这种架构下 qemu 就不需要模拟 NIC 的各种行为,只需要通过 virtqueue 共享内存的方式提升 I/O 的速度。
所有的 I/O 通信架构都有数据平面与控制平面之分,virtio 控制平面是通过 PCI 传输协议实现的(具体还没仔细看),而数据平面正是使用这些通过共享内存实现的 virtqueue 来实现虚拟机与主机之间的通信。
Virtqueue 包括的队列可以是零个也可以是多个,对于网卡的 virtio 实现上主要是有 available vring 和 used vring 两个部分组成,vring 是一个循环队列的数据结构。Guest 发包的时候将数据放到 available vring 中,host 接收到的包放到 used vring 中。
vhost-net & vhost-user
有关网卡的 virtio 有三个典型实现,分别对应的是 qemu 原始实现的 virtio,一个 Linux 社区维护的 virtio-net,以及 dpdk 实现的 virtio-user 三种方案,他们大概的架构图如下所示。(注:这里面隐藏掉了很多信息,只讲明他们之间的速度为什么会有差异,更详细信息请参阅其他文章)
在最原始的 virtio-net 架构中,共享内存是发生在 qemu 和 guest 之间的,由于只能在内核中操作 I/O,所以数据不得不从用户态复制到内核态中,同时需要做一次 trap 操作,让系统从用户态进入内核态,这中间的成本是一次复制和一次 trap。
在 Linux 社区维护的 vhost-net 方案,可以简单的理解为内存共享发生在 guest 和 host 的内核中,是通过 ioctl syscall 提供的 offload 功能实现的。在这种方案下只需要做一次 trap 操作,减少了一次复制的成本。(PS:王旭大佬说过,所谓的高性能就是尽量实现零拷贝)事实上 virtio-net 这种方案在性能上已经有了非常大的提升。
在 dpdk 中实现的 vhost-user-net 则是一种更极端的方案,通过 Userspace I/O (UIO) 和 unix domain socket (uds) 的方式实现的 offload 功能实现了完全在用户态下完成与网卡通信的方案。UIO 的本质是让 I/O 发生在用户态,这样就不需要写一个运行在 kernel 中的驱动。在使用 uio 后会添加一个 /dev/uioX
的设备,使用 read syscall 读取内容的时候会一直被阻塞,直到有硬件中断产生,该 syscall 返回的结果是一个数据,表示中断次数的整数。用户需要自己去轮询 /dev/uioX
设备文件 (Poll Mode Driver, PMD),而不是通过 kernel 中断来实现,同时配合 mmap 映射内存和 NIC 寄存器/内存避免了数据拷贝。
由于内核完全不参与包的转发工作,所以内核协议栈也就没办法被用于包的解析工作,这就导致如果需要使用 dpdk 需要配合自己的 TCP/IP 库等等。
在 QEMU 中运行 DPDK (Running DPDK on QEMU)
测试环境
- dpdk 21.11.1
- qemu 3.1.1
- alios 7.2 (Red Hat Enterprise Linux liked)
由于 dpdk 的文档省略了很多步骤,所以我在这里简单做一下总结。需要注意的是在我的版本中,guest 是没有启用 dpdk 的,只在 host 中启用的 dpdk。
在 host 上设置 hugepages,这里 hugepages 占用的空间是 1 GB(2MB * 512),具体大小可以按照实际情况配置。
$ sudo cat /proc/meminfo | grep Huge
$ sudo mkdir -p /mnt/huge
$ sudo mount -t hugetlbfs nodev /mnt/huge
# use NUMA
$ sudo sh -c "echo 512 > /sys/devices/system/node/node0/hugepages/hugepages-2048kB/nr_hugepages"
# ubtuntu/debian
$ sudo sh -c "echo 512 > /sys/kernel/mm/hugepages/hugepages-2048kB/nr_hugepages"
# see the difference
$ sudo cat /proc/meminfo | grep Huge
编译 dpdk,你需要提前安装 python3。
$ pip3 install meson pyelftools
$ meson -Dexamples=all build
$ ninja -C build
启动 qemu,假设在 hdd.qcow2 中有一个 fedora 35 的操作系统,启动一个 16C32G 的虚拟机。
$ sudo qemu-system-x86_64 \
-smp 16 \
-drive file=hdd.qcow2 \
-machine accel=kvm \
-nographic \
-cpu host \
-m 32768 -object memory-backend-file,id=mem,size=32768,mem-path=/dev/hugepages,share=on -mem-prealloc -numa node,memdev=mem \
-chardev socket,id=char1,path=/tmp/sock0,server \
-netdev type=vhost-user,id=hostnet1,chardev=char1 \
-device virtio-net-pci,netdev=hostnet1,id=net1,mac=52:54:00:00:00:14
在 host 上启动 dpdk,假设目前已经进入 dpdk 源代码的根目录。
$ sudo ./build/examples/dpdk-vhost -l 0-3 -n 4 --socket-mem 1024 \
--huge-dir /mnt/huge \
-- --socket-file /tmp/sock0 --client \
--vm2vm 0 --tso 1 --tx-csum 0 -p 0x1