2017-04-18
記得很早之前分析過KVM內部內存虛擬化的原理,僅僅知道KVM管理一個個slot並以此為基礎轉換GPA到HVA,卻忽略了qemu端最初內存的申請,而今有時間借助於qemu源碼分析下qemu在最初是如何申請並管理虛擬機內存的,坦白講,還真挺復雜的。
一、概述
qemu-kvm 模型下的虛擬化引擎,內存虛擬化部分要說簡單也挺簡單,在虛擬機啟動時,有qemu在qemu進程地址空間申請內存,即內存的申請是在用戶空間完成的。通過kvm提供的API,把地址信息注冊到KVM中,這樣KVM中維護有虛擬機相關的slot,這些slot構成了一個完成的虛擬機物理地址空間。slot中記錄了其對應的HVA,頁面數、起始GPA等,利用它可以把一個GPA轉化成HVA,想到這一點自然和硬件虛擬化下的地址轉換機制EPT聯系起來,不錯,這正是KVM維護EPT的技術核心。整個內存虛擬化可以分為兩部分:qemu部分和kvm部分。qemu完成內存的申請,kvm實現內存的管理。看起來簡單,但是內部實現機制也並非那么簡單。本文重點介紹qemu部分。
二、涉及數據結構
qemu中內存管理的數據結構主要涉及:MemoryRegion、AddressSpace、FlatView、FlatRange、MemoryRegionSection、RAMList、RAMBlock、KVMSlot、kvm_userspace_memory_region等
這幾個數據結構的確不太容易濾清,一下是個人的一些見解。 怎么可以把qemu層的內存管理再分為三個層次,MemoryRegion就位於頂級抽象層或者說比較偏向於host端,qemu中兩個全局的MemoryRegion,分別是system_memory和system_io,不過兩個均是以指針的形式存在,在地址空間的時候才會對對其分配具體的內存並初始化。MemoryRegion負責管理host的內存,理論上是樹狀結構,但是實際上根據代碼來看僅僅有兩層,
struct MemoryRegion { /* All fields are private - violators will be prosecuted */ const MemoryRegionOps *ops; const MemoryRegionIOMMUOps *iommu_ops; void *opaque; struct Object *owner; MemoryRegion *parent;//父區域指針 Int128 size;//區域的大小 hwaddr addr; void (*destructor)(MemoryRegion *mr); ram_addr_t ram_addr;//區域關聯的ram地址,GPA bool subpage; bool terminates; bool romd_mode; bool ram;//是否是ram bool readonly; /* For RAM regions */ bool enabled; bool rom_device; bool warning_printed; /* For reservations */ bool flush_coalesced_mmio; MemoryRegion *alias; hwaddr alias_offset; int priority; bool may_overlap; QTAILQ_HEAD(subregions, MemoryRegion) subregions;//子區域鏈表頭 QTAILQ_ENTRY(MemoryRegion) subregions_link;//子區域鏈表節點 QTAILQ_HEAD(coalesced_ranges, CoalescedMemoryRange) coalesced; const char *name; uint8_t dirty_log_mask; unsigned ioeventfd_nb; MemoryRegionIoeventfd *ioeventfds; NotifierList iommu_notify; };
MemoryRegion結構如上,相關注釋已經列舉,其中parent指向父MR,默認是NULL,size表示區域的大小;默認是64位下的最大地址;ram_addr比較重要,是區域關聯的客戶機物理地址空間的偏移,也就是客戶機物理地址。alias表明該區域是某一類型的區域(先這么說吧),這么說不知是否合適,實際上虛擬機的ram申請時時一次性申請的一個完成的ram,記錄在一個MR中,之后又對此ram按照size進行了划分,形成subregion,而subregion 的alias便指向原始的MR,而alias_offset 便是在原始ram中的偏移。對於系統地址空間的ram,會把剛才得到的subregion注冊到系統中,父MR是剛才提到的全局MR system_memory,subregions_link是鏈表節點。前面提到,實際關聯host內存的是subregion->alias指向的MR,其ram_addr是該MR在虛擬機的物理內存中的偏移,具體是由RAMBlock->offset獲得的,RAMBlock最直接的接觸host的內存,看下其結構
typedef struct RAMBlock { struct MemoryRegion *mr; uint8_t *host;/*block關聯的內存,HVA*/ ram_addr_t offset;/*在vm物理內存中的偏移 GPA*/ ram_addr_t length;/*block的長度*/ uint32_t flags; char idstr[256]; /* Reads can take either the iothread or the ramlist lock. * Writes must take both locks. */ QTAILQ_ENTRY(RAMBlock) next; int fd; } RAMBlock;
僅有的幾個字段意義比較明確,理論上一個RAMBlock就代表host的一段虛擬 內存,host指向申請的ram的虛擬地址,是HVA。所有的RAMBlock通過next字段連接起來,表頭保存在一個全局的RAMList結構中,但是根據代碼來看,原始MR分配內存時分配的是一整塊block,之所以這樣做也許是為了擴展用吧!!RAMList中有個字段mru_block指針,指向最近使用的block,這樣需要遍歷所有的block時,先判斷指定的block是否是mru_block,如果不是再進行遍歷從而提高效率。
qemu的內存管理在交付給KVM管理時,中間又加了一個抽象層,叫做address_space.如果說MR管理的host的內存,那么address_space管理的更偏向於虛擬機。正如其名字所描述的,它是管理地址空間的,qemu中有幾個全局的AddressSpace,address_space_memory和address_space_io,很明顯一個是管理系統地址空間,一個是IO地址空間。它是如何進行管理的呢?展開下AddressSpace的結構;
struct AddressSpace { /* All fields are private. */ char *name; MemoryRegion *root; struct FlatView *current_map;/*對應的flatview*/ int ioeventfd_nb; struct MemoryRegionIoeventfd *ioeventfds; struct AddressSpaceDispatch *dispatch; struct AddressSpaceDispatch *next_dispatch; MemoryListener dispatch_listener; QTAILQ_ENTRY(AddressSpace) address_spaces_link; };
對於該結構,源碼的注釋或許更能解釋:AddressSpace: describes a mapping of addresses to #MemoryRegion objects,很明顯是把MR映射到虛擬機的物理地址空間。root指向根MR,對於address_space_memory來講,root指向系統全局的MR system_memory,current_map指向一個FlatView結構,其他的字段咱們先暫時忽略,所有的AddressSpace通過結構中的address_spaces_link連接成鏈表,表頭保存在全局的AddressSpace結構中。FlatView管理MR展開后得到的所有FlatRange,看下FlatView
struct FlatView { unsigned ref;//引用計數,為0時就銷毀 FlatRange *ranges;/*對應的flatrange數組*/ unsigned nr;/*flatrange 的數目*/ unsigned nr_allocated;//當前數組的項數 };
各個字段的意義就不說了,ranges是一個數組,記錄FlatView下所有的FlatRange,每個FlatRange對應一段虛擬機物理地址區間,各個FlatRange不會重疊,按照地址的順序保存在數組中。FlatRange結構如下
struct FlatRange { MemoryRegion *mr;/*指向所屬的MR*/ hwaddr offset_in_region;/*在MR中的offset*/ AddrRange addr;/*本FR代表的區間*/ uint8_t dirty_log_mask; bool romd_mode; bool readonly;/*是否是只讀*/ };
具體的范圍由一個AddrRange結構描述,其描述了地址和大小,offset_in_region表示該區間在全局的MR中的offset,根據此可以進行GPA到HVA的轉換,mr指向所屬的MR。
到此為止,負責管理的結構基本就介紹完畢,剩余幾個主要起中介的作用,MemoryRegionSection對應於FlatRange,一個FlatRange代表一個物理地址空間的片段,但是其偏向於address-space,而MemoryRegionSection則在MR端顯示的表明了分片,其結構如下
struct MemoryRegionSection { MemoryRegion *mr;//所屬的MemoryRegion AddressSpace *address_space;//region關聯的AddressSpace hwaddr offset_within_region;//在region內部的偏移 Int128 size;//section的大小 hwaddr offset_within_address_space;//首個字節的地址在section中的偏移 bool readonly;//是否是只讀 };
其中注意兩個偏移,offset_within_region和offset_within_address_space。前者描述的是該section在整個MR中的偏移,一個address_space可能有多個MR構成,因此該offset是局部的。而offset_within_address_space是在整個地址空間中的偏移,是全局的offset。
KVMSlot也是一個中介,只不過更加接近kvm了,
typedef struct KVMSlot { hwaddr start_addr;//客戶機物理地址 GPA ram_addr_t memory_size;//內存大小 void *ram;//HVA qemu用戶空間地址 int slot;//slot編號 int flags; } KVMSlot;
kvm_userspace_memory_region是和kvm共享的一個結構,說共享不太恰當,但是其實最終作為參數給kvm使用的,kvm獲取控制權后,從棧中復制該結構到內核,其中字段意思就很簡單,不在贅述。
整體布局大致如圖所示
三、具體實現機制
qemu部分的內存申請流程上可以分為三小部分,分成三小部分主要是我在看代碼的時候覺得這三部分耦合性不是很大,相對而言比較獨立。眾所周知,qemu起始於vl.c中的main函數,那么這三部分也按照在main函數中的調用順序分別介紹。
3.1 回調函數的注冊
涉及函數:configure_accelerator() -->kvm_init()-->memory_listener_register ()
這里所說的accelerator在這里就是kvm,初始化函數自然調用了kvm_init,該函數主要完成對kvm的初始化,包括一些常規檢查如CPU個數、kvm版本等,還會通過ioctl和內核交互創建kvm結構,這些並非本文重點,不在贅述。在kvm_init函數的結尾調用了memory_listener_register
memory_listener_register(&kvm_memory_listener, &address_space_memory);
memory_listener_register(&kvm_io_listener, &address_space_io);
通過memory_listener_register函數,針對地址空間注冊了lisenter,lisenter本身是一組函數表,當地址空間發生變化的時候,會調用到listener中的相應函數,從何保持內核和用戶空間的內存信息的一致性。虛擬機包含有兩個地址空間address_space_memory和address_space_io,很容易理解,正常系統也包含系統地址空間和IO地址空間。
memory_listener_register函數不復雜,咱們看下
void memory_listener_register(MemoryListener *listener, AddressSpace *filter) { MemoryListener *other = NULL; AddressSpace *as; listener->address_space_filter = filter; /*如果listener為空或者當前listener的優先級大於最后一個listener的優先級,則可以直接插入*/ if (QTAILQ_EMPTY(&memory_listeners) || listener->priority >= QTAILQ_LAST(&memory_listeners, memory_listeners)->priority) { QTAILQ_INSERT_TAIL(&memory_listeners, listener, link); } else { /*listener按照優先級升序排列*/ QTAILQ_FOREACH(other, &memory_listeners, link) { if (listener->priority < other->priority) { break; } } /*插入listener*/ QTAILQ_INSERT_BEFORE(other, listener, link); } /*全局address_spaces-->as*/ /*對於每個address_spaces,設置listener*/ QTAILQ_FOREACH(as, &address_spaces, address_spaces_link) { listener_add_address_space(listener, as); } }
系統中可以存在多個listener,listener之間有着明確的優先級關系,通過鏈表進行組織,鏈表頭是全局的memory_listeners。函數中,如果memory_listeners為空或者當前listener的優先級大於最后一個listener的優先級,即直接把當前listener插入。否則需要挨個遍歷鏈表,找到合適的位置。具體按照優先級升序查找。在函數最后還針對每個address_space,調用listener_add_address_space函數,該函數對其對應的address_space管理的flatrange向KVM注冊。當然,實際上此時address_space尚未經過初始化,所以這里的循環其實是空循環。
3.2 Address_Space的初始化
涉及函數:cpu_exec_init_all() memory_map_init()
在第一節中已經注冊了listener,但是addressspace尚未初始化,本節就介紹下其初始化流程。從上節的configure_accelerator()函數往下走,會執行cpu_exec_init_all()函數,該函數主要初始化了IO地址空間和系統地址空間。memory_map_init()函數初始化系統地址空間,有一個全局的MemoryRegion指針system_memory指向該區域的MemoryRegion結構。
static void memory_map_init(void) { /*為system_memory分配內存*/ system_memory = g_malloc(sizeof(*system_memory)); assert(ADDR_SPACE_BITS <= 64); memory_region_init(system_memory, NULL, "system", ADDR_SPACE_BITS == 64 ? UINT64_MAX : (0x1ULL << ADDR_SPACE_BITS)); /*初始化全局的address_space_memory*/ 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"); memory_listener_register(&core_memory_listener, &address_space_memory); if (tcg_enabled()) { memory_listener_register(&tcg_memory_listener, &address_space_memory); } }
所以在函數起始,就對system_memory分配了內存,然后調用了memory_region_init函數對其進行初始化,其中size設置為整個地址空間:如果是64位就是2^64.接着調用了address_space_init函數對address_space_memory進行了初始化。
void address_space_init(AddressSpace *as, MemoryRegion *root, const char *name) { if (QTAILQ_EMPTY(&address_spaces)) { memory_init(); } memory_region_transaction_begin(); /*指定address_space_memory的root為system_memory*/ as->root = root; /*創建並關聯了一個FlatView*/ as->current_map = g_new(FlatView, 1); /*初始化FlatView*/ flatview_init(as->current_map); as->ioeventfd_nb = 0; as->ioeventfds = NULL; /*把address_space_memory插入全局鏈表*/ QTAILQ_INSERT_TAIL(&address_spaces, as, address_spaces_link); as->name = g_strdup(name ? name : "anonymous"); address_space_init_dispatch(as); memory_region_update_pending |= root->enabled; memory_region_transaction_commit(); }
函數主要做了以下幾個工作,設置addressSpaceh和MR的關聯,並初始化對應的FlatView,設置其名稱。最后把address_space_memory加入到全局的address_spaces鏈表中,最后調用memory_region_transaction_commit()提交本次修改,關於memory_region_transaction_commit后imianzai做論述。回到memory_map_init()函數中,接下來按照同樣的模式對IO區域system_io和IO地址空間address_space_io做了初始化。
3.3 實際內存的分配
前面注冊listener也好,或是初始化addressspace也好,實際上均沒有對應的物理內存。順着main函數往下走,會調用到machine—>init,實際上對應於pc_init1函數,在該函數中有pc_memory_init()函數對實際的內存做了分配。我們直接從pc_memory_init()函數開始
FWCfgState *pc_memory_init(MemoryRegion *system_memory, const char *kernel_filename, const char *kernel_cmdline, const char *initrd_filename, ram_addr_t below_4g_mem_size, ram_addr_t above_4g_mem_size, MemoryRegion *rom_memory, MemoryRegion **ram_memory, PcGuestInfo *guest_info) { int linux_boot, i; MemoryRegion *ram, *option_rom_mr; MemoryRegion *ram_below_4g, *ram_above_4g; FWCfgState *fw_cfg; linux_boot = (kernel_filename != NULL); /* Allocate RAM. We allocate it as a single memory region and use * aliases to address portions of it, mostly for backwards compatibility * with older qemus that used qemu_ram_alloc(). */ ram = g_malloc(sizeof(*ram)); //分配具體的內存 memory_region_init_ram(ram, NULL, "pc.ram", below_4g_mem_size + above_4g_mem_size); //那mr中的name設置進block vmstate_register_ram_global(ram); *ram_memory = ram; ram_below_4g = g_malloc(sizeof(*ram_below_4g)); /*對整體ram進行划分*/ memory_region_init_alias(ram_below_4g, NULL, "ram-below-4g", ram, 0, below_4g_mem_size); /******/ memory_region_add_subregion(system_memory, 0, ram_below_4g); e820_add_entry(0, below_4g_mem_size, E820_RAM); if (above_4g_mem_size > 0) { ram_above_4g = g_malloc(sizeof(*ram_above_4g)); memory_region_init_alias(ram_above_4g, NULL, "ram-above-4g", ram, below_4g_mem_size, above_4g_mem_size); memory_region_add_subregion(system_memory, 0x100000000ULL, ram_above_4g); e820_add_entry(0x100000000ULL, above_4g_mem_size, E820_RAM); } /* Initialize PC system firmware */ pc_system_firmware_init(rom_memory, guest_info->isapc_ram_fw); option_rom_mr = g_malloc(sizeof(*option_rom_mr)); memory_region_init_ram(option_rom_mr, NULL, "pc.rom", PC_ROM_SIZE); vmstate_register_ram_global(option_rom_mr); memory_region_add_subregion_overlap(rom_memory, PC_ROM_MIN_VGA, option_rom_mr, 1); fw_cfg = bochs_bios_init(); rom_set_fw(fw_cfg); if (linux_boot) { load_linux(fw_cfg, kernel_filename, initrd_filename, kernel_cmdline, below_4g_mem_size); } for (i = 0; i < nb_option_roms; i++) { rom_add_option(option_rom[i].name, option_rom[i].bootindex); } guest_info->fw_cfg = fw_cfg; return fw_cfg; }
從總體上來講,該函數主要完成了三個工作:分配全局ram(一整個memory region),然后根據below_4g_mem_size、above_4g_mem_size分別對ram進行划分,形成子MR,並注冊子MR到root MR system_memory 的subregions鏈表中。最后需要調用memory_region_transaction_commit()函數提交修改。具體而言,分配全局ram由memory_region_init_ram()完成,
void memory_region_init_ram(MemoryRegion *mr, Object *owner, const char *name, uint64_t size) { memory_region_init(mr, owner, name, size); mr->ram = true; mr->terminates = true; mr->destructor = memory_region_destructor_ram; //為MemoryRegion分配ram 實際上是block.offset,指的是客戶機物理地址空間的偏移即GPA mr->ram_addr = qemu_ram_alloc(size, mr); }
該函數實現很簡單,首先調用memory_region_init對MR做初始化。然后進行簡單的設置,最重要的還是最后的qemu_ram_alloc,咱們重點看下這個函數,該函數不過是qemu_ram_alloc_from_ptr函數的封裝,該函數圍繞RAMBlock結構,在函數起始對size進行頁對齊,然后申請一個RAMBlock結構,之后對其字段進行一些設置如 指定MR 和offset,offset理論上需要調用find_ram_offset在已有的RAMBlock中找到一個可用的。但是此時實際上還沒有其他block,所以這里應該直接返回0的。本部分最重要的莫過於設置其host了,其對應的宿主機虛擬地址空間的虛擬地址。在支持大頁的情況下調用file_ram_alloc()函數進行分配,否則調用phys_mem_alloc()函數進行分配。二者分配其實都是利用mmap分配的,但是在支持大頁的情況下需要傳遞參數mem_path,所以需要兩個函數。在非大頁的情況下,分配好內存嘗試和相鄰的block合並下。最后把block插入到全局的鏈表ram_list中,只是block保持從大到小的順序。
經過上面的初始化,我們得到一個完整的ram,下面的vmstate_register_ram_global僅僅是吧MR的名字設置到對應的block中。此為全局的MR,根據ram_below_4g和ram_above_4g,全局的MR被划分為兩部分,形成兩個子MR。memory_region_init_alias()函數便對兩個子MR進行初始化,這里就不在進行內存的申請,主要是設置alias和alias_offset字段,代碼如下。
void memory_region_init_alias(MemoryRegion *mr, Object *owner, const char *name, MemoryRegion *orig, hwaddr offset, uint64_t size) { memory_region_init(mr, owner, name, size); memory_region_ref(orig); mr->destructor = memory_region_destructor_alias; mr->alias = orig; mr->alias_offset = offset; }
在初始化完畢,需要調用memory_region_add_subregion()函數把子MR注冊到全局的system_memory。該函數實現是比較簡單的,但是需要重點介紹下,因為這里執行了關鍵的更新操作。函數核心交給memory_region_add_subregion_common()函數實現,該函數同樣比較簡單,設置subregion->parent和system_memory的關聯,設置其addr字段為offset,即在全局MR中的偏移。然后就按照優先級順序吧subregion插入到system_memory的subregions鏈表中,這些都比較簡單,目前我們添加了區域,地址空間已經發生變化,自然要把變化和KVM進行同步,這一工作由memory_region_transaction_commit()實現。
void memory_region_transaction_commit(void) { AddressSpace *as; assert(memory_region_transaction_depth); --memory_region_transaction_depth; if (!memory_region_transaction_depth && memory_region_update_pending) { memory_region_update_pending = false; MEMORY_LISTENER_CALL_GLOBAL(begin, Forward); /*更新各個addressspace 拓撲結構*/ QTAILQ_FOREACH(as, &address_spaces, address_spaces_link) { address_space_update_topology(as); } MEMORY_LISTENER_CALL_GLOBAL(commit, Forward); } }
可以看到,這里listener的作用就凸顯出來了。對於每個address_space,調用address_space_update_topology()執行更新。里面涉及兩個重要的函數generate_memory_topology和address_space_update_topology_pass。前者對於一個給定的MR,生成其對應的FlatView,而后者則根據oldview和newview對當前視圖進行更新。我們還是看下address_space_update_topology函數代碼
static void address_space_update_topology(AddressSpace *as) { FlatView *old_view = address_space_get_flatview(as); /*根據AddressSpace對應的MR生成一個新的FlatView*/ FlatView *new_view = generate_memory_topology(as->root); address_space_update_topology_pass(as, old_view, new_view, false); address_space_update_topology_pass(as, old_view, new_view, true); qemu_mutex_lock(&flat_view_mutex); flatview_unref(as->current_map); /*設置新的FlatView*/ as->current_map = new_view; qemu_mutex_unlock(&flat_view_mutex); /* Note that all the old MemoryRegions are still alive up to this * point. This relieves most MemoryListeners from the need to * ref/unref the MemoryRegions they get---unless they use them * outside the iothread mutex, in which case precise reference * counting is necessary. */ flatview_unref(old_view); address_space_update_ioeventfds(as); }
在獲取了新舊兩個FlatView之后,調用了兩次address_space_update_topology_pass()函數,首次調用重在刪除原來的,而后者重在添加。之后設置as->current_map = new_view。並對old_view減少引用,當引用計數為1時會被刪除。接下來重點在兩個地方:1、如何根據一個MR獲取對應的FlatView;2、如何對舊的FlatView進行更新。
前者自然少不了分析generate_memory_topology函數
tatic FlatView *generate_memory_topology(MemoryRegion *mr) { FlatView *view; /*mR是system_memory*/ view = g_new(FlatView, 1); flatview_init(view); /*addrrange_make生成一個0-2^64的地址空間*/ if (mr) { /*最初是讓MR 按照基址為0映射到地址空間中*/ render_memory_region(view, mr, int128_zero(), addrrange_make(int128_zero(), int128_2_64()), false); } flatview_simplify(view); return view; }
首先申請一個FlatView結構,並對其進行初始化,然后調用render_memory_region函數實現核心功能,最后還調用flatview_simplify嘗試合並相鄰的FlatRange.static void render_memory_region(FlatView *view,MemoryRegion *mr,Int128 base,AddrRange clip,bool readonly)是一個遞歸函數,參數中,view表示當前FlatView,mr最初為system_memory即全局的MR,base起初為0表示從地址空間的其實開始,clip最初為一個完整的地址空間。readonly標識是否只讀。
static void render_memory_region(FlatView *view, MemoryRegion *mr, Int128 base, AddrRange clip, bool readonly) { MemoryRegion *subregion; unsigned i; hwaddr offset_in_region; Int128 remain; Int128 now; FlatRange fr; AddrRange tmp; if (!mr->enabled) { return; } int128_addto(&base, int128_make64(mr->addr)); readonly |= mr->readonly; /*獲得當前MR的地址區間范圍*/ tmp = addrrange_make(base, mr->size); /*判斷目標MR和clip是否有交叉,即MR應該落在clip的范圍內*/ if (!addrrange_intersects(tmp, clip)) { return; } /*縮小clip到交叉的部分*/ clip = addrrange_intersection(tmp, clip); /*子區域才有alias字段*/ if (mr->alias) { int128_subfrom(&base, int128_make64(mr->alias->addr)); int128_subfrom(&base, int128_make64(mr->alias_offset)); /*這里base應該為subregion對應的alias中的區間基址*/ render_memory_region(view, mr->alias, base, clip, readonly); return; } /* Render subregions in priority order. */ QTAILQ_FOREACH(subregion, &mr->subregions, subregions_link) { render_memory_region(view, subregion, base, clip, readonly); } if (!mr->terminates) { return; } /*offset_in_region is distance between clip.start and base */ /*clip.start表示地址區間的起始,base為本次映射的基址,差值就為offset*/ offset_in_region = int128_get64(int128_sub(clip.start, base)); /***開始映射時,clip表示映射的區間范圍,base作為一個移動指導每個FR的映射,remain表示clip總還沒映射的大小*/ /*最初base=clip.start */ base = clip.start; remain = clip.size; fr.mr = mr; fr.dirty_log_mask = mr->dirty_log_mask; fr.romd_mode = mr->romd_mode; fr.readonly = readonly; /* Render the region itself into any gaps left by the current view. */ for (i = 0; i < view->nr && int128_nz(remain); ++i) { /*if base > addrrange_end(view->ranges[i].addr)即大於一個range的end*/ if (int128_ge(base, addrrange_end(view->ranges[i].addr))) { continue; } /*如果base < = view->ranges[i].addr.start*/ /*now 表示已經存在的FR 或者本次填充的FR的長度*/ if (int128_lt(base, view->ranges[i].addr.start)) { now = int128_min(remain, int128_sub(view->ranges[i].addr.start, base)); /*fr.offset_in_region表示在整個地址空間中的偏移*/ fr.offset_in_region = offset_in_region; fr.addr = addrrange_make(base, now); flatview_insert(view, i, &fr); ++i; int128_addto(&base, now); offset_in_region += int128_get64(now); int128_subfrom(&remain, now); } /*if base > rang[i].start,means overlap exists,need escape*/ now = int128_sub(int128_min(int128_add(base, remain), addrrange_end(view->ranges[i].addr)), base); int128_addto(&base, now); offset_in_region += int128_get64(now); int128_subfrom(&remain, now); } /*如果還有剩下的clip沒有映射,則下面不會在發生沖突,直接一次性的映射完成*/ if (int128_nz(remain)) { fr.offset_in_region = offset_in_region; fr.addr = addrrange_make(base, remain); flatview_insert(view, i, &fr); } }
在此必須清楚MR和clip的意義,即該函數實現的是把MR的區域填充clip空間。填充的方式就是生成一個個FlatRange,並交由FlatView管理。基本管理邏輯理論上如圖所示
棕色部分意味着已經存在的映射,這種情況下只需要把R1和R2映射進來即可。最終FlatView中的FlatRange按照在物理地址空間的布局,依次排列。但是按照實際情況來講,實際上傳遞進來的MR是整個地址空間system_space,所以像圖中那樣比較復雜的格局應該基本不會出現。不過咱們還是根據代碼來看。首先獲取了當前MR的區間范圍,以base為起點。我們目的是要把MR的區間映射進clip中,所以如果兩者沒有交叉,那么無法完成映射。接着設置clip為二者地址區間重疊的部分,以圖中所示,clip就成了clip1所標的范圍。如果當前MR是某個subregion,則需要對其原始的MR進行展開,因為具體的信息都保存在原始的MR中。但是全局MR system_memory作為參數傳遞進來,那么這里mr->alias為NULL,所以到了下面對system_memory的每個subregion均進行展開。就這樣再次進入render_memory_region函數的時候,MR為某個subregion,clip也為subregion對應的區域和原始clip的交集,由於其mr->alias指向原始MR,進入if判斷,對原始MR的對應區間進行展開,再次調用render_memory_region。這一次就要進行真正的展開操作了,即生成對應的FlatRange。
順着函數往下走,涉及幾個變量這里先介紹下,offset_in_region為對應MR在全局地址空間中的偏移,base為一個移動指針,指向當前映射的小區間,指導每個FR的映射。now當前已經映射的FR的長度,有兩種可能,第一可能是當前映射的FR,第二可能是已經映射的FR。remain表示當前clip中剩下的未映射的部分(不考慮已經存在的FR),有了這些再看下面的代碼就不吃力了。
核心的工作起始於一個for循環,循環的條件是 view->nr && int128_nz(remain),表示當前還有未遍歷的FR並且remain還有剩余。循環中如果MR base的值大於或者等於當前FR的end,則繼續往后遍歷FR,否則進入下面,如果base小於當前FR的start,則表明base到start之間的區間還沒有映射,則為其進行映射,now表示要映射的長度,取remain和int128_sub(view->ranges[i].addr.start, base)之間的最小值,后者表示下一個FR的start和base之間的差值,當然按照clip為准。接下來就沒難度了,設置FR的offset_in_region和addr,然后調用flatview_insert插入到FlatView的FlatRange數組中。不過由於FR按照地址順序排列,如果插入位置靠前,則需要移動較多的項,不知道為何不用鏈表實現。下面就很自然了移動base,增加offset_in_region,減少remain等操作。出了if,此時base已經和FR的start對齊,所以還需要略過當前FR。就這么一直映射下去。
出了for循環,如果remain不為0,則表明還有沒有映射的,但是現在已經沒有已經存在的FR了,所以不會發生沖突,直接把remain直接映射成一個FR即可。
按照這個思路,吧所有的subregion都映射下去,最終把FlatView返回。這樣generate_memory_topology就算是介紹完了,下面的flatview_simplify是對數組表項的嘗試合並,這里就不再介紹。到此為止,已經針對當前MR生成了一個新的FlatView,接下來需要用address_space_update_topology_pass函數對old_view和new_view做對比。連續調用了兩次這個函數,不過最后的adding參數先后為false和true。進入函數分析下
static void address_space_update_topology_pass(AddressSpace *as, const FlatView *old_view, const FlatView *new_view, bool adding) { /*FR計數*/ unsigned iold, inew; FlatRange *frold, *frnew; /* Generate a symmetric difference of the old and new memory maps. * Kill ranges in the old map, and instantiate ranges in the new map. */ iold = inew = 0; while (iold < old_view->nr || inew < new_view->nr) { if (iold < old_view->nr) { frold = &old_view->ranges[iold]; } else { frold = NULL; } if (inew < new_view->nr) { frnew = &new_view->ranges[inew]; } else { frnew = NULL; } /*int128_lt 小於等於*/ /*int128_eq 等於*/ /*frold not null and( frnew is null || frold not null and old.start <= new.start || frold not null and old.start == new.start) and old !=new*/ if (frold && (!frnew || int128_lt(frold->addr.start, frnew->addr.start) || (int128_eq(frold->addr.start, frnew->addr.start) && !flatrange_equal(frold, frnew)))) { /* In old but not in new, or in both but attributes changed. */ /*if not add*/ if (!adding) { MEMORY_LISTENER_UPDATE_REGION(frold, as, Reverse, region_del); } ++iold; /*if old and new and old==new*/ } else if (frold && frnew && flatrange_equal(frold, frnew)) { /* In both and unchanged (except logging may have changed) */ /*if add*/ if (adding) { MEMORY_LISTENER_UPDATE_REGION(frnew, as, Forward, region_nop); if (frold->dirty_log_mask && !frnew->dirty_log_mask) { MEMORY_LISTENER_UPDATE_REGION(frnew, as, Reverse, log_stop); } else if (frnew->dirty_log_mask && !frold->dirty_log_mask) { MEMORY_LISTENER_UPDATE_REGION(frnew, as, Forward, log_start); } } ++iold; ++inew; } else { /* In new */ /*if add*/ if (adding) { MEMORY_LISTENER_UPDATE_REGION(frnew, as, Forward, region_add); } ++inew; } } }
該函數倒是不長,主體是一個while循環,循環條件是old_view->nr和new_view->nr,表示新舊view的可用FlatRange數目。這里依次對FR數組的對應FR 做對比,主要由下面幾種情況:frold和frnew均存在、frold存在但frnew不存在,frold不存在但frnew存在。下面的if划分和上面的略有不同:
1、如果frold不為空&&(frnew為空||frold.start<frnew.start||frold.start=frnew.start)&&frold!=frnew 這種情況是新舊view的地址范圍不一樣,則需要調用lienter的region_del對frold進行刪除。
2、如果frold和frnew均不為空且frold.start=frnew.start 這種情況需要判斷日志掩碼,如果frold->dirty_log_mask && !frnew->dirty_log_mask,調用log_stop回調函數;如果frnew->dirty_log_mask && !frold->dirty_log_mask,調用log_start回調函數。
3、frold為空但是frnew不為空 這種情況直接調用region_add回調函數添加region。
函數主體邏輯基本如上所述,那我們注意到,當adding為false時,執行的只有第一個情況下的處理,就是刪除frold的操作,其余的處理只有在adding 為true的時候才得以執行。這意圖就比較明確,首次執行先刪除多余的,下次直接添加或者對日志做更新操作了。
總結:qemu內存虛擬化部分先介紹到這里,本來還想把kvm_region_add函數介紹下,但是考慮到篇幅,同時也正好利用該函數過渡到KVM中,不會顯得突兀。由於筆者能力有限,文中不免有不對的地方,還望老師們多多指教,大家一起學習,共同進步!!!
參考:qemu源碼 kvm源碼