KVM API 概述,基于 Kernel 2.6.32

目录

1. 字符设备 /dev/kvm

KVM 的 API 是通过/dev/kvm这个字符设备进行访问的:

> ls /dev/kvm -l
crw-rw---- 1 root root 10, 232 Jul 29 16:24 /dev/kvm

/dev/kvm作为 Linux 的一个标准字符型设备,可以使用常见的系统调用如open()close()ioctl()等指令进行操作。不过在 KVM 字符型设备的实现函数中,并没有包含write()read()等操作,因此所有对 KVM 的操作都是通过ioctl()发送相应的控制字实现的

内核源码中的Documentation/kvm/api.txt是 KVM API 的说明文档,可以点击 这里 查看

2. KVM API 功能分类

根据 API 所提供的功能,又可将其分为以下三类:

  • System ioctl系统指令,针对 KVM 的全局性参数设置,例如通过KVM_CREATE_VM创建虚拟机
  • VM ioctl虚拟机指令,针对指定的 VM 进行控制,例如创建 vCPU、设置虚拟机内存。需要在创建 VM 的进程中调用 VM 指令,以确保进程安全
  • vCPU ioctlvCPU 指令,针对指定的 vCPU 进行参数设置,例如寄存器读写、中断控制。需要在创建 vCPU 的线程中调用 vCPU 指令,以确保线程安全

3. KVM API 操作流程

通常情况下,对于 KVM API 的操作是从打开/dev/kvm设备文件开始的:

  1. 使用open系统调用打开/dev/kvm设备文件后,会获得一个文件描述符fd,然后再通过ioctl发送相应的控制字进行之后的操作
  2. 调用KVM_CREATE_VM指令将创建一个虚拟机并返回该虚拟机对应的fd。然后再根据返回的fd对该虚拟机进行控制
  3. 调用KVM_CREATE_VCPU指令将创建一个 vCPU,并返回该 vCPU 对应的fd
  4. 循环调用KVM_RUN,运行 vCPU,启动虚拟机

需要注意的是,通过fork()调用创建的子进程将继承父进程的文件描述符fd,从而实现多进程访问。而在 KVM API 的内部实现中,并没有针对这种情况进行保护。因此api.txt文档也有提示:VM 指令需要在创建该 VM 的进程中调用,vCPU 指令也需要在创建 vCPU 的线程中调用。

上述流程的伪代码示例如下所示:

open("/dev/kvm")
ioctl(KVM_CREATE_VM) // 创建 VM
ioctl(KVM_CREATE_VCPU) // 为 VM 创建 vCPU
for (;;) {
    ioctl(KVM_RUN) // 运行 vCPU,启动 VM
    switch (exit_reason) { // 捕捉 VM-EXIT 原因进行处理
        case KVM_EXIT_IO: /* ... */
        case KVM_EXIT_HLT: /* ... */
    }
}

4. KVM API 概览

4.1 System ioctl

System ioctls 用于控制 KVM 全局的运行环境及参数设置,例如创建虚拟机、检查扩展支持。

通过kvm_main.c中的kvm_dev_ioctl()进行处理,与架构相关的指令(例如 x86)则通过x86.c中的kvm_arch_dev_ioctl()进行处理。

主要指令字如下所示:

指令字 功能说明 返回值
KVM_GET_API_VERSION 查询当前 KVM API 版本 当前版本为 12
KVM_CREATE_VM 创建 KVM 虚拟机 返回创建的 KVM 虚拟机 fd
KVM_GET_MSR_INDEX_LIST 获得 MSR 索引列表 返回 kvm_msr_list 类型的链表 msr_list
KVM_CHECK_EXTENSION 检查扩展支持情况 返回 0 则不支持,非 0 则支持
KVM_GET_VCPU_MMAP_SIZE 返回ioctl(KVM_RUN)与用户空间共享内存区域大小 mmap 区域大小,单位 bytes

其中最重要的是KVM_CREATE_VM。通过该指令字,KVM 将返回一个文件描述符fd,指向内核空间中新创建的 KVM 虚拟机。

4.2 VM ioctl

VM ioctl 用于对虚拟机进行控制,例如内存、vCPU、中断、时钟。

通过kvm_main.c中的kvm_vm_ioctl()进行处理,与架构相关的指令(例如 x86)则通过x86.c中的kvm_arch_vm_ioctl()进行处理。

主要指令字如下所示:

指令字 功能说明 返回值
KVM_CREATE_VCPU 为已经创建好的 VM 添加 vCPU 成功则返回 vCPU fd,失败 -1
KVM_SET_MEMORY_REGION 添加或修改 VM 的内存 成功 0,失败 -1
KVM_SET_USER_MEMORY_REGION api.txt中推荐替代KVM_SET_MEMORY_REGION的新 API 成功 0,失败为负
KVM_GET_DIRTY_LOG 返回上次调用后给定 memory slot 的脏页位图 成功 0,失败 -1
KVM_SET_MEMORY_ALIAS 定义kvm_memory_alias 成功 0,失败 -1
KVM_CREATE_IRQCHIP 创建一个虚拟的 APIC,并且之后创建的 vCPU 都将连接到该 APIC 成功 0,失败 -1
KVM_IRQ_LINE 对给定的虚拟 APIC 触发中断信号 成功 0,失败 -1
KVM_GET_IRQCHIP 读取 APIC 的中断标志信息 成功 0,失败 -1
KVM_SET_IRQCHIP 设置 APIC 的中断标志信息 成功 0,失败 -1
KVM_GET_CLOCK 读取当前 VM kvmclock 中的 timestamp 成功 0,失败 -1
KVM_SET_CLOCK 设置当前 VM kvmclock 中的 timestamp 成功 0,失败 -1

VM ioctl 需要借助通过KVM_CREATE_VM返回的 VM fd 进行操作,例如KVM_CREATE_VCPU

static long kvm_vm_ioctl(struct file *filp,
               unsigned int ioctl, unsigned long arg)
{
    struct kvm *kvm = filp->private_data; // VM 对应的 kvm 结构体
    void __user *argp = (void __user *)arg; // ioctl 入参
    int r;

    if (kvm->mm != current->mm)
        return -EIO;

    switch (ioctl) {
    case KVM_CREATE_VCPU:
        r = kvm_vm_ioctl_create_vcpu(kvm, arg); // 需要借助 kvm 创建 vCPU
        if (r < 0)
            goto out;
        break;
    /* ... */
    }
out:
    return r;
}

4.3 vCPU ioctl

vCPU ioctl 用于对具体的 vCPU 进行配置,例如执行 vCPU 指令,设置寄存器、中断等。

通过kvm_main.c中的kvm_vcpu_ioctl()进行处理,与架构相关的指令(例如 x86)则通过x86.c中的kvm_arch_vcpu_ioctl()进行处理。

主要指令字如下所示:

指令字 功能说明 返回值
KVM_RUN 运行 vCPU 成功 0,失败 -1
KVM_GET_REGS 获取通用寄存器信息 成功 0,失败 -1
KVM_SET_REGS 设置通用寄存器信息 成功 0,失败 -1
KVM_GET_SREGS 获取特殊寄存器信息 成功 0,失败 -1
KVM_SET_SREGS 设置特殊寄存器信息 成功 0,失败 -1
KVM_TRANSLATE 将 GVA 翻译为 GPA 成功 0,失败 -1
KVM_INTERRUPT 通过插入一个中断向量,在 vCPU 上产生中断(当 APIC 无效时) 成功 0,失败 -1
KVM_DEBUG_GUEST 开启 Guest OS 的调试模式 成功 0,失败 -1
KVM_GET_MSRS 获取 MSR 寄存器信息 成功 0,失败 -1
KVM_SET_MSRS 设置 MSR 寄存器信息 成功 0,失败 -1
KVM_SET_CPUID 设置 vCPU 的 CPUID 信息 成功 0,失败 -1
KVM_SET_SIGNAL_MASK 设置 vCPU 的中断信号屏蔽 成功 0,失败 -1
KVM_GET_FPU 获取浮点寄存器信息 成功 0,失败 -1
KVM_SET_FPU 设置浮点寄存器信息 成功 0,失败 -1

其中最重要的指令是KVM_RUN。在通过KVM_CREATE_CPU为虚拟机创建 vCPU,并取得 vCPU 对应的 fd 后,就可以调用KVM_RUN启动虚拟机:

static long kvm_vcpu_ioctl(struct file *filp,
               unsigned int ioctl, unsigned long arg)
{
    struct kvm_vcpu *vcpu = filp->private_data;
    void __user *argp = (void __user *)arg;
    int r;

    /* ... */
    switch (ioctl) {
    case KVM_RUN: // 运行 vCPU
        r = -EINVAL;
        if (arg)
            goto out;
        r = kvm_arch_vcpu_ioctl_run(vcpu, vcpu->run); // 实际的执行函数
        break;
    /* ... */
    }
out:
    kfree(fpu);
    kfree(kvm_sregs);
    return r;
}

可以看到调用kvm_arch_vcpu_ioctl_run()时传递了两个参数:vcpu即为当前的 vCPU,而vcpu->run则指向一个kvm_run结构体(省略部分字段):

/* for KVM_RUN, returned by mmap(vcpu_fd, offset=0) */
struct kvm_run {
    /* in */
    __u8 request_interrupt_window;
    __u8 padding1[7];

    /* out */
    __u32 exit_reason;
    __u8 ready_for_interrupt_injection;
    __u8 if_flag;
    __u8 padding2[2];

    /* in (pre_kvm_run), out (post_kvm_run) */
    __u64 cr8;
    __u64 apic_base;

    union {
        /* KVM_EXIT_UNKNOWN */
        struct {
            __u64 hardware_exit_reason;
        } hw;
        /* KVM_EXIT_FAIL_ENTRY */
        struct {
            __u64 hardware_entry_failure_reason;
        } fail_entry;
        /* KVM_EXIT_EXCEPTION */
        struct {
            __u32 exception;
            __u32 error_code;
        } ex;
        /* KVM_EXIT_IO */
        struct {
#define KVM_EXIT_IO_IN  0
#define KVM_EXIT_IO_OUT 1
            __u8 direction;
            __u8 size; /* bytes */
            __u16 port;
            __u32 count;
            __u64 data_offset; /* relative to kvm_run start */
        } io;
        struct {
            struct kvm_debug_exit_arch arch;
        } debug;
        /* KVM_EXIT_MMIO */
        struct {
            __u64 phys_addr;
            __u8  data[8];
            __u32 len;
            __u8  is_write;
        } mmio;
        /* ... */
    };
};

kvm_run定义在include/linux/kvm.h中,通过读取该结构体可以了解 KVM 内部的运行状态,可以类比为计算机芯片中的寄存器组。

5. API 简单使用示例

#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <sys/ioctl.h>
#include <fcntl.h>
#include <unistd.h>
#include <linux/kvm.h>

#define KVM_FILE "/dev/kvm"

int main()
{
    int dev;
    int ret;
    dev = open(KVM_FILE, O_RDWR|O_NDELAY);

    ret = ioctl(dev, KVM_GET_API_VERSION, 0);    
    printf("----KVM API version is %d----\n", ret);

    ret = ioctl(dev, KVM_CHECK_EXTENSION, KVM_CAP_NR_VCPUS);
    printf("----KVM supports MAX_VCPUS per guest(VM) is %d----\n", ret);

    ret = ioctl(dev, KVM_CHECK_EXTENSION, KVM_CAP_NR_MEMSLOTS);
    printf("----KVM supports MEMORY_SLOTS per guset(VM) is %d----\n", ret);

    ret = ioctl(dev, KVM_CHECK_EXTENSION, KVM_CAP_IOMMU);
    if(ret != 0)
        printf("----KVM supports IOMMU (i.e. Intel VT-d or AMD IOMMU).----\n");
    else
        printf("----KVM doesn't support IOMMU (i.e. Intel VT-d or AMD IOMMU).----\n");

    return 0;
}

编译运行:

> vim kvm-api-test.c
> gcc kvm-api-test.c -o kvm-api-test
> ./kvm-api-test
----KVM API version is 12----
----KVM supports MAX_VCPUS per guest(VM) is 160----
----KVM supports MEMORY_SLOTS per guset(VM) is 32----
----KVM doesn't support IOMMU (i.e. Intel VT-d or AMD IOMMU).----

参考文章

  1. Documentation/kvm/api.txt | Kernel 2.6.32.35
  2. KVM 源代码分析 2:虚拟机的创建与运行 | OenHan
  3. KVM 源代码分析 3:CPU 虚拟化 | OenHan
  4. KVM API 使用实例 - 任永杰 | 笑遍世界