恒天云——QEMU源码分析系列(一)
QEMU虚拟化源码分析概论
一、QEMU介绍
QEMU的官网首页上对其自身的描述如下:
QEMU is a generic and open source machine emulator and virtualizer.
When used as a machine emulator, QEMU can run OSes and programs made for one machine (e.g. an ARM board) on a different machine (e.g. your own PC). By using dynamic translation, it achieves very good performance.
When used as a virtualizer, QEMU achieves near native performance by executing the guest code directly on the host CPU. QEMU supports virtualization when executing under the Xen hypervisor or using the KVM kernel module in Linux. When using KVM, QEMU can virtualize x86, server and embedded PowerPC, and S390 guests.
以上描述说明了QEMU的以下几个特点:
1. QEMU可以被当作模拟器,也可以被当作虚拟机。
2. 当QEMU被当作模拟器时,我们可以在一台机器上通过模拟设备,运行针对不同于本机上CPU的程序或者操作系统。(使用了动态翻译技术,在我们的虚拟化环境中并没有使用,因此不展开叙述)
3. 当QEMU被当作虚拟机使用时,QEMU必须基于Xen Hypervisor或者KVM内核模块才能支持虚拟化。在这种条件下QEMU虚拟机可以通过直接在本机CPU上运行客户机代码获得接近本机的性能。
二、QEMU与KVM的关系
当QEMU在模拟器模式下,运行操作系统时,我们可以认为这是一种软件实现的虚拟化技术,它的效率比真机差很多,用户可以明显地感觉出来。当QEMU在虚拟机模式下,QEMU必须在Linux上运行,并且需要借助KVM或者Xen,利用Intel或者Amd提供的硬件辅助虚拟化技术,才能使虚拟机达到接近真机的性能。QEMU与KVM内核模块协同工作,在虚拟机进程中,各司其职,又相互配合,最终实现高效的虚拟机应用。
QEMU与KVM的关系如下:
KVM在物理机启动时创建/dev/kvm设备文件,当创建虚拟机时,KVM为该虚拟机进程创建一个VM的文件描述符,当创建vCPU时,KVM为每个vCPU创建一个文件描述符。同时,KVM向用户空间提供了一系列针对特殊设备文件的ioctl系统调用。QEMU主要是通过ioctl系统调用与KVM进行交互的。
那么QEMU和KVM具体都实现了哪些功能呢?我们用一张图说明:
QEMU所实现的功能包括:虚拟机的配置和创建、虚拟机运行依赖的虚拟设备、虚拟机运行时用户操作环境和交互(vnc)以及一些针对虚拟机的特殊技术(如动态迁移),都是QEMU自己实现的。同时QEMU还实现了利用KVM提供的接口实现虚拟机硬件加速。
而KVM的主要功能在于初始化CPU硬件,打开虚拟化模式,然后将虚拟客户机运行在虚拟机模式下,并对虚拟客户机的运行提供支持,这些支持主要是以针对相关的特殊设备文件的ioctl系统调用。外设的模拟一般不会由KVM负责,只有对性能要求较高的虚拟设备,如虚拟中断控制器和虚拟时钟,是由KVM模拟的,这样可以大量减少处理器的模式转换的开销。
三、QEMU的代码模型
1)、线程事件驱动模型
QEMU的体系结构正如上图展示的——每个vCPU都是一个线程,这些vCPU线程可以运行客户机的代码,以及虚拟中断控制器、虚拟时钟的模拟。而Main loop主线程则是Event-driver的,通过轮询文件描述符,调用对应的回调函数,处理由Monitor发出的命令、Timers超时,并且实现VNC、完成IO等功能。
QEMU事件驱动的代码主要可以查看include/qemu/main-loop.h,以及相关的实现代码。
2)、设备模拟
QEMU为了实现大量设备的模拟,实现了比较完备的面向对象模型——QOM(QEMU Object Model)。QEMU对于CPU、内存、总线以及主板的模拟都是依赖于QOM的,QEMU中设备相关的数据结构的初始化工作都是依赖于QOM的初始化实现机制。对于它的实现主要可以查看include/qom/object.h。对于具体的CPU、内存等设备的模拟,可以查看include/qom/cpu.h、include/exec/memory.h、include/hw/qdev-core.h
3)、QEMU中的虚拟机管理命令
QEMU中可以使用hmp command对虚拟机进行管理,在虚拟机环境中同时按住ctrl、Alt、2就可以进入QEMU的命令模式。通过输入命令,就可以进行虚拟机的管理。比如savevm命令可以把虚拟机的当前状态保存到虚拟机的磁盘中。这些命令的实现函数都有一个统一的命名方式:hmp_xxx,比如hmp_savevm就是savevm的实现函数的起始位置,hmp_migrate就是migrate的实现函数的起始位置。
因此对于QEMU中的每一条命令都可以很快找到相关的实现函数。
4)、块操作
QEMU实现了大量的块设备驱动,从而支持了包括qcow2、qed、raw等格式的镜像,这些格式的实现代码都在block的文件夹下以及主目录下的block.c中。QEMU设计了BlockDriver数据结构,其中包含了大量的回调函数指针,对于每一种磁盘格式,都有一个对应的BlockDriver的对象,并且实现了BlockDriver中的回调函数,然后将这个BlockDriver的对象注册,即添加到一个全局的BlockDriver的链表中。
四、QEMU源码编译
QEMU的编译过程并不复杂,首先进入QEMU的代码目录后,首先运行./configure –help,查看qemu支持的特性。然后选择相关的特性进行编译。
由于我们使用的X86_64的平台,并且主要查看的是QEMU与KVM协同工作实现虚拟化的代码,我们使用下列命令配置:
./configure –enable-debug –enable-kvm –target-list=x86_64-softmmu
上述命令会生成Makefile文件,然后直接make就可以了,为了加速编译可以使用多线程:make -j number。
./configure命令运行时会检查物理机的环境,检查需要的相关的库是否已经安装在宿主机上。因此可能由于相关库没有安装而中断,其中一些库包括:
pkg-config、zlib1g-dev、libglib2.0-dev、libpixman-1-dev、make等
以上库都可以通过ubuntu的包管理命令apt-get install直接安装。
如果需要把QEMU安装到系统中可以使用make install命令。
五、阅读代码的工具准备和经验
1)、工具准备
阅读代码的工具包括vim、ctags、gdb。
其中ctags会维持一个全局的tag表。通过在源代码主目录下使用ctags –R,可以生成对应的tag表的文件,文件名为tags。在打开一个源代码文件后,用户通过将光标移动到某一个函数调用或者数据结构的位置,同时按住ctrl+],就可以跳转到函数或者数据结构的定义处,如果函数或者数据结构有多个定义,在vim中输入:tn可以看下一个函数的定义。通过ctrl+t可以跳转回原来的位置。另外,如果vim显示没有找到tags文件,需要显式地告知vim,tags的路径,命令:set tags=$tagspath。
2)、经验
由于QEMU是相当大型的代码,定义了大量的数据结构,对于初学者,直接使用gdb调试,在学习和理解上会比较吃力。因此开始学习代码时,可以先使用vim+ctags,学习qemu的基本数据结构开始,这些代码的学习不需要动态调试,只需要静态地查看,就可以比较细致和系统地学习,主要包括include/qemu/文件夹下的.h文件和对应的.c文件、include/qom/文件夹下的.h文件和对应的.c文件、include/hw/文件夹下的.h文件和对应的.c文件等等。这些实现代码是qemu最底层的实现,如果使用gdb的话,一般要进入10层以上的函数堆栈才能跟踪到,但是通过对这些代码的学习,可以基本学习到QEMU是如何对设备进行模拟的,同时可以对QEMU的代码结构有初步的掌握。
对于QEMU实现的某些虚拟化功能,比如热迁移,可以采用gdb调试的方法学习,学习其代码流程。但是在gdb调试之前,仍然建议粗略地看一下代码的相关流程、函数跳转,以便调试时能够清楚应该具体在哪里打断点。
特别需要注意的是QEMU中全局变量的定义和使用,这些全局变量管理了虚拟机的全部资源包括CPU、内存等,理解这些全局变量的使用,对于理解QEMU的执行有很大的帮助。
static constTypeInfomy_device_info = {
.name = TYPE_MY_DEVICE,
.parent = TYPE_DEVICE,
.instance_size = sizeof(MyDevice),
.abstract = true, // or set a default in my_device_class_init
.class_size = sizeof(MyDeviceClass), /*便于给类的数据结构赋予内存*/
};
//调用类的虚拟方法
void my_device_frobnicate(MyDevice *obj){
MyDeviceClass *klass = MY_DEVICE_GET_CLASS(obj);
klass->frobnicate(obj);
}
QEMU中的对象模型QOM
一、介绍
QEMU提供了一套面向对象编程的模型——QOM,即QEMU Object Module,几乎所有的设备如CPU、内存、总线等都是利用这一面向对象的模型来实现的。QOM模型的实现代码位于qom/目录下的文件中。对于开发者而言,虽然只需要知道如何利用QOM模型创建类和对象就可以了,但是只有理解了QOM的相关数据结构,才能清楚知道如何利用QOM模型。因此本文先对QOM的必要性展开叙述,然后说明QOM的相关数据结构。在读者了解了QOM的数据结构的基础上,我们向读者阐述如何使用QOM模型创建新对象和新类,在后续的文章将展开详细介绍QOM是如何实现的。同时对于阅读代码的人来说,理解和掌握QOM是学习QEMU代码的重要一步。
二、为什么QEMU中要实现对象模型?
- 各种架构CPU的模拟和实现
QEMU中要实现对各种CPU架构的模拟,而且对于一种架构的CPU,比如X86_64架构的CPU,由于包含的特性不同,也会有不同的CPU模型。任何CPU中都有CPU通用的属性,同时也包含各自特有的属性。为了便于模拟这些CPU模型,面向对象的变成模型是必不可少的。 - 模拟device与bus的关系
在主板上,一个device会通过bus与其他的device相连接,一个device上可以通过不同的bus端口连接到其他的device,而其他的device也可以进一步通过bus与其他的设备连接,同时一个bus上也可以连接多个device,这种device连bus、bus连device的关系,qemu是需要模拟出来的。为了方便模拟设备的这种特性,面向对象的编程模型也是必不可少的。
三、QOM模型的数据结构
这些数据结构中TypeImpl定义在qom/object.c中,ObjectClass、Object、TypeInfo定义在include/qom/object.h中。在include/qom/object.h的注释中,对它们的每个字段都有比较明确的说明,并且说明了QOM模型的用法。
- TypeImpl:对数据类型的抽象数据结构
structTypeImpl {
const char *name;
size_tclass_size; /*该数据类型所代表的类的大小*/
size_tinstance_size; /*该数据类型产生的对象的大小*/
/*类的 Constructor & Destructor*/
void (*class_init)(ObjectClass *klass, void *data);
void (*class_base_init)(ObjectClass *klass, void *data);
void (*class_finalize)(ObjectClass *klass, void *data);
void *class_data;
/*实例的Contructor& Destructor*/
void (*instance_init)(Object *obj);
void (*instance_post_init)(Object *obj);
void (*instance_finalize)(Object *obj);
bool abstract; /*表示类是否是抽象类*/
const char *parent; /*父类的名字*/
TypeImpl *parent_type; /*指向父类TypeImpl的指针*/
ObjectClass *class; /*该类型对应的类的指针*/
intnum_interfaces; /*所实现的接口的数量*/
InterfaceImpl interfaces[MAX_INTERFACES];
};
其中InterfaceImpl的定义如下,只是一个类型的名字
structInterfaceImpl{
const char *typename;
};
- ObjectClass: 是所有类的基类
typedefstructTypeImpl *Type;
structObjectClass{
/*< private >*/
Type type; /**/
GSList *interfaces;
const char *object_cast_cache[OBJECT_CLASS_CAST_CACHE];
const char *class_cast_cache[OBJECT_CLASS_CAST_CACHE];
ObjectUnparent *unparent;
};
- Object: 是所有对象的base Object
struct Object{
/*< private >*/
ObjectClass *class;
ObjectFree *free; /*当对象的引用为0时,清理垃圾的回调函数*/
GHashTable *properties; /*Hash表记录Object的属性*/
uint32_t ref; /*该对象的引用计数*/
Object *parent;
};
/*< private >*/
ObjectClass *class;
ObjectFree *free; /*当对象的引用为0时,清理垃圾的回调函数*/
GHashTable *properties; /*Hash表记录Object的属性*/
uint32_t ref; /*该对象的引用计数*/
Object *parent;
};
- TypeInfo:是用户用来定义一个Type的工具型的数据结构,用户定义了一个TypeInfo,然后调用type_register(TypeInfo)或者type_register_static(TypeInfo )函数,就会生成相应的TypeImpl实例,将这个TypeInfo注册到全局的TypeImpl的hash表中。
/*TypeInfo的属性与TypeImpl的属性对应,实际上qemu就是通过用户提供的TypeInfo创建的TypeImpl的对象*/
structTypeInfo{
const char *name;
const char *parent;
size_tinstance_size;
void (*instance_init)(Object *obj);
void (*instance_post_init)(Object *obj);
void (*instance_finalize)(Object *obj);
bool abstract;
size_tclass_size;
void (*class_init)(ObjectClass *klass, void *data);
void (*class_base_init)(ObjectClass *klass, void *data);
void (*class_finalize)(ObjectClass *klass, void *data);
void *class_data;
InterfaceInfo *interfaces
structTypeInfo{
const char *name;
const char *parent;
size_tinstance_size;
void (*instance_init)(Object *obj);
void (*instance_post_init)(Object *obj);
void (*instance_finalize)(Object *obj);
bool abstract;
size_tclass_size;
void (*class_init)(ObjectClass *klass, void *data);
void (*class_base_init)(ObjectClass *klass, void *data);
void (*class_finalize)(ObjectClass *klass, void *data);
void *class_data;
InterfaceInfo *interfaces
};
四、怎样使用QOM模型创建新类型?
使用QOM模型创建新类型时,需要用到以上的OjectClass、Object和TypeInfo。关于QOM的用法,在include/qom/object.h一开始就有一长串的注释,这一长串的注释说明了创建新类型时的各种用法。我们下面是对这些用法的简要说明。
- 从最简单的开始,创建一个最小的type:
#include "qdev.h"
#define TYPE_MY_DEVICE "my-device"
// 用户需要定义新类型的类和对象的数据结构
// 由于不实现父类的虚拟函数,所以直接使用父类的数据结构作为子类的数据结构
// No new virtual functions: we can reuse the typedef for the
// superclass.
typedefDeviceClassMyDeviceClass;
typedefstructMyDevice{
DeviceState parent; //父对象必须是该对象数据结构的第一个属性,以便实现父对象向子对象的cast
int reg0, reg1, reg2;
} MyDevice;
#define TYPE_MY_DEVICE "my-device"
// 用户需要定义新类型的类和对象的数据结构
// 由于不实现父类的虚拟函数,所以直接使用父类的数据结构作为子类的数据结构
// No new virtual functions: we can reuse the typedef for the
// superclass.
typedefDeviceClassMyDeviceClass;
typedefstructMyDevice{
DeviceState parent; //父对象必须是该对象数据结构的第一个属性,以便实现父对象向子对象的cast
int reg0, reg1, reg2;
} MyDevice;
static constTypeInfomy_device_info = {
.name = TYPE_MY_DEVICE,
.parent = TYPE_DEVICE,
.instance_size = sizeof(MyDevice), //必须向系统说明对象的大小,以便系统为对象的实例分配内存
};
//向系统中注册这个新类型
static void my_device_register_types(void){
type_register_static(&my_device_info);
}
type_init(my_device_register_types)
.name = TYPE_MY_DEVICE,
.parent = TYPE_DEVICE,
.instance_size = sizeof(MyDevice), //必须向系统说明对象的大小,以便系统为对象的实例分配内存
};
//向系统中注册这个新类型
static void my_device_register_types(void){
type_register_static(&my_device_info);
}
type_init(my_device_register_types)
- 为了方便编程,对于每个新类型,都会定义由ObjectClass动态cast到MyDeviceClass的方法,也会定义由Object动态cast到MyDevice的方法。以下涉及的函数OBJECT_GET_CLASS、OBJECT_CLASS_CHECK、OBJECT_CHECK都在include/qemu/object.h中定义。
#define MY_DEVICE_GET_CLASS(obj) \
OBJECT_GET_CLASS(MyDeviceClass, obj, TYPE_MY_DEVICE)
#define MY_DEVICE_CLASS(klass) \
OBJECT_CLASS_CHECK(MyDeviceClass, klass, TYPE_MY_DEVICE)
#define MY_DEVICE(obj) \
OBJECT_CHECK(MyDevice, obj, TYPE_MY_DEVICE)
OBJECT_GET_CLASS(MyDeviceClass, obj, TYPE_MY_DEVICE)
#define MY_DEVICE_CLASS(klass) \
OBJECT_CLASS_CHECK(MyDeviceClass, klass, TYPE_MY_DEVICE)
#define MY_DEVICE(obj) \
OBJECT_CHECK(MyDevice, obj, TYPE_MY_DEVICE)
- 如果我们在定义新类型中,实现了父类的虚拟方法,那么需要定义新的class的初始化函数,并且在TypeInfo数据结构中,给TypeInfo的class_init字段赋予该初始化函数的函数指针。
#include "qdev.h"
void my_device_class_init(ObjectClass *klass, void *class_data){
DeviceClass *dc = DEVICE_CLASS(klass);
dc->reset = my_device_reset;
}
static constTypeInfomy_device_info = {
.name = TYPE_MY_DEVICE,
.parent = TYPE_DEVICE,
.instance_size = sizeof(MyDevice),
.class_init = my_device_class_init, /*在类初始化时就会调用这个函数,将虚拟函数赋值*/
};
void my_device_class_init(ObjectClass *klass, void *class_data){
DeviceClass *dc = DEVICE_CLASS(klass);
dc->reset = my_device_reset;
}
static constTypeInfomy_device_info = {
.name = TYPE_MY_DEVICE,
.parent = TYPE_DEVICE,
.instance_size = sizeof(MyDevice),
.class_init = my_device_class_init, /*在类初始化时就会调用这个函数,将虚拟函数赋值*/
};
- 如果我们在定义新类型中,希望在class中添加新的虚拟方法,使这个类成为抽象类,我们需要定义新的class的数据结构,同时给TypeInfo的class_size字段赋值,只有给它赋值,系统才能给class数据结构分配适当的内存空间。
#include "qdev.h"
typedefstructMyDeviceClass{
DeviceClass parent; /*必须讲父类的数据结构放在新类的第一个属性上,以便父类动态cast到子类*/
void (*frobnicate) (MyDevice *obj); /*新的虚拟函数*/
} MyDeviceClass;
typedefstructMyDeviceClass{
DeviceClass parent; /*必须讲父类的数据结构放在新类的第一个属性上,以便父类动态cast到子类*/
void (*frobnicate) (MyDevice *obj); /*新的虚拟函数*/
} MyDeviceClass;
static constTypeInfomy_device_info = {
.name = TYPE_MY_DEVICE,
.parent = TYPE_DEVICE,
.instance_size = sizeof(MyDevice),
.abstract = true, // or set a default in my_device_class_init
.class_size = sizeof(MyDeviceClass), /*便于给类的数据结构赋予内存*/
};
//调用类的虚拟方法
void my_device_frobnicate(MyDevice *obj){
MyDeviceClass *klass = MY_DEVICE_GET_CLASS(obj);
klass->frobnicate(obj);
}
- 当我们需要从一个类创建一个派生类时,如果需要覆盖类原有的虚拟方法,派生类中,可以增加相关的属性将类原有的虚拟函数指针保存,然后给虚拟函数赋予新的函数指针,保证父类原有的虚拟函数指针不会丢失。
typedefstructMyStateMyState;
typedef void (*MyDoSomething)(MyState *obj);
typedefstructMyClass {
ObjectClassparent_class;
MyDoSomethingdo_something;
} MyClass;
static void my_do_something(MyState *obj){
// do something
}
static void my_class_init(ObjectClass *oc, void *data){
MyClass *mc = MY_CLASS(oc);
mc->do_something = my_do_something;
}
typedef void (*MyDoSomething)(MyState *obj);
typedefstructMyClass {
ObjectClassparent_class;
MyDoSomethingdo_something;
} MyClass;
static void my_do_something(MyState *obj){
// do something
}
static void my_class_init(ObjectClass *oc, void *data){
MyClass *mc = MY_CLASS(oc);
mc->do_something = my_do_something;
}
static constTypeInfomy_type_info = {
.name = TYPE_MY,
.parent = TYPE_OBJECT,
.instance_size = sizeof(MyState),
.class_size = sizeof(MyClass),
.class_init = my_class_init,
};
typedefstructDerivedClass {
MyClassparent_class;
MyDoSomethingparent_do_something;
} DerivedClass;
static void derived_do_something(MyState *obj){
DerivedClass *dc = DERIVED_GET_CLASS(obj);
// do something here
dc->parent_do_something(obj);
// do something else here
}
static void derived_class_init(ObjectClass *oc, void *data){
MyClass *mc = MY_CLASS(oc);
DerivedClass *dc = DERIVED_CLASS(oc);
dc->parent_do_something = mc->do_something;
mc->do_something = derived_do_something;
}
static constTypeInfoderived_type_info = {
.name = TYPE_DERIVED,
.parent = TYPE_MY,
.class_size = sizeof(DerivedClass),
.class_init = derived_class_init,
};
.name = TYPE_MY,
.parent = TYPE_OBJECT,
.instance_size = sizeof(MyState),
.class_size = sizeof(MyClass),
.class_init = my_class_init,
};
typedefstructDerivedClass {
MyClassparent_class;
MyDoSomethingparent_do_something;
} DerivedClass;
static void derived_do_something(MyState *obj){
DerivedClass *dc = DERIVED_GET_CLASS(obj);
// do something here
dc->parent_do_something(obj);
// do something else here
}
static void derived_class_init(ObjectClass *oc, void *data){
MyClass *mc = MY_CLASS(oc);
DerivedClass *dc = DERIVED_CLASS(oc);
dc->parent_do_something = mc->do_something;
mc->do_something = derived_do_something;
}
static constTypeInfoderived_type_info = {
.name = TYPE_DERIVED,
.parent = TYPE_MY,
.class_size = sizeof(DerivedClass),
.class_init = derived_class_init,
};
没有评论:
发表评论