Table of Contents
这个文章主要针对的是虚拟机的网络虚拟化中常用的 virtio-net 和 vhost-net,这篇文章中参考了 RedHat 公司的《Introduction to virtio-networking and vhost-net》和《Deep dive into Virtio-networking and vhost-net》,同时也参考了部分其他的文章。
基础知识
QEMU & KVM
在看之前先区分下 QEMU 和 KVM 两个组件。QEMU 是一个计算机模拟器,它能够从软件层面模拟一款计算机,包括 CPU、内存等。
在不使用 KVM 时,QEMU 通过截获 guest 的 CPU 指令并在 host 上执行,当然在 CPU 返回结果后 QEMU 同样需要截获后再传递给 guest,这个过程都是纯软件实现的,所以性能非常糟糕。
KVM 作为一个硬件加速手段,能够将 vCPU 与 CPU 绑定,guest 的计算工作将直接运行在物理 CPU 核心上,所以性能就更好一些。
IPC 方式
进程间通讯(Inter-Process Communication, IPC)也是我们要讨论的点,因为网卡什么时候接收到数据、什么时候发送数据,都离不开 guest、QEMU 进程以及 host 的不同进程之间的通讯。
UDS(Unix Domain Socket):socket 本身是为了不可靠通讯设计的,但是在 IPC 情景下通讯是可靠的,所以 UDS 不需要经过网络协议栈,不需要打包拆包、计算校验和、维护序号和应答等,只是将应用层数据从一个进程拷贝到另一个进程,所以效率更高,同时它是一种全双工的通讯方案,功能也更好。
eventfd:可以理解为简化版本的 UDS,UDS 可以传输任意二进制数据,eventfd 只能写入一个数字,用于表示当前有可读事件发生,同样的 eventfd 也是一种全双工通讯方式。
共享内存:两个进程的指针执行同一个内存区域。
Virtio Networking
Virtio
Virtio 是一个通用的 guest 和 host 实现 I/O 半虚拟化的方案,针对网络设备的方案又有 virtio-net 和 vhost-net 等等。
Virtio 在架构层面包含两个层次:
- 控制面(control plane):协商决定建立或终止数据面,控制面的优化方向是更灵活和更通用,支持更多的制造商(vendor)和设备。
- 数据面(data plane):传递数据,因此数据面的优化方向是快,比如网络设备从 virtio-net 进化为 vhost-net 又进化为 vhost-user,主要都是在优化数据面数据传递的速度。
Virtio spec
其中最基本的协议是定义在 virtio spec 中,规定了如何在 host 和 guest 中创建和使用控制面和数据面。为了优化数据面的性能,所以有了 vhost 协议,vhost 协议可以让数据面的数据 offload 到其他的模块中,比如 vhost-net 就是 offload 到 host 的内核态中,vhost-user 则是 offload 到 host 的用户态中。
在 virtio spec 中定义了 device 和 driver 两个组件,其中 hypervisor 暴露给 guest 一个 device(让 guest 看起来像是一个真正的物理设备),guest 通过 driver 来使用该 device。在很多其他的文章中,前端是指处于 guest 的 driver,后端是指处于 host 的 device。
Virtio spec 定义了一个数据结构 virtqueue,每个 device 有零个或者多个 virtqueues,每个 virtqueue 有包含两个 ring buffer(以 guest 向 host 发送数据流为例):
- Driver (on the guest) 将想要发送给 device (on the host) 的数据写在 available buffer 中。
- Device 将结果返回给 driver 的数据写在 used buffer 中,由于内存已经在发送的时候 alloc 过了,所以在返回结果的过程中不需要 alloc 内存。
返回的结果是 host 处理数据的结果(比如网卡告知本次数据是否发送成功等),而非从 host 发送 guest 的外部数据。如果向实现数据从 host 发送 guest 的发送,还需要在建立一个新的 virtqueue。
传输方式也分为 PCI/PCIe 方式和 MMIO 方案两种,在这篇文章中只讲 PCI 的方案。PCI 设备识别原理是在内存的一块特殊区域保存配置项,然后在 guest 启动的时候读取该内存区域后被自动发现,这些配置信息包括 Vendor ID、Device ID 等等,系统通过这些信息确认 devices 需要被哪个特定的 driver 处理,这样就能实现 guest 和 qemu 的数据互通。
PCI 的作用是
- 在 guest 启动时的 device 发现(driver 绑定 device 等)以及创建 virtqueue 等功能。
- 通知交换:guest 向特定地址区域写数据通知 available buffer 可用,device 通过 vCPU 中断发送 used buffer 通知。
- ……
Virtio-net
Virtio-net 就是基于 virtio spec 实现的最原始的方案。前提 QEMU 作为虚拟化软件,有能力访问 guest 的内存区域。
整个的运行流程如上图所示,为了图的简洁我没有画出 syscall 的返回流程(used buffer 使用),二期整体过程比较相似,后面会用文字的方式描述下整个的过程。
- 处于 guest user space 的用户进程想要发送网络数据包,所以它调用 syscall 直接发给 guest 网络栈,然后到达 vritio-net driver。
- Vritio-net driver 会向内存中的特定区域写网络数据。
- (PCI 的通知功能,数据写完了需要告知 device 处理)KVM 通知并唤醒 QEMU 进程的 virtio-net device。
- 之后做的事情(第 4-6 步)与 guest 发包是一样的,通过 syscall 到 host 的网络栈,最后发往实际的物理网卡。
- (图里没有的)syscall 返回之后 virtio-device 再向 used buffer 写入本次的执行的结果,通过 PCI 的 vCPU 中断机制实现通知 guest 有返回数据可用。
可以看到这个过程是比较繁琐的,首先是 guest 从用户态到内核态的切换(context switching),第二个是唤醒 QEMU 进程(context switching + 进程切换),第三个是 host 从用户态到内核态的切换(context switching,因为 QEMU 也是一个用户进程),但是原始的 virtio 这种方案的兼容性较好。
vhost-net
从上面的分析来看,仅仅发送数据包这一个动作就涉及多次 context switching,这还没算上 syscall 的结果返回,对于越来越快的网速就会导致频繁发包,同样也会导致频繁 context switching 和唤醒 QEMU 进程的局面,所以 vhost-net 的出现就是为了解决数据面的上述性能问题。
解决的方案是:数据发送/接受绕过 QEMU 进程,更高级的说法是数据 offload 到 host kernel。
Vhost-net 是位于 host kernel 的一个 driver,在加载完毕后在 host 中创建一个 /dev/vhost-net
设备,QEMU 在启动的时候就会使用 ioctl 命令与 vhost-net 进行一些协商,比如 virtio 特征协商、guest mem 映射等。
根据上面的图可以看出也是六步,因为都是必要的写数据/通知机制,但是可以看到 vhost-net 绕过了 QEMU 进程以及 virtio-net device,减少了一次任务切换成本(QEMU 进程切换)以及两次 context switching(QEMU 进程切换 + QEMU 进程向网络栈发送数据包的 syscall)。
从流程上看前两步是一样的,关键是第三步通过 ioeventfd 来通知 vhost-net,这个是实现数据旁路的关键。Vhost-net 与 qemu 初始化阶段会创建一个 vhost-$pid
的内核线程(worker),其中 $pid
是指 hypervisor 的进程 ID,然后创建一个 eventfd,一头连接 vhost 的 worker,一头连接 KVM,当 guest 向特定的内存区域写/读数据的时候,KVM 会通过 eventfd 通知 worker,而不必唤醒 QEMU。
同样的,host 向 guest 发送数据的时候,会通过一个 eventfd,一头连接 vhost 的 worker,一头连接 KVM,通过 KVM 向内存中写数据可以注入 vCPU 的中断(中断 guest),这种方式称之为 irqfd。