今天的内容包含三个部分:
虚拟机:模拟一个OS。其架构大致如下:
Guest空间
运行了一堆模拟的内核,每个内核之上运行了一堆用户进程VMM
)替代了原本的内核,和硬件交互虚拟机都必须提供提供对于硬件的完全相同的模拟
,这样任何在真实硬件上能工作的代码,也同样能在虚拟机中工作。
实际中出于性能的考虑,这个目标很难达到。在VMM允许的前提下,Linux某些时候知道自己正在与VMM交互,以获得对于设备的高速访问权限。但这是一种被仔细控制的例外,实现虚拟机的大致策略还是完全准确的模拟物理服务器。
除了不希望Guest能够发现自己是否运行在虚拟机中,我们也不希望Guest可以从虚拟机中逃逸,这需要非常严格的隔离
。
虚拟机在很多方面比普通的Linux进程提供了更加严格的隔离。Linux进程经常可以相互交互,它们可以杀掉别的进程,它们可以读写相同的文件,或者通过pipe进行通信。但是在一个普通的虚拟机中,所有这些都不被允许。运行在同一个计算机上的不同虚拟机,彼此之间是通过VMM完全隔离的。所以出于安全性考虑人们喜欢使用虚拟机,这是一种可以运行未被信任软件的方式,同时又不用担心bug和恶意攻击。
container
是进程VMM的架构使得我们可以从另一个角度重新审视我们讨论过的内容,例如内存分配,线程调度等等,这或许可以给我们一些新的思路并带回到传统的操作系统内核中。
某种程度上来说,传统操作系统内核的内容下移了一层到了VMM。
我们该如何构建我们自己的VMM呢?一种实现方式是完全通过软件
来实现,这种方式很慢
你的VMM在解析每一条Guest指令的时候,都可能要转换成几十条实际的机器指令
一种广泛使用的策略是在真实的CPU
上运行Guest指令。
所以这里会使用一些技巧。
User mode
触发trap进入VMM
,并进行相应的处理VMM运行在Supervisor mode,替代原本kernel的功能,可以直接在硬件上启动。其他方案比如硬件上启动Linux,之后要么Linux自带一个VMM,要么通过可加载的内核模块将VMM加载至Linux内核中,这样VMM可以在Linux内核中以Supervisor mode运行。
VMM会为每一个Guest维护一套虚拟状态信息
,例如csr寄存器、mode、hartid。
sret & ecall
得知guest变换了当前的mode,二者都是特权指令guest永远运行在宿主机的user mode
XV6有一个针对每个CPU的变量表明当前运行的是哪个进程,类似的VMM也有一个针对每个CPU的变量表明当前是哪个虚拟机在运行,进而查看对应的虚拟状态信息。
你可以完全通过软件实现这种风格的VMM,也即你的真实计算机可能运行了windows,windows上的软件实现了VMM,VMM之上有一堆guest,每个guest之上有一堆进程
不同类型的CPU上实现Trap and Emulate虚拟机会有不同的难度,不过RISC-V特别适合实现Trap and Emulate虚拟机
举个例子,设计人员确保了每个在Supervisor mode下才能执行的privileged指令,如果在User mode执行都会触发trap。你可以通过这种机制来确保VMM针对Guest中的每个privileged指令,都能看到一个trap。
guest修改satp寄存器时会进入VMM,但我们不能简单的直接使用用户提供的值
但是我们的确又需要为SATP寄存器做点什么,因为我们需要让Guest操作系统觉得Page Table被更新了。且需要能够访问正确的内存地址
gva
映射到 gpa
,hpa
,对应的映射表与PT类似guest写入satp切换页表时,trap handler会创建一个Shadow Page Table,将gva映射到hpa
,Shadow Page Table的地址将会是写入真实satp
的值
Shadow Page Table是这么构建的:
然后返回guest kernel
所以,Guest kernel认为自己使用的是一个正常的Page Table,但是实际的硬件使用的是Shadow Page Table。这是VMM实现隔离的一个关键部分。
Shadow Page Table是实现VMM时一个比较麻烦的地方
如果你的操作系统执行了大量的privileged指令,那么你也会有大量的trap,这会对性能有大的损耗。这里的损耗是现代硬件增加对虚拟机支持的动机。今天要讨论的论文使用的就是现在硬件对于虚拟机的支持,Intel和AMD在硬件上支持更加有效的trap,或者说对于虚拟机方案,会有少得多的trap。所以是的,性能很重要。但是上面介绍的方案,人们也使用了很多年,它能工作并且也很成功,尽管它会慢的多,但是还没有慢到让人们讨厌的程度,人们其实很喜欢这个方案。
虚拟机的外设,例如磁盘、网卡、声卡、显卡、鼠标键盘等等
guest通过Memory Map控制寄存器与设备进行交互,而VMM并不会映射这些gpa,而是将其设为无效。在guest试图访问时,触发handler,然后VMM会模拟外设。最后返回到guest中
这种方式可能会非常的低效,因为每一次Guest与外设硬件的交互,都会触发一个trap。在真实的设备中,trap会被频繁的触发
如果你的目标就是能启动操作系统并使得它们完全不知道自己运行在虚拟机上,你只能使用这种策略。
在这种情况下,OS知道自己运行在虚拟机上
提供虚拟设备接口,而不是模拟一个真实的设备。
虚拟设备接口
,而非使用mm寄存器命令队列
,Guest操作系统将读写设备的命令写到队列中在这个驱动里面要么只使用了很少的,要么没有使用Memory Mapped寄存器,所以它基本不依赖trap,效率比模拟设备高
这里典型的例子就是网卡。现代的网卡具备硬件的支持,可以与VMM运行的多个Guest操作系统交互。
你可以配置你的网卡,使得它表现的就像多个独立的子网卡,每个Guest操作系统拥有其中一个子网卡,且效率非常高
在这种方式中,Guest操作系统驱动可以知道它们正在与这种特别的网卡交互。
为什么Intel和其他的硬件厂商会为虚拟机提供直接的硬件支持呢?
例如:Intel的VT-x硬件,每个CPU核都有一套独立的VT-x
现在,这些虚拟状态信息会保存在硬件中
。这样Guest中的软件可以直接执行privileged指令来修改保存在硬件中的虚拟寄存器,而不用触发trap
VMM创建一个新的虚拟机时,需要配置guest信息。这些信息用一个VMCS结构体表示,并通过一些特殊的指令控制虚拟机:
cr3
寄存器我们不能让用户随意设置自己的页表,所以本方案中有另一个寄存器:EPT
Extended Page Table,来对用户提供的gpa进行二次翻译到hpa
也即guest的每一个虚拟地址都会经过两次翻译:gva–gpa–hpa
利用上述的硬件支持可以实现一些有趣的功能。利用这种完全是为了虚拟机而设计的硬件,可以用来做一些与虚拟机完全无关的事情,例如在一个guest进程中使用特权指令
我们将会在Linux中加载Dune可加载模块,作为kernel的一部分。
我们允许一个进程切换到dune
模式,在dune模式下,即non-root Supervisor mode
或Guest Supervisor mode,允许用户运行一些特权指令。
例如设置自己的cr3寄存器,更加灵活的管理自己的内存。同时这不会使得虚拟机逃逸,它无法突破EPT的阻拦
通过dune,处于user mode下的进程可以设置自己的页表,隔离guest正常的内存,然后运行一些运行未被信任的插件代码。
GC会扫描所有PTE找到正在使用的对象,而GC与程序并行运行,程序会修改GC已扫描过的对象。dune 之后会检查PTE的dirty位,如果被设置则会重新扫描该页
实际中,获取PTE dirty位的过程在普通的Linux中既困难又慢,我甚至都不确定Linux是否支持这个操作,在一些其他操作系统中你可以通过系统调用来查询PTE的dirty位。但是如果你使用Dune和 VT-x,进程可以很快的使用普通的load和store指令获取PTE,进而获取dirty位。所以这里,Dune使得某些需要频繁触发GC的程序明显变得更快。