- QEMU/KVM源码解析与应用
- 李强编著
- 6164字
- 2025-02-17 20:55:48
2.4 QOM介绍
QOM的全称是QEMU Object Model,顾名思义,这是QEMU中对象的一个抽象层。一般来讲,对象是C++这类面向对象编程语言中的概念。面向对象的思想包括继承、封装与多态,这些思想在大型项目中能够更好地对程序进行组织与设计。Linux内核与QEMU虽然都是C语言的项目,但是都充满了面向对象的思想,QEMU中体现这一思想的就是QOM。QEMU的代码中充满了对象,特别是设备模拟,如网卡、串口、显卡等都是通过对象来抽象的。QOM用C语言基本上实现了继承、封装、多态特点。如网卡是一个类,它的父类是一个PCI设备类,这个PCI设备类的父类是设备类,此即继承。QEMU通过QOM可以对QEMU中的各种资源进行抽象、管理(如设备模拟中的设备创建、配置、销毁)。QOM还用于各种后端组件(如MemoryRegion,Machine等)的抽象,毫不夸张地说,QOM遍布于QEMU代码。这一节会对QOM进行详细介绍,以帮助读者理解QOM,进而更加方便地阅读QEMU代码。
要理解QOM,首先需要理解类型和对象的区别。类型表示种类,对象表示该种类中一个具体的对象。比如QEMU命令行中指定"-device edu,id=edu1,-device edu,id=edu2",edu本身是一个种类,创建了edu1和edu2两个对象。QOM整个运作包括3个部分,即类型的注册、类型的初始化以及对象的初始化,3个部分涉及的函数如图2-16所示。

图2-16 QOM对象机制组成部分
本章将对QOM涉及的各个方面进行深入细致的分析。
2.4.1 类型的注册
在面向对象思想中,说到对象时都会提到它所属的类,QEMU也需要实现一个类型系统。以hw/misc/edu.c文件为例,这本身不是一个实际的设备,而是教学用的设备,它的结构简单,比较清楚地展示了QEMU中的模拟设备。类型的注册是通过type_init完成的。

在include/qemu/module.h中可以看到,type_init是一个宏,并且除了type_init还有其他几个init宏,比如block_init、opts_init、trace_init等,每个宏都表示一类module,均通过module_init按照不同的参数构造出来。按照是否定义BUILD_DSO宏,module_init有不同的定义,这里假设不定义该宏,则module_init的定义如下。

可以看到各个QOM类型最终通过函数register_module_init注册到了系统,其中function是每个类型都需要实现的初始化函数,type表示是MODULE_INIT_QOM。这里的constructor是编译器属性,编译器会把带有这个属性的函数do_qemu_init_ ##function放到特殊的段中,带有这个属性的函数会早于main函数执行,也就是说所有的QOM类型注册在main执行之前就已经执行了。register_module_init及相关函数代码如下。

register_module_init函数以类型的初始化函数以及所属类型(对QOM类型来说是MODULE_INIT_QOM)构建出一个ModuleEntry,然后插入到对应module所属的链表中,所有module的链表存放在一个init_type_list数组中。图2-17简单表示了init_type_list与各个module以及ModuleEntry之间的关系。

图2-17 init_type_list结构
综上可知,QEMU使用的各个类型在main函数执行之前就统一注册到了init_type_list [MODULE_INIT_QOM]这个链表中。
进入main函数后不久就以MODULE_INIT_QOM为参数调用了函数module_call_init,这个函数执行了init_type_list[MODULE_INIT_QOM]链表上每一个ModuleEntry的init函数。


以edu设备为例,该类型的init函数是pci_edu_register_types,该函数唯一的工作是构造了一个TypeInfo类型的edu_info,并将其作为参数调用type_register_static,type_register_static调用type_register,最终到达了type_register_internal,核心工作在这一函数中进行。
TypeInfo表示的是类型信息,其中parent成员表示的是父类型的名字,instance_size和instance_init成员表示该类型对应的实例大小以及实例的初始化函数,class_init成员表示该类型的类初始化函数。
type_register_internal以及相关函数代码如下。

type_register_internal函数很简单,type_new函数首先通过一个TypeInfo结构构造出一个TypeImpl,type_table_add则将这个TypeImpl加入到一个哈希表中。这个哈希表的key是TypeImpl的名字,value为TypeImpl本身的值。
这一过程完成了从TypeInfo到TypeImpl的转变,并且将其插入到了一个哈希表中。TypeImpl的数据基本上都是从TypeInfo复制过来的,表示的是一个类型的基本信息。在C++中,可以使用class关键字定义一个类型。QEMU使用C语言实现面向对象时也必须保存对象的类型信息,所以在TypeInfo里面指定了类型的基本信息,然后在初始化的时候复制到TypeImpl的哈希表中。
TypeImpl中存放了类型的所有信息,其定义如下。

下面对其进行基本介绍。
name表示类型名字,比如edu,isa-i8259等;class_size, instance_size表示所属类的大小以及该类所属实例的大小;class_init, class_base_init, class_finalize表示类相关的初始化与销毁函数,这类函数只会在类初始化的时候进行调用;instance_init, instance_post_init, instance_finalize表示该类所属实例相关的初始化与销毁函数;abstract表示类型是否是抽象的,与C++中的abstract类型类似,抽象类型不能直接创建实例,只能创建其子类所属实例;parent和parent_type表示父类型的名字和对应的类型信息,parent_type是一个TypeImpl;class是一个指向ObjectClass的指针,保存了该类型的基本信息;num_interfaces和interfaces描述的是类型的接口信息,与Java语言中的接口类似,接口是一类特殊的抽象类型。
2.4.2 类型的初始化
在C++等面向对象的编程语言中,当程序声明一个类型的时候,就已经知道了其类型的信息,比如它的对象大小。但是如果使用C语言来实现面向对象的这些特性,就需要做特殊的处理,对类进行单独的初始化。在上一节中,读者已经在一个哈希链表中保存了所有的类型信息TypeImpl。接下来就需要对类进行初始化了。类的初始化是通过type_initialize函数完成的,这个函数并不长,函数的输入是表示类型信息的TypeImpl类型ti。
函数首先判断了ti->class是否存在,如果不为空就表示这个类型已经初始化过了,直接返回。后面主要做了三件事。
第一件事是设置相关的filed,比如class_size和instance_size,使用ti->class_size分配一个ObjectClass。

第二件事就是初始化所有父类类型,不仅包括实际的类型,也包括接口这种抽象类型。


第三件事就是依次调用所有父类的class_base_init以及自己的class_init,这也和C++很类似,在初始化一个对象的时候会依次调用所有父类的构造函数。这里是调用了父类型的class_base_init函数。

实际上type_initialize函数可以在很多地方调用,不过,只有在第一次调用的时候会进行初始化,之后的调用会由于ti->class不为空而直接返回。
下面以其中一条路径来看type_initialize函数的调用过程。假设在启动QEMU虚拟机的时候不指定machine参数,那QEMU会在main函数中调用select_machine,进而由find_default_machine函数来找默认的machine类型。在最后那个函数中,会调用object_class_get_list来得到所有TYPE_MACHINE类型组成的链表。
object_class_get_list会调用object_class_foreach,后者会对type_table中所有类型调用object_class_foreach_tramp函数,在该函数中会调用type_initialize函数。

可以看到最终会对类型哈希表type_table中的每一个元素调用object_class_foreach_tramp函数。这里面会调用type_initializ,所以在进行find_default_machine查找所有TYPE_MACHINE的时候就顺手把所有类型都初始化了。
2.4.3 类型的层次结构
上一节中从type_initialize可以看到,类型初始化时会初始化父类型,这一节专门对类型的层次结构进行介绍,QOM通过这种层次结构实现了类似C++中的继承概念。
在edu设备的类型信息edu_info结构中有一个parent成员,这就指定了edu_info的父类型的名称,edu设备的父类型是TYPE_PCI_DEVICE,表明edu设备被设计成为一个PCI设备。
可以在hw/pci/pci.c中找到TYPE_PCI_DEVICE的类型信息,它的父类型为TYPE_DEVICE。更进一步,可以在hw/core/qdev.c中找到TYPE_DEVICE的类型信息,它的父类型是TYPE_OBJECT,接着在qom/object.c可以找到TYPE_OBJECT的类型信息,而它已经没有父类型,TYPE_OBJECT是所有能够初始化实例的最终祖先,类似的,所有interface的祖先都是TYPE_INTERFACE。下面的代码列出了类型的继承关系。

所以这个edu类型的层次关系为:

当然,QEMU中还会有其他类型,如TYPE_ISA_DEVICE,同样是以TYPE_DEVICE为父类型,表示的是ISA设备,同样还可以通过TYPE_PCI_DEVICE派生出其他的类型。总体上,QEMU使用的类型一起构成了以TYPE_OBJECT为根的树。
下面再从数据结构方面谈一谈类型的层次结构。在类型的初始化函数type_initialize中会调用ti->class=g_malloc0(ti->class_size)语句来分配类型的class结构,这个结构实际上代表了类型的信息。类似于C++定义的一个类,从前面的分析看到ti->class_size为TypeImpl中的值,如果类型本身没有定义就会使用父类型的class_size进行初始化。edu设备中的类型本身没有定义,所以它的class_size为TYPE_PCI_DEVICE中定义的值,即sizeof(PCIDeviceClass)。

PCIDeviceClass表明了类属PCI设备的一些信息,如表示设备商信息的vendor_id和设备信息device_id以及读取PCI设备配置空间的config_read和config_write函数。值得注意的是,一个域是第一个成员DeviceClass的结构体,这描述的是属于“设备类型”的类型所具有的一些属性。在device_type_info中可以看到:


DeviceClass定义了设备类型相关的基本信息以及基本的回调函数,第一个域也是表示其父类型的Class,为ObjectClass。ObjectClass是所有类型的基础,会内嵌到对应的其他Class的第一个域中。图2-18展示了ObjectClass、DeviceClass和PCIDeviceClass三者之间的关系,可以看出它们之间的包含与被包含关系,事实上,编译器为C++继承结构编译出来的内存分布跟这里是类似的。

图2-18 PCIDeviceClass的层级结构
父类型的成员域是在什么时候初始化的呢?在type_initialize中会调用以下代码来对父类型所占的这部分空间进行初始化。

回头再看来分析类的初始化type_initialize,最后一句话为:

第一个参数为ti->class,对edu而言就是刚刚分配的PCIDeviceClass,但是这个class_init回调的参数指定的类型是ObjectClass,所以需要完成ObjectClass到PCIDeviceClass的转换。

类型的转换是由PCI_DEVICE_CLASS完成的,该宏经过层层扩展,会最终调用到object_class_dynamic_cast函数,从名字可以看出这是一种动态转换,C++也有类似的dynamic_cast来完成从父类转换到子类的工作。object_class_dynamic_cast函数的第一个参数是需要转换的ObjectClass,第二个typename表示要转换到哪一个类型。
函数首先通过type_get_by_name得到要转到的TypeImpl,这里的typename是TYPE_PCI_DEVICE。


以edu为例,type->name是"edu",但是要转换到的却是TYPE_PCI_DEVICE,所以会调用type_is_ancestor("edu",TYPE_PCI_DEVICE)来判断后者是否是前者的祖先。
在该函数中依次得到edu的父类型,然后判断是否与TYPE_PCI_DEVICE相等,由edu设备的TypeInfo可知其父类型为TYPE_PCI_DEVICE,所以这个type_is_ancestor会成功,能够进行从ObjectClass到PCIDeviceClass的转换。这样就可以直接通过(PCIDeviceClass*)ObjectClass完成从ObjectClass到PCIDeviceClass的强制转换。
2.4.4 对象的构造与初始化
现在总结一下前面两节的内容,首先是每个类型指定一个TypeInfo注册到系统中,接着在系统运行初始化的时候会把TypeInfo转变成TypeImple放到一个哈希表中,这就是类型的注册。系统会对这个哈希表中的每一个类型进行初始化,主要是设置TypeImpl的一些域以及调用类型的class_init函数,这就是类型的初始化。现在系统中已经有了所有类型的信息并且这些类型的初始化函数已经调用了,接着会根据需要(如QEMU命令行指定的参数)创建对应的实例对象,也就是各个类型的object。下面来分析指定-device edu命令的情况。在main函数中有这么一句话。

这里忽略QEMU参数构建以及其他跟对象构造主题关系不大的细节,只关注对象的构造。对每一个-device的参数,会调用device_init_func函数,该函数随即调用qdev_device_add进行设备的添加。通过object_new来构造对象,其调用链如下。
object_new->object_new_with_type->object_initialize_with_type->object_init_with_type

这里省略了object_init_with_type之前的函数调用。简单来讲,object_new通过传进来的typename参数找到对应的TypeImpl,再调用object_new_with_type,该函数首先调用type_initialize确保类型已经经过初始化,然后分配type->instance_size作为大小分配对象的实际空间,接着调用object_initialize_with_type对对象进行初始化。对象的property后面会单独讨论,object_initialize_with_type的主要工作是对object_init_with_type和object_post_init_with_type进行调用,前者通过递归调用所有父类型的对象初始化函数和自身对象的初始化函数,后者调用TypeImpl的instance_post_init回调成员完成对象初始化之后的工作。下面以edu的TypeInfo为例进行介绍。

edu的对象大小为sizeof(EduState),所以实际上一个edu类型的对象是EduState结构体,每一个对象都会有一个XXXState与之对应,记录了该对象的相关信息,若edu是一个PCI设备,那么EduState里面就会有这个设备的一些信息,如中断信息、设备状态、使用的MMIO和PIO对应的内存区域等。
在object_init_with_type函数中可以看到调用的参数都是一个Object,却能够一直调用父类型的初始化函数,不出意外这里也有一个层次关系。


继续看pci_device_type_info和device_type_info,它们的对象结构体为PCIDevice以及DeviceState。可以看出,对象之间实际也是有一种父对象与子对象的关系存在。与类型一样,QOM中的对象也可以使用宏将一个指向Object对象的指针转换成一个指向子类对象的指针。转换过程与类型ObjectClass类似,不再赘述。
这里可以看出,不同于类型信息和类型,object是根据需要创建的,只有在命令行指定了设备或者是热插一个设备之后才会有object的创建。类型和对象之间是通过Object的class域联系在一起的。这是在object_initialize_with_type函数中通过obj->class=type->class实现的。
从上文可以看出,可以把QOM的对象构造分成3部分,第一部分是类型的构造,通过TypeInfo构造一个TypeImpl的哈希表,这是在main之前完成的;第二部分是类型的初始化,这是在main中进行的,这两部分都是全局的,也就是只要编译进去的QOM对象都会调用;第三部分是类对象的构造,这是构造具体的对象实例,只有在命令行指定了对应的设备时,才会创建对象。
现在只是构造出了对象,并且调用了对象初始化函数,但是EduState里面的数据内容并没有填充,这个时候的edu设备状态并不是可用的,对设备而言还需要设置它的realized属性为true才行。在qdev_device_add函数的后面,还有这样一句:

这句代码将dev(也就是edu设备的realized属性)设置为true,这就涉及了QOM类和对象的另一个方面,即属性。
2.4.5 属性
QOM实现了类似于C++的基于类的多态,一个对象按照继承体系可以是Object、DeviceState、PCIDevice等。在QOM中为了便于对对象进行管理,还给每种类型以及对象增加了属性。类属性存在于ObjectClass的properties域中,这个域是在类型初始化函数type_initialize中构造的。对象属性存放在Object的properties域中,这个域是在对象的初始化函数object_initialize_with_type中构造的。两者皆为一个哈希表,存着属性名字到ObjectProperty的映射。
属性由ObjectProperty表示。

其中,name表示名字;type表示属性的类型,如有的属性是字符串,有的是bool类型,有的是link等其他更复杂的类型;get、set、resolve等回调函数则是对属性进行操作的函数;opaque指向一个具体的属性,如BoolProperty等。
每一种具体的属性都会有一个结构体来描述它。比如下面的LinkProperty表示link类型的属性,StringProperty表示字符串类型的属性,BoolProperty表示bool类型的属性。

图2-19展示了几个结构体的关系。
下面介绍几个属性的操作接口,属性的添加分为类属性的添加和对象属性的添加,以对象属性为例,它的属性添加是通过object_property_add接口完成的。

图2-19 属性相关的结构体关系

上述代码片段忽略了属性name中带有通配符*的情况。
object_property_add函数首先调用object_property_find来确认所插入的属性是否已经存在,确保不会添加重复的属性,接着分配一个ObjectProperty结构并使用参数进行初始化,然后调用g_hash_table_insert插入到对象的properties域中。
属性的查找通过object_property_find函数实现,代码如下。

这个函数首先调用object_class_property_find来确认自己所属的类以及所有父类都不存在这个属性,然后在自己的properties域中查找。
属性的设置是通过object_property_set来完成的,其只是简单地调用ObjectProperty的set函数。

每一种属性类型都有自己的set函数,其名称为object_set_XXX_property,其中的XXX表示属性类型,如bool、str、link等。以bool为例,其set函数如下。


可以看到,其调用了具体属性(BoolProperty)的set函数,这是在创建这个属性的时候指定的。
再回到edu设备,在qdev_device_add函数的后面,会调用以下代码。

其中并没有给edu设备添加realized属性的过程,那么这是在哪里实现的呢?设备的对象进行初始化的时候,会上溯到所有的父类型,并调用它们的instance_init函数。可以看到device_type_info的instance_init函数device_initfn,在后面这个函数中,它给所有设备都添加了几个属性。

其中,realized的设置函数为device_set_realized,其调用了DeviceClass的realize函数。

对PCI设备而言,其类型初始化函数为pci_device_class_init,在该函数中设置了其DeviceClass的realize为qdev_realize函数。


在pci_qdev_realize函数中调用了PCIDeviceClass的realize函数,在edu设备中,其在类型的初始化函数中被设置为pci_edu_realize,代码如下。

所以在qdev_device_add中对realized属性进行了设置之后,它会寻找到父设备DeviceState添加的realized属性,并最终调用在edu设备中指定的pci_edu_realize函数,这个时候会对EduState的各个设备的相关域进行初始化,使得设备处于可用状态。这里对edu设备具体数据的初始化不再详述。
本书将设置设备realized属性的过程叫作设备的具现化。
设备的realized属性属于bool属性。bool属性是比较简单的属性,这里再对两个特殊的属性进行简单的介绍,即child属性和link属性。
child属性表示对象之间的从属关系,父对象的child属性指向子对象,child属性的添加函数为object_property_add_child,其代码如下。


首先根据参数中的name(一般是子对象的名字)创建一个child<name>,构造出一个新的名字,然后用这个名字作为父对象的属性名字,将子对象添加到父对象的属性链表中,存放在ObjectProperty的opaque中。
link属性表示一种连接关系,表示一种设备引用了另一种设备,添加link属性的函数为object_property_add_link,其代码如下。

这个函数将会添加obj对象的link<type>属性,其中type为参数child的类型,将child存放在LinkProperty的child域中。设置这个属性的时候,其实也就是写这个child的时候,在object_set_link_property中最关键一句为:

这样就建立起了两个对象之间的关系。
下面以hw/i386/pc_piix.c中的pc_init1函数中为PCMachineState对象添加PC_MACHINE_ACPI_DEVICE_PROP属性为例,介绍属性添加与设置的相关内容。PCMachineState初始化状态如图2-20所示,apci_dev是一个HotplugHandler类型的指针,properties是根对象Object存放所有属性的哈希表。

图2-20 PCMachineState初始状态
pc_init1函数中有下面一行代码。

执行这行代码时,会给类型为PCMachineState的对象machine增加一个link属性,link属性的child成员保存了apci_dev的地址,如图2-21所示。

图2-21 PCMachineState添加link属性
执行下一行代码设置link属性时,会设置指针acpi_dev指向一个类型为TYPE_HOTPLUG_HANDLER的对象。

执行完之后如图2-22所示。
调用object_property_add_link函数时会将pcms->acpi_dev的地址放到link属性中,接下来设置其link属性的值为piix4_pm对象。这里之所以能将一个设备对象设置成一个TYPE_HOTPLUG_HANDLER的link,是因为piix4_pm所属的类型TYPE_PIIX4_PM有TYPE_HOTPLUG_HANDLER接口,所以可以看成TYPE_HOTPLUG_HANDLER类型。从下面的调试结果可以看出,在设置link之后,pcms->acpi_dev指向了piix4_pm。

图2-22 PCMachineState设置link属性
