- QEMU/KVM源碼解析與應用
- 李強編著
- 6164字
- 2021-08-24 11:53:39
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屬性
