qemu內存管理——樹狀視圖

前言

  • 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表示一段邏輯內存區域,它的後端可以是以下類型:
  1. RAM:普通內存,qemu通過向主機申請虛擬內存來實現。一旦qemu成功申請這段內存並給出虛機地址,guest對這段內存的讀寫就和普通進程對虛擬內存的讀寫一樣,沒有物理頁就觸發缺頁,有物理頁就正常讀寫。guest對RAM內存的讀寫不需要qemu干涉,qemu也感知不到有guest對RAM內存進行了讀寫,qemu成功申請到虛擬內存給guest就算完成了自己的任務,其它的事情就交給了內核的內存管理來做。普通內存的ram字段必爲true。
  2. 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字段必不爲空。
  3. ROM:只讀內存,只讀內存的讀操作和RAM相同,禁止寫操作,ROM內存的readonly字段必爲true。
  4. ROM device:只讀設備,讀操作和RAM行爲相同,只讀設備的允許寫操作,寫操作和MMIO行爲相同,會觸發callback。
  5. IOMMU region:將對一段內存的訪問轉發到另一段內存上,這種類型的內存只用於模擬IOMMU的場景。
  6. container:容器,管理多個MR的MR,用於將多個MR組織成一個內存區域,比如一個pci配置空間,可以抽象成一個容器,它由普通內存(RAM)和映射內存(MMIO)組成。再比如整個虛機的內存地址區域,它被抽象成一個容器,包括了所有虛擬的內存區間。
  7. alias:alias MR指向另一段MR,可以將一段連續的內存空間分割成多個不連續的內存空間,alias MR可以指向其它類型的MR,也可以指向alias MR,但不能指向其本身,否則就回環了。alias MR的alias字段和alias_offset字段必不爲空,alias字段指向一段完整的內存空間,alias_offset表示本MR在alias指向的MR的偏移

MR實例

  • 數據結構實例,下面的截圖打印了4個MR,類型各不相同,分別是:
  1. container MR:組織管理多個MR,這裏是兩個,container MR的subregions字段指向其餘3個MR組成的鏈表的頭
  2. MMIO MR:內存映射MR,當虛機對這段內存有寫操作時,會觸發回調
  3. alias MR:指向另一段MR,它是另一段MR的一部分
  4. 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初始化特定硬件類型時。下面分別介紹
  1. 全局鏈表初始化:qemu有鏈表將所有地址空間組織到一起,全局變量address_spaces指向這個鏈表的頭部
static QTAILQ_HEAD(, AddressSpace) address_spaces = QTAILQ_HEAD_INITIALIZER(address_spaces);
  1. 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地址空間如下:
在這裏插入圖片描述

  1. 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. 
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章