一、背景

首先要简单介绍下虚拟化,广义的定义是在一台物理机上可以模拟出多台虚拟机(Virtual Machine,简称VM),每个虚拟机中都可以运行一个操作系统(OS)。

虚拟化的两种运行模式:

Type1:Hypervisor 直接运行在硬件设备上的模式,也叫做 Bare-Metal Hardware Virtualization(裸机虚拟化环境)

典型的Type1虚拟化有 QNX hypervisor(目前车载即将爆发的应用场景), 架构图如下:

Type2:主机托管型,也叫做 Hosted Virtualization (主机虚拟化环境)

如:Vmware workstations、qemu(非kvm方案)等。

Type1和Type2的对比

Type1 型的 Hypervisor 不依赖主机操作系统,其自身具备操作系统的基础功能。设计上更简洁,直接运行于硬件之上,整体代码量和架构更为精简,对内存和存储资源要求更少,可满足功能安全等级要求,也具备进行形式化验证的条件,Type1非常适合自动驾驶系统领域(常说的舱驾一体方案)。

Type2 型 Hypervisor 需要借助宿主操作系统来管理 CPU、内存、网络等资源,由于 Hypervisor 和硬件之间存在一个宿主操作系统,Hypervisor 及 VM 的所有操作都要经过宿主操作系统,所以就不可避免地会存在延迟、性能损耗,同时宿主操作系统的安全缺陷及稳定性问题都会影响到运行在之上的 VM(虚拟机),所以 , Type-2 型 Hypervisor 主要用于对性能和安全要求不高的场合,比如 : 个人 PC 系统。

二、KVM技术

kvm技术框图:

关于KVM是Type1还是Type2 还有一些争论(KVM再次引爆Type 1与Type 2 hypervisor战争),在本文将利用KVM技术的方案当作是Type2, KVM还是在host os下的一个ko,虽然在性能上做了优化但目前使用kvm的方案还是混合的,部分虚拟化在usersapce空间,CPU和MMU的虚拟化虽然利用kvm做了优化,但是混合技术还是算在Host OS之上;典型的如:QEMU 。

三、x86 kvm sample代码

下面还是用一个X86的KVM sample代码来学习下KVM的相关知识,实验环境依赖:

确认使用的测试机环境打开了kvm, 存在节点/dev/kvm,如果没有请检查下kernel config是否打开:

CONFIG_KVM=y

一个虚拟机测试程序分成下面两部分:

1、虚拟机里执行的代码

/*
 * write "Hello"(ASCII) to port 0xf1 
 * compile: 
 * as -32 test.S -o test.o
 * objcopy -O binary test.o test.bin
 */

start:
mov $0x48, %al 
outb %al, $0xf1
mov $0x65, %al 
outb %al, $0xf1
mov $0x6c, %al 
outb %al, $0xf1
mov $0x6c, %al 
outb %al, $0xf1
mov $0x6f, %al 
outb %al, $0xf1
mov $0x0a, %al 
outb %al, $0xf1

hlt

上面一段汇编将Hello的ASCII码写入AL寄存器(RAX寄存器的低8位),每次写入一个ASCII码后,马上利用outb指令写入到地址0xf1

x86通用寄存器信息

2、虚拟机硬件配置代码:

#include <stdio.h>
#include <fcntl.h>
#include <sys/ioctl.h>
#include <linux/kvm.h>
#include <unistd.h>
#include <sys/mman.h>

#define KVM_DEV  "/dev/kvm"
#define MEM_SIZE  4096
int main(void)
{
    struct kvm_sregs sregs;
    int ret;
    int kvmfd = open(KVM_DEV, O_RDWR);

    //1. create vm and get the vm fd handler
    int vmfd =  ioctl(kvmfd, KVM_CREATE_VM, 0);
    unsigned char *ram = mmap(NULL, MEM_SIZE, PROT_READ | PROT_WRITE, 
            MAP_SHARED | MAP_ANONYMOUS, -1, 0);

    //2. load the vm running program to buffer 'ram'
    int kfd = open("test.bin", O_RDONLY);
    read(kfd, ram, MEM_SIZE);

    struct kvm_userspace_memory_region mem = {
        .slot = 0,
        .guest_phys_addr = 0,
        .memory_size = MEM_SIZE,
        .userspace_addr = (unsigned long)ram,
    };

    //3. set the vm userspace program ram to vm fd handler
    ret = ioctl(vmfd, KVM_SET_USER_MEMORY_REGION, &mem);

    if(ret < 0) {
        printf("set user memory region failed\n");
        return -1;
    }

    //4. create vcpu
    int vcpufd = ioctl(vmfd, KVM_CREATE_VCPU, 0);
    if(vcpufd < 0) {
        printf("create vcpu failed\n");
        return -1;
    }

    int mmap_size = ioctl(kvmfd, KVM_GET_VCPU_MMAP_SIZE, NULL);

    if(mmap_size  < 0) {
        printf("get vcpu mmap size failed\n");
        return -1;
    }

    //5. get kvm_run  status from vcpu
    struct kvm_run *run = mmap(NULL, mmap_size, PROT_READ | PROT_WRITE, MAP_SHARED, vcpufd, 0);

    //6. change the vcpu register info, first get then set 
    ret = ioctl(vcpufd, KVM_GET_SREGS, &sregs);

    sregs.cs.base = 0;
    sregs.cs.selector = 0;

    ret = ioctl(vcpufd, KVM_SET_SREGS, &sregs);

    struct kvm_regs regs = {
        .rip = 0, //指令寄存器 (RIP)
    };

    ret = ioctl(vcpufd, KVM_SET_REGS, &regs);

    //7. run the vcpu and get the vcpu result
    while(1) {
        ret = ioctl(vcpufd, KVM_RUN, NULL);
        if (ret == -1)
        {
            printf("exit unknow\n");
            return -1;
        }

        switch(run->exit_reason)
        {
            case KVM_EXIT_HLT:
                puts("KVM_EXIT_HLT");
                return 0;
                break;
            case KVM_EXIT_IO:
                putchar(*(((char*)run) + run->io.data_offset));
                break;
            case KVM_EXIT_FAIL_ENTRY:
                puts("entry error");
                return -1;
            default:
                puts("other error");
                printf("exit_reason: %d\n", run->exit_reason);
                return -1;
        }
    }
}

1、打开/dev/kvm获取系统KVM子系统的文件句柄,然后通过这个句柄创建一个虚拟机(VM),并返回一个vmfd的文件句柄,通过这个vmfd可以控制虚拟机的内存,vcpu等

2、分配一个用户空间的内存,然后通过结构体kvm_userspace_memory_region 将内存信息传递给vm, 这一步相当于给虚拟机指定了内存条

3、下一步就是给虚拟机创建一个VCPU了,通过struct kvm_run结构体来在用户空间和内核空间传递VCPU的状态和数据

4、设置VCPU的寄存器信息,在X86平台,段寄存器和控制寄存器等特殊寄存器在kvm_sregs中;通用寄存器信息通过kvm_regs传递;

代码涉及的结构体及宏定义代码在:arch/x86/include/uapi/asm/kvm.h

上面的例子中,虚拟地址及IP还有物理地址都是0,所以虚拟机VCPU运行的时候直接从地址0处取指令开始运行;虚拟机内核向端口写数据会产生KVM_EXIT_IO的退出,表示虚拟机内部读写了端口,在输出了端口数据之后让虚拟机继续执行,执行到最后一个hlt指令时,会产生KVM_EXIT_HLT类型的退出,此时虚拟机运行结束。

3、编译脚本makefile

all:
    as -32 test.S -o test.o
    objcopy -O binary test.o test.bin
    gcc kvm_sample.c -g -o kvm_sample

4、执行结果

四、小结

上面一个简单的sample程序来自《QEMU/KVM源码解析与应用》一书, 这个例子很好的介绍了一个最简单的虚拟机构成的部分,我这边简单整理了一下,也算是自己在kvm学习的第一课

参考:

https://blackberry.qnx.com/content/dam/qnx/products/hypervisor/hypervisor-product-brief.pdf

兰新宇:虚拟化技术 - 概览 [一]

智能汽车虚拟化(Hypervisor)技术详解_腾讯新闻

KVM再次引爆Type 1与Type 2 hypervisor战争

《QEMU/KVM源码解析与应用》 -- 李强

x86_64汇编之二:x86_64的基本架构(寄存器、寻址模式、指令集概览)_x86_64 如何表示 某个段的地址-CSDN博客

Logo

华为开发者空间,是为全球开发者打造的专属开发空间,汇聚了华为优质开发资源及工具,致力于让每一位开发者拥有一台云主机,基于华为根生态开发、创新。

更多推荐