上一期,我们详细介绍了QEMU-KVM的交互通道IOCTL,这一“里应外合”的交互方式有如下好处:

1、策略与机制分离,加速的机制由KVM负责,而如何调用加速的机制由Qemu负责;

2、可以由Qemu设置多种内存模型,如UMA、NUMA等等;

3、方便Qemu对特殊内存的管理(如MMIO);

4、内存的分配、回收、换出等都可以采用Linux原有的机制,不需要为KVM单独开发;

5、兼容其他加速器模型(或者无加速器,单纯使用Qemu做模拟)。

但是,我们脑子里总是有一系列的问题,比如,到底这两者是怎么互动,才能创建、加载和运行一个虚拟机的。因为什么事情,只有落实到了代码层面,才能心里有底。这一期,我们将通过分析代码,详解一下。下面,先上几张图,对QEMU-KVM有个感性认识。

8d2994de5dbd58ec21063da66692c6f4.png

图一

0c8004d2ae25a6066308859176da850c.png

图二

4b27f6c221f6360ef06fa63b37d1b295.png

图三

82aad4d480f40cbdde6bcb45781e1fb3.png

图四

上面几张图,从不同的侧面反映了QEMU-KVM的体系结构,侧重点不同。

有没有看晕?我还没醉。下面,总结一下qemu-kvm软件的架构特点:

1、Kvm本身只提供两个内核模块。Kvm实现了vcpu和内存的管理;

2、Qemu控制逻辑,负责创建虚拟机,创建vcpu等。

在详细介绍,kvm提供了三种通过不同的io_ctl接口来控制的概念:

1、struct kvm:代表kvm模块本身,用来管理kvm版本信息,创建一个vm;

2、struct vm:代表一个虚拟机。通过vm的io_ctl接口,可以为虚拟机创建vcpu,设置内存区间,创建中断控制芯片,分配中断等等;

3、struct vcpu:代表一个vcpu。通过vcpu的io_ctl接口,可以启动或者暂停vcpu,设置vcpu的寄存器,为vcpu注入中断等等。

首先,定义一个简单地虚拟机需运行代码:

mov $0x3f8, %dxadd %bl, %aladd $'0', %alout %al, (%dx)mov $'\n', %alout %al, (%dx)hlt

这段代码比较简单,也就是先将al和bl寄存器的值相加(初始默认值均为2),结果转换后,输出至0x3f8端口,最后停机。然后,我们通过gcc和objdump将上述二进制代码转换为机器码,内容如下:

constuint8_t code[]={  0xba,0xf8,0x03,/* mov $0x3f8, %dx */  0x00,0xd8,/* add %bl, %al */  0x04,'0',/* add $'0', %al */  0xee,/* out %al, (%dx) */  0xb0,'\n',/* mov $'\n', %al */  0xee,/* out %al, (%dx) */  0xf4,/* hlt */};

需要指出的是,运行这段代码需要CPU"unrestricted guest"特性支持。

下面,简略叙述一下QEMU、KVM的交互过程。

先定义并初始化几个变量:

  /* 向KVM注册用户态内存空间,也即向虚拟机添加“物理内存” */  /* 注意该“物理内存”是qemu进程向host申请的用户态内存 */struct kvm_userspace_memory_region region = {  .slot = 0,  .guest_phys_addr = 0x1000,  .memory_size = 0x1000,  .userspace_addr = (uint64_t)mem,};  /* 通用寄存器信息初始化,此处可以看到a、b寄存器值初始化为2 */struct kvm_regs regs = {  .rip = 0x1000,  .rax = 2,  .rbx = 2,  .rflags = 0x2,};  /* cs段寄存器信息初始化 */sregs.cs.base = 0;sregs.cs.selector = 0;

运行过程如下:

void main(){  /* 打开kvm控制的总设备文件/dev/kvm */  kvm = open("/dev/kvm", O_RDWR | O_CLOEXEC);  /* 检查API版本信息,检测ret值 */  ret = ioctl(kvm, KVM_GET_API_VERSION, NULL);  if (ret == -1)    err(1, "KVM_GET_API_VERSION");  if (ret != 12)    errx(1, "KVM_GET_API_VERSION %d, expected 12", ret);/* 创建虚拟机 */   vmfd = ioctl(kvm, KVM_CREATE_VM, (unsigned long)0);  /* 获取页对齐且初始化为0的一个内存页 */  mem = mmap(NULL, 0x1000, PROT_READ | PROT_WRITE, MAP_SHARED | MAP_ANONYMOUS, -1, 0);  /* 将前述二进制代码拷贝至该页内 */  memcpy(mem, code, sizeof(code));  /* 将二进制页赋予虚拟机 */   /* 此时,虚拟机将其当做物理内存,且只有一个slot */  ioctl(vmfd, KVM_SET_USER_MEMORY_REGION, &region);  /* 创建VCPU,且每个VCPU关联一个struct kvm_run结构体 */  vcpufd = ioctl(vmfd, KVM_CREATE_VCPU, (unsigned long)0);  /* 计算需要kernel和用户空间共享的struct kvm_run结构体大小 */  mmap_size = ioctl(kvm, KVM_GET_VCPU_MMAP_SIZE, NULL);  /* 执行共享struct kvm_run结构体*/  run = mmap(NULL, mmap_size, PROT_READ | PROT_WRITE, MAP_SHARED, vcpufd, 0);  /* 初始化虚拟机的VCPU寄存器信息,准备运行 */  ioctl(vcpufd, KVM_GET_SREGS, &sregs);  ioctl(vcpufd, KVM_SET_REGS, &regs);  /* 运行 */  while (1) {    /* 进入运行 */    ioctl(vcpufd, KVM_RUN, NULL);    /* 退出处理 */    switch (run->exit_reason) {      case KVM_EXIT_HLT:        puts("KVM_EXIT_HLT");        return 0;      case KVM_EXIT_IO:        if (run->io.direction == KVM_EXIT_IO_OUT &&          run->io.size == 1 &&          run->io.port == 0x3f8 &&          run->io.count == 1)          putchar(*(((char *)run) + run->io.data_offset));        else          errx(1, "unhandled KVM_EXIT_IO");        break;      case KVM_EXIT_FAIL_ENTRY:        errx(1, "KVM_EXIT_FAIL_ENTRY: hardware_entry_failure_reason = 0x%llx",         (unsigned long long)run->fail_entry.hardware_entry_failure_reason);      case KVM_EXIT_INTERNAL_ERROR:        errx(1, "KVM_EXIT_INTERNAL_ERROR: suberror = 0x%x",           run->internal.suberror);      default:printf("error\n"); break;    }  }  }

       上面已经简略地注释了各行代码。我们大体上搞明白了创建、加载、运行虚拟机的基本流程。

但还有很多问题没说呢?比如,struct kvm_run结构体为什么要共享、同步?那一页slot内存是怎么给虚拟机的?到底CPU的VT技术是怎么支持KVM的?一系列的问号啊!!!

下期精彩继续!!!

Logo

DAMO开发者矩阵,由阿里巴巴达摩院和中国互联网协会联合发起,致力于探讨最前沿的技术趋势与应用成果,搭建高质量的交流与分享平台,推动技术创新与产业应用链接,围绕“人工智能与新型计算”构建开放共享的开发者生态。

更多推荐