前言
本代码参考自社区的经典Demo:kvmtest.c [LWN.net]lwn.net/Articles/658512/
代码去掉了错误验证,只保留了最最核心的代码, 并进行了详细的注释.
其中相关CPU虚拟化知识可以学习以下两本书:
《现代操作系统:原理与实现》
《深入浅出系统虚拟化:原理与实践》
陈海波老师的书,可以作为虚拟化知识的入门,是我读过的最好的虚拟化入门教材, 当然也是学习操作系统好教材, 强烈推荐.
戚正伟老师对虚拟化背景进行了全面的总结和分析. 然后分章节解析CPU, 内存, IO虚拟化以及Qemu/KVM的源码实现, 难度较大, 非常详细!!!! 我正在细读.
两书中都提到了本文的代码!
此外我和两位老师都见过面, 哈哈哈哈哈.
代码注释
标题中10行, 其实是核心代码. 哈哈哈哈哈哈哈
#include <fcntl.h>
#include <linux/kvm.h>
#include <stdint.h>
#include <stdio.h>
#include <string.h>
#include <sys/ioctl.h>
#include <sys/mman.h>
int main(void) {
/**
* kvm 用于承接打开的设备文件dev/kvm的文件描述符
* vmfd 用于承接创建的VM的文件描述符
* vcpufd 用于承接创建的vCPU的文件描述符
*
* mem 指向一块4KB内存空间,用于存放虚拟机执行的代码
*
* sregs 用于初始化段寄存器状态
*
* mmap_size用于记录vCPU的共享内存空间大小
* run 指向vCPU的共享内存映射
*
* */
int kvm, vmfd, vcpufd;
uint8_t *mem;
struct kvm_sregs sregs;
size_t mmap_size;
struct kvm_run *run;
/// 以下汇编代码,是依次将"HELLO KVM\n"通过in/out指令写入0x3f8端口
/// 每次执行in/out都会触发VM-Exit,KVM将处理VM-Exit,如果是IO导致的,则KVM将继续向上提交
/// 即,交由QEMU等用户程序将继续处理
const uint8_t code[] = {
0xba, 0xf8, 0x03, /* mov $0x3f8, %dx */
0xb0, 'H', /* mov $'H', %al */
0xee, /* out %al, (%dx) */
0xb0, 'E', /* mov $'E', %al */
0xee, /* out %al, (%dx) */
0xb0, 'L', /* mov $'L', %al */
0xee, /* out %al, (%dx) */
0xb0, 'L', /* mov $'L', %al */
0xee, /* out %al, (%dx) */
0xb0, 'O', /* mov $'O', %al */
0xee, /* out %al, (%dx) */
0xb0, ' ', /* mov $' ', %al */
0xee, /* out %al, (%dx) */
0xb0, 'K', /* mov $'K', %al */
0xee, /* out %al, (%dx) */
0xb0, 'V', /* mov $'V', %al */
0xee, /* out %al, (%dx) */
0xb0, 'M', /* mov $'M', %al */
0xee, /* out %al, (%dx) */
0xb0, '\n', /* mov $'\n', %al */
0xee, /* out %al, (%dx) */
0xf4, /* hlt */
};
/// 打开KVM模块设备文件
kvm = open("/dev/kvm", O_RDWR | O_CLOEXEC);
/// 创建虚拟机, 并获取其文件描述符
vmfd = ioctl(kvm, KVM_CREATE_VM, (unsigned long) 0);
/// 创建4KB大小的内存空间(一页匿名页),用于存放VM执行的代码
mem = mmap(NULL, 0x1000, PROT_READ | PROT_WRITE, MAP_SHARED | MAP_ANONYMOUS, -1, 0);
/// 将上述代码拷贝到匿名页中
memcpy(mem, code, sizeof(code));
/// 将代码映射到VM物理内存(GPA)的第二个页框处,因为第一个页框被实模式保留,用于存放IDT(中断向量表)
struct kvm_userspace_memory_region region = {
.slot = 0, // 内存卡槽
.guest_phys_addr = 0x1000, // GPA的起始映射地址,即第二个页框
.memory_size = 0x1000, // 映射内存的大小,4KB
.userspace_addr = (uint64_t) mem, // 映射内存的起始地址
};
ioctl(vmfd, KVM_SET_USER_MEMORY_REGION, ®ion);
/// 创建vCPU并获取其文件描述符
vcpufd = ioctl(vmfd, KVM_CREATE_VCPU, (unsigned long) 0);
/// 映射vCPU的共享内存到run中,使我们可以访问其kvm_run结构
/// 当VM_Exit时,kvm_run结构中将保留exit_reason,我们将基于此知悉退出原因,从而进行相应的处理
mmap_size = ioctl(kvm, KVM_GET_VCPU_MMAP_SIZE, NULL);
run = mmap(NULL, mmap_size, PROT_READ | PROT_WRITE, MAP_SHARED, vcpufd, 0);
/// 设置代码段寄存器和代码段基址, 这是内存的分段管理
/// 此时我们并没有开启分页管理,因此虚拟地址直接等于物理地址
/// 而虚拟地址是根据线性地址+代码段基址计算的
/// 代码段基址是根据GDT(全局描述符表)和CS(代码段,选择子)确定的
ioctl(vcpufd, KVM_GET_SREGS, &sregs);
sregs.cs.base = 0;
sregs.cs.selector = 0;
ioctl(vcpufd, KVM_SET_SREGS, &sregs);
/// 关于kvm_sregs和kvm_regs(并不严格):
/// kvm_regs 用于通用寄存器的配置
/// kvm_sregs 用于段计算器的配置
/// 下用于设置rip寄存器, cs.base + rip 既是物理地址(没开启分页,虚拟地址等于物理地址,rip则表示线性地址),
struct kvm_regs regs = {
.rip = 0x1000,
};
ioctl(vcpufd, KVM_SET_REGS, ®s);
/// 开启一个循环,执行VM的代码,并处理IO事件
while (1) {
/// 开启vCPU
ioctl(vcpufd, KVM_RUN, NULL);
/// 执行到这里说明发生了VM_Exit
switch (run->exit_reason) {
case KVM_EXIT_HLT: /// VM关机
puts("KVM_EXIT_HLT");
return 0;
case KVM_EXIT_IO: /// IO事件
/// 在这里,输出写入0x3f8的字符
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));
break;
}
}
}