最近在学习虚拟化相关的内容,想着使用Rust构建一个最小的kvm用户空间实例。也就是直接调用kvm的api,然后创建虚拟机。网络上关于kvm的内容大部分是使用libvirt的,然后kvm用户空间实例也是使用C编写的。因此想着使用Rust写一个简单的。
思路
话不多说,直接讲思路:
- 创建kvm实例
- 初始化内存
- 初始化virtual cpu
- 加载镜像文件到客户机内存
- 运行vcpu
查了一下crates.io,发现有2个库,分别是
- kvm_bindings
- kvm_ioctls
利用kvm_bindings和kvm_ioctls这两个库对kvm api的封装,能够简化我们的代码编写。
代码讲解
代码讲解将分为2个部分,分别是用户空间实例以及客户操作系统的代码。主要是讲解kvm用户空间实例。
完整的代码在这里:https://github.com/fslongjin/kvm_userspace
用户空间实例
在这里,将结合main.rs的代码,对创建并运行虚拟机的全过程进行讲解。
main.rs的代码放在这里:https://github.com/fslongjin/kvm_userspace/blob/main/kvm_userspace/src/main.rs
这个代码是一个使用Rust编写的kvm用户空间实例,用于创建一个虚拟机并运行一个内核。
下面是创建虚拟机的过程:
1.创建Kvm实例
代码语言:javascript复制let kvm = Kvm::new().unwrap();
这个语句创建了一个Kvm实例。Kvm是一个结构体,代表了/dev/kvm。
2.创建VmFd实例
代码语言:javascript复制let vm = kvm.create_vm().unwrap();
这个语句创建了一个VmFd实例。VmFd是一个结构体,代表了一个虚拟机实例。
3.设置虚拟机内存
代码语言:javascript复制fn setup_memory(&mut self, ram_size: usize) {
// ...
let ptr = unsafe {
mmap(
0 as *mut c_void,
ram_size,
PROT_READ | PROT_WRITE,
MAP_SHARED | MAP_ANONYMOUS,
-1,
0,
)
};
// ...
}
这个函数使用mmap分配一块内存用于虚拟机,并设置虚拟机的内存区域。首先,把内存大小按照4096对齐,然后使用mmap函数分配一块内存。mmap函数的参数依次是:
- 0 as *mut c_void:分配的内存地址,这里使用0表示由系统自动分配。
- ram_size:分配的内存大小,按照4096对齐。
- PROT_READ | PROT_WRITE:内存的读写权限。
- MAP_SHARED | MAP_ANONYMOUS:分配匿名内存,多个进程可以共享这块内存。
- -1:文件描述符,这里使用-1表示不使用文件。
- 0:文件偏移量,这里使用0表示从文件开头开始分配内存。
然后,将分配的内存地址存储在Vm结构体的hva_ram_start字段中。接着,创建一个kvm_userspace_memory_region结构体,设置虚拟机的内存区域,然后使用VmFd的set_user_memory_region函数设置虚拟机的内存区域。
4.创建虚拟CPU
代码语言:javascript复制fn setup_cpu(&mut self) {
// ...
let vcpu = self.vm.create_vcpu(0).unwrap();
// ...
}
这个函数创建一个虚拟CPU,使用VmFd的create_vcpu函数创建。参数0表示创建一个编号为0的虚拟CPU。
5.设置虚拟CPU的寄存器
代码语言:javascript复制let mut vcpu_sregs: kvm_sregs = self
.vcpu
.as_ref()
.unwrap()
.get_sregs()
.expect("get sregs failed");
vcpu_sregs.cs.selector = 0;
vcpu_sregs.cs.base = 0;
self.vcpu
.as_ref()
.unwrap()
.set_sregs(&vcpu_sregs)
.expect("set sregs failed");
let mut vcpu_regs: kvm_regs = self
.vcpu
.as_ref()
.unwrap()
.get_regs()
.expect("get regs failed");
vcpu_regs.rax = 0;
vcpu_regs.rbx = 0;
vcpu_regs.rip = 0;
self.vcpu.as_ref().unwrap().set_regs(&vcpu_regs).unwrap();
这个代码块设置虚拟CPU的寄存器。首先,使用VcpuFd的get_sregs函数获取虚拟CPU的状态寄存器,然后设置代码段寄存器(cs)的选择符(selector)和基地址(base)。接着,使用VcpuFd的set_sregs函数设置虚拟CPU的状态寄存器。然后,使用VcpuFd的get_regs函数获取虚拟CPU的一般寄存器,然后将rax、rbx和rip寄存器设置为0。最后,使用VcpuFd的set_regs函数设置虚拟CPU的一般寄存器。
6. 加载内核镜像
代码语言:javascript复制fn load_image(&mut self, image: PathBuf) {
// ...
let kernel = std::fs::read(image).unwrap();
// ...
}
这个函数加载内核镜像。使用std::fs的read函数读取内核镜像文件,然后把内核镜像写入虚拟机的内存中。使用VmFd的set_user_memory_region函数设置内存区域。
7.运行虚拟机
代码语言:javascript复制fn run(&mut self) {
// ...
let vcpu = self.vcpu.as_mut().unwrap();
loop {
match vcpu.run().expect("run failed") {
kvm_ioctls::VcpuExit::Hlt => {
println!("KVM_EXIT_HLT");
// sleep 1s using rust std
std::thread::sleep(std::time::Duration::from_secs(1));
}
kvm_ioctls::VcpuExit::IoOut(port, data) => {
let data_str = String::from_utf8_lossy(data);
print!("{}", data_str);
}
kvm_ioctls::VcpuExit::FailEntry(reason, vcpu) => {
println!("KVM_EXIT_FAIL_ENTRY");
break;
}
_ => {
println!("Other exit reason");
break;
}
}
}
}
这个函数运行虚拟机。使用VcpuFd的run函数运行虚拟CPU,如果虚拟CPU执行HLT指令,则休眠1秒钟,然后继续执行。如果虚拟CPU执行IOOUT指令,则将输出字符串打印到标准输出。如果虚拟CPU执行失败,则退出循环。
客户操作系统代码
这个客户机操作系统,其实也不算是操作系统了,就是一段汇编代码而已,循环往IO端口输出HELLO,然后hlt。
完整的代码文件:https://github.com/fslongjin/kvm_userspace/blob/main/guest_os/kernel.S
详解:
这段汇编代码是一个简单的内核程序,它向0xf1端口输出一些字符(”HELLOn”),然后进入hlt指令,等待中断或重置。
下面是对代码的逐行解释
代码语言:javascript复制.code16gcc
这个指令告诉编译器使用16位代码,以便与实模式兼容。
代码语言:javascript复制.text
这个指令告诉编译器下面的代码是代码段。
代码语言:javascript复制.global _start
这个指令告诉编译器,_start标签是一个全局符号,可以在其他文件中使用。
代码语言:javascript复制.type _start, @function
这个指令告诉编译器,_start标签是一个函数。
代码语言:javascript复制_start:
这个标签是程序的入口点。
代码语言:javascript复制1:
这个标签定义了一个循环的起点。
代码语言:javascript复制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
这段代码使用outb指令将字符”H”, “E”, “L”, “L”, “O”, “n”写入0xf1端口。outb指令的第一个操作数是要写入的数据,第二个操作数是要写入的端口地址。
代码语言:javascript复制hlt
这个指令让处理器进入hlt状态,等待中断或重置。hlt指令会使处理器停止执行指令,但不会禁用中断。当有中断发生时,处理器会退出hlt状态。
代码语言:javascript复制jmp 1b
这个指令跳转到标签1,实现了一个简单的循环,使程序不停地向端口输出字符。
运行结果
接着,首先在guest_os文件夹下执行make命令编译guest os,接着在外层目录执行cargo run,就能运行起这个kvm用户空间实例了。
执行现象就是,会不断输出HELLO,然后hlt。
如图所示: