文章目錄
數據結構
qemu臟頁記錄涉及到三種數據結構的位圖,分別是:
查詢位圖:下發位圖查詢的ioctl命令到內核,該位圖由qemu分配內存,kvm填充,是臨時的,這里簡稱查詢位圖。 全局位圖:位圖查詢到之后,有一個全局變量專用于保存此位圖,這里簡稱存儲位圖。該位圖描述了qemu進程分給虛機使用的RAM內存的臟頁情況,即虛機內存在RAM Address space地址空間的臟頁情況,這是跟蹤虛機內存臟頁的核心數據結構。有三種場景需要使用,跟蹤虛機顯存變化、跟蹤遷移過程虛機內存變化、TCG模式下跟蹤內存變化。qemu初始設計時將一個內存頁狀態用一個字節來表示,其中的3位分別用來表示這三種場景下內存的狀態。這個設計的實現有諸多問題,后面就改進成用三個獨立的位圖來跟蹤三種場景下虛機內存的變化。本文關注的,是遷移這個場景下用于跟蹤虛機內存變化的位圖。 RAMBlock位圖:主機上跟蹤qemu進程分配給虛機的內存的位圖,該內存虛機可以讀寫,用于跟蹤虛機對內存的寫操作,如果有寫操作內存頁會變臟,需要重新拷貝。qemu在遷移內存的時候,依據這個位圖判斷是否拷貝內存頁。
查詢位圖
查詢位圖涉及兩個數據結構,一個是內核KVM_GET_DIRTY_LOG ioctl命令字接收的參數kvm_dirty_log ,該數據結構由qemu分配,作為ioctl命令字的參數傳入到kvm,由kvm填充后返回。該數據結構是臨時使用,但命令返回的位圖,qemu會緩存起來,方便后面的引用。另外一個數據結構就是緩存從kvm得到的位圖,由于位圖查詢的單元是一個slot,因此KVMSlot就作為緩存位圖的數據結構。
/* for KVM_GET_DIRTY_LOG */
struct kvm_dirty_log {__u32 slot; /* 要查詢的內存區域所在的slot id */__u32 padding1;union { /* slot區域包含的所有內存的狀態,每個位表示一個內存頁 */void *dirty_bitmap; /* one bit per page */__u64 padding2;};
};
qemu啟動時需要為虛機分配內存空間,首先通過mmap向內核申請內存,然后以slot為單位向kvm注冊這段內存區域,因此可以說,qemu與kvm打交道,如果內存相關的操作,都以slot為單位進行。比如qemu注冊內存操作,首先從kvm查詢空閑slot,然后將內存起止區間填入,向內核注冊;再比如qemu開啟臟頁統計操作,首先遍歷memory_listeners上所有MemoryListener,找到包含的虛機物理地址區間MemoryRegionSection,起止地址都是GPA,然后遍歷虛機的所有slot,找到MemoryRegionSection對應的slot,最后依然通過slot向內核傳遞地址區間。 qemu向kvm下發ioctl查詢臟頁時,以前的做法是將位圖放到kvm_dirty_log結構的dirty_bitmap域中,然后同步給dirty_memory全局位圖,新版本的qemu新增了一個動作,將位圖緩存到KVMSlot的dirty_bitmap域中,就是說,新版本qemu kvm_dirty_log中的dirty_bitmap和KVMSlot中的dirty-bitmap值是相同的,這樣改動的原因是其它的臟頁同步手段也可以往KVMSlot的臟頁位圖中填入臟頁信息,比如Dirty Ring。
typedef struct KVMSlot
{hwaddr start_addr; /* slot包含的內存區域的起始物理地址 */ram_addr_t memory_size; /* slot包含的內存區域大小 */void *ram; /* 指向內存區域的起始虛擬地址,當要拷貝虛機內存內容是需要它 */int slot; /* slot在全局slot數組中的索引 */....../* Dirty bitmap cache for the slot */unsigned long *dirty_bmap; /* 用來緩存向內核查詢臟頁時返回的位圖 */unsigned long dirty_bmap_size; /* 位圖大小 *//* Cache of the address space ID */int as_id; /* 緩存slot所在的地址空間 *//* Cache of the offset in ram address space */ram_addr_t ram_start_offset; /* 緩存slot起始地址在ramlist地址空間的偏移 */
} KVMSlot;
全局位圖
全局的內存位圖有三個,除了描述遷移狀態的,還有描述顯存以及跟蹤TCG模式下內存的。分析下面的結構體,遷移的內存位圖保存在ram_list.dirty_memory[DIRTY_MEMORY_MIGRATION ]->block[]指向的內存空間,block[]數組的每個元素是一個指向整型變量的指針,該指針指向位圖所在的內存,位圖一共有DIRTY_MEMORY_BLOCK_SIZE個比特位,可以描述DIRTY_MEMORY_BLOCK_SIZE個內存頁的臟狀態
/* 全局內存位圖,描述三個狀態,用三個單獨的位圖來表示 */
#define DIRTY_MEMORY_VGA 0
#define DIRTY_MEMORY_CODE 1
#define DIRTY_MEMORY_MIGRATION 2
#define DIRTY_MEMORY_NUM 3 /* num of dirty bits */#define DIRTY_MEMORY_BLOCK_SIZE ((ram_addr_t)256 * 1024 * 8)
typedef struct {....../* block是一個二維數組,也是指針數組,每個元素指向一個2M大小的內存區域* 這段區域全部用來存放位圖,如果頁的大小是4k,那么一個block[i]指向的位圖* 空間,可以描述8G大小的內存臟頁情況* */unsigned long *blocks[];
} DirtyMemoryBlocks;typedef struct RAMList {......QLIST_HEAD(, RAMBlock) blocks; /* 所有RAMBlock的鏈表頭 */DirtyMemoryBlocks *dirty_memory[DIRTY_MEMORY_NUM]; /* 全局位圖 */
} RAMList;extern RAMList ram_list;
RAMBlock位圖
struct RAMBlock {......uint8_t *host; /* block在主機上的起始虛擬地址 */......ram_addr_t offset; /* block在ramlist空間的偏移 */ram_addr_t used_length; /* block的大小 */ram_addr_t max_length;....../* dirty bitmap used during migration */unsigned long *bmap; /* 存放該block在遷移所有迭代中的臟頁信息位圖 */....../** bitmap to track already cleared dirty bitmap. When the bit is* set, it means the corresponding memory chunk needs a log-clear.* Set this up to non-NULL to enable the capability to postpone* and split clearing of dirty bitmap on the remote node (e.g.,* KVM). The bitmap will be set only when doing global sync.** NOTE: this bitmap is different comparing to the other bitmaps* in that one bit can represent multiple guest pages (which is* decided by the `clear_bmap_shift' variable below). On* destination side, this should always be NULL, and the variable* `clear_bmap_shift' is meaningless.*/unsigned long *clear_bmap; /* 引入KVM_CLEAR_DIRTY_LOG特性后的清零位圖 */uint8_t clear_bmap_shift;
};
全局位圖初始化
全局位圖描述的是qemu為虛擬機分配的可讀寫內存,也就是可讀寫內存的元數據,因此它在qemu為虛機分配內存的時候初始化,而分配內存主要有兩種場景,一是虛機啟動、二是內存熱添加,都調用的ram_block_add接口從主機映射一段qemu進程的地址空間,分配給虛機,全局位圖的初始化也隨之進行。
/* 添加RAMBlock入口 */
ram_block_add qemu_anon_ram_alloc /* mmap映射qemu進程匿名文件地址空間,實現內存分配 */if (new_ram_size > old_ram_size) { /* 如果映射的內存比原來的大,全局的位圖也要擴展才能描述完整的內存空間 */dirty_memory_extend(old_ram_size, new_ram_size); /* 擴展全局的位圖 */}
dirty_memory_extend/* 根據DIRTY_MEMORY_BLOCK_SIZE表示* 數組ramlist.dirty_memory[N->block[ ] 中一個block包含的位圖個數 * 內存大小除以DIRTY_MEMORY_BLOCK_SIZE表示描述內存大小需要多少個block* 由上一節知道,一個block可以描述8G的內存空間,如果增加的內存大小超過8G* 就需要增加block存放位圖,這里計算出描述舊/新的內存空間需要的block數* */ram_addr_t old_num_blocks = DIV_ROUND_UP(old_ram_size,DIRTY_MEMORY_BLOCK_SIZE);ram_addr_t new_num_blocks = DIV_ROUND_UP(new_ram_size,DIRTY_MEMORY_BLOCK_SIZE);/* 檢查新舊內存空間,需要的block數是否一樣,如果一樣* 則原來的位圖足夠描述新增的空間,不需要擴展block* 反之,需要擴展block* *//* Only need to extend if block count increased */if (new_num_blocks <= old_num_blocks) {return;}/* 針對三種用途的位圖,都需要對應擴展,因此遍歷 */for (i = 0; i < DIRTY_MEMORY_NUM; i++) {DirtyMemoryBlocks *old_blocks;DirtyMemoryBlocks *new_blocks;int j;old_blocks = qatomic_rcu_read(&ram_list.dirty_memory[i]);new_blocks = g_malloc(sizeof(*new_blocks) +sizeof(new_blocks->blocks[0]) * new_num_blocks);/* 將老的位圖的block拷貝 */if (old_num_blocks) { memcpy(new_blocks->blocks, old_blocks->blocks,old_num_blocks * sizeof(old_blocks->blocks[0]));} /* 擴展位圖空間,按block的粒度分配位圖空間 */for (j = old_num_blocks; j < new_num_blocks; j++) {new_blocks->blocks[j] = bitmap_new(DIRTY_MEMORY_BLOCK_SIZE);}/* 將新分配好的位圖設置到全局位圖上 */qatomic_rcu_set(&ram_list.dirty_memory[i], new_blocks);if (old_blocks) {g_free_rcu(old_blocks, rcu);}}
同步流程
遷移的每輪迭代開始前,會同步臟頁的信息,分三步進行,首先從kvm獲取一段內存區域的臟頁狀態,然后存放到全局的dirty_memory位圖中,之后在遷移開始前qemu將dirty_memory位圖中包含的臟頁信息同步到每個RAMBlock的位圖中,用于判斷RAMBlock中的頁是否發送。如下圖所示:
查詢
想要查詢臟頁,首先得開啟臟頁記錄,這個動作通過KVM_SET_USER_MEMORY_REGION ioctl完成,KVM流程如下圖所示,具體分析見KVM同步臟頁原理 開啟臟頁記錄后,KVM在每次虛機退出時,檢查pml buffer是否有新增臟頁,如果有,將其同步到memslot的臟頁位圖中,方便qemu在下一次訪問時從memslot中獲取臟頁信息,其流程如下圖所示,具體分析見KVM同步臟頁原理
dirty bitmap
我們重點分析qemu側獲取臟頁信息后怎么處理,首先分析qemu的獲取位圖的流程,這里存在兩種方式,一種是傳統的dirty bitmap,另一種是dirty ring,這兩種方式不能同時存在,但共同的目標都是將虛機的臟頁位圖保存到KVMSlot結構體的dirty_bmap中,首先分析dirty bitmap的流程:
/* kvm臟頁同步實現入口 */
kvm_log_synckvm_physical_sync_dirty_bitmapkvm_slot_get_dirty_log /* 從kvm獲取臟頁信息 *//* 設置kvm填充臟頁信息的位圖dirty_bmap指向KVMSlot->dirty_bmap */d.dirty_bitmap = slot->dirty_bmap;/* 要查詢臟頁位圖的slot由KVMSlot->slot指定 */d.slot = slot->slot | (slot->as_id << 16);/* 調用KVM_GET_DIRTY_LOG ioctl命令字查詢臟頁位圖,命令返回后結果緩存到KVMSlot中 */kvm_vm_ioctl(s, KVM_GET_DIRTY_LOG, &d);
如下圖所示,通過KVM_GET_DIRTY_LOG查詢到的臟頁信息保存在dirty_bmap中,它能夠反映整個slot的臟頁情況,干凈的頁和臟頁都有記錄。
dirty ring
dirty ring是另一種查詢臟頁信息的方式,它的目標和dirty bitmap一致,都是填充slot中的dirty_bmap位圖,但dirty ring并非一次性將slot中的dirty_bmap填充,而是通過kvm-reaper線程周期性的填充多次,每次填充一個bit位,流程如下:
/* kvm-reaper線程 */
kvm_dirty_ring_reaper_thread/* 周期性檢查每個vcpu的dirty ring上是否由臟頁產生* 如果有,將slot的dirty_bmap中對應的bit位置位,標記為臟 */while (true) {sleep(1);kvm_dirty_ring_reapkvm_dirty_ring_reap_locked/* 遍歷每個vcpu,獲取新增的臟頁條目 */CPU_FOREACH(cpu) {total += kvm_dirty_ring_reap_one(s, cpu);}}
kvm_dirty_ring_reap_one/* 取出vcpu上維護的和kvm通過mmap共享的dirty ring */struct kvm_dirty_gfn *dirty_gfns = cpu->kvm_dirty_gfns/* 取出環的大小 */ring_size = s->kvm_dirty_ring_size/* 取出上一次查詢在環中取臟頁條目的位置,從上次的位置繼續取臟頁條目 */fetch = cpu->kvm_fetch_index/* 遍歷環上的條目直到沒有新增臟頁條目 */while (true) {/* 取出條目 */cur = &dirty_gfns[fetch % ring_size]/* 判斷該條目中表示的頁是否為臟,如果不為臟,表示沒有新增的臟頁,停止循環 */if (!dirty_gfn_is_dirtied(cur)) {break;}/* 如果判斷有臟頁條目,將slot的位圖中對應的bit置位 */kvm_dirty_ring_mark_page/* 從KVMState中找到對應的slot */kml = s->as[as_id].ml;mem = &kml->slots[slot_id];/* 將slot的dirty_bmap中對應的bit置位*/set_bit(offset, mem->dirty_bmap);
從上面的流程可以看到,dirty ring獲取臟頁信息時,不是一次性獲取整個slot的位圖信息,而是周期性查詢,每次更新一個頁,最終填滿整個dirty_bmap位圖,如下圖所示: 使能dirty ring之后,原有的kvm_log_sync函數,kvm_slot_get_dirty_log將不在有效,它調用到KVM_GET_DIRTY_LOG ioctl命令查詢時,KVM會檢查是否與dirty ring沖突,如果沖突,則會返回ENXIO,如下:
kvm_vm_ioctlcase KVM_GET_DIRTY_LOG:kvm_vm_ioctl_get_dirty_logkvm_get_dirty_log_protect/* Dirty ring tracking is exclusive to dirty log tracking */if (kvm->dirty_ring_size)return -ENXIO;
保存
從KVM獲取到臟頁信息存放KVMSlot的dirty_bmap之后,這只是個臨時存放位圖的結構,緊接著會將其保存到全局的dirty_memory結構中,流程如下:
kvm_log_synckvm_physical_sync_dirty_bitmap/* 從內核獲取臟頁信息 */kvm_slot_get_dirty_log/* 將臟頁信息保存到ram_list.dirty_memory[]中 */kvm_slot_sync_dirty_pages
kvm_slot_sync_dirty_pages/* 取出slot在ram_addr_t空間的偏移,dirty_memory描述的是整個虛機* ram_addr_t地址空間的臟頁情況,因此需要根據ram_start_offset* 計算slot起始地址在dirty_memory臟頁位圖中的第幾位* */ram_addr_t start = slot->ram_start_offset;/* slot包含的內存大小,除以頁大小,得到slot包含的內存頁數 */ram_addr_t pages = slot->memory_size / qemu_real_host_page_size;/* 輸入slot的起始偏移和包含的內存頁個數,以及slot的臟頁信息* 將其保存到dirty_memory中* */cpu_physical_memory_set_dirty_lebitmap(slot->dirty_bmap, start, pages);
分析保存臟頁信息到位圖的詳細過程,如下圖所示,qemu設計了ram_list.dirty_memory[]用于存放一個虛機的在不同場景下的內存臟頁的位圖,第一個是VGA、第二個是CODE、第三個是MIGRATION,統一由dirty_memory的數組維護,對于遷移過程中臟頁的保存,核心動作就是slot中的臟頁位圖保存到dirty_memory[DIRTY_MEMORY_MIGRATION]的位圖上。 slot是dirty_memory[DIRTY_MEMORY_MIGRATION]位圖的一個子集,需要根據slot.ram_start_offset找到它在整個MIGRATION位圖的位置,而且dirty_memory位圖是以block為單位的,每個block包含了DIRTY_MEMORY_BLOCK_SIZE個bit位,因此slot.ram_start_offset除了用于計算slot第一個頁對應bit在dirty_memory中的偏移,還用來計算第一個頁在第幾個block以及block中的偏移。 保存位圖到dirty bits的過程由cpu_physical_memory_set_dirty_lebitmap函數實現,該函數主要針對兩種情況進行處理:
如果要保存的位圖bitmap,其起始地址start在dirty bits地址區間是long對齊的,那么保存位圖的方式比較簡單,就是按照long長度逐一取出位圖,保存到dirty bits中。 如果要保存的位圖bitmap,其起始地址start在dirty bits地址區間不是long對齊的,從bitmap中取出位圖的方式相同,都是按照long長度取出位圖,但保存位圖到dirty bits時,需要逐一判斷long位圖,取出其中的1bit,然后將dirty bits中對應的bit位置1。
static inline void cpu_physical_memory_set_dirty_lebitmap(unsigned long *bitmap,ram_addr_t start,ram_addr_t pages)
{unsigned long i, j;unsigned long page_number, c;hwaddr addr;ram_addr_t ram_addr;/* 計算要保存pages個頁位圖,需要多少個long型 * HOST_LONG_BITS表示一個long型的變量包含的比特位數* 如果long的長度是64位,則包含64bit,每個bit可以描述一個頁的臟狀態* 一個long可以表示64個頁的臟狀態,以下操作將slot包含的內存頁數* 向上取整并對齊64,得到的len就是64對齊的。* 可以由len/64個long變量來存放pages個頁需要的位圖**/unsigned long len = (pages + HOST_LONG_BITS - 1) / HOST_LONG_BITS;unsigned long hpratio = qemu_real_host_page_size / TARGET_PAGE_SIZE;/* 根據slot的起始地址start,計算slot的第一個頁在dirty_memory中對應的bit位* 需要由多少個long型位圖表示,page代表long型的位圖個數* */unsigned long page = BIT_WORD(start >> TARGET_PAGE_BITS);/* 如果slot的起始地址對應的bit位,恰好是long對齊的* 那么就可以直接通過拷貝long型位圖到dirty_memory,以此保存位圖 * *//* start address is aligned at the start of a word? */if ((((page * BITS_PER_LONG) << TARGET_PAGE_BITS) == start) &&(hpratio == 1)) {unsigned long **blocks[DIRTY_MEMORY_NUM];unsigned long idx;unsigned long offset;long k;/* 計算要保存的pages位圖可以由多少個long型位圖表示 */long nr = BITS_TO_LONGS(pages);/* dirty_memory[]中的一個block可以表示DIRTY_MEMORY_BLOCK_SIZE個* bit位,(start >> TARGET_PAGE_BITS)表示slot包含的內存第一個頁對應bit位* 如果超過一個block可以表示的bit范圍,需要存放到下一個 * (start >> TARGET_PAGE_BITS) / DIRTY_MEMORY_BLOCK_SIZE表示* slot包含的內存第一個頁對應bit位落在第幾個block位圖中 */idx = (start >> TARGET_PAGE_BITS) / DIRTY_MEMORY_BLOCK_SIZE;/* offset表示slot包含的內存第一個頁的位在block位圖中的偏移 */offset = BIT_WORD((start >> TARGET_PAGE_BITS) %DIRTY_MEMORY_BLOCK_SIZE);WITH_RCU_READ_LOCK_GUARD() {/* 取出dirty_memory中的三個位圖,保存到block臨時變量中 */for (i = 0; i < DIRTY_MEMORY_NUM; i++) {blocks[i] =qatomic_rcu_read(&ram_list.dirty_memory[i])->blocks;}/* * 在slot的起始頁bit位是long對齊的情況下,只需要每次拷貝一個long型長度的位圖* nr表示slot包含的內存可以占用多少個long型的位圖* 逐個從slot的dirty_bmap中取出保存到dirty_memory中**/for (k = 0; k < nr; k++) {if (bitmap[k]) {unsigned long temp = leul_to_cpu(bitmap[k]);qatomic_or(&blocks[DIRTY_MEMORY_VGA][idx][offset], temp);if (global_dirty_tracking) {qatomic_or(&blocks[DIRTY_MEMORY_MIGRATION][idx][offset],temp);}}if (++offset >= BITS_TO_LONGS(DIRTY_MEMORY_BLOCK_SIZE)) {offset = 0;idx++;}}}} else {/* 如果start地址在dirty bits位圖中不是long對齊的 */if (!global_dirty_tracking) {clients &= ~(1 << DIRTY_MEMORY_MIGRATION);}/** bitmap-traveling is faster than memory-traveling (for addr...)* especially when most of the memory is not dirty.*/for (i = 0; i < len; i++) {if (bitmap[i] != 0) {/* 從bitmap中按照long長度逐一取出位圖 */c = leul_to_cpu(bitmap[i]);do {/* 從c的低位開始統計0的個數,直到遇到1 * 即從c的右邊開始數0的位數,直到遇到1* */j = ctzl(c);/* 獲取到1的位置j后,將其從c中清除,方便下一次統計 */c &= ~(1ul << j);/* 根據j的位置算出在dirty bits位圖的位置,將其置位 */page_number = (i * HOST_LONG_BITS + j) * hpratio;addr = page_number * TARGET_PAGE_SIZE;ram_addr = start + addr;cpu_physical_memory_set_dirty_range(ram_addr,TARGET_PAGE_SIZE * hpratio, clients);} while (c != 0);}}}
}
分析保存動作是或操作而不是直接賦值的原因,dirty_memory中的bit位有兩種狀態:
置位:表示該臟頁qemu雖然查詢到了,但是還沒有更新到RAMBlock的位圖中,只有從dirty_memory讀取信息到RAMBlock的bmap后,dirty_memory中的臟頁位才會清零,詳細分析見下一小節。因此原來的頁是臟的,如果新一輪查詢是干凈的,不能直接將其清零,因為qemu還沒有使用這個信息(沒有發送臟頁),需要保留臟狀態,直到qemu使用后(將這個狀態保存到RAMBlock的位圖中),才能清零,因此需要進行或操作。如果新一輪查詢是臟的,那么正好,這一輪和上一輪查詢結果一樣,都是臟頁的,將頁設置為臟,雖然上一輪查詢的信息qemu沒有用到(也沒有發送臟頁),但本輪和上一輪結果一樣,可以直接使用這一輪的結果。這樣進行或操作的目的,是保證無論原來的頁是臟的還是干凈的,經過本輪的查詢,上一輪應該發送的頁仍然不會被漏掉,只是會推遲到本輪發送。 清零:兩種情況會出現dirty_memory中bit清零,第一種是上輪查詢到該位對應頁是干凈的,第二種是上輪查詢到該位對應頁是臟的,但qemu已經發送出去了,兩種情況下,如果本輪查詢到頁狀態是干凈的,都應該置位,表示頁變臟頁了。因此同樣需要做或操作。
總結一下,由于bit位為1表示臟頁,反之表示干凈頁,或操作的本質是將本輪查詢的臟頁保存到dirty_memory中,但對于dirty_memory中原來存在的臟頁信息,需要保留下來。
RAMBlock同步
臟頁位圖信息被保存到ram_list.dirty_memory之后,上面介紹到可以用在三個場合,這里分析在遷移時候怎么用這個信息,遷移有三個地方用到了ram_list.dirty_memory保存的位圖,將位圖信息取出,同步到RAMBlock的bmap中,分別是:
遷移開始前,臟頁日志記錄打開之后 每輪遷移迭代開始前 遷移進入最后一輪迭代前
migration_bitmap_sync_precopymigration_bitmap_syncramblock_sync_dirty_bitmapcpu_physical_memory_sync_dirty_bitmap
同步的核心目的是將dirty_memory中的位圖信息同步到RAMBlock的位圖中,同時將dirty_memory中的位清零,如下圖所示: 分析同步位圖到RAMBlock的bmap細節:
/* 函數的參數用于保存從dirty_memory同步的位圖信息 * rb表示要同步的RAMBlock,start表示從RAMBlock區間的什么位置開始同步* 通常是0,表示從RAMBlock的區間開始處,length表示要同步到的RAMBlock的長度 */
cpu_physical_memory_sync_dirty_bitmap(RAMBlock *rb, ram_addr_t start, ram_addr_t length) /* start + rb->offset 表示RAMBlock開始同步的頁在ram address space的位置,長度是字節 * (start + rb->offset) >> TARGET_PAGE_BITS 表示RAMBlock開始同步的頁距離ram address space起始位置多少個頁 * BIT_WORD((start + rb->offset) >> TARGET_PAGE_BITS)表示,將頁用位圖表示后可以占用多少個long型的位圖* 因此word表示RAMBlock開始同步的頁距ram address space起始位置有多少個long型的位圖* */ unsigned long word = BIT_WORD((start + rb->offset) >> TARGET_PAGE_BITS);/* 取出要同步到RAMBlock的位圖 */unsigned long *dest = rb->bmap;/* 如果要同步的RAMBlock的起始位置對應頁的bit恰好是long位圖對齊的,那么可以直接將dirty_memory中位圖按照* long型位圖的長度依次拷貝到bmap中,這里就是判斷是否屬于這種情況 */if (((word * BITS_PER_LONG) << TARGET_PAGE_BITS) == (start + rb->offset))) {/* 計算要拷貝多少個long型的位圖 */int nr = BITS_TO_LONGS(length >> TARGET_PAGE_BITS);/* 計算開始拷貝的頁的位圖在第幾個block */unsigned long idx = (word * BITS_PER_LONG) / DIRTY_MEMORY_BLOCK_SIZE;/* 計算開始拷貝的頁的位圖在block中的偏移 */unsigned long offset = BIT_WORD((word * BITS_PER_LONG) % DIRTY_MEMORY_BLOCK_SIZE);/* 因為允許從RAMBlock位圖的任意位置開始同步* 因此計算起始同步的位置對應的位距RAMBlock起始區間有多少個long型位圖* 如果start為0,page也是0,那么表示從RAMBlock起始區間開始同步位圖**/unsigned long page = BIT_WORD(start >> TARGET_PAGE_BITS);/* 取出dirty_memory的位圖信息 */src = qatomic_rcu_read(&ram_list.dirty_memory[DIRTY_MEMORY_MIGRATION])->blocks;/* 從page開始拷貝long型位圖到RAMBlock的bmap位圖上,拷貝nr個long型位圖 */for (k = page; k < page + nr; k++) {if (src[idx][offset]) {/* 將dirty_memory中的位圖讀取到bits中,完成后將dirty_memory對應的區域清零 */unsigned long bits = qatomic_xchg(&src[idx][offset], 0);unsigned long new_dirty;/* 保存bmap中原來干凈臟頁位,這里用于計算新增的臟頁數,并非用于同步操作 */new_dirty = ~dest[k];/* 核心的同步操作,將從dirty_memory中取到的位圖信息保存到RAMBlock的bmap中 * dest指向的bmap位圖,這里同樣是或操作,對于原來是臟頁的位,bmap中保持不變* 對于原來是干凈的位,如果dirty_memory中為臟,則結果為臟,同步到bmap中 */dest[k] |= bits;/* 計算新增的臟頁數 */new_dirty &= bits;/* 累加新增的臟頁數 */num_dirty += ctpopl(new_dirty);}} }
發送臟頁
臟頁位圖dirty_bitmap同步到RAMBlock->bmap中之后,遷移線程依據此信息查找RAMBlock中所有臟頁,然后發送,下面介紹遷移線程從位圖查臟頁的原理,流程圖如下: 遷移線程根據RAMBlock中的bmap信息,查尋臟頁,如果有臟頁,在發送之前將位圖清零,每發送一個頁就清零對應臟位,如下圖所示: 具體發送流程如下:
/* 開始查找并發送RAMBlock里面的臟頁 */
ram_find_and_save_block/* 從給定的RAMBlock的位圖中查找下一個被置位的 bit,找到第一個臟頁對應位就停止 */find_dirty_blockmigration_bitmap_find_dirtyfind_next_bit/* 如果RAMBlock中有臟頁,開始查找并發送 */ram_save_host_page/* 遍歷位圖中的每個bit,檢查是否置位,如果置位,完成兩個事情* 1. 如果使能了KVM_CLEAR_DIRTY_LOG特性,臟頁查詢中的重保護動作將從臟頁查詢的系統調用時段推遲到用戶態發送臟頁時進行* 因此這里需要檢查是否開啟此特性,如果開啟,調用kvm_log_clear回調重保護kvm中的頁表* 2. 如果bit被設置成1,將其清零,因為后面即將發送臟頁,因此在發送前將其清零,表示該臟頁已經發送。同時,將遷移的剩余臟頁數減1,更新統計信息方便計算臟頁速率和判斷是否進入最后一輪迭代* */migration_bitmap_clear_dirty/* 執行上面介紹的第一個事情,發送系統調用重保護kvm中的頁表 */memory_region_clear_dirty_bitmap/* 如果bit被置位,將其清零 */test_and_clear_bit/* 更新統計信息 */rs->migration_dirty_pages--
總結
以上是生活随笔 為你收集整理的QEMU同步脏页原理 的全部內容,希望文章能夠幫你解決所遇到的問題。
如果覺得生活随笔 網站內容還不錯,歡迎將生活随笔 推薦給好友。