最近在学习虚拟化相关的内容,想着使用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实例

let kvm = Kvm::new().unwrap();

这个语句创建了一个Kvm实例。Kvm是一个结构体,代表了/dev/kvm。

2.创建VmFd实例

let vm = kvm.create_vm().unwrap();

这个语句创建了一个VmFd实例。VmFd是一个结构体,代表了一个虚拟机实例。

3.设置虚拟机内存

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

fn setup_cpu(&mut self) {
    // ...
    let vcpu = self.vm.create_vcpu(0).unwrap();
    // ...
}

这个函数创建一个虚拟CPU,使用VmFd的create_vcpu函数创建。参数0表示创建一个编号为0的虚拟CPU。

5.设置虚拟CPU的寄存器

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. 加载内核镜像

fn load_image(&mut self, image: PathBuf) {
    // ...
    let kernel = std::fs::read(image).unwrap();
    // ...
}

这个函数加载内核镜像。使用std::fs的read函数读取内核镜像文件,然后把内核镜像写入虚拟机的内存中。使用VmFd的set_user_memory_region函数设置内存区域。

7.运行虚拟机

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端口输出一些字符(”HELLO\n”),然后进入hlt指令,等待中断或重置。

下面是对代码的逐行解释

.code16gcc

这个指令告诉编译器使用16位代码,以便与实模式兼容。

.text

这个指令告诉编译器下面的代码是代码段。

.global _start

这个指令告诉编译器,_start标签是一个全局符号,可以在其他文件中使用。

.type _start, @function

这个指令告诉编译器,_start标签是一个函数。

_start:

这个标签是程序的入口点。

1:

这个标签定义了一个循环的起点。

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指令的第一个操作数是要写入的数据,第二个操作数是要写入的端口地址。

hlt

这个指令让处理器进入hlt状态,等待中断或重置。hlt指令会使处理器停止执行指令,但不会禁用中断。当有中断发生时,处理器会退出hlt状态。

jmp 1b

这个指令跳转到标签1,实现了一个简单的循环,使程序不停地向端口输出字符。

运行结果

接着,首先在guest_os文件夹下执行make命令编译guest os,接着在外层目录执行cargo run,就能运行起这个kvm用户空间实例了。

执行现象就是,会不断输出HELLO,然后hlt。

如图所示:

转载请注明来源:https://longjin666.cn/?p=1739

欢迎关注我的公众号“灯珑”,让我们一起了解更多的事物~

你也可能喜欢

发表评论

您的电子邮箱地址不会被公开。 必填项已用*标注