gVisor kvm平台学习

本文是我将gVsior移植到RISC-V架构过程中的学习记录,移植项目在我的Github上:TeddyNight/gvisor-riscv。这篇文章仅代表我写完文章以前对gVisor的认识;同时我深知自己对gVisor的认识还不充分,对Linux等相关知识仍然知之甚少,故保留内省的权利,当然也欢迎朋友们交流和批评指正。

KVM平台的代码集中在pkg/sentry/platform/kvm(主要是VM的创建,页表初始化,虚拟内存管理等)和pkg/ring0(实现一个简单的Guest Kernel

本文将以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中断的位置继续往后执行。

标签: none

添加新评论