上篇文章主要分析了qemu中對虛擬機內存管理的關鍵數據結構及他們之間的聯系,這篇文章則主要分析在地址空間發生變化時,如何將其更新至KVM中,保持用戶空間與內核空間的同步。
這一系列操作與之前說的AddressSpace注冊綁定的listener相關,針對地址空間注冊listener的操作在函數kvm_init()中:
kvm_memory_listener_register(s, &s->memory_listener, &address_space_memory, 0); memory_listener_register(&kvm_io_listener, &address_space_io);
在初始化過程中為 address_space_memory 和 address_space_io 分別注冊了 memory_listener 和 kvm_io_listener 。前者類型為 KVMMemoryListener ,后者類型為 MemoryListener。 KVMMemoryListener 主體就是 MemoryListener ,而 MemoryListener 包含大量函數指針,用來指向 address_space 成員發生變化時調用的回調函數,從何保持內核和用戶空間的內存信息的一致性。函數kvm_memory_listener_register()中對相關回調函數進行了注冊:
void kvm_memory_listener_register(KVMState *s, KVMMemoryListener *kml, AddressSpace *as, int as_id) { int i; kml->slots = g_malloc0(s->nr_slots * sizeof(KVMSlot)); kml->as_id = as_id; for (i = 0; i < s->nr_slots; i++) { kml->slots[i].slot = i; } 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); }
實際上,任何對 AddressSpace 和 MemoryRegion 的操作,都以 memory_region_transaction_begin 開頭,以 memory_region_transaction_commit 結尾. 如在父MR中添加子MR時,調用函數memory_region_add_subregion(),該函數進一步調用函數memory_region_add_subregion_common(),主要實現為函數memory_region_update_container_subregions():
static void memory_region_update_container_subregions(MemoryRegion *subregion) { MemoryRegion *mr = subregion->container; MemoryRegion *other; memory_region_transaction_begin(); memory_region_ref(subregion); QTAILQ_FOREACH(other, &mr->subregions, subregions_link) { if (subregion->priority >= other->priority) { QTAILQ_INSERT_BEFORE(other, subregion, subregions_link); goto done; } } QTAILQ_INSERT_TAIL(&mr->subregions, subregion, subregions_link); done: memory_region_update_pending |= mr->enabled && subregion->enabled; memory_region_transaction_commit(); }
該函數按照優先級順序把subregion插入到system_memory的subregions鏈表中,添加區域后地址空間已經發生變化,然后通過memory_region_transaction_commit()實現將這一變化與KVM進行同步。
接下來對於這一變化的同步過程主要轉載自:https://www.cnblogs.com/ck1020/p/6729224.html 及 https://www.cnblogs.com/ck1020/p/6738116.html
void memory_region_transaction_commit(void) { AddressSpace *as; assert(memory_region_transaction_depth); --memory_region_transaction_depth; if (!memory_region_transaction_depth) { if (memory_region_update_pending) { MEMORY_LISTENER_CALL_GLOBAL(begin, Forward); //從前向后調用全局列表 memory_listeners 中所有 listener 的 begin 函數 QTAILQ_FOREACH(as, &address_spaces, address_spaces_link) { address_space_update_topology(as);//對 address_spaces 中的所有 address space,調用 address_space_update_topology ,更新 QEMU 和 KVM 中維護的 slot 信息 } MEMORY_LISTENER_CALL_GLOBAL(commit, Forward);//從后向前調用全局列表 memory_listeners 中所有 listener 的 commit 函數 } else if (ioeventfd_update_pending) { QTAILQ_FOREACH(as, &address_spaces, address_spaces_link) { address_space_update_ioeventfds(as); } } memory_region_clear_pending(); } }
該函數對於每個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);//獲取原來 FlatView(AddressSpace.current_map) FlatView *new_view = generate_memory_topology(as->root);//根據AddressSpace對應的MR生成新的FlatView //比較新老 FlatView,對其中不一致的 FlatRange,執行相應的操作 address_space_update_topology_pass(as, old_view, new_view, false); address_space_update_topology_pass(as, old_view, new_view, true); /* Writes are protected by the BQL. */ atomic_rcu_set(&as->current_map, new_view);//設置新的FlatView call_rcu(old_view, flatview_unref, rcu); /* 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函數實現:
/* Render a memory topology into a list of disjoint absolute ranges. */ static FlatView *generate_memory_topology(MemoryRegion *mr) { FlatView *view; view = g_new(FlatView, 1); flatview_init(view); if (mr) { render_memory_region(view, mr, int128_zero(), addrrange_make(int128_zero(), int128_2_64()), false);// 從根級 region 開始,遞歸將 region 映射到線性地址空間中,產生一個個 FlatRange,構成 FlatView. addrrange_make創建起始地址為 0,結束地址為 2^64 的地址空間,作為 guest 的線性地址空間 } flatview_simplify(view);//將 FlatView 中連續的 FlatRange 進行合並為一個 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表示映射的部分在所屬MR中的偏移*/ 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管理。對於某一個MR而言,其基本管理邏輯理論上如圖所示:
藍色部分意味着已經存在的映射,對於某一個MR而言,可能存在如上圖中的情況,由於重疊的關系,原本的region只需要把MRS1和MRS2映射進來即可,而這兩部分也可以看成是原本region的一個分段,即MemoryRegionSection。(但是如果如圖中所示,最終也會合並連續的flatRange,所以最后會是一個大的FlatRange)最終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(在映射MRS1時,當前FR為MRS1后面已經映射的FR,而完成MRS1的映射之后,將當前映射的FR插入了view中,且i++,故退出if后view->range[i]還是MRS1后面的FR,由於該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的時候才得以執行。這意圖就比較明確,首次執行先刪除多余的,下次直接添加或者對日志做更新操作了。
上述對於要添加的FR,調用了 MEMORY_LISTENER_UPDATE_REGION(frnew, as, Forward, region_add)
#define MEMORY_LISTENER_UPDATE_REGION(fr, as, dir, callback, _args...) \ do { \ MemoryRegionSection mrs = section_from_flat_range(fr, as); \ MEMORY_LISTENER_CALL(as, callback, dir, &mrs, ##_args); \ } while(0) section_from_flat_range(FlatRange *fr, AddressSpace *as) {//根據flatrange生成MRS return (MemoryRegionSection) { .mr = fr->mr, .address_space = as, .offset_within_region = fr->offset_in_region, .size = fr->addr.size, .offset_within_address_space = int128_get64(fr->addr.start), .readonly = fr->readonly, }; } #define MEMORY_LISTENER_CALL(_as, _callback, _direction, _section, _args...) \ do { \ MemoryListener *_listener; \ struct memory_listeners_as *list = &(_as)->listeners; \ \ switch (_direction) { \ case Forward: \ QTAILQ_FOREACH(_listener, list, link_as) { \ if (_listener->_callback) { \ _listener->_callback(_listener, _section, ##_args); \ } \ } \ break; \ case Reverse: \ QTAILQ_FOREACH_REVERSE(_listener, list, memory_listeners_as, \ link_as) { \ if (_listener->_callback) { \ _listener->_callback(_listener, _section, ##_args); \ } \ } \ break; \ default: \ abort(); \ } \ } while (0)
宏MEMORY_LISTENER_UPDATE_REGION中首先根據需要添加的FR生成MRS,然后調用宏MEMORY_LISTENER_CALL,這里有個_direction,其實就是遍歷方向,因為listener按照優先級從低到高排列,所以這里其實就是確定讓誰先處理。Forward就是從前向后,而reverse就是從后向前。然后根據傳入的_callback參數調用相應的回調函數。如參入region_add,則調用kvm_region_add(),該函數把region信息告知KVM,KVM以此對內存信息做記錄,函數核心在static void kvm_set_phys_mem(MemoryRegionSection *section, bool add)函數,上一篇中由GPA及HVA的計算有對該函數的內容進行分析,以下分段介紹:
KVMState *s = kvm_state; KVMSlot *mem, old; int err; MemoryRegion *mr = section->mr; bool log_dirty = memory_region_is_logging(mr); /*是否可寫*/ bool writeable = !mr->readonly && !mr->rom_device; bool readonly_flag = mr->readonly || memory_region_is_romd(mr); //section中數據的起始偏移 hwaddr start_addr = section->offset_within_address_space; /*section的size*/ ram_addr_t size = int128_get64(section->size); void *ram = NULL; unsigned delta; /* kvm works in page size chunks, but the function may be called with sub-page size and unaligned start address. */ /*內存對齊后的偏移*/ delta = TARGET_PAGE_ALIGN(size) - size; if (delta > size) { return; } start_addr += delta; size -= delta;//這樣可以保證size是頁對齊的 size &= TARGET_PAGE_MASK; if (!size || (start_addr & ~TARGET_PAGE_MASK)) { return; } /*如果不是rom,則不能進行寫操作*/ if (!memory_region_is_ram(mr)) { if (writeable || !kvm_readonly_mem_allowed) { return; } else if (!mr->romd_mode) { /* If the memory device is not in romd_mode, then we actually want * to remove the kvm memory slot so all accesses will trap. */ add = false; } }
第一部分主要是一些基礎工作,獲取section對應的MR的一些屬性,如writeable、readonly_flag。獲取section的start_addr和size,其中start_addr就是section中的offset_within_address_space也就是FR中addr的start,接下來對size進行了對齊操作 。如果對應的MR關聯的內存並不是作為ram存在,就要進行額外的驗證。這種情況如果writeable允許寫操作或者kvm不支持只讀內存,那么直接返回。
接下來是函數的重點處理部分,即把當前的section轉化成一個slot進行添加,但是在此之前需要處理已存在的slot和新的slot的重疊問題,當然如果沒有重疊就好辦了,直接添加即可。進入while循環
ram = memory_region_get_ram_ptr(mr) + section->offset_within_region + delta; /*對重疊部分的處理*/ while (1) { /*查找重疊的部分*/ mem = kvm_lookup_overlapping_slot(s, start_addr, start_addr + size); /*如果沒找到重疊,就break*/ if (!mem) { break; } /*如果要添加區間已經被注冊*/ if (add && start_addr >= mem->start_addr && (start_addr + size <= mem->start_addr + mem->memory_size) && (ram - start_addr == mem->ram - mem->start_addr)) { /* The new slot fits into the existing one and comes with * identical parameters - update flags and done. */ kvm_slot_dirty_pages_log_change(mem, log_dirty); return; } old = *mem; if (mem->flags & KVM_MEM_LOG_DIRTY_PAGES) { kvm_physical_sync_dirty_bitmap(section); } /*移除重疊的部分*/ /* unregister the overlapping slot */ mem->memory_size = 0; err = kvm_set_user_memory_region(s, mem); if (err) { fprintf(stderr, "%s: error unregistering overlapping slot: %s\n", __func__, strerror(-err)); abort(); } /* Workaround for older KVM versions: we can't join slots, even not by * unregistering the previous ones and then registering the larger * slot. We have to maintain the existing fragmentation. Sigh. * * This workaround assumes that the new slot starts at the same * address as the first existing one. If not or if some overlapping * slot comes around later, we will fail (not seen in practice so far) * - and actually require a recent KVM version. */ /*如果已有的size小於申請的size,則需要在原來的基礎上,添加新的,不能刪除原來的再次添加*/ if (s->broken_set_mem_region && old.start_addr == start_addr && old.memory_size < size && add) { mem = kvm_alloc_slot(s); mem->memory_size = old.memory_size; mem->start_addr = old.start_addr; mem->ram = old.ram; mem->flags = kvm_mem_flags(s, log_dirty, readonly_flag); err = kvm_set_user_memory_region(s, mem); if (err) { fprintf(stderr, "%s: error updating slot: %s\n", __func__, strerror(-err)); abort(); } start_addr += old.memory_size; ram += old.memory_size; size -= old.memory_size; continue; } /* register prefix slot */ /*new 的start_addr大於old.start_addr,需要補足前面多余的部分*/ if (old.start_addr < start_addr) { mem = kvm_alloc_slot(s); mem->memory_size = start_addr - old.start_addr; mem->start_addr = old.start_addr; mem->ram = old.ram; mem->flags = kvm_mem_flags(s, log_dirty, readonly_flag); err = kvm_set_user_memory_region(s, mem); if (err) { fprintf(stderr, "%s: error registering prefix slot: %s\n", __func__, strerror(-err)); #ifdef TARGET_PPC fprintf(stderr, "%s: This is probably because your kernel's " \ "PAGE_SIZE is too big. Please try to use 4k " \ "PAGE_SIZE!\n", __func__); #endif abort(); } } /* register suffix slot */ /**/ if (old.start_addr + old.memory_size > start_addr + size) { ram_addr_t size_delta; mem = kvm_alloc_slot(s); mem->start_addr = start_addr + size; size_delta = mem->start_addr - old.start_addr; mem->memory_size = old.memory_size - size_delta; mem->ram = old.ram + size_delta; mem->flags = kvm_mem_flags(s, log_dirty, readonly_flag); err = kvm_set_user_memory_region(s, mem); if (err) { fprintf(stderr, "%s: error registering suffix slot: %s\n", __func__, strerror(-err)); abort(); } } }
首先調用了kvm_lookup_overlapping_slot函數找到一個沖突的slot,注意返回結果是按照slot為單位,只要兩個地址范圍有交叉,就意味着存在沖突,就返回沖突的slot。如果沒有,那么直接break,添加新的。否則就根據以下幾種情況進行分別處理。其實主要有兩種大情況:
1、新的slot完全包含於引起沖突的slot中,並且參數都是一致的。
2、新的slot和引起沖突的slot僅僅是部分交叉。
針對第一種情況,如果flag有變動,則只更新slot的flags,否則,不需要變動。第二種情況,首先要把原來的region delete掉,具體方式是設置mem->memory_size=0,然后調用kvm_set_user_memory_region()函數。由於 新的region和delete的region不是完全對應的,僅僅是部分交叉,所以就會連帶 刪除多余的映射,那么接下來的工作就 是分批次彌補映射。如圖所示
如圖所示,old region是找到的和new region重疊的slot,首次刪除把整個slot都刪除了,造成了region1部門很無辜的受傷,所以在映射時,要把region1在彌補上。而黑色部分就是實際刪除的,這樣接下來就可以直接映射new region了,如果多余的部分在后方,也是同樣的道理。在把無辜被刪除region映射之后,接下來就調用kvm_set_user_memory_region把new slot映射進去。基本思路就是這樣。下面看下核心函數kvm_set_user_memory_region
struct kvm_userspace_memory_region { __u32 slot; // 對應 kvm_memory_slot 的 id __u32 flags; __u64 guest_phys_addr; // GPA __u64 memory_size; /* bytes */ // 大小 __u64 userspace_addr; /* start of the userspace allocated memory */ // HVA };
static int kvm_set_user_memory_region(KVMState *s, KVMSlot *slot) { struct kvm_userspace_memory_region mem; mem.slot = slot->slot; mem.guest_phys_addr = slot->start_addr; mem.userspace_addr = (unsigned long)slot->ram; mem.flags = slot->flags; if (s->migration_log) { mem.flags |= KVM_MEM_LOG_DIRTY_PAGES; } if (slot->memory_size && mem.flags & KVM_MEM_READONLY) { /* Set the slot size to 0 before setting the slot to the desired * value. This is needed based on KVM commit 75d61fbc. */ mem.memory_size = 0; kvm_vm_ioctl(s, KVM_SET_USER_MEMORY_REGION, &mem); } mem.memory_size = slot->memory_size; return kvm_vm_ioctl(s, KVM_SET_USER_MEMORY_REGION, &mem); }
該函數使用了一個kvm_userspace_memory_region對應,該結構本質上作為參數傳遞給KVM,只是由於不能共享堆棧,在KVM中需要把該結構復制到內核空間,代碼本身沒什么難度,只是這里如果是只讀的mem,需要調用兩次kvm_vm_ioctl,第一次設置mem的size為0.
KVM接收端在kvm_vm_ioctl()函數中
…… case KVM_SET_USER_MEMORY_REGION: { struct kvm_userspace_memory_region kvm_userspace_mem; r = -EFAULT; if (copy_from_user(&kvm_userspace_mem, argp, sizeof (kvm_userspace_mem))) goto out; kvm_userspace_mem->flags |= 0x1; r = kvm_vm_ioctl_set_memory_region(kvm, &kvm_userspace_mem); break; } ……
可以看到首要任務就是把參數復制到內核,然后調用了kvm_vm_ioctl_set_memory_region()函數。
int kvm_vm_ioctl_set_memory_region(struct kvm *kvm, struct kvm_userspace_memory_region *mem) { if (mem->slot >= KVM_USER_MEM_SLOTS) return -EINVAL; return kvm_set_memory_region(kvm, mem); }
函數檢查下slot編號如果超額,那沒辦法,無法添加,否則調用kvm_set_memory_region()函數。而該函數沒做別的,直接調用了__kvm_set_memory_region。該函數比較長,咱們還是分段介紹。開始就是一些常規檢查。
if (mem->memory_size & (PAGE_SIZE - 1)) goto out; if (mem->guest_phys_addr & (PAGE_SIZE - 1)) goto out; /* We can read the guest memory with __xxx_user() later on. */ if ((mem->slot < KVM_USER_MEM_SLOTS) && ((mem->userspace_addr & (PAGE_SIZE - 1)) || !access_ok(VERIFY_WRITE, (void *)(unsigned long)mem->userspace_addr, mem->memory_size))) goto out; if (mem->slot >= KVM_MEM_SLOTS_NUM) goto out; if (mem->guest_phys_addr + mem->memory_size < mem->guest_phys_addr) goto out;
如果memory_size 不是頁對齊的,則失敗;如果mem的客戶機物理地址不是頁對齊的,也失敗;如果slot的id在合法范圍內但是用戶空間地址不是頁對齊的或者地址范圍內的不能正常訪問,則失敗;如果slot id大於等於KVM_MEM_SLOTS_NUM,則失敗。
/*定位到指定slot*/ slot = id_to_memslot(kvm->memslots, mem->slot); base_gfn = mem->guest_phys_addr >> PAGE_SHIFT; npages = mem->memory_size >> PAGE_SHIFT; r = -EINVAL; if (npages > KVM_MEM_MAX_NR_PAGES) goto out; /*如果npages為0,則設置*/ if (!npages) mem->flags &= ~KVM_MEM_LOG_DIRTY_PAGES; /*new 為用戶空間傳遞過來的mem,old為和用戶空間mem id一致的mem*/ new = old = *slot; new.id = mem->slot; new.base_gfn = base_gfn; new.npages = npages; new.flags = mem->flags; r = -EINVAL; /*如果new 的 npage不為0*/ if (npages) { /*如果old 的npage為0,則創建新的mem*/ if (!old.npages) change = KVM_MR_CREATE; /*否則修改已有的mem*/ else { /* Modify an existing slot. */ if ((mem->userspace_addr != old.userspace_addr) || (npages != old.npages) || ((new.flags ^ old.flags) & KVM_MEM_READONLY)) goto out; /*如果兩個mem映射的基址不同*/ if (base_gfn != old.base_gfn) change = KVM_MR_MOVE; /*如果標志位不同則更新標志位*/ else if (new.flags != old.flags) change = KVM_MR_FLAGS_ONLY; else { /* Nothing to change. */ /*都一樣的話就什么都不做*/ r = 0; goto out; } } } /*如果new的npage為0而old的npage不為0,則需要delete已有的*/ else if (old.npages) { change = KVM_MR_DELETE; } else /* Modify a non-existent slot: disallowed. */ goto out;
這里如果檢查都通過了,首先通過傳遞進來的slot的id在kvm維護的slot數組中找到對應的slot結構,此結構可能為空或者為舊的slot。然后獲取物理頁框號、頁面數目。如果頁面數目大於KVM_MEM_MAX_NR_PAGES,則失敗;如果npages為0,則去除KVM_MEM_LOG_DIRTY_PAGES標志。使用舊的slot對新的slot內容做初始化,然后對new slot做設置,參數基本是從用戶空間接收的kvm_userspace_memory_region的參數。然后進入下面的if判斷
1、如果npages不為0,表明本次要添加slot此時如果old slot的npages為0,表明之前沒有對應的slot,需要添加新的,設置change為KVM_MR_CREATE;如果不為0,則需要先修改已有的slot,注意這里如果old slot和new slot的page數目和用戶空間地址必須相等,還有就是兩個slot的readonly屬性必須一致。如果滿足上述條件,進入下面的流程。如果映射的物理頁框號不同,則設置change KVM_MR_MOVE,如果flags不同,設置KVM_MR_FLAGS_ONLY,否則,什么都不做。
2、如果npages為0,而old.pages不為0,表明需要刪除old slot,設置change為KVM_MR_DELETE。到這里基本是做一些准備工作,確定用戶空間要進行的操作,接下來就執行具體的動作了
if ((change == KVM_MR_CREATE) || (change == KVM_MR_MOVE)) { /* Check for overlaps */ r = -EEXIST; kvm_for_each_memslot(slot, kvm->memslots) { if ((slot->id >= KVM_USER_MEM_SLOTS) || (slot->id == mem->slot)) continue; if (!((base_gfn + npages <= slot->base_gfn) || (base_gfn >= slot->base_gfn + slot->npages))) goto out; } } /* Free page dirty bitmap if unneeded */ if (!(new.flags & KVM_MEM_LOG_DIRTY_PAGES)) new.dirty_bitmap = NULL; r = -ENOMEM; if (change == KVM_MR_CREATE) { new.userspace_addr = mem->userspace_addr; if (kvm_arch_create_memslot(&new, npages)) goto out_free; } /* Allocate page dirty bitmap if needed */ if ((new.flags & KVM_MEM_LOG_DIRTY_PAGES) && !new.dirty_bitmap) { if (kvm_create_dirty_bitmap(&new) < 0) goto out_free; } /*如果用戶層請求釋放*/ if ((change == KVM_MR_DELETE) || (change == KVM_MR_MOVE)) { r = -ENOMEM; slots = kmemdup(kvm->memslots, sizeof(struct kvm_memslots), GFP_KERNEL); if (!slots) goto out_free; /*先根據id定位具體的slot*/ slot = id_to_memslot(slots, mem->slot); /*首先設置非法*/ slot->flags |= KVM_MEMSLOT_INVALID; old_memslots = install_new_memslots(kvm, slots, NULL); /* slot was deleted or moved, clear iommu mapping */ kvm_iommu_unmap_pages(kvm, &old); /* From this point no new shadow pages pointing to a deleted, * or moved, memslot will be created. * * validation of sp->gfn happens in: * - gfn_to_hva (kvm_read_guest, gfn_to_pfn) * - kvm_is_visible_gfn (mmu_check_roots) */ kvm_arch_flush_shadow_memslot(kvm, slot); slots = old_memslots; } r = kvm_arch_prepare_memory_region(kvm, &new, mem, change); if (r) goto out_slots; r = -ENOMEM; /* * We can re-use the old_memslots from above, the only difference * from the currently installed memslots is the invalid flag. This * will get overwritten by update_memslots anyway. */ if (!slots) { slots = kmemdup(kvm->memslots, sizeof(struct kvm_memslots), GFP_KERNEL); if (!slots) goto out_free; } /* * IOMMU mapping: New slots need to be mapped. Old slots need to be * un-mapped and re-mapped if their base changes. Since base change * unmapping is handled above with slot deletion, mapping alone is * needed here. Anything else the iommu might care about for existing * slots (size changes, userspace addr changes and read-only flag * changes) is disallowed above, so any other attribute changes getting * here can be skipped. */ if ((change == KVM_MR_CREATE) || (change == KVM_MR_MOVE)) { r = kvm_iommu_map_pages(kvm, &new); if (r) goto out_slots; } /* actual memory is freed via old in kvm_free_physmem_slot below */ if (change == KVM_MR_DELETE) { new.dirty_bitmap = NULL; memset(&new.arch, 0, sizeof(new.arch)); } old_memslots = install_new_memslots(kvm, slots, &new); kvm_arch_commit_memory_region(kvm, mem, &old, change); kvm_free_physmem_slot(&old, &new); kfree(old_memslots); return 0;
這里就根據change來做具體的設置了,如果 KVM_MR_CREATE,則設置new.用戶空間地址為新的地址。如果new slot要求KVM_MEM_LOG_DIRTY_PAGES,但是new並沒有分配dirty_bitmap,則為其分配。如果change為KVM_MR_DELETE或者KVM_MR_MOVE,這里主要由兩個操作,一是設置對應slot標識為KVM_MEMSLOT_INVALID,更新頁表。二是增加slots->generation,撤銷iommu mapping。接下來對於私有映射的話(memslot->id >= KVM_USER_MEM_SLOTS),如果是要創建,則需要手動建立映射。
接下來確保slots不為空,如果是KVM_MR_CREATE或者KVM_MR_MOVE,就需要重新建立映射,使用kvm_iommu_map_pages函數 ,而如果是KVM_MR_DELETE,就沒必要為new設置dirty_bitmap,並對其arch字段的結構清零。最終都要執行操作install_new_memslots,不過當為delete操作時,new的memory size為0,那么看下該函數做了什么。
static struct kvm_memslots *install_new_memslots(struct kvm *kvm, struct kvm_memslots *slots, struct kvm_memory_slot *new) { struct kvm_memslots *old_memslots = kvm->memslots; update_memslots(slots, new, kvm->memslots->generation); rcu_assign_pointer(kvm->memslots, slots); kvm_synchronize_srcu_expedited(&kvm->srcu); kvm_arch_memslots_updated(kvm); return old_memslots; }
這里核心操作在update_memslots里,如果是添加新的(create or move),那么new 的memory size肯定不為0,則根據new的id,在kvm維護的slot 數組中找到對應的slot,然后一次性吧new的內容賦值給old slot.如果頁面數目不一樣,則需要進行排序。如果是刪除操作,new的memorysize 為0,這里就相當於把清空了一個slot。update之后就更改kvm->memslots,該指針是受RCU機制保護的,所以不能直接修改,需要先分配好,調用API修改。最后再刷新MMIO頁表項。
void update_memslots(struct kvm_memslots *slots, struct kvm_memory_slot *new, u64 last_generation) { if (new) { int id = new->id; struct kvm_memory_slot *old = id_to_memslot(slots, id); unsigned long npages = old->npages; *old = *new; /*如果是刪除操作,那么new.npages就是0*/ if (new->npages != npages) sort_memslots(slots); } slots->generation = last_generation + 1; }
可見qemu中創建了一系列 MemoryRegion ,分別表示 Guest 中的 ROM、RAM 等區域。 MemoryRegion 之間通過 alias 或 subregion 的方式維護相互之間的關系,從而進一步細化區域的定義。
對於一個實體 MemoryRegion(非 alias),在初始化內存的過程中會創建它所對應的 RAMBlock 。 RAMBlock 通過 mmap 的方式從 QEMU 的進程空間中分配內存,並負責維護該 MemoryRegion 管理內存的起始 HVA/GPA/size 等信息。
AddressSpace 表示 VM 的物理地址空間。如果 AddressSpace 中的 MemoryRegion 發生變化,則 listener 被觸發,將所屬 AddressSpace 的 MemoryRegion 樹展平,形成一維的 FlatView ,比較 FlatRange 是否發生了變化。如果是調用相應方法如 region_add 對變化的 section region 進行檢查,更新 QEMU 內的 KVMSlot,同時填充 kvm_userspace_memory_region 結構,作為 ioctl 的參數更新 KVM 中的 kvm_memory_slot ,維護兩者信息的一致性。
該文章主體內容轉自:https://www.cnblogs.com/ck1020/p/6729224.html https://www.cnblogs.com/ck1020/p/6738116.html 對部分地方進行了一些修改