qemu中的內存管理


qemu負責模擬虛機的外設,因此虛機的線性地址空間主要由qemu進行管理,也就是確定線性地址空間中哪段地址屬於哪個設備或者DRAM或者其他的什么。

1、數據結構

1、RAMBLOCK

(最直接接觸host內存,有hva)

RAMBLOCK才是真正分配了host內存的地方,如果把它直接理解成一個內存條也是非常合適的,但實際上不僅僅如此,還有設備自有內存,顯存。ram_list則是RAMBlock的鏈表。

每個RAMBLOCK都有一個唯一的MemoryRegion對應,但不是每個MemoryRegion都有RAMBLOCK對應的。

typedef struct RAMBlock {

    uint8_t *host;   //對應宿主的內存地址hva

    ram_addr_t offset;   //block在ramlist中的偏移 gpa

    ram_addr_t length;   //block長度

    char idstr[256]; //block名字

    QLIST_ENTRY(RAMBlock) next;

} RAMBlock;

typedef struct RAMList {

    uint8_t *phys_dirty; // list的head

    QLIST_HEAD(ram, RAMBlock) blocks;

} RAMList;

 

2、MemoryRegion

管理虛擬機內存,通過內存屬性,GUEST物理地址等特點對內存分類,就形成了多個MemoryRegion,這些MemoryRegion 通過樹狀組織起來,掛接到根MemoryRegion下。每個MemoryRegion樹代表了一類作用的內存,qemu中兩個全局的MemoryRegion,分別是system_memory和system_io

 

為了方便描述,將MemoryRegion分為三類:

1. 根MemoryRegion:不分配真正的物理內存,通過subregions將所有的子MemoryRegion管理起來,如圖中的system_memory

2. 實體MemoryRegion:這種MemoryRegion中真正的分配物理內存,最主要的就是pc.ram和pci。分配的物理內存的作用分別是內存、PCI地址空間以及fireware空間。QEMU是用戶空間代碼,分配的物理內存返回的是HVA,被保存到host域。同時這個結構還會為本段虛擬機內存分配虛擬機物理地址空間起始地址,該起始地址(GPA)保存到ram_addr域,該段內存大小為size。通過實體MemoryRegion就可以將HOST地址HVA和GUEST地址GPA對應起來,這種實體MemoryRegion起到了轉換的作用。

3. 別名MemoryRegion:這種MemoryRegion中不分配物理內存,代表了實體MemoryRegion的一個部分,通過alias域指向實體MemoryRegion,alias_offset代表了該別名MemoryRegion所代表內存起始GPA相對於實體MemoryRegion所代表內存起始GPA的偏移量,通常用來計算別名MemoryRegion對應的物理內存的HVA值:HVA = 起始HVA + alias_offset。如圖中的ram_above_4g和ram-below-4g

 

3、Address_space

(qemu的內存管理在交付給KVM管理時,中間又加了一個抽象層。MR管理的host的內存,那么address_space管理的更偏向於虛擬機。把MR映射到虛擬機的物理地址空間)

不管是DRAM還是設備的資源都要通過memory region添加到address space里。

本來不同的設備使用的地址空間不同,但是QEMU X86里面只有兩種,address_space_memory(虛機的線性地址空間(設備的mmio分布在這個地址空間))和address_space_io(虛機的io地址空間(設備的io port就分布在這個地址空間里))。

初始化:main -> cpu_exec_init_all -> memory_map_init -> address_space_init(system_memory) 和 address_space_init(system_io)-> TAILQ_INSERT_TAIL(&address_spaces, as, address_spaces_link); 最后調用memory_region_transaction_commit()提交本次修改。

注冊為回調(保持內核和用戶空間的內存信息的一致性):kvm_init最后執行memory_listener_register,注冊了address_space_memory和address_space_io兩個。涉及的另外一個結構體則是MemoryListener,有kvm_memory_listener和kvm_io_listener,就是用於監控內存映射關系發生變化之后執行回調函數

實際內存分配:下面的pc_memory_init:分配全局ram(一整個memory region),然后根據below_4g_mem_size、above_4g_mem_size分別對ram進行划分,形成子MR,並注冊子MR到root MR system_memory 的subregions鏈表中。最后需要調用memory_region_transaction_commit()函數提交修改。

 

4、Flatview、Flatrange、MemoryRegionSection

(每個FlatRange對應一段虛擬機物理地址區間,各個FlatRange不會重疊,按照地址的順序保存在數組中)

MR和Flatview關系:MemoryRegion是QEMU管理內存的樹狀結構,便於按照功能、屬性分類;但這只是管理結構。但虛擬機的內存需要通過KVM_SET_USER_MEMORY_REGION,將HVA和GPA的對應關系注冊到KVM模塊的memslot,才可以生效成為EPT。如果QEMU直接使用MemoryRegion進行注冊,那么注冊的過程將會很麻煩,也容易不斷的出現重疊判斷等。所以在通過KVM_SET_USER_MEMORY_REGION注冊前,加了一層轉換機制,先將樹狀的MemoryRegion展開物理內存樣子的一維區間結構,然后再通過KVM_SET_USER_MEMORY_REGION將這個展開的物理內存注冊到KVM內核模塊中,就方便了許多。這個轉換機制是FlatView模型。整個轉換過程:函數address_space_update_topology,將指定的AddressSpace下的MemoryRegion樹進行展平,形成了對應一維內存邏輯表示的FlatView,然后再address_space_update_topology_pass中將FlatView模型通過KVM_SET_USER_MEMORY_REGION注冊到KVM模塊中。

當memory region發生變化的時候,執行memory_region_transaction_commit,address_space_update_topology,address_space_update_topology_pass最終完成更新FlatView的目標。【此處就涉及到KVM中更新kvm->memslots】

 

Flatview原理:

1. 首先FlatView模型是通過FlatView和FlatRange兩個對象組成。

2. FlatView是該段內存的整體視圖的管理結構,一個FlatView由一組FlatRange組成。

3. 每個FlatRange代表了虛擬機上的一段內存,多個FlagRange就組成了一個內存視圖,這些FlatRange在物理地址空間上不一定是相鄰的。

3. 每個FlatView代表了某一類內存的組合,用作特殊的用途(如系統內存空間,MMIO內存地址空間),通常一個FlatView同一個特定用途的Address_Space進行關聯

4. 每個FlatRange通過AddrRange標記該段GUEST內存的大小和長度。

5. FlatRange數組在FlatView初始化的時候為0個,也就是沒有分配數組。當進行flatview_insert()的操作的時候,才會動態分配出來

6. 為了簡化FlatView,通常將地址空間上連續的FlatRange進行合並,合並為1個FlatRange。如圖中的r3,r4,r5就可以進行合並的區間,合並后都合並為r3。r4和r5的內容將被后面的FlatRange的數組元素覆蓋掉

每一個flat range都投射到同一個地址空間的平面上,而上圖中的R1,R2等對應的則是struct MemoryRegionSection(MemoryRegionSection對應於FlatRange,一個FlatRange代表一個物理地址空間的片段,但是其偏向於address-space,而MemoryRegionSection則在MR端顯示的表明了分片)

 

5、KVMslot、kvm_userspace_memory_region

(更接近kvm)

在MEMORY_LISTENER_CALL中調用kvm_region_add和kvm_region_del,執行kvm_set_phys_mem,組裝KVMSlot,再把對應信息轉給kvm_userspace_memory_region,將其通過kvm_vm_ioctl傳給KVM用於更新kvm->memslots

 

2、qemu到kvm實際分配vm內存流程

QEMU在pc_init1調用pc_cpus_init創建完vcpu返回后,走到初始化內存pc_memory_init-》

1、memory_region_init_ram-》qemu_ram_alloc-》qemu_ram_alloc_from_ptr-》

(1)使用find_ram_offset賦值給new block的offset(find_ram_offset在線性區間內找到沒有使用的一段空間,可以完全容納新申請的ramblock length大小,找到滿足新申請length的最小區間,把ramblock安插進去即可,返回的offset即是新分配區間的開始地址);

(2)new_block->host = kvm_vmalloc(size) 分配真正物理內存,內部qemu_vmalloc使用qemu_memalign頁對齊分配內存。后續的都是對RAMBlock的插入等處理。

以上:memory_region_init_ram已經將qemu內存模型和實際的物理內存初始化了(嚴格意義講真實內存空間分配,是在QEMU發生缺頁host做分配的時候。tdp_page_fault函數只是做的guest物理地址到host線性地址的映射關系,不真正涉及guest真實物理空間的分配。)

 

2、從memory_region_init_ram退出到pc_memory_init時已經初始化完成MemoryRegion ram,然后執行vmstate_register_ram_global,負責將前面提到的ramlist中的ramblock和memory region的初始地址對應一下,將mr->name填充到ramblock的idstr里面,就是讓二者有確定的對應關系,如此mr就有了物理內存使用

 

3、memory_region_add_subregion-》memory_region_transaction_commit修改虛擬機的內存。引入了新的結構address_spaces(AS),對所有AS執行address_space_update_topology該函數內address_space_get_flatview直接獲取當前的FlatView,然后generate_memory_topology根據前面已經變化的mr重新生成FlatView,然后調用address_space_update_topology_pass

(1)兩個FlatView逐條的FlatRange進行對比,以后一個FlatView為准,如果前面FlatView的FlatRange和后面的不一樣,則對前面的FlatView的這條FlatRange進行處理。

(2)比較結束后,主要是MEMORY_LISTENER_UPDATE_REGION函數,將變化的FlatRange構造一個MemoryRegionSection,然后遍歷所有的memory_listeners,如果memory_listeners監控的內存區域和MemoryRegionSection一樣,則執行第四個入參函數,如region_del函數,即kvm_region_del函數,這個是在kvm_init中初始化的(不一樣則利用kvm_region_add添加內存到KVM的kvm->memslots:)。

kvm_region_del主要是kvm_set_phys_mem函數,主要是將MemoryRegionSection有效值轉換成KVMSlot形式,然后調用kvm_set_user_memory_region中使用kvm_vm_ioctl(s, KVM_SET_USER_MEMORY_REGION, &mem);傳遞給kernel。(其中mem就是kvm_userspace_memory_region結構) 

 

參考:

oenhen的qemu下內存結構

oenhen的內存虛擬化

六六哥的一系列關於qemu內存數據結構分析

jackchen的qemu-kvm內存分析1-2

jessica的qemu對虛機地址空間管理


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM