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).----