gVisor kvm平台学习
本文是我将gVsior移植到RISC-V架构过程中的学习记录,移植项目在我的Github上:TeddyNight/gvisor-riscv,其他文档在这里。
KVM平台的代码集中在pkg/sentry/platform/kvm(主要是VM的创建,页表初始化,虚拟内存管理等)和pkg/ring0(实现一个简单的Guest Kernel
KVM平台状态机图
更新:后面又写了一篇关于KVM Platform的介绍放在比赛的项目文档里,感兴趣的可以看这里。
运行过程详解
本文将以TestKernelSyscall为例子展开介绍一个gVisor KVM平台工作的基本流程和我在移植到RISC-V架构上完成的一些工作。
TestKernelSyscall最内层是kvmTest函数,负责VM初始化,获取VCPU,执行测试代码。
func kvmTest(t testHarness, setup func(*KVM), fn func(*vCPU) bool) {
// Create the machine.
deviceFile, err := OpenDevice("")
if err != nil {
t.Fatalf("error opening device file: %v", err)
}
k, err := New(deviceFile)
if err != nil {
t.Fatalf("error creating KVM instance: %v", err)
}
defer k.machine.Destroy()
// Call additional setup.
if setup != nil {
setup(k)
}
var c *vCPU // For recovery.
defer func() {
redpill()
if c != nil {
k.machine.Put(c)
}
}()
for {
c = k.machine.Get()
if !fn(c) {
break
}
// We put the vCPU here and clear the value so that the
// deferred recovery will not re-put it above.
k.machine.Put(c)
c = nil
}
}
New()
首次运行KVM平台的New函数时会调用updateGlobalOnce()→physicalInit()
physicalInit这个函数根据/proc/pid/maps初始化sentry内核的内存区域,建立起guest physical address到supervisor virtual address的映射
New()中向内核KVM子系统发起一个创建VM的ioctl请求:KVM_CREATE_VM
NewMachine()
New()完成后接着到newMachine()
首先建立页表:包括应用进程共享的上半部分页表和Sentry的页表,建立起guest virtual address到guest physical address的映射(即VS-stage translation),这两个页表都映射到同一个sentry的地址空间
setMemoryRegion发起ioctl请求KVM_SET_USER_MEMORY_REGION,使用前面两步建立的映射关系,在内核的KVM子系统完成guest physical address到supervisor physical address的映射关系(即G-stage translation)的设置
接着m.initArchState()会准备好VCPU池,预先创建VCPU
createVCPU()创建VCPU,初始化VCPU控制区域,跳转到c.initArchState()初始化VCPU寄存器和初始化timer
- 设置ISA寄存器,启用浮点数扩展
- 设置TP寄存器为指向当前VCPU数据结构的指针,以供guest kernel引导进入Sentry时使用
- 设置SSCRATCH寄存器为指向当前VCPU数据结构的指针,供中断处理程序保存上下文使用
- 设置SP寄存器,方便guest kernel中的函数调用
- 设置SATP寄存器,初始化MMU
- 设置PC寄存器为ring0中引导进入Sentry的start函数入口地址
- 设置SIE寄存器,默认关闭中断
- 设置STVEC寄存器为中断处理程序的入口地址
machine.Get()获取并锁定一个vCPU,再执行bluepillTest里的匿名函数
bluepill()
bluepill()通过执行一条特权指令触发SIGILL,跳转到设置好的sighandler中,sighandler再跳转到bluepillHandler()
bluepillHandler()首先会调用bluepillArchEnter()将上下文信息复制到VCPU数据结构中,之后发送ioctl请求KVM_RUN,让VCPU运行,此时host上这个进程会一直等待直到它主动退出,此时完成了sentry从U到VS模式的切换
guest中从ring0.start恢复上下文,设置SEPC寄存器和SSTATUS寄存器之后,执行SRET指令,跳到VS模式从bluepill()发生SIGILL的位置继续往后执行TestKernelSyscall中的匿名函数
func bluepillTest(t testHarness, fn func(*vCPU)) {
kvmTest(t, nil, func(c *vCPU) bool {
bluepill(c)
fn(c)
return false
})
}
func TestKernelSyscall(t *testing.T) {
bluepillTest(t, func(c *vCPU) {
redpill() // Leave guest mode.
if got := c.state.Load(); got != vCPUUser {
t.Errorf("vCPU not in ready state: got %v", got)
}
})
}
redpill()
执行redpill(),redpill会执行一个编号为-1的系统调用
在x86_64和arm64上运行在guest内核态的sentry的syscall都能够触发exception跳转到guest内部的中断处理程序进行处理,但是在risc-v上,运行在VS模式的sentry调用syscall执行ecall指令后,因为SBI的关系,exception从M委托到HS后并不会直接委托给VS,而是S模式下执行ecall指令一样,将它视作一次SBI调用,而从SBI spec来看,Linux中选定作为sysnum的a7寄存器,在SBI中是ext编号,而且sysnum数值范围和SBI ext数值范围有冲突,似乎不能直接对SBI扩展。
这是KVM平台移植到RISC-V遇到的第一个麻烦。
目前暂时的解决方案是修改kvm相关代码,将来自VS模式的ecall委托回到VS,HS不做处理。
需要对内核打patch:
diff --git a/arch/riscv/kvm/vcpu_exit.c b/arch/riscv/kvm/vcpu_exit.c
index 2415722c01b8..0f2d97c5a29d 100644
--- a/arch/riscv/kvm/vcpu_exit.c
+++ b/arch/riscv/kvm/vcpu_exit.c
@@ -201,8 +201,11 @@ int kvm_riscv_vcpu_exit(struct kvm_vcpu *vcpu, struct kvm_run *run,
ret = gstage_page_fault(vcpu, run, trap);
break;
case EXC_SUPERVISOR_SYSCALL:
- if (vcpu->arch.guest_context.hstatus & HSTATUS_SPV)
- ret = kvm_riscv_vcpu_sbi_ecall(vcpu, run);
+ if (vcpu->arch.guest_context.hstatus & HSTATUS_SPV) {
+ //ret = kvm_riscv_vcpu_sbi_ecall(vcpu, run, trap);
+ kvm_riscv_vcpu_trap_redirect(vcpu, trap);
+ ret = 1;
+ }
break;
default:
break;
Halt()
中断处理程序判断scause是来自VS模式的ecall之后会调用HaltEcallAndResume(),先调用Halt()从VS模式退回到HS模式。
Halt的实现采用了和ARM64一样的方案:通过往一个不可写的地址(中断处理程序的入口地址)进行写操作触发MMIO_EXIT,从而退回到HS的bluepillHandler()中
TEXT ·Halt(SB),NOSPLIT,$0
// Trigger MMIO_EXIT/_KVM_HYPERCALL_VMEXIT.
//
// Using the same approach on ARM64, it will trigger a MMIO-EXIT by writing to
// a read-only space
FENCE
WORD $0x10502573 // csrr a0, stvec
MOVW ZERO, (A0)
RET
bluepillHandler()根据退出的原因:MMIO_EXIT且出错的地址是中断处理程序的入口地址,得知是Halt()调用,之后将VCPU中保存的寄存器上下文环境拷贝到ucontext中,之后sighandler恢复上下文环境,回到之前syscall调用的位置
此时syscall在U模式调用,就能和运行在HS-Mode的Linux内核进行通信,完成syscall调用
在TestKernelSyscall,redpill调用之后,在U模式下接着往后执行
如果后续需要回到VCPU的话,重新执行bluepill(),VCPU会回到上次执行HaltEcallAndResume()退出的位置,虽然此时仍然是旧的上下文,但是HaltEcallAndResume()在执行Halt()返回之后会通过kernelExitToSupervisor()恢复上下文之后ERET回到sentry中断的位置继续往后执行。