虚拟化
intel书里的定义,
|
|
虚拟化定义
|
一台物理机构建多个虚拟机
|
hypervisor就是物理机和虚拟机之间的中间层,允许多个操作系统和应用共享硬件
|
|
|
|
和emulator的区别
虚拟化,物理环境与虚拟环境体系结构一致
|
通构
|
isa理解成指令集架构
|
指令集可以直接转交给物理环境
|
|
|
|
|
|
|
虚拟化的两种类型
1型效率比较高,hypervisor是专用软件,就用来实现虚拟化
|
2型比如linux的kvm机制
|
这里用1型
|
|
|
支撑资源对象层次
vm包括整个机器的地址空间
|
vm中几个执行流就有几个vcpu
|
在vcpu之间调度实现虚拟机并发
|
vmem根据虚拟机平台规格来模仿的
|
vDevice有些虚拟设备在现实中不存在,模拟,存在的就映射
|
vutilities是把剩下的归为一大类,中断虚拟化或者总线发现设备
|
|
|
vCPU和CPU关系
虚拟cpu,通过多任务
|
vcpu直接绑定单个cpu,受限cpu数量
|
|
|
内存
虚拟机认为自己有一块或几块物理内存设备,且内存连续
|
实际都是不连续的,间隔零散的状态
|
Hypervisor基于页表扩展等机制,维护两者之间的关系
|
|
|
设备
跟cpu类似
|
比如网卡,设备可以当做一个网关或网桥
|
虚拟设备把数据发到网关和网桥上
|
还有一种由于插槽有限,有一种网卡只需要一个插槽,
|
但里面包含多个核,可以当做多个物理网卡
|
|
|
|
h_1_0建立最简化的Hypervisor
跟宏内核类似
|
这里虚拟化环境要启动一个Guest OS内核
|
|
|
特权级H扩展
没有设置之前,只有3个特权级
|
M最高,用来运行sbi
|
低一点S特权级,super啥啥内核一般在这一级
|
最低user,放应用
|
开虚拟化后
|
从S级做一个分层扩展,叫HS,虚拟化S
|
既有S特权级的性质,又能转向hypervisor虚拟化的特性
|
可以受控地通过vm_entry进入与host并列的guest环境
|
这里有虚拟化S特权级VS,和VU
|
一般情况,虚拟化应用在VU只会与VS进行切换
|
只有超出了虚拟化范围容纳范围,才会进入HS
|
|
|
体系结构H扩展后的寄存器
之前只有s打头的寄存器
|
加了hypervisor后会新加一组h打头的寄存器
|
hsxxx寄存器,面向guest域进行路径控制
|
对它进行某些特殊设置以后,会创造一种条件,在特定情况下会切换到guest运行虚拟机的内核或应用
|
同样两组,guest里的s寄存器vsxxx跟原本的s寄存器有对应关系
|
还有一组寄存器在vu里,很少实现user级别的控制,很少用到
|
|
|
准备条件
第一个是否能启用虚拟化扩展
|
ISA寄存器在misa第七位代表Hypervisor启用禁止,m态寄存器,比s高
|
如果支持这个是1,不然是0
|
|
opensbi会打印支持哪些扩展mafdch最后一个h代表支持
|
写入0可以禁用这个h扩展
|
|
|
|
|
做一个路径控制
|
|
通过hstatus第七位的spv来决定进入是vs还是u
|
|
决定在进入s特权级之前上一级是谁
|
如果上一次从虚拟化vs过来的话,spv是1
|
spvp的supervisor是决定虚拟化hs有没有对vs的操作权限,现在希望有操作权限,所以是1
|
|
|
|
前面设置好后,后面
|
|
主要设置guest里的sstatus,初始状态设置成s特权级
|
切过去从s态开始运行
|
|
第二个设置guest里的sepc,内核启动入口,RIscv64启动入口就是0x8020_0000所以设置成这个就可以
|
sret后会从sepc的启动入口开始运行
|
|
|
|
真正的切换
封装了一个run_guest函数
|
首先把hypervisor里有可能破坏的寄存器保存起来
|
如果之前有guest上下文,或者伪造的上下文,加载到虚拟机的寄存器组里
|
|
|
退出
|
|
退出原因
第一种原因,执行了guest里无法承担的操作
|
典型是调用了sbi-call
|
guest里没有m模式特权级
|
|
第二种原因
|
故意制造的缺页异常
|
应用触发第一阶段缺页异常,这时候从GVA->GPA,由于没有真正的物理页帧,则会触发进一步的缺页
|
跟U用户态应用缺页进入host kernel的缺页类似
|
|
|
|
简化实验模型
因为只有一个虚拟机,没有虚拟机管理的对象,被忽略了
|
只有一个全局的虚拟机
|
也没有启动多任务,只启动了一个虚拟机任务
|
|
|
目标和需要解决的问题
两种虚拟方式
|
模拟方式
|
跟u_3_0一样,给pflash准备一个文件
|
针对每个虚拟机都可以设置个文件
|
里面存pflash内容
|
上面访问,hypervisor就返回文件里内容
|
|
透传模式
|
直接把qemu里的pflash设备给映射进虚拟机
|
行成一对绑定关系
|
|
|
|
第一种方式,如果多虚拟机,每个都可以提供一个不同的文件
|
第二种方式,效率高,如果绑给了一个就不能给其他虚拟机了
|
|
两种模式都涉及到两阶段映射问题
|
|
|
guest域host的地址空间关系
guest是虚拟机所在的执行环境
|
host是hypervisor所在的执行环境
|
|
hypervisor在下面支撑guest虚拟机运行
|
需要3个地址空间
|
|
hypervisor需要管理所有物理地址空间,简称Hpa
|
给guest虚拟一个guest的物理地址空间,gpa
|
|
guest有了gpa后,hypervisor工作完成,后续guest会根据自己的gpa建立自己的虚拟地址空间gva
|
如果gva缺页,会找gpa补全,如果gpa已有,可以在小循环内完成
|
如果gpa也没有,一般也是个假象,一般没有,就继续找hpa要映射
|
|
|
|
体系结构riscv64对guest地址映射支持
两阶段映射
|
vsatp第一阶段页表翻译,v代表virtual s代表super
|
第二阶段hgatp,h代表host,g代表guest
|
|
现在需要映射范围小,可以支撑,没有处理扩展
|
|
|
虚拟化情况下,对三个区域采用不同处理方式
mmio设备模拟透传
|
sbi加载区域,直接映射,少量lazy映射
本来这段区域用来加载sbi
|
但这里没有加载
|
因为guest里没有m态
|
guest里用不到sbi所以null
|
|
上面区域都是lazy映射方式
|
什么时候缺页再填充
|
|
|
第二个实验
只有一个vm,忽略对象
|
hypervisor第一部创建和初始化两个资源,地址空间和vcpu
|
|
流程
前三步完成了地址空间的工作主要看axmm和axfs
|
然后设置vcpu的启动入口,就是启动后第一行指令从哪里开始
|
设置成guest内核里的入口,就是那个啥0x80xxxxx
|
还有设置页表,是guest里的的页表
|
|
|
|
vcpu准备代码
一般体系结构相关,所以会做个hal层,但这里直接就进体系结构相关的init了
|
创建对象,设置entry设置ept页表
|
set_entry,进入虚拟机前,设置的spec会被进一步写入sepc寄存器
|
sepc是发生异常的时候,保存的断点
|
当通过run_guest进入虚拟机的时候,这个断点会被恢复出来作为指令地址
|
|
一旦进入guest,就会从这个地址开始执行
|
|
|
set_ept_root设置二级页表根地址
|
hgatp
俩值,前面的值是模式,8代表sv39
|
后面是根页表的页根,
|
|
直接通过指令csrw写进去就可以了
|
然后刷一下快表
|
页表才能真正生效,不然cache里的内容是旧的
|
|
|
host与guest环境之间切换的框架
hypervisor是起点,做准备工作
|
然后它执行run_guest特殊函数
|
完成切换
|
|
里面要做的工作是把Hypervisor里可能被破坏的寄存器的值保存起来
|
然后把已经准备好的或者退出的guest上下文载入寄存器组
|
到此准备好了虚拟机进入条件
|
|
然后通过sret,从host环境切换到guest环境
|
之后开始运行guest os内核
|
如果guestos要执行特权指令,或者要访问的资源超出了虚拟化能容纳的范围
|
会发生退出vm-exit
|
|
hypervisor提前准备好了退出的入口
|
退出入口里的工作跟上面的工作相反
|
把当前虚拟机涉及的寄存器存起来
|
恢复host的寄存器组
|
于是恢复到hypervisor发生断点进入前的状态
|
可以继续执行hypervisor的逻辑
|
处理好异常后,继续调用上面的过程
|
|
|
|
环境切换框架的实现
无论首次进入guest,还是将来进入guest,通过同一个入口vcpu_run
|
vcpu_run在关中断情况下调用具体体系结构的vcpu_run
|
就是那个_run_guest进入guest
|
vmexit_handler处理体系结构相关exit_reason,回去后如果第一个ok没法处理,进入err处理
|
|
|
具体的5步骤
保存host状态,以备从guest恢复
|
|
设置sepc等寄存器,指示guest从断点处继续的位置
|
首次就是入口地址那个0x8020
|
|
上面是状态
第四步是恢复guest的寄存器状态
|
最后是sret,之前设置过hstatus指示上一步是从guest进入的(假装的),所以执行sret会回到guest
|
|
第三个跟guest退出相关,
stvec寄存器值临时设成了_guest_exit
|
这样guest发生虚拟机退出,就是从这个入口找的
|
走_guest_exit
|
|
|
|
guest_exit如何实现
跟run_guest是同一个文件
|
跟run_guest就是个逆过程
|
|
需要注意
|
先恢复hyp_ra保存的ra值恢复到ra
|
然后ret的时候会把ra的值作为指令寄存器的值
|
就是会从ra指向的值,做一个函数返回
|
这个值是run_guest的返回值
|
|
再看右边函数
|
从_run_guest(regs)是为了从host进入guest
|
保存寄存器的时候其中要保存的一项就是ra项
|
当时把ra存到了hyp_ra
|
这个值是这个函数的返回地址
|
进入到guest之后,发生了虚拟机退出
|
退出的时候恢复这个hyp_ra
|
正好就是那个函数的返回地址
|
因而会执行到下一步vmexit_handler()
|
|
所以_run_guest会发生特权级切换,进到guest虚拟机运行,这边就停住了,block?
|
而guest退出的时候刚好能落回来
|
|
|
|
模拟pflash的两种方式
第一种没有映射,触发嵌套的缺页异常
|
回到Hypervisor
|
如果发现是pflash的,直接跟qemu的pflash地址做一个关联
|
采用map_linear线性映射
|
完成了一个绑定
|
|
另一种在Hypervisor同样截获之后,采用读文件的方式读取后备文件
|
使用map_alloc申请一个page frame
|
从文件里把对应的文件内容给填充进去
|
因为已经完成了两阶段映射
|
异常返回以后,内核再次访问这个地址就能访问到真正的页帧
|
也在h_2_0实验里面
|
这边注释掉了,放开,并注释掉左边,就能看到
|
|
|
|
简单对比宏内核和Hypervisor
宏内核是SU切换
|
guest回到Hypervisor没有sys-call方式
|
都是异常或中断的方式
|
|
都要做地址空间的隔离
|
|
|
两阶段地址映射补充说明
Hypervisor也启用了分页
|
也有va->pa的过程
|
|
在hypervisor内部va通过走satp指向的页表从虚拟地址映射到物理地址
|
另一条路Hypervisor起一个guest运行操作系统内核
|
|
guest里也分页
|
访问guest里的gva地址的时候
|
首先做第一阶段映射,通过vsatp,这个寄存器指向根页表
|
完成从gva到gpa的映射
|
|
gpa本身是虚拟的,guest当它是真实pa
|
如果gpa已经有Hypervisor准备好了
|
就完成了没有第二阶段
|
|
很多情况下gpa是没有的
|
会发生第二阶段地址映射,通过hgatp
|
|
这一步是从gpa到hpa,hpa就是hypervisor的pa
|
hgatp和satp指向的根页表,是两个不同的页表
|
|
|
|
两个实验
如果虚拟机里想支持中断,时钟中断
|
虚拟机外设的支持
|
pflash本来需要封装成外设,这里就是封装这个外设
|
|
第二个实验尝试启动宏内核
|
要加载用户应用的img
|
通过透传pflash的方式加载应用
|
|
|
|
实验目标
配合的Hypervisor内核是第一周做的第六个实验
|
引入了抢占式调度,依赖时钟中断
|
里面会多出来很多timer相关的日志
|
timer irq emulation是在Hypervisor发现guest因为时钟中断退出后再将时钟中断注入回去
|
|
|
具体机制和实现
受限在axhal注册时钟中断
|
axtask会向runqueue传递定时事件
|
会传给特定调度器task_tick
|
每次触发后会检查下内部状态
|
内外条件都具备,会导致任务队列重排
|
|
|
内部俩组件
一个axruntime
|
会去注册定时器响应函数
|
更新定时器关键update_timer
|
|
核心代码最后一行,通过axhal设置了一个一次性的定时器
|
前面计算下一次触发需要什么时间
|
|
具体实现最后调用的sbi_call
|
sbi服务提供了设置定时器
|
|
|
|
u_6_0面临的问题
具体怎么在虚拟机里运行
|
两个办法,能让虚拟机内核仍然响应时钟中断
高级方法,riscv有个aia机制,高级中断架构,可以把特定中断,委托到guest环境下
|
需要平台支持,实现比较复杂
|
|
|
|
|
|
时钟中断设置和注入的实现
受限响应虚拟机发出的SBI-Call如果调用的是setTimer
|
要代替它去调用set_timer
|
每次guest触发中断都会通过sbi-call调用set_timer
|
|
|
Hypervisor替它调用
触发时钟中断,不管在guest还是hypervisor都会被中断运行
|
|
然后退进Hypervisor
|
|
禁用时钟中断
|
然后给注入回去
|
因为虚拟机被中断打断之后,不会进入虚拟机的中断入口表stvec
|
只会退出
|
|
而我们需要guest虚拟机能感受到时钟中断,触发响应时钟中断的内部函数
|
可以设置hvip注入时钟中断
|
这个stvec可以加个v嗯?
|
|
|
|
时钟中断设置和注入的实现
第一步响应虚拟机发出的SBI-Call
|
在Hypervisor就能截获到原因
|
第一步把pending bit给清零,就是hvip时钟中断对应bit
|
因为这时候还没来时钟中断
|
把host时钟中断的bit给打开
|
|
然后等时钟到期
|
虚拟机又被打断
|
退回到Hypervisor
|
再检查原因,发现是S级的定时中断导致的
|
设置hvip,显然是时钟到期了
|
把这个事通知到guest里去
|
完成了时钟中断的注入
|
把sie,host的时钟中断给禁掉
|
不能在这个过程里再次触发时钟中断
|
等到什么时候虚拟机再次settimer的时候,也就是左边,再给打开
|
|
|
|
h.4.0实验目标
前面的功能串一串
|
配合的guest内核是m_1_1
|
|
|
如何从hypervisor加载宏内核image
宏内核启动后,如何获得应用的image
|
让Hypervisor给虚拟机模拟出来一个pflash
|
模式是透传模式
|
把下面qemu的pflash给透传上来
|
|
|
建立对虚拟设备管理的支持
第一层是虚拟机简称vm
|
为了方便管理外设,增加group一层
|
理解成一种容器,便于管理
|
|
然后是代表虚拟机设备对象vmdev
|
riscv通过mmio访问虚拟机
|
核心资源就是一个地址区域
|
提供方法只有handle_mmio()响应访问
|
本来还应该分read和write
|
|
|
|
具体到实现
|
更优雅应该从配置注册进来
|
一开始没设备,访问的时候会触发嵌套式页面异常
|
查看是不是在物理之下,还是之内
|
然后找目标设备
|
调用handle_mmio
|
|
|
|
多vcpu支持
每个vcpu对应一个task
|
有时一个虚拟机里会有多个cpu
|
一个虚拟机有多vcpu但也是一个地址空间
|
因为调度器看到的是task
|
同一个vm下的task切换,不会切换根页表
|
其他的会
|
|
|
对应于task扩展,需要添加额外资源
这里是vcpu资源和关联虚拟机资源
|
其实可以省掉,axvmref,因为vcpu里有一个引用
|
使用宏内核的搞法类似定义
|
最后调用宏
|
|
|
hypervisor的配置
虚拟机的配置
|
比如vmware
|
通过一个图形化工具,一部分是配置软件,可以配置cpu,内存,和哪些设备
|
还有个os镜像,或者根文件系统
|
|
|