简介
虚拟化技术的发展
- 主机虚拟化,一台物理机可以被划分为若干个小的机器,每个机器的硬件互不共享,各自安装操作系统
- 硬件虚拟化,同一个物理机上隔离多个操作系统实例
- 操作系统虚拟化,由操作系统创建虚拟的系统环境,使应用感知不到其他应用的存在
在数据中心里面用的虚拟机,我们通常叫作 Type-1 型的虚拟机,客户机的指令交给虚拟机监视器之后,可以直接由虚拟机监视器去调用硬件,指令不需要做什么翻译工作,可以直接往下传递执行就好了。因为虚拟机监视器需要直接和硬件打交道,所以它也需要包含能够直接操作硬件的驱动程序。所以 Type-1 的虚拟机监视器更大一些,同时兼容性也不能像 Type-2 型那么好。不过,因为它一般都是部署在我们的数据中心里面,硬件完全是统一可控的,这倒不是一个问题了。
在 Type-2 虚拟机里,我们上面说的虚拟机监视器好像一个运行在操作系统上的软件。你的客户机的操作系统呢,把最终到硬件的所有指令,都发送给虚拟机监视器。而虚拟机监视器,又会把这些指令再交给宿主机的操作系统去执行。
cpu 虚拟化
KVM 的「基于内核的虚拟机」是什么意思?早期比较常见的虚拟化解决方案,它的工作原理很简单:把CPU的所有寄存器都写在一组变量中(这组变量我们称为CPUFile),然后用一片内存当作被模拟CPU的内存(这片内存这里称为vMEM),然后在用一些数据结构表示IO设备的状态(这里称为vIO),三者的数据结构综合在一起,就是代表一个虚拟化的环境了(这里称之为VM),之后按顺序读出一条条的指令,根据这个指令的语义,更改VM的数据结构的状态(如果模拟了硬件,还要模拟硬件的行为,比如发现显存被写了一个值,可以在虚拟屏幕上显示一个点等),这样,实施虚拟的那个程序就相当于给被虚拟的程序模拟了一台计算机,这种技术,我称它为“解释型虚拟化技术”。指令是被一条一条解释执行的。随着技术的发展,有人就开始取巧了:很多时候,我们仅仅是在x86上模拟x86,这些指令何必要一条条解释执行?我们可以用CPU直接执行这些指令啊,执行到特权指令的时候,我们直接异常,然后在异常中把当前CPU的状态保存到CPUFile中,然后再解释执行这个特权指令,这样不是省去了很多”解释“的时间了?
KVM 介绍(二):CPU 和内存虚拟化 - Avaten的文章 - 知乎因为宿主操作系统是工作在 ring0 的,客户操作系统就不能也在ring0 了,但是它不知道这一点,以前执行什么指令,现在还是执行什么指令,但是没有执行权限是会出错的。所以这时候虚拟机管理程序(VMM)需要避免这件事情发生。虚机怎么通过 VMM 实现 Guest CPU 对硬件的访问,根据其原理不同有三种实现技术:
- 基于二进制翻译的全虚拟化。客户操作系统运行在 Ring 1,它在执行特权指令时,会触发异常(CPU的机制,没权限的指令会触发异常),然后 VMM 捕获这个异常,在异常里面做翻译,模拟,最后返回到客户操作系统内,客户操作系统认为自己的特权指令工作正常,继续运行。但是这个性能损耗,就非常的大,简单的一条指令,执行完,了事,现在却要通过复杂的异常处理过程。
- 半虚拟化/操作系统辅助虚拟化。修改操作系统内核,替换掉不能虚拟化的指令,通过超级调用(hypercall)直接和底层的虚拟化层hypervisor来通讯,hypervisor 同时也提供了超级调用接口来满足其他关键内核操作,比如内存管理、中断和时间保持。所以像XEN这种半虚拟化技术,客户机操作系统都是有一个专门的定制内核版本,这也是为什么XEN只支持虚拟化Linux,无法虚拟化windows原因,微软不改代码啊。
- 硬件辅助的全虚拟化。2005年后,CPU厂商Intel 和 AMD 开始支持虚拟化了,这种 CPU,有 VMX root operation 和 VMX non-root operation两种模式,VMM 可以运行在 VMX root operation模式下,客户 OS 运行在VMX non-root operation模式下。因为 CPU 中的虚拟化功能的支持,并不存在虚拟的 CPU,KVM Guest 代码是运行在物理 CPU 之上。对 KVM 虚机来说,运行在 VMX Root Mode 下的 VMM 在需要执行 Guest OS 指令时执行 VMLAUNCH 指令将 CPU 转换到 VMX non-root mode,开始执行客户机代码 ==> 运行某些指令或遇到某些事件时 ==> 硬件自动挂起 Guest OS ==> 切换到 VMX root operation 模式 ,恢复VMM 的运行 (而不是cpu 直接报错) ==> VMM 将操作转到 guest os 对应的内存、设备驱动等。
总结一下:cpu 虚拟化的核心难点是特权指令,对于特权指令,guest os 要么改自己不执行特权指令,要么VMM 为cpu 报错擦屁股,要么cpu 不报错。
《编程高手必学的内存知识》
在虚拟化技术中涉及的有三个核心角色,分别是宿主机Host,客户机Guest和虚拟机监控器 (Virtual Machine Monitor , VMM)。VMM负责为客户机准备虚拟 CPU,虚拟内存等虚拟资源,并同时对客户机进行管理。
- 我们可以很容易想到一个实现虚拟化技术的方案是:通过纯软件模拟 CPU 执行过程。也就是说,这里需要对完整的底层硬件进行模拟,包括处理器、物理内存和外部设备等等。这样的话,Guest 的所有程序都相当于运行在 Host 的一个解释器里,来一条指令就解释一条指令,资源限制以及运行等价的要求都很容易满足。不过这个方案的缺陷也非常明显,因为你是用软件来对 CPU 的指令进行了翻译,通常一条指令最终会被翻译成非常多的指令,那效率自然也是非常低的。既然对指令进行翻译的效率是如此低下,那我们为什么不能让 Guest 程序的代码直接运行在 Host 的 CPU 上呢?我们本来翻译指令的目的,是为了让 VMM 能够对 Guest 执行的指令进行监管,防止 Guest 对计算资源的滥用,那如果又让 Guest 的执行直接运行在 CPU 上,VMM 又哪里有机会能够对 Guest 进行监管呢?
- 为了解决这个问题。人们提出一个重要的模型,这就是陷入模拟(Trap-and-Emulate)模型。将 Guest 运行的指令进行分类,一类是安全的指令,也就是说这些指令可以让 Host 的 CPU 正常执行而不会产生任何副作用,例如普通的数学运算或者逻辑运算,或者普通的控制流跳转指令等;另一类则是一些“不安全”的指令,又称为“Trap”指令,也就是说,这些指令需要经过 VMM 进行模拟执行,例如中断、IO 等特权指令等。接下来,我们来看一下它的具体实现过程:对于“安全”的指令,Guest 在执行时可以交由 Host 的 CPU 正常运行,这样可以保证大部分场景的性能。不过,当 Guest 执行一些特权指令时就需要发出 Trap,通知 VMM 来接管 Guest 的控制流。VMM 会对特权指令进行模拟 (Emulate),从而达到资源控制的效果。当然在进行模拟的过程中需要保证执行结果的等价性。比如”int 0x80”这条指令就是一个特权指令,它会导致当前进程切入内核态执行。在虚拟化场景下遇到这种特权指令,我们不能直接交给宿主机的真实 CPU 去执行,因为宿主机 CPU 会使用宿主机的 IDT 来处理这次中断请求。而我们真正希望的是,使用客户机的 IDT 去查找相应的中断服务程序。这就需要 Guest 退回到 VMM,让 VMM 模拟 CPU 的动作去解析 IDT 中的中断描述符,找到 Guest 的中断服务程序并调用它。在这个例子中,Geust 退回 VMM 的操作就是 Trap,VMM 模拟 CPU 的动作去调用 Guest 的中断服务程序就是 Emulate。
- 不过,这里仍然存在一个问题:当 Guest 的内核代码在 Host 的 CPU 上执行的时候,Guest 没有办法区分“安全”指令和“非安全”指令,也就是说 Guest 不知道哪条指令应该触发 Trap。幸好,现代的芯片对这种情况做了硬件上的支持。现代的 X86 芯片提供了 VMX 指令来支持虚拟化,并且在 CPU 的执行模式上提供了两种模式:root mode 和 non-root mode,这两种模式都支持 ring 0 ~ ring 3 三种特权级别。VMM 会运行在 root mode 下,而 Guest 操作系统则运行在 non-root mode 下。所以,对于 Guest 的系统来讲,它也和物理机一样,可以让 kernel 运行在 ring 0 的内核态,让用户程序运行在 ring 3 的用户态, 只不过整个 Guest 都是运行在 non-root 模式下。有了 VMX 硬件的支持,Trap-and-Emulate 就很好实现了。Guest 可以在 non-root 模式下正常执行指令,就如同在执行物理机的指令一样。当遇到“不安全”指令时,例如 I/O 或者中断等操作,就会触发 CPU 的 trap 动作,使得 CPU 从 non-root 模式退出到 root 模式,之后便交由 VMM 进行接管,负责对 Guest 请求的敏感指令进行模拟执行。这个过程称为 VM Exit。而处于 root 模式下的 VMM,在一开始准备好 Guest 的相关环境,准备进入 Guest 时,或者在 VM Exit 之后执行完 Trap 指令的模拟准备,再次进入 Guest 的时候,可以继续通过 VMX 提供的相关指令 VMLAUNCH 以及 VMResume,来切换到 non-root 模式中由 Guest 继续执行。 这个过程也被称为 VM Entry。
PS:这段把Host、Guest和VMM的协作方式说清楚了,但是VMM 如何帮助Guest 操作外设没提。
Guest 内存管理 未读。
KVM + QEMU
KVM(Kernel-based Virtual Machine)利用修改的QEMU提供BIOS、显卡、网络、磁盘控制器等的仿真,但对于I/O设备(主要指网卡和磁盘控制器)来说,则必然带来性能低下的问题。因此,KVM也引入了半虚拟化的设备驱动,通过虚拟机操作系统中的虚拟驱动与主机Linux内核中的物理驱动相配合,提供近似原生设备的性能。从此可以看出,KVM支持的物理设备也即是Linux所支持的物理设备。
为了简化代码,KVM 在 QEMU 的基础上做了修改。VM 运行期间,QEMU 会通过 KVM 模块提供的系统调用进入内核,由 KVM 负责将虚拟机置于处理的特殊模式运行。遇到虚机进行 I/O 操作,KVM 会从上次的系统调用出口处返回 QEMU,由 QEMU 来负责解析和模拟这些设备。从 QEMU 的角度看,也可以说是 QEMU 使用了 KVM 模块的虚拟化功能,为自己的虚机提供了硬件虚拟化加速。除此以外,虚机的配置和创建、虚机运行说依赖的虚拟设备、虚机运行时的用户环境和交互,以及一些虚机的特定技术比如动态迁移,都是 QEMU 自己实现的。
guest os 与 物理机cpu 之间vmm 如何发挥作用的,还没找到直观感觉(未完成)。vm 是一个进程,vCpu 是其中的一个线程,由VMM 管理,这个味道咋跟docker 这么一样呢?
KVM原理简介 写的更好一些。KVM是作为一个内核模块出现的,所以它还得借助用户空间的程序QEMU 来和用户进行交互。QEMU是一套由法布里斯·贝拉(Fabrice Bellard)所编写的以GPL许可证分发源码的模拟处理器,在GNU/Linux平台上使用广泛。其本身是一个纯软件的支持CPU虚拟化、内存虚拟化及I/O虚拟化等功能的用户空间程序。借助KVM提供的虚拟化支持可以将CPU、内存等虚拟化工作交由KVM处理,自己则处理大多数I/O虚拟化的功能,可以实现极高的虚拟化效率。
- 正常我们在执行WFI指令时会使CPU进入一个低功耗的状态,但是对于HOST OS来说,如果让CPU真正进入低功耗状态,显然会影响其他VM的运行。如果我们配置了HCR_EL2.TWI==1时,那么Guest OS在执行WFI时就会触发EL2的异常,然后陷入(PS: 感觉跟异常调用一样)Hypervisor,那么此时Hypervisor就可以将对应VCPU所处的线程调出出去,将CPU让给其他的VCPU线程使用。PS: Hypervisor 感觉就是提供了 系统调用处理这个场景
- 内存虚拟化的目的是给虚拟客户机操作系统提供一个从0开始的连续的地址空间,同时在多个客户机之间实现隔离与调度。arm主要通过Stage 2转换来提供对内存虚拟化的支持,其允许Hypervisor控制虚拟机的内存视图,而在这之前则是使用及其复杂的影子页表技术来实现。Stage 2转换可以控制虚拟机是否可以访问特定的某一块物理内存,以及该内存块出现在虚拟机内存空间的位置。这种能力对于虚拟机的隔离和沙箱功能来说至关重要。这使得虚拟机只能看到分配给它自己的物理内存。为了支持Stage 2 转换, 需要增加一个页表,我们称之为Stage 2页表。操作系统控制的页表转换称之为stage 1转换,负责将虚拟机视角的虚拟地址转换为虚拟机视角的物理地址。而stage 2页表由Hypervisor控制,负责将虚拟机视角的物理地址转换为真实的物理地址。虚拟机视角的物理地址在Armv8中有特定的词描述,叫中间物理地址(intermediate Physical Address, IPA)。
- I/O虚拟化,有四种方案:设备模拟,比如qemu 模拟具体的I/O设备的特性;前后端驱动接口;设备直接分配,将一个物理设备直接分配给Guest OS使用;设备共享分配,其主要就是让一个物理设备可以支持多个虚拟机功能接口,将不同的接口地址独立分配给不同的Guest OS使用。如SR-IOV协议。
命令行方式创建一个传统虚拟机
qemu 创建传统虚拟机流程(virtualbox是一个图形化的壳儿)
# 创建一个虚拟机镜像,大小为 8G,其中 qcow2 格式为动态分配,raw 格式为固定大小
qemu-img create -f qcow2 ubuntutest.img 8G
# 创建虚拟机(可能与下面的启动虚拟机操作重复)
qemu-system-x86_64 -enable-kvm -name ubuntutest -m 2048 -hda ubuntutest.img -cdrom ubuntu-14.04-server-amd64.iso -boot d -vnc :19
# 在 Host 机器上创建 bridge br0
brctl addbr br0
# 将 br0 设为 up
ip link set br0 up
# 创建 tap device
tunctl -b
# 将 tap0 设为 up
ip link set tap0 up
# 将 tap0 加入到 br0 上
brctl addif br0 tap0
# 启动虚拟机, 虚拟机连接 tap0、tap0 连接 br0
qemu-system-x86_64 -enable-kvm -name ubuntutest -m 2048 -hda ubuntutest.qcow2 -vnc :19 -net nic,model=virtio -nettap,ifname=tap0,script=no,downscript=no
# ifconfig br0 192.168.57.1/24
ifconfig br0 192.168.57.1/24
# VNC 连上虚拟机,给网卡设置地址,重启虚拟机,可 ping 通 br0
# 要想访问外网,在 Host 上设置 NAT,并且 enable ip forwarding,可以 ping 通外网网关。
# sysctl -p
net.ipv4.ip_forward = 1
sudo iptables -t nat -A POSTROUTING -o eth0 -j MASQUERADE
# 如果 DNS 没配错,可以进行 apt-get update
可以看到至少在网络配置这个部分,传统虚拟机跟容器区别并不大
其它
QEMU尽管非常的强大,但也正是应为它的强大导致其对初学者非常的不友好。这里推荐大家刚开始学习KVM时可以先学习kvm tool,这是一个基于C语言开发的KVM虚拟化工具,其代码非常精简易懂,同时也可以支持完整的linux虚拟化,非常适合初学者入门使用。其项目地址为https://github.com/kvmtool/kvmtool。
- 神龙架构从CPU手头接过虚拟化的重任,一路带飞存储、网络、安全等关键性能。
- 到今天为止,最适合做DPU的还是可编程可升级的FPGA。 从 VMWare 到阿里神龙,虚拟化技术 40 年演进史