基本逻辑
嵌套虚拟化是在虚机里支持vEL2的模拟,这样在虚机里可以再起一个KVM虚机。一般的术语里,
最底层到最上层分别为L0/L1/L2,定义如下:
L0: host kvm (EL2)
L1: guest hypervisor (virtual EL2)
L2: nested guest OS (virtual EL0, virtual EL1)
嵌套虚拟化的一个大致示意图如下:
1 | guest2 |
其中guest1是普通虚机,guest2是支持嵌套的虚机,guest3是其中的嵌套虚机。
目前为止ARM关于嵌套虚拟化的特性是FEAT_NV、FEAT_NV2和FEAT_NV3。软件支持方面,由于
NV性能很差,Linux主线没有支持FEAT_NV,主线直接支持了NV2和NV3。支持嵌套虚拟化的核心
是要高效的把vEL2模拟出来。
vEL2寄存器模拟
L1/L2实际上是跑在EL0/EL1上的,L1运行的时候可能会访问EL2的寄存器,软硬件需要一起
维持住L1可以操作EL2寄存器的语义。这里混用了EL2和vEL2,两者是一个意思,EL2是从L1
的视角看,vEL2是从上帝视角看。
vEL2的模拟需要:1. 使得软件可以访问vEL2的寄存器,2. 对应的vEL2寄存器的语义是正常
工作的。逻辑上讲,VHE下整个EL2的系统寄存器可以分为两大类,一类是host内核自己要用
的寄存器(这类寄存器是EL1和EL2成对出现的),另一类是hypervisor管理虚机用的寄存器。
所以,vEL2寄存器的模拟可以分为对这两类寄存器的模拟,也就是支持L1 host内核正常工作,
以及支持L1作为hypervisor的功能。
先看NV2协议的定义,看下架构如何支持。NV2定义,软件对vEL2寄存器访问可以重定向到一
段内存里,NV2新增系统寄存器VNCR_EL2表示这段内存的基地址。注意,这里配置的是host的
VA地址,需要经过EL0&2 translation regime的翻译(VHE场景下)。
在L2运行时,触发异常硬件需要直接更新对应的vEL2寄存器(e.g. 触发一个vEL2的异常,硬
件就需要把异常原因记录在vEL2 ESR_EL2)。所以,L2也需要让硬件感知到这段内存的地址。
这时,硬件依然可以用如上配置的VA得到具体的PA。
注意,这里有三个页表基地址,TTBR0/1_EL1指向Stage1的页表,TTBR0/1_EL2指向host的页表,
VTTBR_EL2指向Stage2的页表。所以,L1/L2上线时,硬件也可以感知到host页表。
基于如上的定义,L1对EL2寄存器的访问会直接访问内存的对应位置,直到下次到L0的时候,
再做实际配置,使得这些vEL2的寄存器的功能实际生效。这样可以避免L1每次访问EL2寄存器
都trap到L0处理。
FEAT_NV2有定义,把EL1访问EL2寄存器的行为重定向到对应的EL1寄存器。所以,如果是L1
host上线,只要把L1 host的EL2寄存器的值写入EL1对应的寄存器就好。
基于如上的认识,我们再次总结下:
对于L1 hypervisor寄存器,需要在返回L2之前,在L0里配置对应的寄存器,使得硬件控制
实际生效。一般是L0里把控制整合到对应物理寄存器里。
1 | HCR_EL2 L0读VNCR -> __compute_hcr()合并进物理HCR |
对于L1 host寄存器,需要在L0上线L1时换上VNCR中的vEL2寄存器,直接写入对应的EL1寄存器,
L1下线的时候保存相关寄存器? 对应寄存器有:
1 | VBAR_EL2、SCTLR_EL2、TCR_EL2、SPSR_EL2、ELR_EL2、ESR_EL2、FAR_EL2 ... |
基本逻辑如下:
1 | vcpu_load(L1): vcpu_put(L1): |
另外,VHE下还有一类EL12类型寄存器,是为了在EL2访问EL1寄存器。这类访问是否也可以
直接使用VNCR内存里的值?
举个实际vEL2 trap处理的例子串下如上的逻辑。如下是一个vEL2异常处理示意图:
1 | guest2 |
系统一开始在L2运行,vEL2异常直接trap到L0,L0准备vEL2的执行环境(因为L2之前在用EL1,
所以,这里L0要重新为L1准备环境),L0使系统进入L1处理vEL2的异常,L1处理完异常后使用
eret返回L2(L1视角),实际上NV时,eret触发系统trap进入L0,L0换上L2的上下文后把系统
返回到L2。
另外,看下嵌套虚拟化下vCPU的调度逻辑。L0只调度L1的vCPU,L2的vCPU的调度逻辑在L1
host内核和L1 hypervisor里。只不过,具体实现上,L1上线L2虚机都会先trap到L0,由L0
准备L2上下文后把L2投入运行。
vEL2 Stage2页表管理
ARM嵌套虚拟化的EL层级示意图如下:
1 | guest2 |
guest1是普通虚机,guest2是有vEL2的虚机,其中可以再起一个基于KVM的虚机(guest3)。
先从普通虚机看地址翻译的逻辑,每个类似guest1的普通虚机都需要有独立的S2翻译,S1+S2
完成虚机内虚拟地址到物理地址的翻译。所以,逻辑上,guest2需要有处于vEL2的S2翻译,
如果这样,嵌套虚机里的翻译就要经过S1+S2(vEL2)+S2(EL2)。
实际上,vEL2是模拟出来的,物理上只有S1和S2(EL2)的翻译。所以,实际上vEL2的S2是和
EL2的S2合并成一个新的S2,最终配置在EL2 S2的是这个新S2(Merged S2)。
注意,这里的Merged S2是完全独立的S2,只服务于guest3这个嵌套虚机。同一时刻在一个
物理核上,只能运行guest1/guest3/guest2-user三种状态之一,每个虚机的S2都是独立的,
不同的运行状态要换上与之对应的S2。对于一个支持嵌套虚拟化(也就是支持vEL2)的虚机,
可以启动嵌套虚机(guest3),也可以不启动嵌套虚机,直接运行本虚机(guest2 kernel和user)。
guest1和guest2不运行嵌套虚机时的S2类型是一样的,都是原本不merge的S2。
考虑merged S2的基本逻辑。IPA->IPA’的映射是vEL2软件建立的,IPA’->PA的映射是EL2软件
建立的。当IPA->IPA’建立,需要建merged S2的时候,需要查看IPA’->PA是否已经建立,如果
IPA’->PA已经存在,就可以建立IPA->PA,如果不存在,需要申请物理内存,然后建立IPA’->PA
以及IPA->PA的映射;当IPA->IPA’没有建立,就需要先建立这个映射,再完成剩余的逻辑。
1 | guest2 |
guest2(非嵌套)和guest3都要从L0进入,进入guest2(非嵌套)时,EL2配置的就是普通S2,
进入guest3时,EL2配置的是merged S2页表。
guest3运行发生S2缺页的时候,进入L0(EL2)处理缺页,L0需要先查看L1(vEL2)有没有IPA->IPA’。
如果有,L0需要补上IPA’->PA以及IPA->PA的映射;如果没有,L0不能直接处理这个缺页,
vEL2的逻辑需要在vEL2中自己处理,EL2把这个缺页注入给vEL2处理,vEL2处理这个缺页
逻辑,建立IPA->IPA’映射,这个映射只是存在于vEL2的页表里,这个页表并没有和物理硬件
有关系,vEL2处理完vEL2 S2缺页使用eret返回guest3,在嵌套虚拟化下,vEL2的eret会trap
到EL2,EL2换上merged S2页表后把L2投入运行,随后L2访问相同的地址,因为IPA’->PA可能
没有,可能会再次触发异常,重新走一遍上述流程,L0发现IPA->IPA’已经有了,于是补齐
IPA’->PA以及IPA->PA后,把L2投入运行。
在IPA->IPA’存在的情况下,通过PTW可以拿到IPA’,直接在L0建立IPA->PA。否则,不但要
进入L1处理IPA->IPA’,还可能会再次trap到L0里处理IPA’->PA。(实际代码是怎么样的,这里
是否有优化空间?)
TLBI整体逻辑
对于TLB基本的介绍可以参考这里。嵌套虚机和它的外层虚机应该有不同的VMID,也就是
guest3和guest2(非嵌套)应该有不同的VMID。虚机的combined TLB和S2 TLB被虚机自己的VMID
标记。考虑虚机TLB失效的时间,当虚机上任意一段地址翻译改变时,就应该失效对应的TLB。
在嵌套虚拟化的场景,当如上逻辑视图中三段映射任意一段映射改变的时候,都要无效与之
相关的所有TLB,这意味着嵌套虚机会有更多的TLB无效化操作。
打开具体看下:
EL2 S2映射变化。需要无效EL2 S2 TLB、S1 combined TLB(注意这两个是guest2(非嵌套)
的TLB)、EL2 merged S2 TLB、S1 combined TLB(后面这两个是guest3的TLB)vEL2 S2映射变化。需要无效EL2 merged S2 TLB、S1 combined TLB。这个只影响嵌套虚机
的TLB。S1映射变化。虚机内做S1 combined TLB无效化就可以。
1和2需要在EL2做对应TLB无效化。3在虚机内做,本来就是虚机内系统的行为。
考虑vEL2具体做TLB无效化的方式。首先,vEL2的TLBI大概分三类,1. 刷vEL2的S2 TLB,
2. vEL2作为host时,刷这个虚拟host的TLB,3. 在vEL2可以刷嵌套虚机的TLB。
第一点,理论上,硬件在L1就可以完成TLBI的处理,但是,vEL2的页表改动的时候,EL2的
merged S2 页表是无法感知的,这就需要trap TLBI到L0, L0的软件可以做merged S2的无效化
和对应TLB的无效化。
第二点,和第一点类似,理论上硬件同样可以在L1完成TLBI。硬件支持L1和L2的VMID独立存
放,但是并没有信息区分现在该用哪个VMID,其实就是没有信息区分TLBI EL1针对的是L1还
是L2。所以这种情况也得trap到L0里处理。
第三点的逻辑和第二点一样,也需要trap到L0里处理。
FEAT_NV3的协议还没有完全release,但是从已有的KVM FEAT_NV3的支持代码可以看出。这个
特性增加了NVHCR_EL2寄存器,L1写HCR_EL2会直接把信息写入NVHCR_EL2,这样硬件就可以
感知当前的状态是L2还是L1,这样如上第二点和第三点就可以在L1硬件直接处理,不用trap。
同时L1不用每次eret都trap到L0,L1不跑嵌套的情况,就是eret到L1用户态可以直接执行。
L1 vEL2 eret到L2虚机还是需要trap到L0。
vtimer整体逻辑
普通KVM虚拟化的vtimer支持逻辑可以参考这里。简单讲就是host和虚机各用一个定时器,
并且这两个定时器用不同的中断号。嵌套虚拟化下,host和虚机的timer还和以前一样,需要
给嵌套虚机一个逻辑上独立的vEL2 timer。
只看VHE下normal状态下的各个timer:
1 | L0 L1 L2 |
最新的v7.2内核中,L0 timer使用EL2 vtimer,L1 timer使用EL1 vtimer,就是说按照irq27
报中断到host,但kvm给L1注入的是irq28,这样vEL2看到一个和L0逻辑一样的vEL2 timer。
L2 timer也是使用EL1 vtimer,但是L2/L1的timer是复用的EL1 vtimer。基本逻辑是,L2访问
EL1 vtimer寄存器会被重定向到VNCR的内存区域,触发trap到L0(todo),随后L0上线L2的时候,
把内存区域的值写入EL1 vtimer寄存器,EL1 vtimer中原来的timer由L0软件模拟。
普通虚机有CNTVOFF_EL2表示CNTV_CNT/CNTP_CNT之间offset的机制,在嵌套虚拟化下,这样
的逻辑怎么继续? vEL2有CNTVOFF_EL2寄存器,L1访问这个寄存器时,值保存在VNCR内存里。
L0上线L2,目的是要使得L2 CNTV_CNT/CNTP_CNT里的值保持逻辑正确。逻辑上,
CNTV_CNT(L2) = CNTP_CNT(L1) + CNTVOFF_EL2(L1),这里右边的两个值都是L1的逻辑值,
继续展开得到逻辑上的计算公式:
1 | CNTV_CNT(L2) = CNTP_CNT(L0) + CNTPOFF_EL2(L0) + CNTVOFF_EL2(L1) |
物理上,需要把后两项之和配置给CNTVOFF_EL2(L0)。
vIRQ整体逻辑
逐个看下vPPI/vSGI/vSPI/vLPI的控制面和数据面的逻辑。
对于vPPI/vSPI。中断先到L0,L0把这个中断注入给L1,L1有完整的EL0/EL1/EL2,这之后的
逻辑和host上收到一个虚拟中断的逻辑一致,L1的KVM判断这个中断是给自己的还是要注入
L2,如果是注入L2,L1把信息写入L1的ICH_LR寄存器,eret到L2的时候把中断注入给L2。
对于vSGI。L2中发起vSGI(写ICC_SGIxx_EL1),触发trap到L0,L0判断是直接注入L1的系统,
还是目的地是L2,如果是L2,L0需要把这个中断注入L1,L1的KVM处理对应的逻辑,通过L1
的GITS_SGIR给L2注入vSGI,写GITS_SGIR导致trap进L0,L0完成给L2实际注入vSGI以及上线
L2虚机。
对于vLPI。嵌套虚机目前不支持vLPI直通,所以vLPI还是依赖ICH_LR注入,所以基本的处理
应该和vSGI是类似的。
虚拟化损耗
可以从特权级切换、内存管理、中断、IO等方面分析嵌套虚拟化的损耗。