qemu kvm添加新feature虚拟化

  1. 学习
  2. 实验
  3. 后记
  4. 参考

不太严谨了,应该算是添加新cap的虚拟化

学习

先找一个当前已经存在的feature看一下这个过程,比如KVM_CAP_S390_IRQCHIP这个cap。内核commit为(“KVM: s390: irq routing for adapter interrupts.”)

libvirt侧:virsh start

##virsh start启动一个虚机
cmdStart
  --->virDomainCreateWithFlags	##去调用具体驱动的create domian,一般是qemu
    --->qemuDomainCreateWithFlags
      --->qemuProcessBeginJob
        --->qemuDomainObjStart
          --->qemuProcessStart
            --->qemuProcessLaunch
              --->qemuBuildCommandLine	##构建qemu命令
              --->virCommandRun
                --->virCommandRunAsync
                  --->virExec

qemu侧:开始创建虚机

main			##softmmu/main.,这主要就三条,qemu_init,qemu_main_loop,qemu_cleanup
  --->qemu_init			##这里会parse传入的参数,然后根据参数调用对应的函数
    --->configure_accelerators		##配置加速器,其实一般也就是kvm
      --->do_configure_accelerator
        --->accel_init_machine		##这里会调用acc->init_machine(ms);,kvm的话,也就是kvm_init
          --->kvm_init
            --->kvm_irqchip_create
              --->kvm_check_extension(s, KVM_CAP_S390_IRQCHIP)			##检测是否存在这个feture(实际上是一个发一个KVM_CHECK_EXTENSION的ioctl),也就是实际上是检查kvm是否支持这个feature
              --->kvm_vm_enable_cap(s, KVM_CAP_S390_IRQCHIP, 0);		##如果存在的话就enable

#########看一下kvm_vm_enable_cap函数
##其实是个宏,大致看上去就是初始化一个kvm_enable_cap结构体,其中cap是KVM_CAP_S390_IRQCHIP,flag是0
##然后初始化一个临时数组把后面的可变参数复制过来
##然后获取args_tmp和cap.args长度的最小值,应该是怕如果传入的可变参数太多导致cap.args溢出
##然后把可变参数的args_tmp数组内容copy到cap.args
##最后去调用kvm_vm_ioctl

#define kvm_vm_enable_cap(s, capability, cap_flags, ...)             \
    ({                                                               \
        struct kvm_enable_cap cap = {                                \
            .cap = capability,                                       \
            .flags = cap_flags,                                      \
        };                                                           \
        uint64_t args_tmp[] = { __VA_ARGS__ };                       \
        size_t n = MIN(ARRAY_SIZE(args_tmp), ARRAY_SIZE(cap.args));  \
        memcpy(cap.args, args_tmp, n * sizeof(cap.args[0]));         \
        kvm_vm_ioctl(s, KVM_ENABLE_CAP, &cap);                       \
    })


int kvm_vm_ioctl(KVMState *s, int type, ...)
{
    int ret;
    void *arg;
    ##这块其实是对可变参数的处理,这是一个可变参数的列表
    va_list ap;

    ##初始化ap,指向type后的第一个参数
    va_start(ap, type);
    ##从ap中提取一个void *类型的参数
    arg = va_arg(ap, void *);
    va_end(ap);

    trace_kvm_vm_ioctl(type, arg);
    accel_ioctl_begin();
    ret = ioctl(s->vmfd, type, arg);
    accel_ioctl_end();
    if (ret == -1) {
        ret = -errno;
    }
    return ret;
}

可以看到其实跟KVM_CAP_S390_IRQCHIP相关的就是初始化过程中通过KVM_CHECK_EXTENSION类型的ioctl去查kvm是否支持该feature,如果支持的话,就发KVM_ENABLE_CAP类型的ioctl去enable它

kernel侧:

##首先它在include/uapi/linux/kvm.h中定义了这个宏
#define KVM_CAP_S390_IRQCHIP 99

##然后看接收KVM_CHECK_EXTENSION ioctl的处理
kvm_vm_ioctl
  --->kvm_vm_ioctl_check_extension_generic
    --->kvm_vm_ioctl_check_extension		##这里直接返回了1,所以qemu的ioctl收到1就认为kvm支持这个feature

##在qemu的流程中,这里KVM_CHECK_EXTENSION的ioctl查到kvm支持这个feature了,就发KVM_ENABLE_CAP的ioctl去enable了
/* for KVM_ENABLE_CAP */
struct kvm_enable_cap {
	/* in */
	__u32 cap;
	__u32 flags;
	__u64 args[4];
	__u8  pad[64];
};

kvm_vm_ioctl
  --->copy_from_user		##从userspace把kvm_enable_cap结构体的内容copy过来
  --->kvm_vm_ioctl_enable_cap_generic
    --->kvm_vm_ioctl_enable_cap			##然后这里检查cap中的一些内容,然后打开进行enable的相关操作

另外,需要注意下,如果要对一些msr进行虚拟化或透传的话,如果是只有读操作还好,如果会往msr中进行写操作,就需要考虑很多事情了,比如:是否需要考虑虚机和宿主机侧修改msr数据不一致导致问题,是否需要补偿?vcpu从cpu间进行迁移的话需要怎么做?多个vcpu运行在一个cpu上需要怎么做?

实验

有一个MSR_PPIN msr,这个在虚机中肯定是没有虚拟化的,将它虚拟化试试看。添加一个新cap,协商cap并允许虚机访问这个msr

首先先看一下我的宿主机上这个msr的内容(左图)是可以读到的,然后看一下我的虚机中,是读不到的

这是一张图片

开搞!!!

  • 修改

qemu侧修改:

diff --git a/accel/kvm/kvm-all.c b/accel/kvm/kvm-all.c
index be2a7adba7..2a37beaac9 100644
--- a/accel/kvm/kvm-all.c
+++ b/accel/kvm/kvm-all.c
@@ -2661,6 +2661,10 @@ static int kvm_init(MachineState *ms)
         }
     }

+    if(kvm_vm_check_extension(s, KVM_CAP_PPIN)) {
+        kvm_vm_enable_cap(s, KVM_CAP_PPIN, 0);
+    }
+
 #ifdef KVM_CAP_VCPU_EVENTS
     s->vcpu_events = kvm_check_extension(s, KVM_CAP_VCPU_EVENTS);
 #endif
diff --git a/linux-headers/linux/kvm.h b/linux-headers/linux/kvm.h
index 20a3755fa5..28f70cf1f9 100644
--- a/linux-headers/linux/kvm.h
+++ b/linux-headers/linux/kvm.h
@@ -1114,6 +1114,7 @@ struct kvm_ppc_resize_hpt {
 #define KVM_CAP_ARM_MTE 205
 #define KVM_CAP_XSAVE2 208
 #define KVM_CAP_SYS_ATTRIBUTES 209
+#define KVM_CAP_PPIN 250

 #define KVM_CAP_APERFMPERF 500
 #define KVM_CAP_APERFMPERF2 501

这块其实主要就是在linux-headers/linux/kvm.h新定义一个KVM_CAP_PPIN cap号;然后通过kvm_vm_check_extension和kvm_vm_enable_cap查询和开启这个cap

kernel侧修改:

diff --git a/arch/x86/include/asm/kvm_host.h b/arch/x86/include/asm/kvm_host.h
index 97c9363700ef..b3355fa61149 100644
--- a/arch/x86/include/asm/kvm_host.h
+++ b/arch/x86/include/asm/kvm_host.h
@@ -958,6 +958,7 @@ struct kvm_vcpu_arch {
 #if IS_ENABLED(CONFIG_HYPERV)
        hpa_t hv_root_tdp;
 #endif
+       u64 ppin;
 };

 struct kvm_lpage_info {
diff --git a/arch/x86/kvm/x86.c b/arch/x86/kvm/x86.c
index 50841b390a2b..45db5ff78505 100644
--- a/arch/x86/kvm/x86.c
+++ b/arch/x86/kvm/x86.c
@@ -4149,6 +4149,9 @@ int kvm_get_msr_common(struct kvm_vcpu *vcpu, struct msr_data *msr_info)
                msr_info->data = vcpu->arch.guest_fpu.xfd_err;
                break;
+       case MSR_PPIN:
+               msr_info->data = vcpu->arch.ppin;
+               break;
 #endif
        default:
                if (kvm_pmu_is_valid_msr(vcpu, msr_info->index))
@@ -4327,6 +4330,7 @@ int kvm_vm_ioctl_check_extension(struct kvm *kvm, long ext)
        case KVM_CAP_VAPIC:
        case KVM_CAP_ENABLE_CAP:
        case KVM_CAP_VM_DISABLE_NX_HUGE_PAGES:
+       case KVM_CAP_PPIN:
                r = 1;
                break;
        case KVM_CAP_EXIT_HYPERCALL:
@@ -6366,6 +6370,9 @@ split_irqchip_unlock:
                }
                mutex_unlock(&kvm->lock);
                break;
+       case KVM_CAP_PPIN:
+               r = 0;
+               break;
        default:
                r = -EINVAL;
                break;
diff --git a/include/uapi/linux/kvm.h b/include/uapi/linux/kvm.h
index 85365bd8ec8d..2f5959fea170 100644
--- a/include/uapi/linux/kvm.h
+++ b/include/uapi/linux/kvm.h
@@ -1184,6 +1184,7 @@ struct kvm_ppc_resize_hpt {
 #define KVM_CAP_S390_ZPCI_OP 221
 #define KVM_CAP_S390_CPU_TOPOLOGY 222
 #define KVM_CAP_DIRTY_LOG_RING_ACQ_REL 223
+#define KVM_CAP_PPIN 250
 #define KVM_CAP_APERFMPERF 500
 #define KVM_CAP_APERFMPERF2 501

这块的主要修改有:新定义一个KVM_CAP_PPIN cap号,和qemu中相同;在kvm_vm_ioctl_check_extension和kvm_vm_ioctl_enable_cap中分别提供该cap的查询和enable;在kvm_vcpu_arch定义一个新的ppin变量,主要用于虚机访问msr时 导致vm exit退出时返回的msr的值;kvm_get_msr_common中处理MSR_PPIN,给msr_info->data赋值,也就是刚才的ppin

这里说明一下,这个并不是一个完整的例子,只是简单测试走一下流程。实际上一般来说,在enable这个cap的时候还需要做一些操作,比如设置一些标志之类,然后在create vcpu的时候根据标志进行处理;然后并没有给ppin进行赋值,实际上应该在vcpu_enter_guest中在vm enter和vm exit的时候对ppin进行赋值,这样在虚机中访问的时候得到的就是和宿主机上相同的值了。在在我上面的例子中,虚机中输出的应该是0,因为并没有给ppin赋值。

  • 结果

此时虚机侧已经可以看到

这是一张图片

后记

  • 读msr的方法

    • msr-tools

然后rdmsr 0x0000004f这样就可以读,比如

这是一张图片

ps:但是我的虚机没有网,没法安装msr-tools

    • 内核模块

可以写内核模块,使用内核rdmsr系列函数

ps:但是我的宿主机和虚机内核不是同一个内核,还得重编内核或者安装devel包,也有点麻烦

    • 自己编写c程序

strace msr-tools的rdmsr过程可以看到,其实这个是通过读/dev/cpu/X/msr来实现的

那同样我们也可以读这个设备文件来实现,类似下面这样

#include <stdio.h>
#include <unistd.h>
#include <stdint.h>
#include <fcntl.h>

#define MSR_PPIN    0x0000004f
int main()
{
    uint64_t val;
    int fd = open("/dev/cpu/0/msr", O_RDONLY);
    if (fd < 0) {
        printf("open msr failed\n");
        return -1;
    }

    if(pread(fd, &val, sizeof(val), MSR_PPIN) != sizeof(val)) {
        printf("read msr failed\n");
        close(fd);
        return -1;
    }

    printf("MSR_PPIN val is %lx\n", val);
    return 0;
}

参考

https://www.cnblogs.com/hanyonglu/archive/2011/05/07/2039916.html


转载请注明来源,欢迎对文章中的引用来源进行考证,欢迎指出任何有错误或不够清晰的表达。可以在下面评论区评论,也可以邮件至 857879363@qq.com