qemu爲了模擬虛機內存,必須對虛擬機的內存地址空間進行管理,當內存拓撲發生變化時qemu模擬的內存映像需要隨之調整。本文主要介紹爲了管理虛機內存的地址空間,qemu設計的FlatView和FlagRange數據結構
扁平化視圖
FlatView
數據結構
- FlatView是AdressSpace扁平化展開後的視圖。表示虛擬機的物理地址空間。
/* Flattened global view of current active memory hierarchy. Kept in sorted
* order.
*/
struct FlatView {
struct rcu_head rcu;
unsigned ref;
FlatRange *ranges; /* 每個FlatView包含一組內存地址區間,構成整個內存地址空間 */
unsigned nr;
unsigned nr_allocated;
struct AddressSpaceDispatch *dispatch;
MemoryRegion *root; /* 指向關聯的Root MR */
};
FlatView初始化
- 每個FlatView都關聯到一個地址空間,地址空間初始化時由Root MR作爲輸入
FlatRange
數據結構
- FlatRange表示虛機的一段物理地址區間,一個MR由多個FlatRang,FlatRange與MR是多對一的關係
/* Range of memory in the global map. Addresses are absolute. */
struct FlatRange {
MemoryRegion *mr; /* 指向所屬的MR */
hwaddr offset_in_region; /* 相對MR的offset */
AddrRange addr; /* 虛機的物理地址區間,由起始地址(start)和長度(size)組成 */
uint8_t dirty_log_mask;
bool romd_mode;
bool readonly;
bool nonvolatile;
int has_coalesced_range;
};
- TODO
組織結構
初始狀態
- 以虛擬機IO地址空間爲例,在IO地址空間初始化之後,打印出IO地址空間的組織結構如下:
- 虛擬機的IO地址區間爲0 ~ 65535,有一個全局變量address_space_io指向IO地址區間,表示一個邏輯地址空間,這個結構體沒有記錄虛機物理地址範圍
- AdressSpace沒有記錄虛機物理地址範圍,它的兩個字段root和current_map從不同角度描述了這段內存,並且都記錄了虛機的IO地址區間範圍,兩個字段的類型分別時MemoryRegion和FlatView
- IO地址區間在初始化後,其組織結構如下:
新增IO區間
- IO MemoryRegion是個容器,當qemu在初始化增加新的硬件設備時,該設備可能會佔用IO地址空間,在虛擬機裏面通過/proc/ioports可以查詢。添加新的IO地址區間會分割系統的IO MR,將其逐漸碎片化。這裏我們以增加kvmvapic爲例,這個MR屬於IO地址區間的子MR,佔兩個字節,地址區間爲126 ~ 128,如下:
- 當IO MemoryRegion容器成功添加kvmvapic MR之後,會對應的生成一段FlatRange,它是MR對應的扁平化視圖,子MR的添加將原來的FlatRange分成了三個區間,區間1是0 ~ 126,區間2是126 ~ 128,區間3是128 ~ 65535,如下圖所示:
- 通過
virsh qemu-monitor-command vm --hmp info mtree
命令查看IO地址空間,可以驗證上面的組織結構信息
內存拓撲變更分析
- qemu在虛機啓動過程中,首先爲虛機提供一個邏輯物理地址空間,隨着硬件的增加,這個物理區間被不斷分割,變成一段一段更小的虛機物理地址區間,直到最終完成整個虛機硬件的模擬。這個過程中,
address_spaces
保存了物理地址空間鏈表頭,隨qemu初始化而不斷更新,flat_views
保存地址空間關聯的(Root MR,FlatView)
鍵值對,也會隨qemu初始化不斷更新。通過分析這兩個數據結構,可以更感性地認識qemu的內存組織結構。
新建內存和IO地址空間
static void memory_map_init(void)
{
system_memory = g_malloc(sizeof(*system_memory));
memory_region_init(system_memory, NULL, "system", UINT64_MAX);
address_space_init(&address_space_memory, system_memory, "memory");
system_io = g_malloc(sizeof(*system_io));
memory_region_init_io(system_io, NULL, &unassigned_io_ops, NULL, "io", 65536);
address_space_init(&address_space_io, system_io, "I/O");
}
- 內存地址空間是qemu創建的第一個地址空間,在創建該地址空間之前,address_spaces僅僅完成了初始化,而flat_views還是空的
- 執行
address_space_init(&address_space_memory, system_memory, "memory")
函數之後,內存地址空間初始化完成,有了第一個地址空間,此時address_spaces和flat_views信息如下。可以看到,內存地址空間初始化完成後,只是將flat_views初始化並設置了默認的空FlatView,這個hash表裏面沒有實際的內存地址區間信息,因此,可以說,內存地址空間初始化之後,對應的Root MR沒有生成對應的FlatView並添加到全局的hash表中。
- 執行
address_space_init(&address_space_io, system_io, "I/O")
函數之後,IO地址空間初始化完成,IO地址空間的增加改變了內存的拓撲,flat_views hash表中記錄了IO地址空間生成的FlatView,此時IO地址空間由一段FlatRange組成,隨着初始化的進行,IO地址空間會進一步被切割成多段。
細分內存地址空間
- 從上面看出,qemu在初始化內存地址空間和IO地址空間時,內存地址空間並沒有生成FlatView。在cpu初始化過程中,對msi類型的中斷實現,qemu需要實現內存模擬,這段內存是MMIO類型的物理內存。中斷控制器的MR的聲明和初始化流程如下,MR被保存到了APICCommonState結構體的io_memory字段中
x86_cpu_apic_realize
/* 聲明並初始化kvm-apic-msi的MR */
object_property_set_bool(OBJECT(cpu->apic_state), true, "realized", errp);
k->realize = kvm_apic_realize
memory_region_init_io(&s->io_memory, OBJECT(s), &kvm_apic_io_ops, s, "kvm-apic-msi", APIC_SPACE_SIZE);
apic = APIC_COMMON(cpu->apic_state);
/* 將kvm-apic-msi的MR添加到系統內存中,地址區間爲(apicbase,apicbase + mr->size)*/
memory_region_add_subregion_overlap(get_system_memory(),
apic->apicbase &
MSR_IA32_APICBASE_BASE,
&apic->io_memory,
0x1000);
- kvm-apic-msi的MR申請之後,就向系統內存空間中添加此MR,這一步在memory_region_add_subregion_overlap中完成。這個過程涉及到內存地址空間拓撲的更新,在此之前地址空間有三個,分別是memory,I/O和cpu-memory-0:
- flat_views存儲的(MR,FlatView)鍵值對有兩個,一個默認的empty_view,一個是IO地址空間的MR和FlatView
- 在往系統的內存地址空間中添加子MR apic->io_memory之後,flat_views包含的鍵值對增加了1個,如下圖所示,這個鍵值對的key是系統內存的根MR,但對應的FlatView只包含了一個range並且沒有將根MR描述的2^64大小的區間表示完,它只是其中的一個子集(0xfee00000,0xfee00000 + 100000)