CPU计算虚拟化:KVM与QEMU

Table of Contents

本文主要分析下KVM/QEMU对CPU的虚拟化。KVM/QEMU还完成了许多的功能,比如内存虚拟化,中断虚拟化等,但这些功能不是本文的主题。

本文分五个方面来分析CPU的虚拟化,依次展开。

1. 虚拟机的创建

QEMU在初始化阶段,通过如下调用链到kvm_init函数:

main->qemu_init->configure_accelerators->qemu_opts_foreach->do_configure_accelerator->accel_init_machine->kvm_init

在kvm_init函数中有如下代码:

KVMState *s;
s = KVM_STATE(ms->accelerator);
s->fd = qemu_open_old("/dev/kvm", O_RDWR);

可以看到,这里打开了/dev/kvm文件,并且将返回的文件描述符保存在了KVMState的fd成员里。将来通过这个fd可以调用ioctl,内核的kvm模块其功能就是通过/dev/kvm设备文件导出的。关于这点后面再详细分析。现在还需要继续详细分析这几行QEMU代码。

首先是类型为KVMState的变量s的来源。代码中出现了KVM_STATE宏,QEMU代码中到处充斥中这种大写的宏,其定义都是类似的方式,针对KVM_STATE来说如下代码定义了它:

DECLARE_INSTANCE_CHECKER(KVMState, KVM_STATE,
                         TYPE_KVM_ACCEL)

而DECLARE_INSTANCE_CHECKER定义在include/qom/object.h中。

简单介绍下qom,qom是QEMU Object Model的简写,是一种用于构建和管理QEMU设备的对象模型。QOM允许以一种层次化和组合性的方式构建虚拟设备,使得设备的模拟和管理更加灵活和可扩展。

QOM为设备提供了一种统一的方式来定义和组织属性、方法和信号。每个设备都是一个QOM实例,具有一组属性(例如,CPU的时钟频率、内存大小等)、方法(例如,设备初始化、读取寄存器等)以及可能的信号(事件通知)。同时也提供了更好的代码复用性。

在QEMU源代码中,有许多与QOM相关的代码,包括设备的定义、实例化、属性、方法和信号的设置等。

说回DECLARE_INSTANCE_CHECKER,其定义为:

#define DECLARE_INSTANCE_CHECKER(InstanceType, OBJ_NAME, TYPENAME)      \
        static inline G_GNUC_UNUSED InstanceType *                      \
        OBJ_NAME(const void *obj)                                       \
        { return OBJECT_CHECK(InstanceType, obj, TYPENAME); }

可以看到,OBJ_NAME作为传进来的参数,被定义为只有一行return的函数,当然这个函数很短,肯定是要内联的,但这不影响在分析代码时,就把它当作函数看待。继续看OBJECT_CHECK:

#define OBJECT_CHECK(type, obj, name)                                   \
        ((type *)object_dynamic_cast_assert(OBJECT(obj), (name),        \
                                            __FILE__, __LINE__, __func__))

看到这里就明白了,其实就是个强转,将传进来的void *指针转换为指向KVMState类型的指针。ms->accelerator的类型是AccelState,那么想必KVMState的第一个成员就是AccelState类型的成员了,这样它们才能强转。相当于(ms)拥有一个AccelState类型的指针时,而该AccelState来自KVMState,那么可以直接强转为KVMState类型,就可以引用KVMState里除开AccelState外的其它成员了。这种强转其实就是个继承关系了,在各种软件项目里屡见不鲜,不论它们以何种方式(外衣)出现。

那么现在就是分析ms->accelerator的出处了。在accel_init_machine里有:

int accel_init_machine(AccelState *accel, MachineState *ms)
{
        ms->accelerator = accel;
}

还需要再往上看父函数,在do_configure_accelerator中:

static int do_configure_accelerator(void *opaque, QemuOpts *opts, Error **errp)
{
        const char *acc = qemu_opt_get(opts, "accel");
        AccelClass *ac = accel_find(acc);
        AccelState *accel;
        accel = ACCEL(object_new_with_class(OBJECT_CLASS(ac)));
        ret = accel_init_machine(accel, current_machine);
}

看到这里就明晰了,qemu_opt_get就是根据传入的参数去找accelerator的字符串名字,比如在启动qemu时传入了-enable-kvm,那么这里的acc就是指向串“kvm”了。而accel_find往下一路调用,最后会来到glib提供的g_hash_table_lookup函数,也就是说,包括accelerator在内的很多对象,都会事先存在hash表里,然后后面要用时通过“kvm”这样的key去找,当然真正的key串可能是组合了一些其它的字符,比如kvm-accel,见accel_find->ACCEL_CLASS_NAME。

顺便提下,current_machine也是先在初始化阶段的qemu_create_machine里创建出来:

current_machine = MACHINE(object_new_with_class(OBJECT_CLASS(machine_class)));

这里以kvm accelerator这种type类型的对象注册为例,来分析下一个对象的注册到hash表的流程,有此一例,其它对象的注册类似。在accel/kvm/kvm-all.c中,有如下代码:

static const TypeInfo kvm_accel_type = {
        .name = TYPE_KVM_ACCEL,
        .parent = TYPE_ACCEL,
        .instance_init = kvm_accel_instance_init,
        .class_init = kvm_accel_class_init,
        .instance_size = sizeof(KVMState),
};
static void kvm_type_init(void)
{
        type_register_static(&kvm_accel_type);
}

从type_register_static一路往下的话,就会看到g_hash_table_insert,这个就和前面通过 g_hash_table_lookup去寻找加速器对应起来了,只有这里insert,后面才能lookup。现在还有唯一一 个问题,那就是kvm_type_init这个函数何时执行,qemu项目的代码里并没有找到直接调用这个函数的 地方。秘密就在这行代码:

type_init(kvm_type_init);

type_init定义如下:

#define type_init(function) module_init(function, MODULE_INIT_QOM)
#define module_init(function, type)                                           \
static void __attribute__((constructor)) do_qemu_init_ ## function(void)    \
{                                                                             \
    register_module_init(function, type);                               \ 
}

可以看到这里用了constructor修饰,被这个gcc属性修饰的函数,会先于main函数的执行,而register_module_init关键的就下面几行:

ModuleEntry *e;
e->init = fn;
QTAILQ_INSERT_TAIL(l, e, node);

可以看到也就是把传进来的kvm_type_init给放到了e->init里,可以理解为先于main执行的这段代码,主要是个注册作用,而真正调用这个动作还是在main里做的:

main->qemu_init->qemu_init_subsystems->module_call_init->kvm_type_init

上述流程当然是先于configure_accelerator里通过accel_find去寻找加速器的,也就是main调用的qemu_init函数里,是先调用qemu_init_subsystems而后调用configure_accelerator的。

继续看给accel真正分配空间就是在object_new_with_with_class里了:

object_new_with_class->object_new_with_type->g_malloc

这里又出现了大写的ACCEL宏,可以想见其作用无非就是强转指针类型了:

#define ACCEL(obj)                                      \
        OBJECT_CHECK(AccelState, (obj), TYPE_ACCEL)

到现在就有了一个KVMState,现在回过头来给出KVMState的一些作用,可以理解为内核实现了KVM模块,那么它在QEMU这样的用户程序里有一个代表就是KVMState,其中的fd成员就保存了打开/dev/kvm时返回的fd。每次运行一个QEMU程序,就会申请一个这个结构体。

回到kvm_init函数,再往下有如下重要的代码:

do{
        ret = kvm_ioctl(s, KVM_CREATE_VM, type);
 } while (ret == -EINTR);
s->vmfd = ret;
int kvm_ioctl(KVMState *s, int type, ...)
{
        ret = ioctl(s->fd, type, arg);
        return ret;
}

可以看到这里就使用了前面打开/dev/kvm的fd来调用ioctl,这个ioctl支持KVM_CREATE_VM这样的命令,在内核里的实现是kvm_dev_ioctl。并且将返回的ret文件描述符保存到了KVMState的vmfd成员,将来又可以在这个fd上调用它的ioctl,比如创建vcpu,在内核里对应这个vmfd的ioctl函数实现是kvm_vm_ioctl。关于这点后面还会分析。

可以总结下,通过调用qemu_open_old函数,由/dev/kvm文件得到的fd,表示了一个KVM模块功能的合集,而通过KVM_CREATE_VM命令在/dev/kvm上的fd调用ioctl得到的vmfd,表示了一台虚拟机。

在分析内核侧前,想特别的提下,QEMU作为一个用户程序,不论其代码怎么写,其起始入口函数都是softmmu/main.c:main,从这里入口会运行很多复杂的流程。而分析代码从这个函数起始,也算是扭住了千头万绪的线头。一些流程上先后顺序的确定也会很清晰。

现在可以看下内核KVM侧关于虚拟机创建的实现了。首先是初始化阶段/dev/kvm设备文件的注册,只有注册好了这个文件,用户程序才能open它,并在这上面使用ioctl函数。

内核的虚拟化实现是作为一个模块而存在,amd的svm,和intel的vmx实现不一样。但是内核抽出了公共部分在virt/kvm下。具体来说,针对/dev/kvm的初始化注册,由kvm_init函数里调用misc_register来实现。而对kvm_init的调用,对于intel来说就是arch/x86/kvm/vmx/vmx.c:

module_init(vmx_init);
static int __init vmx_init(void)
{
        r = kvm_init(&vmx_x86_ops, sizeof(struct vcpu_vmx),
                     __alignof__(struct vcpu_vmx), THIS_MODULE);
}
int kvm_init(void *opaque, unsigned vcpu_size, unsigned vcpu_align,
             struct module *module)
{
        r = misc_register(&kvm_dev);
}

这里的kvm_dev定义为:

static struct file_operations kvm_chardev_ops = {
        .unlocked_ioctl = kvm_dev_ioctl,
        .llseek         = noop_llseek,
        KVM_COMPAT(kvm_dev_ioctl),
};

static struct miscdevice kvm_dev = {
        KVM_MINOR,
        "kvm",
        &kvm_chardev_ops,
};

可以看到,/dev/kvm文件关联的fop就是kvm_chardev_ops,对/dev/kvm使用ioctl时,就会来到kvm_dev_ioctl函数。现在主要关心kvm_ioctl以KVM_CREATE_VM调用ioctl时的内核代码,在这个命令下,内核会进入kvm_dev_ioctl_create_vm函数。该函数比较关键的流程如下:

int r;
struct kvm *kvm;
kvm = kvm_create_vm(type);
r = get_unused_fd_flags(O_CLOEXEC);
file = anon_inode_getfile("kvm-vm", &kvm_vm_fops, kvm, O_RDWR);
fd_install(r, file);
return r;

先是创建一个kvm结构体,一台vm都有一个kvm结构体,而kvm结构体里有内核对VCPU的表示比如kvm_cpu结构体,所有VCPU都在vcpus数组里,也有mm_struct结构体,vm所用的虚拟内存就是用户进程的虚拟地址空间,当然还有其它很多信息。

然后通过get_unused_fd_flags得到一个文件描述符r,最后返回的是这个文件描述符。随后调用anon_inode_getfile分配一个匿名的inode,file->private_data就是kvm,将来通过这个file(或fd)都可以找到kvm结构体。当然,fd和file要关联到当前进程的打开文件描述符表current->files里,后面通过fdget函数得到fd关联的file。

前面已经总结强调过了,get_unused_fd_flags返回的vmfd就是代表了一台虚拟机,Linux一切皆文件,针对这个文件(虚拟机),其操作的函数集就是kvm_vm_fops,这个fops里最关键的就是kvm_vm_ioctl函数了。比如针对一台虚拟机可以创建vcpu,这些事情都是后面小节的主题了。

总之到目前,总算是有了一台虚拟机,对比真实硬件机器来说,可以理解为主板以及上面的插槽这些都做好了,就等后面插上CPU(创建VCPU),插上需要的外设等。

2. QEMU CPU创建

QEMU里虚拟CPU的创建主要是通过kvm_start_vcpu_thread,新起一个线程去创建的,并且这个线程就代表了VCPU去运行。以下是kvm_start_vcpu_thread被如下调用链调用:

main->qemu_init->qmp_x_exit_preconfig->qemu_init_board->machine_run_board_init->pc_init1->x86_cpus_init->x86_cpu_new->qdev_realize->object_property_set_bool->object_property_set_qobject->object_property_set->
property_set_bool->device_set_realized->x86_cpu_realizefn->qemu_init_vcpu->kvm_start_vcpu_thread

以上其实就是主板的初始化流程,就这个调用链而言,最后的函数才关心的是CPU具现化。以下分析这个调用链本身是怎么被调用起来的,分析这点更多的是涉及到QOM,也就是QEMU代码的组织,而关于在QEMU代码里VCPU创建本身在本节偏后部分会介绍到。

首先看machine_run_board_init是如何调用起pc_init1的,在machine_run_board_init函数里:

void machine_run_board_init(MachineState *machine, const char *mem_path, Error **errp)
{
        MachineClass *machine_class = MACHINE_GET_CLASS(machine);
        machine_class->init(machine);
}

这里init函数在我的硬件平台就是pc_init1。这个回调是怎么设置的呢?

QEMU里x86平台的虚拟主板分为两类,一是i440fx,二是q35。i440fx是一个用于模拟Intel 440fx芯片组的虚拟平台。Intel i440fx芯片组是20世纪90年代早期的一个常见的PC主板芯片组,用于支持Intel的Pentium和Pentium Pro处理器。在虚拟化环境中,i440fx主板模拟了这个旧型号的主板,以便运行旧的操作系统或应用程序,或者为测试和开发目的。而q35模拟了更现代的主板和硬件特性,以便更好地支持现代操作系统和应用程序。

不论是i440fx还是q35,都是通过DEFINE_PC_MACHINE宏来注册一个具体的pc machine实例。而DEFINE_PC_MACHINE里就会设置好init函数,init本身是pc_init_##suffix,但这个函数里面会调用pc_init1。对于i440fx来说,就是DEFINE_I440FX_MACHINE里调用DEFINE_PC_MACHINE。

继续往后看object_property_set函数,在object_property_set里有个set回调,它主要是看一个对象(这里是CPU)是否具有realized属性, 若有的话, 那么object_property_set里怎么就能按如下方式调用set函数呢?

bool object_property_set(Object *obj, const char *name, Visitor *v,
                         Error **errp)
{
        ObjectProperty *prop = object_property_find_err(obj, name, errp);
        prop->set(obj, v, name, prop->opaque, errp);
}

这里set函数其实就是property_set_bool。 先列一个调用链:

main->qemu_init->qemu_create_machine->select_machine->object_class_get_list->object_class_foreach->g_hash_table_foreach->object_class_foreach_tramp->type_initialize(递归三次)->device_class_init->object_class_property_add_bool

这里先分析下这个调用链是在干什么,再分析最后的object_class_property_add_bool。简要的说,这个调用链是要对加入type_table哈希表中的所有类型进行初始化,具体的说,包括设置( 不是调用 )具现化的回调函数,比如device_set_realized,后面真正具现化的时候再调用这样的回调函数。

到这里可以总结下QEMU代码的流程了,主要是以下几个阶段:

  1. 注册。
  2. 初始化。
  3. 具现化。
  4. 运行时。

记住这几个阶段,以后在处理QEMU+KVM的具体问题或者继续深挖代码时,能时刻清晰自己处于哪个阶段,而不只是具体问题的“指哪打哪”很局部,还多了一点全局观念或角度。

上面object_class_property_add_bool的调用链包括后面分析的object_class_property_add_bool都是归结于初始化阶段。而本节一开始分析CPU的创建过程其实是具体设备的具现化阶段了。下面再介绍一下注册阶段。以设备类的TypeInfo,device_type_info量为例来分析这个注册过程。其实关于这个type init的过程上节对kvm_accel_type注册的分析已经涉及过了,已经不是新鲜玩意了,首先有:

type_init(qdev_register_types)

这个type_init怎么运行起来上节已经介绍了,这里只是从qdev_register_types一路往下,看看type_table_add函数:

static void type_table_add(TypeImpl *ti)
{
        g_hash_table_insert(type_table_get(), (void *)ti->name, ti);
}
static GHashTable *type_table_get(void)
{
        static GHashTable *type_table;
        if (type_table == NULL) {
                type_table = g_hash_table_new(g_str_hash, g_str_equal);
        }
        return type_table;
}

这里可以看到这个类型表的落脚点,名字就叫:type_table,是个static类型的,初始化一次为NULL,第一次运行创建这样一个表,在函数退出时依旧有效。并且可以将各种类型加入到这个表中。这样注册完成后,就可以在初始化阶段时,调用各个TypeInfo的class_init函数了,就是在type_initialize(可能递归多次)里调用class_init函数。

以上其实又涉及到了QOM的概念, 在上节已经首次提出了QOM的一方面。这里针对本节的角度再次总结QOM的另一方面。 QOM:Qemu Object Model。所谓model,就是有一定的套路或范式,不论来多少类型、设备,都按这个注册、初始化最后具现的流程来编码,具体点就是到处设置回调函数。

后面还会遇到QOM的体现,本文会更多的以具体代码里去阐述QOM的概念,不然只是上节的QOM概念会空洞。

现在开始分析object_class_property_add_bool,以解答前面的问题“object_property_set里怎么就能按如下方式调用set函数呢”。在这个函数中有:

ObjectProperty *
object_class_property_add_bool(ObjectClass *klass, const char *name,
                               bool (*get)(Object *, Error **),
                               void (*set)(Object *, bool, Error **))
{
        BoolProperty *prop = g_malloc0(sizeof(*prop));
        prop->get = get;
        prop->set = set;
        return object_class_property_add(klass, name, "bool",
                                         get ? property_get_bool : NULL,
                                         set ? property_set_bool : NULL,
                                         NULL,
                                         prop);
}

上面的set函数其实就是device_set_realized,这在object_class_property_add_bool的父函数device_class_init里可以看到,这里要注意,object_class_property_add_bool自己也构造了一个BoolProperty类型的prop属性,这个BoolProperty的set函数是device_set_realized,并将它作为最后一个参数传递给了object_class_property_add,这个函数在下面马上会分析,它也构造了一个prop,不过是ObjectProperty类型的。

ObjectProperty *
object_class_property_add(ObjectClass *klass,
                          const char *name,
                          const char *type,
                          ObjectPropertyAccessor *get,
                          ObjectPropertyAccessor *set,
                          ObjectPropertyRelease *release,
                          void *opaque)
{
        ObjectProperty *prop;
        prop = g_malloc0(sizeof(*prop));
        prop->set = set;
        prop->opaque = opaque;
        g_hash_table_insert(klass->properties, prop->name, prop);
        return prop;
}

可以看到这里调用了g_hash_table_insert函数将一个prop加入到了properties哈希表中,其key就是传进来的name字符串,为realized。这样在前面的object_property_set函数中就可以通过object_property_find_err函数,以name参数为realized找到其对应的prop,进而调用这个prop对应的set函数:property_set_bool。同时还有很重要的一点,opaque被保存在了prop->opaue成员里,这样在早先分析过的object_property_set函数里,在先找到了ObjectProperty类型的prop后,才能从这个prop里取出opaque,而opaque又作为一个BoolProperty类型的参数传递给object_property_set里调用的set函数(也就是property_set_bool),这样在这个set函数里又才调用BoolProperty的set回调为device_set_realized,进而去执行具现化的流程。

分析完device_set_realized,往下打算分析下x86_cpu_realizefn函数的调用。device_set_realized里对x86_cpu_realizefn调用是这样的:

static void device_set_realized(Object *obj, bool value, Error **errp)
{
        DeviceState *dev = DEVICE(obj);
        DeviceClass *dc = DEVICE_GET_CLASS(dev);
        if (dc->realize) {
                dc->realize(dev, &local_err);
        }
}

这里出现了大写的DEVICE,这点在前面提到过,也是QOM的一个方面。但是具体的DEVICE的实现,前面没有提到,这里简要分析下。

DEVICE的最开始定义在include/hw/qdev-core.h里有:

OBJECT_DECLARE_TYPE(DeviceState, DeviceClass, DEVICE)
#define OBJECT_DECLARE_TYPE(InstanceType, ClassType, MODULE_OBJ_NAME) \
        typedef struct InstanceType InstanceType; \
        typedef struct ClassType ClassType; \
        \
        G_DEFINE_AUTOPTR_CLEANUP_FUNC(InstanceType, object_unref) \
        \
        DECLARE_OBJ_CHECKERS(InstanceType, ClassType, \
                             MODULE_OBJ_NAME, TYPE_##MODULE_OBJ_NAME)
#define DECLARE_OBJ_CHECKERS(InstanceType, ClassType, OBJ_NAME, TYPENAME) \
        DECLARE_INSTANCE_CHECKER(InstanceType, OBJ_NAME, TYPENAME)      \
        \
        DECLARE_CLASS_CHECKERS(ClassType, OBJ_NAME, TYPENAME)

可以看到上节介绍过的DECLARE_INSTANCE_CHECKER,里面会有DEVICE的定义,就不再进一步贴代码了。这里主要关心下DECLARE_CLASS_CHECKERS,这个前面没有介绍过。

define DECLARE_CLASS_CHECKERS(ClassType, OBJ_NAME, TYPENAME) \
     static inline G_GNUC_UNUSED ClassType * \
     OBJ_NAME##_GET_CLASS(const void *obj) \
{ return OBJECT_GET_CLASS(ClassType, obj, TYPENAME); } \
\
static inline G_GNUC_UNUSED ClassType * \
OBJ_NAME##_CLASS(const void *klass) \
{ return OBJECT_CLASS_CHECK(ClassType, klass, TYPENAME); }

可以看到,OBJ_NAME被替换为OBJECT_DECLARE_TYPE宏的第三个参数为DEVICE,这样就有了device_set_realized函数里可以用DEVICE_GET_CLASS了。再往下跟OBJECT_GET_CLASS->object_get_class的话会知道,最后实际获取的就是obj->class,这里就不再贴代码了。

回到device_set_realized函数,里面最主要的就是通过dc调用了realize函数(就是x86_cpu_realizefn)了。这又是一个回调,那么这个回调在哪里设置的呢,分析这个问题,又要引出QOM的另一方面了: object的继承 。下面分析这个问题。

先看几个结构体:

static const TypeInfo x86_cpu_type_info = {
        name = TYPE_X86_CPU,
        .parent = TYPE_CPU,
        .class_init = x86_cpu_common_class_init,
};
static const TypeInfo cpu_type_info = {
        .name = TYPE_CPU,
        .parent = TYPE_DEVICE,
        .class_init = cpu_class_init,
};
static const TypeInfo device_type_info = {
        .name = TYPE_DEVICE,
        .parent = TYPE_OBJECT,
        .class_init = device_class_init,
};
static const TypeInfo object_info = {
        .name = TYPE_OBJECT,
        .class_init = object_class_init,
};

观察以上TypeInfo定义,可以很清楚的看到,它们构成了父子继承关系,这就是QOM对各种虚拟计算机对象的一种抽象,在类型的初始化阶段时,会递归调用type_initialize函数,就是如果一个TypeInfo如果有parent,会先对parent这个TypeImpl调用type_initialize,然后到递归的最底层时,会调用class_init(如果有的话):

static void type_initialize(TypeImpl *ti)
{
        parent = type_get_parent(ti);
        if (parent) {
                type_initialize(parent);
        }
        if (ti->class_init) {
                ti->class_init(ti->class, ti->class_data);
        }
}

而对于x86_cpu_type_info的class_init来说(x86_cpu_common_class_init),就会调用device_class_set_parent_realize来设置realize回调函数:

static void x86_cpu_common_class_init(ObjectClass *oc, void *data)
{
        X86CPUClass *xcc = X86_CPU_CLASS(oc);
        DeviceClass *dc = DEVICE_CLASS(oc);
        device_class_set_parent_realize(dc, x86_cpu_realizefn,
                                        &xcc->parent_realize);
}
void device_class_set_parent_realize(DeviceClass *dc,
                                     DeviceRealize dev_realize,
                                     DeviceRealize *parent_realize)
{
        *parent_realize = dc->realize;
        dc->realize = dev_realize;
}

这样设置好以后,在device_set_realized中就可以以dc->realize这样的方式调用了。至于上面具有继承关系的各个TypeInfo,它们是如何注册的,这点不再赘述,前面已经有多个例子分析到。

再往后看调用链,来到qemu_init_vcpu,里面有:

void qemu_init_vcpu(CPUState *cpu)
{
        cpus_accel->create_vcpu_thread(cpu);
}

create_vcpu_thread(就是kvm_start_vcpu_thread)是一开始kvm_start_vcpu_thread调用链里的最后一个回调了,对于它笔者不打算详细分析了,因为到这里对于QOM的套路已经驾轻就熟了,这里只是简单列下代码并简单解释下,以验证或加深理解。以后的QEMU+KVM关于QOM的主题分析也不会这么详细了,在那些文档里,涉及到QOM的分析,都会请移步至此。

关于如何调用起create_vcpu_thread,如下一些代码所示:

main->qemu_init->qmp_x_exit_preconfig->qemu_init_board->machine_run_board_init->accel_init_interfaces->accel_init_ops_interfaces->cpus_register_accel

static const AccelOpsClass *cpus_accel;

void cpus_register_accel(const AccelOpsClass *ops)
{
        cpus_accel = ops;
}
void accel_init_ops_interfaces(AccelClass *ac)
{
        const char *ac_name;
        char *ops_name;
        ObjectClass *oc;
        AccelOpsClass *ops;
        ac_name = object_class_get_name(OBJECT_CLASS(ac));
        ops_name = g_strdup_printf("%s" ACCEL_OPS_SUFFIX, ac_name);
        oc = module_object_class_by_name(ops_name);
        ops = ACCEL_OPS_CLASS(oc);
        cpus_register_accel(ops);
}
void machine_run_board_init(MachineState *machine, const char *mem_path, Error **errp)
{
        accel_init_interfaces(ACCEL_GET_CLASS(machine->accelerator));
}

可以看到,在machine有了accelerator之后,就可以从里面取出ops交给全局静态变量cpus_accel了。

但这些还不涉及到把create_vcpu_thread设置为kvm_start_vcpu_thread。可以想见这是在object_class_foreach_tramp->type_initialize里调用class_init完成的:

DECLARE_CLASS_CHECKERS(AccelOpsClass, ACCEL_OPS, TYPE_ACCEL_OPS)
static void kvm_accel_ops_class_init(ObjectClass *oc, void *data)
{
        AccelOpsClass *ops = ACCEL_OPS_CLASS(oc);
        ops->create_vcpu_thread = kvm_start_vcpu_thread;
}
static const TypeInfo kvm_accel_ops_type = {
        .name = ACCEL_OPS_NAME("kvm"),
        .parent = TYPE_ACCEL_OPS,
        .class_init = kvm_accel_ops_class_init,
        .abstract = true,
};
static void kvm_accel_ops_register_types(void)
{
        type_register_static(&kvm_accel_ops_type);
}
type_init(kvm_accel_ops_register_types);

看到type_init一切就明晰了。

到目前为止,本文花了很多篇幅去阐述QOM,还较少涉及虚拟化本身,笔者认为这是必要的,从某种角度来说,QOM就代表了QEMU代码的组织,并且弄清楚这种组织或调用关系有可能比虚拟化本身更费劲。但对QOM的清晰,会有助于对QEMU代码的全局把握。当然虚拟化本身后面肯定还会深入的。

以上着重对QOM进行了介绍, 从现在开始,会更多的偏向于虚拟化本身了 ,在本节也就是QEMU里对于CPU的创建到底做了哪些事情。

在本节一开始,列出了创建VCPU的函数流程,只是到目前都只关注了这个调用流程是怎么调用起来的(在哪里设置回调,在哪里调用回调).下面再贴下这个函数流程,后续就是主要关注这些流程上函数的逻辑了:

main->qemu_init->qmp_x_exit_preconfig->qemu_init_board->machine_run_board_init->pc_init1->x86_cpus_init->x86_cpu_new->qdev_realize->object_property_set_bool->object_property_set_qobject->object_property_set->
property_set_bool->device_set_realized->x86_cpu_realizefn->qemu_init_vcpu->kvm_start_vcpu_thread

从machine_run_board_init调用pc_init1说起。QEMU中,主板模型有两种,一是i440fx,另外一个是q35,本文主要基于前者研究。不论是i440fx还是q35,都要通过DEFINE_PC_MACHINE去定义MachineClass的init函数:

#define DEFINE_I440FX_MACHINE(suffix, name, compatfn, optionfn) \
    static void pc_init_##suffix(MachineState *machine) \
    { \
        void (*compat)(MachineState *m) = (compatfn); \
        if (compat) { \
            compat(machine); \
        } \
        pc_init1(machine, TYPE_I440FX_PCI_HOST_BRIDGE, \
                 TYPE_I440FX_PCI_DEVICE); \
    } \
    DEFINE_PC_MACHINE(suffix, name, pc_init_##suffix, optionfn)
#define DEFINE_PC_MACHINE(suffix, namestr, initfn, optsfn) \
    static void pc_machine_##suffix##_class_init(ObjectClass *oc, void *data) \
    { \
        MachineClass *mc = MACHINE_CLASS(oc); \
        optsfn(mc); \
        mc->init = initfn; \
        mc->kvm_type = pc_machine_kvm_type; \
    } \
    static const TypeInfo pc_machine_type_##suffix = { \
        .name       = namestr TYPE_MACHINE_SUFFIX, \
        .parent     = TYPE_PC_MACHINE, \
        .class_init = pc_machine_##suffix##_class_init, \
    }; \
    static void pc_machine_init_##suffix(void) \
    { \
        type_register(&pc_machine_type_##suffix); \
    } \
    type_init(pc_machine_init_##suffix)

对于q35的话就是宏DEFINE_Q35_MACHINE。这里suffix其实就是i440fx主板的版本,比如v8_2、v8_1等。可以看到initfn其实主要就是调用了pc_init1,而initfn又是作为MachineClass的init回调,所以在machine_run_board_init的最后可以这样调用:

machine_class->init(machine);

下面继续分析pc_init1调用的x86_cpus_init函数,该函数的最后会调用x86_cpu_new:

possible_cpus = mc->possible_cpu_arch_ids(ms);
for (i = 0; i < ms->smp.cpus; i++) {
    x86_cpu_new(x86ms, possible_cpus->cpus[i].arch_id, &error_fatal);
}

可以看到这里先调用了possible_cpu_arch_ids函数去返回一个CPUArchIDList类型的指针,该类型里主要是存放了当前所有可用CPU的arch_id以及其它一些信息。possible_cpu_arch_ids函数对于x86架构就是x86_possible_cpu_arch_ids函数,这是在x86_machine_class_init函数中设置的回调。在x86_possible_cpu_arch_ids中可以看到,返回的CPUArchIdList实际就来自ms->possible_cpus成员,第一次调用x86_possible_cpu_arch_ids时,possible_cpus指针是没有空间的,x86_possible_cpu_arch_ids第一次被调用时,会使用g_malloc0给ms->possible_cpus分配空间。同时在这个函数里还会使用ms->smp.max_cpus获取当前的最大可用cpu数目,这个smp.max_cpus在machine_parse_smp_config函数中通过用户配置获得,比如-smp参数设置为16,那么 x86_possible_cpu_arch_ids中看到的smp.max_cpus可能就是这个16。x86_possible_cpu_arch_ids中最重要的逻辑就是为每个cpu构建arch_id,这个arch_id其实就是个uint32_t类型,32位的数值,里面按比特位存放了pkg_id、die_id、core_id以及smt_id。这里有必要简单的介绍下cpu的拓扑结构,看两个结构体就明白了:

typedef uint32_t apic_id_t;

typedef struct X86CPUTopoIDs {
    unsigned pkg_id;
    unsigned die_id;
    unsigned core_id;
    unsigned smt_id;
} X86CPUTopoIDs;

typedef struct X86CPUTopoInfo {
    unsigned dies_per_pkg;
    unsigned cores_per_die;
    unsigned threads_per_core;
} X86CPUTopoInfo;

可以很清楚的看到处理器的拓扑结构,首先处理器有若干个package,一个package下有若干个dies,而每个die下又有若干个cores,core下又有若干个threads,最下层又分是否是否支持超线程(SMT,Simultaneous Multi-threading)从而有smt_id,每个id成员其实就是指明自己在哪个层级的id号。

现在回到x86_possible_cpu_arch_ids函数,继续研究arch_id是如何构建的:

for (i = 0; i < ms->possible_cpus->len; i++) {
        ms->possible_cpus->cpus[i].arch_id =
                x86_cpu_apic_id_from_index(x86ms, i);
 }

uint32_t x86_cpu_apic_id_from_index(X86MachineState *x86ms,
                                    unsigned int cpu_index)
{
        X86CPUTopoInfo topo_info;

        init_topo_info(&topo_info, x86ms);

        return x86_apicid_from_cpu_idx(&topo_info, cpu_index);
}

static void init_topo_info(X86CPUTopoInfo *topo_info,
                           const X86MachineState *x86ms)
{
        MachineState *ms = MACHINE(x86ms);

        topo_info->dies_per_pkg = ms->smp.dies;
        topo_info->cores_per_die = ms->smp.cores;
        topo_info->threads_per_core = ms->smp.threads;
}

可以看到,函数x86_cpu_apic_id_from_index用于构造arch_id,该函数首先使用init_topo_info初始化一个topo_info结构体,这个结构体里有一些成员,会记录每个pkg有多少die,每个die又有多少个core,每个core又有多少个thread,这些成员的值都来自machine_parse_smp_config函数,这个函数前面提到过,都是依据用户的配置填充便是。

继续看x86_apicid_from_cpu_idx函数:

static inline apic_id_t x86_apicid_from_cpu_idx(X86CPUTopoInfo *topo_info,
                                                unsigned cpu_index)
{
    X86CPUTopoIDs topo_ids;
    x86_topo_ids_from_idx(topo_info, cpu_index, &topo_ids);
    return x86_apicid_from_topo_ids(topo_info, &topo_ids);
}

static inline void x86_topo_ids_from_idx(X86CPUTopoInfo *topo_info,
                                         unsigned cpu_index,
                                         X86CPUTopoIDs *topo_ids)
{
    unsigned nr_dies = topo_info->dies_per_pkg;
    unsigned nr_cores = topo_info->cores_per_die;
    unsigned nr_threads = topo_info->threads_per_core;

    topo_ids->pkg_id = cpu_index / (nr_dies * nr_cores * nr_threads);
    topo_ids->die_id = cpu_index / (nr_cores * nr_threads) % nr_dies;
    topo_ids->core_id = cpu_index / nr_threads % nr_cores;
    topo_ids->smt_id = cpu_index % nr_threads;
}

x86_apicid_from_cpu_idx调用了x86_topo_ids_from_idx初始化一个topo_ids结构体,该结构体记录了针对现在传进来的cpu_index(实际就是for循环里的循环变量),其对应的pkg_id、die_id、core_id以及smt_id各是多少,注意这些id在x86_topo_ids_from_idx函数里是如何计算的,针对每级id,要计算其下的级别共能容纳多少,然后除以这个数字得到的值再对本级能容纳多少取余得到了本级的id。

有了各级别的id存于topo_ids里就可以调用x86_apicid_from_topo_ids了:

static inline apic_id_t x86_apicid_from_topo_ids(X86CPUTopoInfo *topo_info,
                                                 const X86CPUTopoIDs *topo_ids)
{
    return (topo_ids->pkg_id  << apicid_pkg_offset(topo_info)) |
           (topo_ids->die_id  << apicid_die_offset(topo_info)) |
           (topo_ids->core_id << apicid_core_offset(topo_info)) |
           topo_ids->smt_id;
}


在这里就可以看到各级id左移拼接在一起形成一个apic_id,至于apicid_xxx_offset的代码细节就不分析了,原理就是左移出来下级所有id需要的bit位就可以了。

x86_possible_cpu_arch_ids再后面的逻辑就是填充possible_cpus里各个cpu属性(props)的各级id了,就不详细分析了。

介绍完了x86_possible_cpu_arch_ids,得到了possible_cpus,现在回到x86_cpus_init,这时就可以使用possible_cpus->cpus[i].arch_id去调用x86_cpu_new函数了,x86_cpu_new函数定义如下:

void x86_cpu_new(X86MachineState *x86ms, int64_t apic_id, Error **errp)
{
    Object *cpu = object_new(MACHINE(x86ms)->cpu_type);

    if (!object_property_set_uint(cpu, "apic-id", apic_id, errp)) {
        goto out;
    }
    qdev_realize(DEVICE(cpu), NULL, errp);

out:
    object_unref(cpu);
}

object_new函数定义如下:

Object *object_new(const char *typename)
{
    TypeImpl *ti = type_get_by_name(typename);

    return object_new_with_type(ti);
}

从object_new函数定义可以看到,type_get_by_name里根据传入的字符串名(cpu_type或typename,在x86-64平台这个串就是qemu64-x86_64-cpu)找到TypeImpl,那么可以想见对应typename的TypeImpl先前就已经放入到type_table哈希表里了,这个流程如下:

main->qemu_init->qemu_init_subsystems->module_call_init->x86_cpu_register_types->x86_register_cpudef_types->x86_register_cpu_model_type->type_register->type_register_internal->type_table_add

注意用于注册到type_table哈希表的TypeInfo在x86_register_cpu_model_type里初始化:

static void x86_register_cpu_model_type(const char *name, X86CPUModel *model)
{
    g_autofree char *typename = x86_cpu_type_name(name);
    TypeInfo ti = {
        .name = typename,
        .parent = TYPE_X86_CPU,
        .class_init = x86_cpu_cpudef_class_init,
        .class_data = model,
    };

    type_register(&ti);
}

注意这里ti是个局部变量,插入到type_table的当然不可能就是这个ti,因为局部变量在x86_register_cpu_model_type函数返回时就会销毁,真正插入到type_table的TypeInfo在type_register_internal中调用type_new里会使用malloc函数申请内存,而这里定义的局部ti会在type_new里作为一个构建插入到type_table中ti的信息来源。

查看这个局部ti的定义,可以看到被初始化的成员很有限,尤其是在object_new函数里调用object_new_with_type时,要使用instance_size成员作为参数调用g_malloc申请内存,该成员在定义TypeInfo类型的ti变量时没有被初始化,这个成员的初始化流程如下:

main->qemu_init->qemu_create_machine->select_machine->object_class_get_list->object_class_foreach->g_hash_table_foreach->object_class_foreach_tramp->type_initialize

type_initialize里有如下代码:

ti->instance_size = type_object_get_size(ti);
static size_t type_object_get_size(TypeImpl *ti)
{
    if (ti->instance_size) {
        return ti->instance_size;
    }

    if (type_has_parent(ti)) {
        return type_object_get_size(type_get_parent(ti));
    }

    return 0;
}

可以看到是在类型初始化时设置instance_size成员,其实际是父TypeImpl的大小,还有很多成员都来自父TypeImpl。在前面x86_register_cpu_model_type函数中定义局部变量ti时,就有parent了,其为TYPE_X86_CPU,就是串x86_64-cpu,这对应的TypeImpl其实就是x86_cpu_type_info,具体定义这里就不展开了。

回到object_new函数,现在终于搞清楚了这个函数的第一行变量ti的来龙去脉,在object_new就可以继续调用object_new_with_type,在这里面可以使用得到的ti里的信息去malloc出cpu Object,并返回到x86_cpu_new函数里。

回到x86_cpu_new继续分析,随后调用了object_property_set_uint来设置cpu的apic-id属性为刚生成的apic_id。

现在详细分析object_property_set_uint函数,按照上面说的它的作用,对它的分析从两个方面来进行,一是apic-id这个属性的设置流程,二是对这个设的值本身进行的操作。

先看第一个方面,有一个下面调用的流程:

x86_cpu_new->object_property_set_uint->object_property_set_qobject->object_property_set

最后的object_property_set里面有下面的代码:

...
ObjectProperty *prop = object_property_find_err(obj, name, errp);
prop->set(obj, v, name, prop->opaque, errp);
...

所谓第一个方面,就是研究上面的set函数是怎么被设置,进而在这里可以调用起来。x86架构有个Property数组叫x86_cpu_properties,里面定义了所有X86 cpu可能有的Property,针对apic-id属性使用DEFINE_PROP_UINT32宏定义一个类型为Property数组元素:

static Property x86_cpu_properties[] = {
        ...
        DEFINE_PROP_UINT32("apic-id", X86CPU, apic_id, UNASSIGNED_APIC_ID),
        ...
}
#define DEFINE_PROP_UINT32(_n, _s, _f, _d)                      \
    DEFINE_PROP_UNSIGNED(_n, _s, _f, _d, qdev_prop_uint32, uint32_t)
#define DEFINE_PROP_UNSIGNED(_name, _state, _field, _defval, _prop, _type) \
    DEFINE_PROP(_name, _state, _field, _prop, _type,                       \
                .set_default = true,                                       \
                .defval.u  = (_type)_defval)
#define DEFINE_PROP(_name, _state, _field, _prop, _type, ...) {  \
        .name      = (_name),                                    \
        .info      = &(_prop),                                   \
        .offset    = offsetof(_state, _field)                    \
            + type_check(_type, typeof_field(_state, _field)),   \
        __VA_ARGS__                                              \
        }
const PropertyInfo qdev_prop_uint32 = {
    .name  = "uint32",
    .get   = get_uint32,
    .set   = set_uint32,
    .set_default_value = qdev_propinfo_set_default_value_uint,
};

有了上面的x86_cpu_properties数组,再看下面的一个调用流程:

main->qemu_init->qemu_create_machine->select_machine->object_class_get_list->object_class_foreach->g_hash_table_foreach->object_class_foreach_tramp->type_initialize->type_initialize->x86_cpu_common_class_init->device_class_set_props->qdev_class_add_property

最后三个函数有下面的代码:

static void x86_cpu_common_class_init(ObjectClass *oc, void *data)
{
        device_class_set_props(dc, x86_cpu_properties);
}
void device_class_set_props(DeviceClass *dc, Property *props)
{
        Property *prop;

        dc->props_ = props;
        for (prop = props; prop && prop->name; prop++) {
                qdev_class_add_legacy_property(dc, prop);
                qdev_class_add_property(dc, prop->name, prop);
        }
}
static void qdev_class_add_property(DeviceClass *klass, const char *name,
                                    Property *prop)
{
        ObjectClass *oc = OBJECT_CLASS(klass);
        ObjectProperty *op;

        if (prop->info->create) {
                op = prop->info->create(oc, name, prop);
        } else {
                op = object_class_property_add(oc,
                                               name, prop->info->name,
                                               field_prop_getter(prop->info),
                                               field_prop_setter(prop->info),
                                               prop->info->release,
                                               prop);
        }
        if (prop->set_default) {
                prop->info->set_default_value(op, prop);
        }
        object_class_property_set_description(oc, name, prop->info->description);
}

分析上面的代码知道,x86_cpu_properties数组中的属性在device_class_set_props中通过循环逐一添加,那么到底添加到什么数据上以及添加过程到底做了什么事情,这就需要分析object_class_property_add函数了:

ObjectProperty *
object_class_property_add(ObjectClass *klass,
                          const char *name,
                          const char *type,
                          ObjectPropertyAccessor *get,
                          ObjectPropertyAccessor *set,
                          ObjectPropertyRelease *release,
                          void *opaque)
{
    ObjectProperty *prop;

    assert(!object_class_property_find(klass, name));

    prop = g_malloc0(sizeof(*prop));

    prop->name = g_strdup(name);
    prop->type = g_strdup(type);

    prop->get = get;
    prop->set = set;
    prop->release = release;
    prop->opaque = opaque;

    g_hash_table_insert(klass->properties, prop->name, prop);

    return prop;
}

在这里就看的比较清楚了,首先set函数通过field_prop_setter得到:

static ObjectPropertyAccessor *field_prop_setter(const PropertyInfo *info)
{
    return info->set ? field_prop_set : NULL;
}

这里的info就是之前定义好的qdev_prop_uint32,其是有set函数的,所以field_prop_setter返回的是field_prop_set函数,也就是说在object_class_property_add里给到prop->set的就是field_prop_set函数,另外要注意的是prop->opaque其实就是qdev_prop_uint32,这个在后面调用field_prop_set时会使用到这个指针。object_class_property_add里是真正申请一个ObjectProperty结构体的地方,并且插入的数据结构是properties这个结构体。

前面分析了设置属性值的回调函数是在什么地方初始化以及怎么调用起来。接下来的就是要关注要设置的值本身,关于这点又可以分为两个小的方面,一是将值设置到什么地方,二是这个值本身是怎么构造的。

下面先看第一个小的方面,要把属性值设置到什么地方:

x86_cpu_new->object_property_set_uint->object_property_set_qobject->object_property_set->field_prop_set(prop->set)->set_uint32(prop->info->set)->object_field_prop_ptr

上面括号中的两个回调此处就不再赘述了,这正是前面第一个方面要解决的问题。

在object_field_prop_ptr函数中有:

void *object_field_prop_ptr(Object *obj, Property *prop)
{
    void *ptr = obj;
    ptr += prop->offset;
    return ptr;
}

分析上面几行代码,首先obj是靠前的父函数传进来的cpu这个Object,前面介绍过它来自object_new,根据前面的分析,这个cpu obj其大小其实就是父object的大小,类型为TYPE_X86_CPU,所以上面的object_field_prop_ptr的obj参数,其类型其实就是X86CPU,注意qemu代码里没有看到直接的X86CPU的定义,而是通过如下的宏定义了X86CPU类型:

OBJECT_DECLARE_CPU_TYPE(X86CPU, X86CPUClass, X86_CPU)
#define OBJECT_DECLARE_CPU_TYPE(CpuInstanceType, CpuClassType, CPU_MODULE_OBJ_NAME) \
    typedef struct ArchCPU CpuInstanceType; \
    OBJECT_DECLARE_TYPE(ArchCPU, CpuClassType, CPU_MODULE_OBJ_NAME);

可以知道X86CPU实际是ArchCPU的另一个名字,而ArchCPU是每个架构都会定义的,比如i386架构的ArchCPU就是定义在target/i386/cpu.h,里面就会有apic_id成员,在前面定义apic-id属性时,offset就设置为X86CPU里apic_id成员的偏移,所以前面object_field_prop_ptr函数最后返回的指针实际就指向了apic_id成员的地址,换言之设置的属性值就会设置到这个成员上。

关于要设置的值本身怎么构造的,就不细节分析了,最终设置上apic_id的值就是object_property_set_uint的第三个参数value,只是这个值要经过QNum以及Visitor包装,但这些包装不重要,只是抽象出一套统一的访问方法,最终的值肯定是value。

接下来可以介绍x86_cpu_new调用的第二个重要的函数了:qdev_realize。

qdev_realize里最重要的代码就是如下:

return object_property_set_bool(OBJECT(dev), "realized", true, errp);

object_property_set_bool一路向下来到object_property_set函数:

prop->set(obj, v, name, prop->opaque, errp);

set函数是什么,这是前面的节一直在解释的,这里就不赘述了,其实就是property_set_bool函数,而property_set_bool里又有:

prop->set(obj, value, errp);

这里的set又是函数:device_set_realized,关于这两个函数如何设置,参考device_class_init里的代码:

object_class_property_add_bool(class, "realized",
                               device_get_realized, device_set_realized);

此处不再详细描述,前文都有介绍。

device_set_realized里最重要的代码就是下面这一行回调:

if (dc->realize) {
            dc->realize(dev, &local_err);
            if (local_err != NULL) {
                goto fail;
            }
        }

realize又是哪个函数呢?它是通过x86_cpu_common_class_init->device_class_set_parent_realize设置为x86_cpu_realizefn,具体细节前文也有描述,此处也不再详细介绍,直接分析x86_cpu_realizefn:

3. KVM CPU创建

4. VCPU的运行

5. VCPU的调度

Author: Cauchy(pqy7172@gmail.com)

Created: 2024-02-03 Sat 20:20

Validate