QEMU源码全解析30 —— QEMU/KVM API 使用实例

news/2024/5/18 2:21:25

本文内容参考:

《QEMU/KVM》源码解析与应用 —— 李强,机械工业出版社

特此致谢!

在本系列之前文章中,讲解了QEMU参数解析、QEMU Module(模块)、并且花了大力气(十几篇文章)重点讲解了QOM。这些基础讲过之后,要正式开始对于各个硬件模块的详细介绍了。但在此之前,先以一个QEMU/KVM API的使用实例作为引子和序幕。

一提到QEMU,总是会提到KVM,而反之亦是如此。正所谓是“焦不离孟,孟不离焦”。但实际上从本质上来说,QEMU和KVM可以不必互相依赖。之所以将这两者“捆绑”在一起,是因为KVM创立之初重用了QEMU的设备模拟部分。本回以一个简单的示例展示QEMU和KVM的关系,也为后边深入各模块的研究提供一个最为简单和基础的框架流程上的参考。

示例包括两部分:第一部分是一个极简版内核,其任务仅仅是向I/O端口写入数据;第二部分可以看做是一个精简版的QEMU,它的任务也很简单,就是将此极简内核写入端口的数据打印出来。

  • 极简内核

极简内核代码如下(x86汇编,test.S):

start:
    # 输出'H'
    mov $0x48, %a1
    outb %a1, $0xf1
    # 输出 'e'
    mov $0x65, %a1
    outb %a1, $0xf1
    # 输出'l'
    mov $0x6c, %a1
    outb %a1, $0xf1
    # 输出'l'
    mov $0x6c, %a1
    outb %a1, $0xf1
    # 输出'o'
    mov $0x6f, %a1
    outb %a1, $0xf1
    # 输出换行符'\a'
    mov $0x0a, %a1
    outb %a1, $0xf1

    # 停机
    hlt

代码很简单,代码注释已经写得很清楚了,功能是向端口0xf1写入Hello字符串,然后调用hlt指令。

使用如下命令编译上述汇编代码:

$ as -32 test.S -o test.o

$ objcopy -O binary test.o test.bin
  • 精简版QEMU

精简版的QEMU代码如下(qemu.c):

int main(void)
{
    struct kvm_sregs sregs;
    int ret = 0;

    int kvmfd = open("/dev/kvm", O_RDWR);

    ioctl(kvmfd, KVM_GET_API_VERSION, NULL);

    int vmfd = ioctl(kvmfd, KVM_CREATE_VM, 0);

    unsigned char *ram = mmap(NULL, 0x1000, PORT_READ | PORT_WRITE, MAP_SHARED | MAP_ANONYMOUS, -1, 0);
    int  kfd = open("test.bin", O_RDONLY);
    read(kfd, ram, 4096);

    struct kvm_userspace_memory_region mem = {
        .slot = 0,
        .guest_phys_addr = 0,
        .memory_size = 0x1000,
        .userspace_addr = (unsigned long)ram,
    };
    ret = ioctl(vmfd, KVM_SET_USER_MEMORY_REGION, &mem);

    int vcpufd = ioctl(vmfd, KVM_CREATE_VCPU, 0);

    int mmap_size = ioctl(kvmfd, KVM_GET_VCPU_MMAP_SIZE, NULL);
    struct kvm_run *run = mmap(NULL, mmap_size, PORT_READ | PORT_WRITE, MAP_SHARED, vcpufd, 0);

    ret = ioctl(vcpufd, KVM_GET_SREGS, &sregs);
    sregs.cds.base = 0;
    sregs.cs.selector = 0;
    ret = ioctl(vcpufd, KVM_SET_SREGS, &sregs);

    struct kvm_regs regs = {
        .rip = 0,
    };
    ret = ioctl(vcpufd, KVM_SET_REGS, &regs);

    while (1)
    {
        ret = ioctl(vcpufd, KVM_RUN, NULL);
        if (ret == -1)
        {
            printf("exit unknown\n");
            return -1;
        }
        switch(run->exit_reason)
        {
            case KVM_EXIT_HLT:
                puts("KVM_EXIT_HLT");
                return 0;
            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;
        }
    }

    return 0;
}

编译qemu.c文件:

$ gcc qemu.c -o light-qemu

执行编译后生成的目标可执行程序,命令及结果如下:

$ ./light-qemu
Hello
KVM_EXIT_HLT

可以看到,这个名为light-gemu的精简版QEMU输出了精简版内核向端口写入的数据。下边对qemu.c代码进行详细说明。

KVM通过一组ioctl系统调用向用户空间导出接口,这些接口能够用于虚拟机的创建、虚拟机内存的设置、虚拟机VCPU的创建与运行等。根据接口所使用的文件描述符(file descriptor, fd),KVM的这组系统调用可以分为三类:

(1)系统全局的ioctl。这类ioctl的作用对象是KVM模块本身,比如一些全局的配置项,创建虚拟机的ioctl也在此例。

(2)虚拟机相关的ioctl。这类ioctl的作用对象是一台虚拟机,比如设置虚拟机的内存布局、创建虚拟机VCPU也在此例。

(3)虚拟机VCPU相关的ioctl。这类ioctl的作用对象是一个虚拟机的VCPU,比如说开始虚拟机VCPU的运行。

以下是qemu.c中主函数代码的详细步骤:

(1)首先通过打开"/dev/kvm"获取系统中KVM子系统的文件描述符kvmfd。代码片段如下:

int kvmfd = open("/dev/kvm", O_RDWR);

(2)为了保持应用层和内核的统一,可以通过ioctl获取KVM的版本号,从而使应用层知道相关接口在内核是否有支持。代码片段如下:

ioctl(kvmfd, KVM_GET_API_VERSION, NULL);

(3)接着在kvmfd上调用ioctl创建一个虚拟机。代码片段如下:

int vmfd = ioctl(kvmfd, KVM_CREATE_VM, 0);

该ioctl返回一个代表虚拟机的文件描述符vmfd。这代表了一个完整的虚拟机系统,可以通过vmfd控制虚拟机的内存、VCPU等。

(4)内存是一个计算机必不可少的组件,因此在创建了虚拟机之后,需要给虚拟机分配物理内存。代码片段如下:

    unsigned char *ram = mmap(NULL, 0x1000, PORT_READ | PORT_WRITE, MAP_SHARED | MAP_ANONYMOUS, -1, 0);
    int  kfd = open("test.bin", O_RDONLY);
    read(kfd, ram, 4096);

虚拟机的物理内存对应QEMU的进程地址空间,这里使用mmap系统调用分配了一页(4096,4K)虚拟机内存,并且将精简版的内核代码读入了这段空间中。

(5)之后用分配的虚拟内存地址初始化kvm_userspace_memory_region对象,然后调用ioctl(KVM_SET_USER_MEMORY_REGION),这就为虚拟机指定了一个内存条。代码片段如下:代码片段如下:

    struct kvm_userspace_memory_region mem = {
        .slot = 0,
        .guest_phys_addr = 0,
        .memory_size = 0x1000,
        .userspace_addr = (unsigned long)ram,
    };
    ret = ioctl(vmfd, KVM_SET_USER_MEMORY_REGION, &mem);

其中:

slot —— 用来表示不同的空间;

guest_phys_addr —— 表示这段空间在虚拟机物理内存空间的位置;

memory_size —— 表示这段物理内存的大小;

userspace_addr —— 表示这段物理空间对应在宿主机上的虚拟机地址。

(6)设置好虚拟机的内存后,接着在vmfd上调用ioctl来创建虚拟机VCPU。代码片段如下:

int vcpufd = ioctl(vmfd, KVM_CREATE_VCPU, 0);

(7)每一个VCPU都有一个struct kvm_run结构,用来在用户态(QEMU)和内核态(KVM)共享数据。用户态程序需要将这段空间映射到用户空间,为此首先调用ioctl(KVM_GET_VCPU_MMAP_SIZE)得到这个结构的大小。代码片段如下:

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

注意这里是对kvmfd调用ioctl,因为这是对KVM所有VCPU都是一样的。

(8)接着调用mmap函数,将struct kvm_run的对象映射到用户态空间。代码片段如下:

struct kvm_run *run = mmap(NULL, mmap_size, PORT_READ | PORT_WRITE, MAP_SHARED, vcpufd, 0);

(9)为了让虚拟机VCPU运行起来,需要设置VCPU相关的寄存器。其中,段寄存器和控制寄存器等特殊寄存器存放在kvm_sregs结构中,通过ioctl(KVM_GET_SREGS)和ioctl(KVM_SET_SREGS)读取和修改;通用寄存器存放在kvm_regs结构中,通过ioctl(KVM_GET_REGS)和ioctl(KVM_SET_REGS)读取和修改。代码片段如下:

    ret = ioctl(vcpufd, KVM_GET_SREGS, &sregs);
    sregs.cds.base = 0;
    sregs.cs.selector = 0;
    ret = ioctl(vcpufd, KVM_SET_SREGS, &sregs);

    struct kvm_regs regs = {
        .rip = 0,
    };
    ret = ioctl(vcpufd, KVM_SET_REGS, &regs);

其中最重要的是设置CS和IP寄存器,本示例中都将其设置为0。由于代码加在了虚拟机物理地址0处,所以虚拟机VCPU运行的时候直接从地址0处开始运行。

(10)至此,一个简单的虚拟机和虚拟机VCPU、内存都已经准备完毕,寄存器也设置好了,此时可以让虚拟机运行起来了。通常在一个循环中对vcpufd调用ioctl(KVM_RUN),代码片段如下:

    while (1)
    {
        ret = ioctl(vcpufd, KVM_RUN, NULL);
        if (ret == -1)
        {
            printf("exit unknown\n");
            return -1;
        }
        ……
    }

(11)内核在处理这个ioctl时,会把VCPU调度到物理CPU上运行。VCPU在运行过程中遇到一些敏感指令时会退出。如果内核态的KVM不能处理,就会交给应用层软件处理。此时ioctl系统调用返回,并且将一些信息保存到kvm_run结构中,这样用户态程序就能够知道导致虚拟机退出的原因了,然后根据具体原因进行相应的处理。代码片段如下:

        switch(run->exit_reason)
        {
            case KVM_EXIT_HLT:
                puts("KVM_EXIT_HLT");
                return 0;
            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;
        }

在本示例中,虚拟机内核向端口写数据会产生原因为KVM_EXIT_IO的退出,表示虚拟机内部读写了端口。在输出了数据之后,让虚拟机继续执行,执行到最后一个hlt指令时,会产生KVM_EXIT_HLT类型的退出,此时虚拟机运行结束。

当然,与极简的内核和精简的QEMU相比,实际的QEMU和实际的操作系统内核的复杂度要远远超过这个水平,但是其基本原理都是类似的。


http://www.niftyadmin.cn/n/4924367.html

相关文章

【CHI】(一)基础概念

基于CHI issueF 本章介绍了CHI体系结构和术语。它包含以下部分: 体系结构概述拓扑结构术语事务分类一致性概述组件命名读数据来源 一、CHI架构 CHI架构是一个可扩展的、支持一致性的集线器接口和由多个组件使用的片上互连。根据系统要求的PPA(perform…

十四、深度学习之卷积+池化+全连接各层

1、神经网络 人脑中有大量的脑神经元。每个脑神经元(图中黑点)都可以看做是一个小的记忆体负责不同的记忆,神经元之间通过树突(图中细线)连接起来。 假如人看到一只猫,一个神经元之前见过猫,那么就会把信息往后传,此时神经元处于激活状态;没有见过的啥也不做,…

C++执行程序计时函数详解

通常计时函数主要有两个,分别是getTickCount()和getTickFrequency(). getTickCount()函数,返回的是CPU自某个时间(如启动电脑)以来走过的时钟周期数;getTickFrequency()函数,返回的是CPU一秒钟所走的时钟周…

适配器模式来啦

网上的大多数的资料中适配器模式和代理模式都是紧挨着进行介绍的,为什么呢??? 是因为适配器模式和代理模式有太多的相似之处,可以进行联动记忆但是也要做好区分。 在菜鸟教程中,适配器模式的定义是作为两…

【构造】CF1798 D

Problem - D - Codeforces 题意&#xff1a; 思路&#xff1a; 首先如果 a 全是 00&#xff0c;那么显然无解。 否则考虑从左到右构造新数列&#xff0c;维护新数列的前缀和 s。 如果 s≥0&#xff0c;则在剩余未加入的数中随便选择一个非正数添加到新数列末尾。如果 s<…

Linux root用户执行修改密码命令,提示 Permission denied

问题 linux系统中&#xff08;ubuntu20&#xff09;&#xff0c;root用户下执行passwd命令&#xff0c;提示 passwd: Permission denied &#xff0c;如下图&#xff1a; 排查 1.执行 ll /usr/bin/passwd &#xff0c;查看文件权限是否正确&#xff0c;正常情况是 -rwsr-xr…

linux 内存 - KO内存占用

说明 KO(kernel module)占用的内存分为两部分&#xff1a; 静态占用 &#xff1a;ko insmod时系统固定分配的内存。动态申请 &#xff1a;代码中动态申请的内存&#xff0c;由于申请方式不同&#xff0c;统计的方式也可能不同&#xff0c;例如&#xff1a;使用vmalloc和kmall…

Netty:查看ByteBuf的实现类

io.netty.buffer.ByteBuf是一个抽象类&#xff0c;我们看看它最终的实现类。实现类有多个&#xff0c;具体用的是哪个实现类&#xff0c;跟分配ByteBuf的方式有关。 作为举例&#xff0c;分别用Unpooled和ByteBufAllocator.DEFAUL来分配一个ByteBuf。 package com.thb;import …