目录:
多队列网卡
-
实现多队列网卡硬件
-
内核对多队列网卡的支持
-
多队列网卡结构
-
DPDK 多队列网卡
虚拟化
-
CPU 虚拟化
-
内存虚拟化
-
I/O 虚拟化
Virtio
-
为什么是 virtio?
多队列网卡
实现多队列网卡硬件
有四个硬件队列(Queue0, Queue1, Queue2, Queue三、收到报文时,通过 hash 包头
的(sip, sport, dip, dport)四元组,一条流总是收到相同的队列,同时触发与队列绑定的中断。
核对多队列网卡的支持
Linux 内核中, RPS ( Receive Packet Steering )这种机制提供在接收端。RPS 主要是平衡软中断的负载 CPU 的各个 core 上面,网卡驱动生成每个流量 hash 标识,这个 hash值可通过四元组(源组) IP 地址 SIP ,源四层端口 SPOR T,目的 IP 地址 DIP ,四层端口的目的DPORT )来计算,然后根据这个地方进行中断处理 hash 相应地分配标识 core 这样才能充分发挥多核能力。
NAPI 是 linux 采用提高网络处理效率的技术,其核心概念是不中断读取数据,而是中断唤醒数据接收的服务程序 POLL 轮询数据的方式。
NAPI 的优点:
-
中断缓解,网卡在日常使用中产生多少 k/s,每次中断都需要系统来处理,压力很大, NAPI 使用轮询是禁止了网卡接收中断,减小处理中断的压力。
-
数据包节流,NAPI 之前的 NIC 总是在接收数据包后产生一个 IRQ,然后中断服务函数 skb 加入本地 softnet,然后触发本地 NET_RX_SOFTIRQ 软中断后续处理。因为包速过高 IRQ 优先级高于 SoftIRQ,该系统的大部分资源都中断了响应,但 softnet 队列尺寸有限,收到的超额数据包只能丢失,因此该模型正在使用宝贵的系统资源。而 NAPI 在这种情况下,直接丢失包,不会继续将需要丢失的数据包扔进核心处理,使网卡尽早丢弃需要丢失的数据包,核心将看不到需要丢失的数据包,这也降低了核心的压力。
NAPI 的缺点:
-
对于上层应用程序,系统在接收每个数据包时无法及时处理,累计数据包随着传输速度的增加会消耗大量内存。
-
另一个问题是很难处理大数据包,因为大数据包传输到网络层需要比短数据包长得多的时间(即使使用 DMA 方法)。NAPI 该技术适用于高速短长数据包的处理。
QDisc 是 queueing discipline 简写就是理解流量(traffic control)的基础。无论何时,如果内核需要通过网络接口发送数据包,则需要根据接口配置 qdisc(排队规则)将数据包添加到队列中。然后,内核会尽可能多地从 qdisc 取出数据包,交给网络适配器模块。最简单的 QDisc 是 pfifo,他不处理进入的数据包,数据包采用先入先出的方式。
多队列网卡结构
Linux 内核网卡结构由内核网卡结构组成 net_device 数据包是由表示的 sk_buff 表示的
接收端
发送端
DPDK 多队列网卡
虚拟化
虚拟化是资源的逻辑表达,不受物理设备的限制。虚拟化技术的实现形式是在系统中添加一个虚拟化层,将下层资源抽象成另一种资源供上层使用。
CPU 虚拟化
内存虚拟化
虚拟机本质上是 Host 机器上的一个过程应该使用 Host 机器的虚拟地址空间,但在虚拟化模式下,虚拟机处于非 Root 不能直接访问模式 Root 模式下的 Host 机器上的内存。
此时需要 VMM 的介入, VMM 需要 intercept (截获)虚拟机内存访问指令,然后 virtualize(模拟)Host 上面的内存相当于 VMM 虚拟地址空间和虚拟地址空间 Host 在机器的虚拟地址空间中间增加了一层,即虚拟机的物理地址空间,也可视为是 Qemu 虚拟地址空间(有点绕,但请记住,虚拟机是由 Qemu 模拟生成更清楚)。因此,内存软件虚拟化的目标是虚拟机的虚拟地址(Guest Virtual Address, GVA)转化
为 Host 的物理地址(Host Physical Address, HPA),中间要经过虚拟机的物理地址(GuestPhysical Address, GPA)和 Host 虚拟地址(Host Virtual Address)转化,即:
GVA -> GPA -> HVA -> HPA
前两步由虚拟机系统页面完成,中间两步由虚拟机系统页面完成 VMM 定义的映射表(数据结构)kvm_memory_slot 记录)完成,它可以将连续的虚拟机物理地址映射成非连续的 Host 机器虚拟地址,后两步由 Host 完成机器的系统页表。如下图所示。
-
从零开始为虚拟机提供连续的物理内存空间。
-
有效隔离、调度和共享虚拟机之间的内存资源。
影子页面技术
从上图可以看出,传统的内存虚拟化模式需要虚拟机的每一次内存访问 VMM 干预,软件多次转换地址,效率很低。因此,阴影页面技术和 EPT 技术。
影子页面简化了地址转换过程,实现了 Guest 虚拟地址空间到 Host 直接映射物理地址空间。要实现这种映射,必须是 Guest 设计一套相应的影子页表,然后将影子页表放入其中 Host 的 MMU 中,这样当 Guest 访问 Host 可以根据内存 MMU 影子页表示映射关系,完成 GVA 到 HPA 的直接映射。维护这套影子页表的工作是由 VMM 来完成。
由于 Guest 每个过程都有自己的虚拟地址空间,这意味着 VMM 要为 Guest 当每个过程页面都维护一套相应的阴影页面时 Guest 在访问过程中的内存时,将过程的阴影页表安装到 Host 的 MMU 完成地址转移 换。
我们也看到,虽然这种方法减少了地址转换的次数,但本质上是纯软件实现的,效率不高, VMM 维护阴影页表太多,设计不好。
为了改善这一问题,提出了基于硬件的内存虚拟化方法,将这些繁琐的工作交给硬件,大大提高了效率。
EPT 技术
这方面 Intel 和 AMD 走在前面,Intel 的 EPT 和 AMD 的 NT 是硬件辅助内存虚拟化的代表,两者在原理上类似,本文重点介绍一下 EPT 技术。
如下图是 EPT 的基本原理图示,EPT 在原有 CR3 页表地址映射的基础上,引入了 EPT 页表来实现另一层映射,这样,GVA->GPA->HPA 的两次地址转换都由硬件来完成
这里举一个小例子来说明整个地址转换的过程。假设现在 Guest 中某个进程需要访问内存,CPU 首先会访问 Guest 中的 CR3 页表来完成 GVA 到 GPA 的转换,如果 GPA 不为空,则 CPU 接着通过 EPT 页表来实现 GPA 到 HPA 的转换(实际上,CPU 会首先查看硬件 EPT TLB 或者缓存,如果没有对应的转换,才会进一步查看 EPT 页表),如果 HPA 为空呢,则 CPU 会抛出 EPT Violation 异常由 VMM 来处理。如果 GPA 地址为空,即缺页,则 CPU 产生缺页异常,注意,这里,如果是软件实现的方式,则会产生 VM-exit,但是硬件实现方式,并不会发生 VM-exit,而是按照一般的缺页中断处理,这种情况下,也就 是 交 给 Guest 内 核 的 中 断 处 理 程 序 处 理 。 在 中 断 处 理 程 序 中 会 产 生EXIT_REASON_EPT_VIOLATION,Guest 退出,VMM 截获到该异常后,分配物理地址并建立GVA 到 HPA 的映射,并保存到 EPT 中,这样在下次访问的时候就可以完成从 GVA 到HPA 的转换了。
I/O 虚拟化
这种方式比较好理解,简单来说,就是通过纯软件的形式来模拟虚拟机的 I/O 请求。以qemu-kvm 来举例,内核中的 kvm 模块负责截获 I/O 请求,然后通过事件通知告知给用户空间的设备模型 qemu,qemu 负责完成本次 I/O 请求的模拟。
优点:不需要对操作系统做修改,也不需要改驱动程序,因此这种方式对于多种虚拟化技术的「可移植性」和「兼容性」比较好。
缺点:纯软件形式模拟,自然性能不高,另外,虚拟机发出的 I/O 请求需要虚拟机和 VMM之间的多次交互,产生大量的上下文切换,造成巨大的开销。
针对 I/O 全虚拟化纯软件模拟性能不高这一点, I/O 半虚拟化前进了一步。它提供了一种机制,使得 Guest 端与 Host 端可以建立连接,直接通信,摒弃了截获模拟这种方式,从而获得较高的性能。
值得注意的有两点:1)采用 I/O 环机制,使得 Guest 端和 Host 端可以共享内存,减少了虚拟机与 VMM 之间的交互;2)采用事件和回调的机制来实现 Guest 与 Host VMM 之间的通信。这样,在进行中断处理时,就可以直接采用事件和回调机制,无需进行上下文切换,减少了开销。
要实现这种方式, Guest 端和 Host 端需要采用类似于 C/S 的通信方式建立连接,这也就意味着要修改 Guest 和 Host 端操作系统内核相应的代码,使之满足这样的要求。为了描述方便,我们统称 Guest 端为前端,Host 端为后端。
前后端通常采用的实现方式是驱动的方式,即前后端分别构建通信的驱动模块,前端实现在内核的驱动程序中,后端实现在 qemu 中,然后前后端之间采用共享内存的方式传递数据。关于这方面一个比较好的开源实现是 virtio
性能较 I/O 全虚拟化有了较大的提升
要修改操作系统内核以及驱动程序,因此会存在移植性和适用性方面的问题,导致其使用受限。
上面两种虚拟化方式,还是从软件层面上来实现,性能自然不会太高。最好的提高性能的方式还是从硬件上来解决。如果让虚拟机独占一个物理设备,像宿主机一样使用物理设备,那无疑性能是最好的。
I/O 直通技术就是提出来完成这样一件事的。它通过硬件的辅助可以让虚拟机直接访问物理设备,而不需要通过 VMM 或被 VMM 所截获。
由于多个虚拟机直接访问物理设备,会涉及到内存的访问,而内存又是共享的,那怎么来隔离各个虚拟机对内存的访问呢,这里就要用到一门技术——IOMMU,简单说,IOMMU就是用来隔离虚拟机对内存资源访问的。
I/O 直通技术需要硬件支持才能完成,这方面首选是 Intel 的 VT-d 技术,它通过对芯片级的改造来达到这样的要求,这种方式固然对性能有着质的提升,不需要修改操作系统,移植性也好。但该方式也是有一定限制的,这种方式仅限于物理资源丰富的机器,因为这种方式仅仅能满足一个设备分配给一个虚拟机,一旦一个设备被虚拟机占用了,其他虚拟机时无法使用该设备的。
Virtio
virtio 是一种 I/O 半虚拟化解决方案,是一套通用 I/O 设备虚拟化的程序,是对半虚拟 化 Hypervisor 中 的 一 组 通 用 I/O 设 备 的 抽 象 。 提 供 了 一 套 上 层 应 用 与 各Hypervisor 虚拟化设备(KVM,Xen,VMware 等)之间的通信框架和编程接口,减少跨平台所带来的兼容性问题,大大提高驱动程序开发效率。
为什么是 virtio?
在完全虚拟化的解决方案中,guest VM 要使用底层 host 资源,需要 Hypervisor 来截获所有的请求指令,然后模拟出这些指令的行为,这样势必会带来很多性能上的开销。半虚拟化通过底层硬件辅助的方式,将部分没必要虚拟化的指令通过硬件来完成,Hypervisor只负责完成部分指令的虚拟化,要做到这点,需要 guest 来配合,guest 完成不同设备的前端驱动程序,Hypervisor 配合 guest 完成相应的后端驱动程序,这样两者之间通过某种交互机制就可以实现高效的虚拟化过程。
由于不同 guest 前端设备其工作逻辑大同小异(如块设备、网络设备、 PCI 设备、 balloon驱动等),单独为每个设备定义一套接口实属没有必要,而且还要考虑扩平台的兼容性问题,另外,不同后端 Hypervisor 的实现方式也大同小异(如 KVM、Xen 等),这个时候,就需要一套通用框架和标准接口(协议)来完成两者之间的交互过程,virtio 就是这样一套标准,它极大地解决了这些不通用的问题。
virtio 的架构
从总体上看,virtio 可以分为四层,包括前端 guest 中各种驱动程序模块,后端 Hypervisor(实现在 Qemu 上)上的处理程序模块,中间用于前后端通信的 virtio 层和 virtio-ring 层,virtio 这一层实现的是虚拟队列接口,算是前后端通信的桥梁,而 virtio-ring 则是该桥梁的具体实现,它实现了两个环形缓冲区,分别用于保存前端驱动程序和后端处理程序执行的信息。
严格来说, virtio 和 virtio-ring 可以看做是一层, virtio-ring 实现了 virtio 的具体通信机制和数据流程。或者这么理解可能更好, virtio 层属于控制层,负责前后端之间的通知机制(kick,notify)和控制流程,而 virtio-vring 则负责具体数据流转发。
virtio 数据流交互机制
vring 主要通过两个环形缓冲区来完成数据流的转发,如下图所示。
vring 包含三个部分,描述符数组 desc,可用的 available ring 和使用过的 usedring。
desc 用于存储一些关联的描述符,每个描述符记录一个对 buffer 的描述,available ring 则用于 guest 端表示当前有哪些描述符是可用的,而 used ring 则表示 host 端哪些描述符已经被使用。
Virtio 使用 virtqueue 来实现 I/O 机制,每个 virtqueue 就是一个承载大量数据的队列,具体使用多少个队列取决于需求,例如,virtio 网络驱动程序(virtio-net)使用两个队列(一个用于接受,另一个用于发送),而 virtio 块驱动程序(virtio-blk)仅使用一个队列。
具体的,假设 guest 要向 host 发送数据,首先,guest 通过函数 virtqueue_add_buf将存有数据的 buffer 添加到 virtqueue 中,然后调用 virtqueue_kick 函数,virtqueue_kick 调用 virtqueue_notify 函数,通过写入寄存器的方式来通知到 host。
host 调用 virtqueue_get_buf 来获取 virtqueue 中收到的数据。
存放数据的 buffer 是一种分散-聚集的数组,由 desc 结构来承载,如下是一种常用的 desc的结构:
当 guest 向 virtqueue 中写数据时,实际上是向 desc 结构指向的 buffer 中填充数据,完了会更新 available ring,然后再通知 host。当 host 收到接收数据的通知时,首先从 desc 指向的 buffer 中找到 available ring 中添加的 buffer,映射内存,同时更新 used ring,并通知 guest 接收数据完毕。
virtio 是 guest 与 host 之间通信的润滑剂,提供了一套通用框架和标准接口或协议来完成两者之间的交互过程,极大地解决了各种驱动程序和不同虚拟化解决方案之间的适配问题。virtio 抽象了一套 vring 接口来完成 guest 和 host 之间的数据收发过程,结构新颖,接口清晰。
Vhost
vhost 是 virtio 的一种后端实现方案,在 virtio 简介中,我们已经提到 virtio 是一种半虚拟化的实现方案,需要虚拟机端和主机端都提供驱动才能完成通信,通常, virtio主机端的驱动是实现在用户空间的 qemu 中,而 vhost 是实现在内核中,是内核的一个模块 vhost-net.ko。
为什么要用 vhost
在 virtio 的机制中,guest 与 用户空间的 Hypervisor 通信,会造成多次的数据拷贝和 CPU 特权级的上下文切换。例如 guest 发包给外部网络,首先,guest 需要切换到host kernel,然后 host kernel 会切换到 qemu 来处理 guest 的请求, Hypervisor通过系统调用将数据包发送到外部网络后,会切换回 host kernel , 最后再切换回guest。这样漫长的路径无疑会带来性能上的损失。
vhost 正是在这样的背景下提出的一种改善方案,它是位于 host kernel 的一个模块,用于和 guest 直接通信,数据交换直接在 guest 和 host kernel 之间通过 virtqueue来进行,qemu 不参与通信,但也没有完全退出舞台,它还要负责一些控制层面的事情,比如和 KVM 之间的控制指令的下发等。
vhost 的数据流程
下图左半部分是 vhost 负责将数据发往外部网络的过程, 右半部分是 vhost 大概的数据交互流程图。其中,qemu 还是需要负责 virtio 设备的适配模拟,负责用户空间某些管理控制事件的处理,而 vhost 实现较为纯净,以一个独立的模块完成 guest 和 host kernel 的数据交换过程。
vhost 与 virtio 前端的通信主要采用一种事件驱动 eventfd 的机制来实现,guest 通知 vhost 的事件要借助 kvm.ko 模块来完成,vhost 初始化期间,会启动一个工作线程work 来监听 eventfd,一旦 guest 发出对 vhost 的 kick event,kvm.ko 触发ioeventfd 通知到 vhost,vhost 通过 virtqueue 的 avail ring 获取数据,并设置used ring。同样,从 vhost 工作线程向 guest 通信时,也采用同样的机制,只不过这种情况发的是一个回调的 call envent,kvm.ko 触发 irqfd 通知 guest。
参考资料