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 ioctl:vCPU 指令,针对指定的 vCPU 进行参数设置,例如寄存器读写、中断控制。需要在创建 vCPU 的线程中调用 vCPU 指令,以确保线程安全
3. KVM API 操作流程
通常情况下,对于 KVM API 的操作是从打开/dev/kvm设备文件开始的:
- 使用
open系统调用打开/dev/kvm设备文件后,会获得一个文件描述符fd,然后再通过ioctl发送相应的控制字进行之后的操作 - 调用
KVM_CREATE_VM指令将创建一个虚拟机并返回该虚拟机对应的fd。然后再根据返回的fd对该虚拟机进行控制 - 调用
KVM_CREATE_VCPU指令将创建一个 vCPU,并返回该 vCPU 对应的fd - 循环调用
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).----