前言
- qemu模擬虛機內存,核心是維護虛機物理地址空間。這個地址空間既要方便qemu管理,向虛機側提供內存,又要方便展示和導出,向平臺側提供內存視圖。因此qemu抽象的內存區域有兩種組織結構,一種是樹狀的,用於qemu管理並模擬內存,一種是扁平的,用於展示和導出內存視圖,也方便傳遞給KVM
- 樹狀視圖有兩個元素,一是AddressSpace,表示一個cpu可訪問的地址空間;一是MemoryRegion,表示一段邏輯內存區域。AddressSpace地址空間由許多邏輯內存區域MemoryRegion組成
- 扁平化視圖同樣有兩個元素,一是FlatView,cpu可訪問地址空間的扁平化表示;一是FlatRange,邏輯內存區域的扁平化描述,表示一段段內存區域。同樣地,FlatView由許多FlatRange組成。
- 本章介紹樹狀視圖
MemoryRegion
數據結構
struct MemoryRegion {
......
bool ram; /* 標記是否爲ram類型的MR */
bool readonly; /* For RAM regions,標記是否爲ROM類型的MR */
RAMBlock *ram_block; /* 是否關聯一段真實的虛擬內存 */
const MemoryRegionOps *ops; /* 是否爲MMIO類型的MR */
MemoryRegion *container; /* 指向包含此MR的容器MR */
Int128 size; /* 虛機內存的物理地址大小 */
hwaddr addr; /* 虛機內存的絕對物理地址 */
MemoryRegion *alias; /* 指向別名MR的MR */
hwaddr alias_offset; /* 別名MR在所屬MR內的偏移*/
QTAILQ_HEAD(, MemoryRegion) subregions; /* 容器MR的子MR組成的鏈表頭部 */
QTAILQ_ENTRY(MemoryRegion) subregions_link; /* 用於將子MR組織成鏈表的成員 */
unsigned ioeventfd_nb; /* MR包含的ioeventfd個數 */
MemoryRegionIoeventfd *ioeventfds; /* MR包含的ioeventfd數組,用於和內核通信 */
......
};
分類
- MemoryRegion表示一段邏輯內存區域,它的後端可以是以下類型:
- RAM:普通內存,qemu通過向主機申請虛擬內存來實現。一旦qemu成功申請這段內存並給出虛機地址,guest對這段內存的讀寫就和普通進程對虛擬內存的讀寫一樣,沒有物理頁就觸發缺頁,有物理頁就正常讀寫。guest對RAM內存的讀寫不需要qemu干涉,qemu也感知不到有guest對RAM內存進行了讀寫,qemu成功申請到虛擬內存給guest就算完成了自己的任務,其它的事情就交給了內核的內存管理來做。普通內存的ram字段必爲true。
- MMIO:映射內存,MMIO模擬的不是普通內存。想像pci設備配置空間的第一個字段device id,它是隻讀的,通過讀取這個字段可以獲取設備id用來識別是什麼類型的pci設備(網卡,顯卡,virtio設備)。在物理環境下,設備id的信息由硬件設備提供,在qemu模擬的環境下,信息就需要qemu提供。因此當guest讀取這個字段時,qemu需要做一些“動作”來模擬這個信息。所以讀取這個字段時會觸發callback,callback種實現對信息的模擬。再考慮配置空間的command字段,它的功能是用來控制pci設備,使能pci設備的某些硬件特性。它是可讀寫的,當guest寫這個字段時,意味着對這個設備進行了配置,qemu需要感知這個“配置”從而模擬該配置的結果,因此也要觸發callback,callback種實現對配置結果的模擬。簡單講,MMIO內存在讀寫時會觸發一系列的操作,這些操作由MemoryRegionOps實現,初始化這種類型的MemoryRegion時需要提供MemoryRegionOps。映射內存的ops字段必不爲空。
- ROM:只讀內存,只讀內存的讀操作和RAM相同,禁止寫操作,ROM內存的readonly字段必爲true。
- ROM device:只讀設備,讀操作和RAM行爲相同,只讀設備的允許寫操作,寫操作和MMIO行爲相同,會觸發callback。
- IOMMU region:將對一段內存的訪問轉發到另一段內存上,這種類型的內存只用於模擬IOMMU的場景。
- container:容器,管理多個MR的MR,用於將多個MR組織成一個內存區域,比如一個pci配置空間,可以抽象成一個容器,它由普通內存(RAM)和映射內存(MMIO)組成。再比如整個虛機的內存地址區域,它被抽象成一個容器,包括了所有虛擬的內存區間。
- alias:alias MR指向另一段MR,可以將一段連續的內存空間分割成多個不連續的內存空間,alias MR可以指向其它類型的MR,也可以指向alias MR,但不能指向其本身,否則就回環了。alias MR的alias字段和alias_offset字段必不爲空,alias字段指向一段完整的內存空間,alias_offset表示本MR在alias指向的MR的偏移
MR實例
- 數據結構實例,下面的截圖打印了4個MR,類型各不相同,分別是:
- container MR:組織管理多個MR,這裏是兩個,container MR的subregions字段指向其餘3個MR組成的鏈表的頭
- MMIO MR:內存映射MR,當虛機對這段內存有寫操作時,會觸發回調
- alias MR:指向另一段MR,它是另一段MR的一部分
- RAM MR:表示一段真實內存,它的ram字段必爲true,並且關聯一個RAMBlock
- 各個數據結構關係,system MR是acpi MR和ram-blew-4g MR的容器,subregions_link將acpi MR和ram-blew-4g MR鏈接到一起。pc.ram MR則關聯了可用的虛擬內存
AddressSpace
數據結構
/**
* AddressSpace: describes a mapping of addresses to #MemoryRegion objects
*/
struct AddressSpace {
......
MemoryRegion *root; /* 關聯的根MR,地址空間擁有了它,就擁有了整棵MR樹的內存信息,結構體初始化時這一字段作爲輸入 */
/* Accessed via RCU. */
struct FlatView *current_map; /* Root MR對應的扁平化內存視圖 */
int ioeventfd_nb;
struct MemoryRegionIoeventfd *ioeventfds;
QTAILQ_HEAD(, MemoryListener) listeners; /* 用於當地址空間發生變化時通知qemu其它模塊或者內核 */
QTAILQ_ENTRY(AddressSpace) address_spaces_link;
};
- 從數據結構標註的解釋看,地址空間將MR轉換成更接近虛擬機側的內存地址映射。它是一箇中間層,起到連結MemoryRegion和FlatView的作用。對比來看,MemoryRegion描述的內存接近主機側,AddressSpace作爲向FlatView轉換的中間層,更接近虛機側。對於qemu模擬的cpu,如果是tcg,一個cpu可以擁有多個地址空間;如果是kvm,只支持一個地址空間(系統地址空間),在其下,可以管理多個子地址空間,比如IO地址空間或內存地址空間。qemu的cpu結構體的cpu_ases是存放地址空間的字段,如下:
/**
struct CPUState {
......
CPUAddressSpace *cpu_ases; /* 地址空間數組 */
int num_ases;
AddressSpace *as; /* 指向第一個內存地址空間,as和cpu_ases[0]->as都存放了首個內存地址空間的指針 */
MemoryRegion *memory;
......
}
* CPUAddressSpace: all the information a CPU needs about an AddressSpace
* @cpu: the CPU whose AddressSpace this is
* @as: the AddressSpace itself
* @memory_dispatch: its dispatch pointer (cached, RCU protected)
* @tcg_as_listener: listener for tracking changes to the AddressSpace
*/
struct CPUAddressSpace {
CPUState *cpu;
AddressSpace *as;
struct AddressSpaceDispatch *memory_dispatch;
MemoryListener tcg_as_listener;
};
AdressSpace初始化
- 地址空間的初始化有三個地方,1是靜態全局鏈表,2是qemu準備cpu執行環境時,3是qemu初始化特定硬件類型時。下面分別介紹
- 全局鏈表初始化:qemu有鏈表將所有地址空間組織到一起,全局變量address_spaces指向這個鏈表的頭部
static QTAILQ_HEAD(, AddressSpace) address_spaces = QTAILQ_HEAD_INITIALIZER(address_spaces);
- qemu準備cpu執行環境:將系統內存和IO內存的Root MR和地址空間都進行了初始化。Root MR作爲地址空間初始化的輸入
cpu_exec_init_all
memory_map_init
system_memory = g_malloc(sizeof(*system_memory)); /* Root MemoryRegion */
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"); /* 初始化系統IO空間並添加到全局鏈表中 */
初始化後的地址空間和根MR關係圖如下,除了內存和IO地址空間外,還有cpu的地址空間
通過virsh qemu-monitor-command vm --hmp info mtree
命令可以打印一個虛機的所有地址空間,內存地址空間如下:
IO地址空間如下:
- cpu初始化:每個cpu都有自己可訪問的地址空間,因此在cpu對象中有一個cpu_ases數組用來保存地址空間,如果是kvm模擬的cpu,只允許擁有一個地址空間;如果是tcg模擬的cpu,允許有多個地址空間。
machine_run_board_init
pc_init1
pc_cpus_init
......
x86_cpu_realizefn
if (tcg_enabled()) {
cpu->cpu_as_mem = g_new(MemoryRegion, 1);
cpu->cpu_as_root = g_new(MemoryRegion, 1);
/* Outer container... */
memory_region_init(cpu->cpu_as_root, OBJECT(cpu), "memory", ~0ull);
memory_region_set_enabled(cpu->cpu_as_root, true);
cs->num_ases = 2;
cpu_address_space_init(cs, 0, "cpu-memory", cs->memory);
cpu_address_space_init(cs, 1, "cpu-smm", cpu->cpu_as_root);
......
cpu地址空間初始化後拓撲如下,每個cpu的第一個地址空間都指向pc_memory_init中初始化的系統內存地址空間
AdressSpace Listener初始化
- 地址空間還有一個關鍵數據結構listeners,它是一個鏈表頭,鏈表的每個元素是一個MemoryListener成員。當qemu模擬的內存地址空間發生變化時,需要有機制通知到其它模塊,對於kvm的模擬,內存地址空間變化後要通知內核,從而保證內核與用戶態內存信息一致。對於tcg的模擬,雖然不通知到內核,但在內存地址信息變化時也有其它事情需要做,MemoryListener結構體就是爲此而設計,每個地址空間都維護了這樣一個結構體的鏈表,當內存信息變化時,會觸發相關的回調。同時還有一個全局的鏈表維護所有註冊的Listener結構體,這些結構體在鏈表內通過優先級排序
/**
* MemoryListener: callbacks structure for updates to the physical memory map
*
* Allows a component to adjust to changes in the guest-visible memory map.
* Use with memory_listener_register() and memory_listener_unregister().
*/
struct MemoryListener {
void (*begin)(MemoryListener *listener); /* 1 */
void (*commit)(MemoryListener *listener);
void (*region_add)(MemoryListener *listener, MemoryRegionSection *section);
void (*region_del)(MemoryListener *listener, MemoryRegionSection *section);
void (*region_nop)(MemoryListener *listener, MemoryRegionSection *section);
void (*log_start)(MemoryListener *listener, MemoryRegionSection *section,
int old, int new);
void (*log_stop)(MemoryListener *listener, MemoryRegionSection *section,
int old, int new);
void (*log_sync)(MemoryListener *listener, MemoryRegionSection *section);
void (*log_global_start)(MemoryListener *listener);
void (*log_global_stop)(MemoryListener *listener);
void (*eventfd_add)(MemoryListener *listener, MemoryRegionSection *section,
bool match_data, uint64_t data, EventNotifier *e);
void (*eventfd_del)(MemoryListener *listener, MemoryRegionSection *section,
bool match_data, uint64_t data, EventNotifier *e);
unsigned priority; /* 2 */
AddressSpace *address_space;
QTAILQ_ENTRY(MemoryListener) link;
QTAILQ_ENTRY(MemoryListener) link_as;
......
};
1. 當內存地址空間有變化時,比如添加一個MR或者刪除一個MR,整個地址空間都會變化,某些感興趣的實體可能想要
讓自己被通知到並調用提前註冊的鉤子函數,這些函數的原型就在這裏定義。一個MemoryListener可以只實現其中部分
2. MemoryListener代表的是某個對地址空間感興趣的實體,這些實體不只一個,需要被管理起來,有兩個地方管理這
些實體,一是全局鏈表memory_listeners,它管理所有註冊的Listener,結構體的link成員用作連接到這個鏈表。二是地
址空間,它管理對自己感興趣的Listener,地址空間的listeners成員維護這個鏈表頭,結構體的link_as成員用作鏈接到這個鏈表。成員的address_space指向這個所屬的地址空間。同時所有listener有一個優先級,由priority表示,決定了在鏈表中的順序。
- tcg模擬的cpu地址空間初始化時,會註冊這個Listener,它只實現了Listener結構體中的commit方法
cpu_address_space_init
if (tcg_enabled()) {
newas->tcg_as_listener.commit = tcg_commit; /* 註冊commit */
memory_listener_register(&newas->tcg_as_listener, as);
}
- kvm模擬的情況下, 會註冊以下方法
kvm_init
kvm_memory_listener_register(s, &s->memory_listener, &address_space_memory, 0) /* 3 */
......
kml->listener.region_add = kvm_region_add;
kml->listener.region_del = kvm_region_del;
kml->listener.log_start = kvm_log_start;
kml->listener.log_stop = kvm_log_stop;
kml->listener.log_sync = kvm_log_sync;
kml->listener.priority = 10;
memory_listener_register(&kml->listener, as);
memory_listener_register(&kvm_io_listener, &address_space_io); /* 4 */
static MemoryListener kvm_io_listener = { /* 5 */
.eventfd_add = kvm_io_ioeventfd_add,
.eventfd_del = kvm_io_ioeventfd_del,
.priority = 10,
}
3. 系統內存空間的Listener註冊
4. 系統IO空間的Listener註冊
5. IO空間Listener的接口實現,這裏實現ioeventfd的兩個回調函數,兩個函數在IO空間更新的時候會被調用
Memory Listener的註冊分析
void memory_listener_register(MemoryListener *listener, AddressSpace *as)
{
......
QTAILQ_FOREACH(other, &memory_listeners, link) {
if (listener->priority < other->priority) {
break;
}
}
QTAILQ_INSERT_BEFORE(other, listener, link); /* 6 */
......
QTAILQ_FOREACH(other, &as->listeners, link_as) {
if (listener->priority < other->priority) {
break;
}
}
QTAILQ_INSERT_BEFORE(other, listener, link_as); /* 7 */
listener_add_address_space(listener, as); /* 8 */
}
6. 將Listener按優先級加入到全局鏈表memory_listerners中
7. 將Listener按優先級加入到地址空間的link_as成員中
8.