文章目錄 前言 內存鏡像格式 libvirt元數據 qemu內存數據 section分析 configuration section start section part section end section full section vmdescription section Q&A 
 
前言  
qemu內存遷移功能基于savevm和loadvm接口實現,savevm可以保存一個運行態虛擬機所有內存和設備狀態到鏡像文件,loadvm可以實現從鏡像狀態文件讀取信息,恢復虛擬機。 qemu虛機內存的遷出通過savevm功能實現,遷入通過loadvm實現。libvirt使用此接口不僅實現了內存遷移,還實現了內存快照。內存遷移,將一個節點上運行的虛擬機,動態遷移到另一個節點上;內存快照,將運行虛擬機的內存快照保存到鏡像文件中,通過快照還原,可以將虛擬機還原到做內存快照時刻的狀態。內存遷移和快照都基于savevm/loadvm接口實現,因此分析快照鏡像相當于于分析內存遷移的靜態數據。本文基于這個原理,通過分析libvirt save命令保存的鏡像文件,來間接分析qemu內存遷移的內存和格式。 內存鏡像格式  
我們分析的內存鏡像使用下面這條命令產生,c75_test是測試虛擬機,后面內存鏡像文件的輸出路徑: 下面這條命令可以啟動并將虛擬機還原到快照時刻的狀態: save命令生成的內存鏡像格式由兩部分組成,第一部分由libvirt寫入,第二部分由qemu寫入。libvirt寫入的元數據,主要用于內存快照的恢復,由header,xml和cookie組成。qemu寫入的部分是內存數據,包括描述內存的元數據和真正的內存數據。如下圖所示: libvirt元數據  
virQEMUSaveData為libvirt元數據數據結構,如下:  
#define QEMU_SAVE_MAGIC   "LibvirtQemudSave"
#define QEMU_SAVE_PARTIAL "LibvirtQemudPart"struct _virQEMUSaveHeader {char magic[sizeof(QEMU_SAVE_MAGIC)-1];uint32_t version;uint32_t data_len;uint32_t was_running;uint32_t compressed;uint32_t cookieOffset;uint32_t unused[14];
};typedef struct _virQEMUSaveData virQEMUSaveData;
typedef virQEMUSaveData *virQEMUSaveDataPtr;
struct _virQEMUSaveData {virQEMUSaveHeader header;char *xml;char *cookie;
};
 
打印內存鏡像的前128字節,libvirt元數據header占了前92(0X5C)字節,之后是xml的內容,如下: header字段的data_len是0XAAB=2731,表示xml和cookie的總長度為2731,加上header的92字節之后,就是qemu內存數據在內存鏡像中的偏移:2731+92=2823(0XB07),如下: qemu內存數據  
遷移模型  
 
標臟所有的內存頁 迭代遷移所有臟頁,直到剩余臟頁降低到一定水線 暫停虛擬機,一次性遷移剩余臟頁,然后遷移設備狀態,啟動目的端虛擬機  
遷移第一階段會把所有頁標臟,首次遷移肯定會傳輸所有內存頁,第二次遷移前如果計算得到的剩余臟頁降低到水線以下,可以暫停虛擬機剩余臟頁一次性遷移完,因此遷移最理想的狀態是迭代兩次;當虛擬機內存變化大時,會不斷有臟頁產生,遲遲不能降到水線以下,內存變化越大遷移越難收斂,最糟糕的情況是內存臟頁永遠無法降到水線以下,遷移永遠無法完成 針對上述問題,qemu提出postcopy遷移模式,把傳統遷移模式稱為precopy,兩種模型的不同點在于第二次及其之后的內存臟頁拷貝時機不同。precopy模型的臟頁拷貝在目的端虛擬機啟動之前必須完成;postcopy模型的臟頁拷貝在啟動之后還會繼續。 postcopy的內存遷移也有三個階段: 遷移設備狀態 標臟所有內存頁,將源端所有內存頁拷貝到目的端,啟動虛擬機 當目的端虛機訪問到內存臟頁時,會觸發缺頁異常,qemu從源端拷貝臟頁對應內存  
分析這種遷移模型可以知道,隨著目的端虛機內存訪問覆蓋的地址空間越來越多,臟頁的拷貝會越來越少,直到不存在。并且第一次內存訪問任何地址都會造成缺頁,從而觸發源端的拷貝。本文介紹的是precopy模式下的內存遷移格式。 數據結構  
SaveStateEntry是內存遷移的靜態單位,它是一個可遷移信息的抽象,遷移的實現原理就是將一個個SaveStateEntry傳送到目的端。SaveStateEntry包含的信息可以是內存頁(pages),可以是設備狀態(VMState),通過其is_ram成員可以區分。對于運行的虛擬機,這些信息隨時可能改變,是動態變化的,因此SaveStateEntry還必須包含收集這些信息的操作函數。SaveStateEntry數據結構如下:  
typedef struct SaveStateEntry {QTAILQ_ENTRY(SaveStateEntry) entry;	 // 所有SaveState組織成隊列由全局變量savevm_state.handler維護,entry用來加入該隊列char idstr[256];	 	/* qemu將同類可遷移信息組織成一個SaveStateEntry,比如timer,ram,dirty-bitmap,apic等,idstr是這類信息的類名 */int instance_id;		/* 同一個idstr的不同se,用instance_id來區分 *//* SaveStateEntry.idstr這個域表示的僅僅是相同類型se的名字* 同類se中還有不同se實例,這些實例在savevm_state.handlersl鏈表中* 通過instance_id或者alias_id區分*/int alias_id;int version_id;/* version id read from the stream * 從源端讀取到的VMState的版本ID*/int load_version_id;int section_id; /* 可遷移信息遷移過程中以section為單位傳輸,qemu為每個添加到SaveState.hanler鏈表上se分配一個id,從0開始遞增  *//* section id read from the stream */int load_section_id;const SaveVMHandlers *ops; // 內存信息的收集操作,比如ram,ops包括了內存傳輸前的設置操作,內存傳輸操作等const VMStateDescription *vmsd; // 設備狀態信息,包含設備狀態的搜集操作,比如保存設備狀態,加載設備狀態等void *opaque;CompatEntry *compat;int is_ram;	// 區分SaveStateEntry包含的是內存信息,還是設備狀態信息
} SaveStateEntry;
 
SaveState管理所有的SaveStateEntry,它將所有SaveStateEntry添加到自己的handlers成員中,通過global_section_id為每個entry分配section_id。初始化時global_section_id為0,每添加一個entry,global_section_id加1。SaveState數據結構如下:  
typedef struct SaveState {QTAILQ_HEAD(, SaveStateEntry) handlers;int global_section_id;uint32_t len; const char *name;uint32_t target_page_bits;uint32_t caps_count;MigrationCapability *capabilities;
} SaveState;
 
內存遷移的核心實現,是遍歷全局變量savevm_state的handlers成員,它指向一個隊列,隊列的每個成員是個SaveStateEntry,遷移內存就是將其中包含內存信息(is_ram)的SaveStateEntry傳輸,遷移設備狀態就是將其中包含設備狀態的SaveStateEntry傳輸。全局變量savevm_state的聲明如下:  
static SaveState savevm_state = {.handlers = QTAILQ_HEAD_INITIALIZER(savevm_state.handlers),.global_section_id = 0, 
};
 
內存遷移需要遷移的最重要的SaveStateEntry是ram entry,它的idstr就是"ram",它集合了qemu向主機申請的所有虛擬內存,這個內存用來供虛擬機使用,是遷移內存最重要的內容,ram SaveStateEntry的注冊如下:  
void ram_mig_init(void) 
{       qemu_mutex_init(&XBZRLE.lock);register_savevm_live(NULL, "ram", 0, 4, &savevm_ram_handlers, &ram_state);
}
 
總結一下,QEMU的內存遷移設計是將所有設備分類,抽象出遷移一類設備的結構體SaveStateEntry,其中內存設備是一大類,普通設備是另一大類,這兩類設備的SaveStateEntry被組織到一個全局的SaveStateEntry鏈表。遷移時遍歷鏈表中的這些結構體,然后通過其提供的方法遷移數據就可以了。 總體布局  
qemu內存遷移以section為單位,遷移的所有信息都被封裝成一個個section寫入輸出流。precopy模式下會先迭代傳輸內存,當剩余內 內存遷移的總體布局如下,首先是用于標記qemu內存遷移開始的magic,然后是版本信息。之后的所有內容,都以section為單位,section的格式見下圖右上角,第1個字節是類型字段,后面的內容隨不同section而變化,section有以下幾種:  
#define QEMU_VM_SECTION_START        0x01
#define QEMU_VM_SECTION_PART         0x02
#define QEMU_VM_SECTION_END          0x03
#define QEMU_VM_SECTION_FULL         0x04
#define QEMU_VM_SUBSECTION           0x05
#define QEMU_VM_VMDESCRIPTION        0x06
#define QEMU_VM_CONFIGURATION        0x07
 
 
遷移的第一個section比較特殊,是configuration section,顧名思義,它的作用是配置遷移,是一個設備狀態的section,表明了源端虛擬機的machine type,當目的端解析該section時會比較machine type字段,如果不相同,不允許進行遷移,由此限制了源端和目的端不同machine type的虛機遷移。 遷移的第二個section也比較特殊,它是所有遷移內存的元數據,包括所有內存SaveStateEntry包含的內存總大小,每個內存SaveStateEntry指向的RAMBlock的總長度等。 從第三個section開始是內存SaveStateEntry對應的section,它的內容結束后,是設備SaveStateEntry對應的section。 最后一個section描述所有遷移的設備狀態域。 標記遷移開始  
qemu遷移從migration_thread開始,在qemu_savevm_state_header中發送magic和版本信息,可能還會發送configuration section,如下:  
#define QEMU_VM_FILE_MAGIC           0x5145564d
#define QEMU_VM_FILE_VERSION         0x00000003void qemu_savevm_state_header(QEMUFile *f)
{   trace_savevm_state_header();qemu_put_be32(f, QEMU_VM_FILE_MAGIC);		// 發送"QEVM" magicqemu_put_be32(f, QEMU_VM_FILE_VERSION);		// 發送版本信息if (migrate_get_current()->send_configuration) {	// 如果標記了發送configuration,發送qemu_put_byte(f, QEMU_VM_CONFIGURATION);	vmstate_save_state(f, &vmstate_configuration, &savevm_state, 0);}
} 
 
目的端接收遷移內容從qemu_loadvm_state開始,當判斷到magic和版本不對時,會終止遷移,如下:  
int qemu_loadvm_state(QEMUFile *f)
{......v = qemu_get_be32(f);if (v != QEMU_VM_FILE_MAGIC) {	// 判斷magicerror_report("Not a migration stream");return -EINVAL;}       ......            if (v != QEMU_VM_FILE_VERSION) {	// 判斷版本error_report("Unsupported migration stream version");return -ENOTSUP;}......
}
 
magic和version各占用遷移開始的4字節,如下: section分析  
configuration section  
configration section是一個VMState的section,它傳輸的VMStateDescription如下:  
static const VMStateDescription vmstate_configuration = {.name = "configuration",.version_id = 1,.pre_load = configuration_pre_load,.post_load = configuration_post_load,.pre_save = configuration_pre_save,	// 保存VMState信息的操作函數.fields = (VMStateField[]) {VMSTATE_UINT32(len, SaveState),VMSTATE_VBUFFER_ALLOC_UINT32(name, SaveState, 0, NULL, len),VMSTATE_END_OF_LIST()},.subsections = (const VMStateDescription*[]) {&vmstate_target_page_bits,&vmstate_capabilites,NULL}
};  
 
vmstate_configuration就是一個VMState,其中SaveState的len和name成員值是必須傳送的,因為它們在vmstate_configuration.fields數組成員中,而fields描述的是VMState必須傳送的信息。還有一個prev_save成員,它是搜集VMState信息的操作函數,如下:  
static int configuration_pre_save(void *opaque)
{   SaveState *state = opaque;const char *current_name = MACHINE_GET_CLASS(current_machine)->name;	// 獲取machine type......state->len = strlen(current_name);	// 計算machine type長度state->name = current_name;			// 獲取machine type......             
}
 
在接收端,qemu_loadvm_state中檢查是否傳輸了configuration section,如果需要傳輸但是沒有傳輸,終止,流程如下:  
qemu_loadvm_stateif (migrate_get_current()->send_configuration) {if (qemu_get_byte(f) != QEMU_VM_CONFIGURATION) {	// 如果沒有configuratin section,終止error_report("Configuration section missing");qemu_loadvm_state_cleanup();return -EINVAL;}ret = vmstate_load_state(f, &vmstate_configuration, &savevm_state, 0)......}
 
如果傳輸了configuration section,首先查看源端發來的VMState版本是否高于本地的,如果高于本地的,終止檢查和本地的machine type是否相同,如果不同,也終止,流程如下:  
vmstate_load_state(f, &vmstate_configuration, &savevm_state, 0)
static int configuration_post_load(void *opaque, int version_id)
{   SaveState *state = opaque;const char *current_name = MACHINE_GET_CLASS(current_machine)->name;/* 比較machine type是否與本地不同,不同就終止 */if (strncmp(state->name, current_name, state->len) != 0) {error_report("Machine type received is '%.*s' and local is '%s'",(int) state->len, state->name, current_name);return -EINVAL;}   ......
}
 
configuration section內容如下,section type為0x07,machine type的長度為14,name為pc-i440fx-2.12 start section  
qemu_savevm_state_setup會發送start section,函數會遍歷全局變量savevm_state.handlers,通過判斷ops,ops->setup,ops->is_active,將不滿足條件的SaveStateEntry篩選出去,最終會獲取到ram對應的SaveStateEntry,調用其對應的save_setup函數ram_save_setup,如下:  
void qemu_savevm_state_setup(QEMUFile *f)
{   SaveStateEntry *se;Error *local_err = NULL; int ret;trace_savevm_state_setup();QTAILQ_FOREACH(se, &savevm_state.handlers, entry) {if (!se->ops || !se->ops->save_setup) {continue;}if (se->ops && se->ops->is_active) {if (!se->ops->is_active(se->opaque)) {continue;}}save_section_header(f, se, QEMU_VM_SECTION_START);ret = se->ops->save_setup(f, se->opaque);save_section_footer(f, se);......}
}
 
savevm_state的組織如下,section_id為2的entry就是ram entry,它會在setup的遍歷中被選中,然后調用它的save_setup回調函數ram_save_setup ram_save_setup會遍歷ram_list.blocks上的每一個RAMBlock,發送其長度和idstr,流程如下:  
static int ram_save_setup(QEMUFile *f, void *opaque)
{       RAMState **rsp = opaque;RAMBlock *block;....../* 發送ram總長度 */qemu_put_be64(f, ram_bytes_total_common(true) | RAM_SAVE_FLAG_MEM_SIZE);/* 遍歷ram_list.blocks的每個RAMBlock,發送其idstr和已使用長度 */       RAMBLOCK_FOREACH_MIGRATABLE(block) {qemu_put_byte(f, strlen(block->idstr));qemu_put_buffer(f, (uint8_t *)block->idstr, strlen(block->idstr));qemu_put_be64(f, block->used_length);if (migrate_postcopy_ram() && block->page_size != qemu_host_page_size) {qemu_put_be64(f, block->page_size);}if (migrate_ignore_shared()) {qemu_put_be64(f, block->mr->addr);qemu_put_byte(f, ramblock_is_ignored(block) ? 1 : 0);}}   ....../* 結束發送 */qemu_put_be64(f, RAM_SAVE_FLAG_EOS);qemu_fflush(f);......
}
 
ram_list是一個全局變量,它維護著qemu向主機申請的所有虛擬內存,被組織成一個鏈表,每個成員是一個RAMBlock結構,它表示主機分配給qemu的一段物理內存,qemu正是通過這些內存實現內存模擬,RAMBlock數據結構如下:  
struct RAMBlock {           struct rcu_head rcu;struct MemoryRegion *mr;                uint8_t *host;	/* HVA,qemu通過malloc向主機申請得到 */uint8_t *colo_cache; /* For colo, VM's ram cache */ram_addr_t offset;	/* 本段內存相對host地址的偏移 */ram_addr_t used_length; /* 已使用的內存長度 */ram_addr_t max_length;	/* 申請的內存長度*/void (*resized)(const char*, uint64_t length, void *host);uint32_t flags;/* Protected by iothread lock.  */char idstr[256];/* RCU-enabled, writes protected by the ramlist lock */QLIST_ENTRY(RAMBlock) next;	/* 指向鏈表的下一個成員 */QLIST_HEAD(, RAMBlockNotifier) ramblock_notifiers;int fd;                    size_t page_size;/* 用于遷移時記錄臟頁的位圖 *//* dirty bitmap used during migration */unsigned long *bmap; /* bitmap of pages that haven't been sent even once* only maintained and used in postcopy at the moment* where it's used to send the dirtymap at the start* of the postcopy phase*/unsigned long *unsentmap;/* bitmap of already received pages in postcopy */unsigned long *receivedmap;
}; 
 
ram_list的組織結構圖如下: 選取其中的幾個RAMBlock,在qemu進程的內存映射中查看其所屬的vm_area_struct區域,首先通過命令 pc.ram vga.vram pc.bios  
start section中主要發送RAMBlock元數據信息,主要是RAMBlock的idstr和used_length,下圖中綠色部分,這里可以看到qemu RAMBlock的構成,我們熟悉的內存有pc.ram,vga.vram,pc.bios等 下面是內存鏡像中start section的分析結果: part section  
part section傳輸內存頁,是內存遷移的主要內容,在qemu_savevm_state_iterate中發起,該函數和qemu_savevm_state_setup類似,也會遍歷全局變量savevm_state.handlers,調用滿足條件的SaveStateEntry對應的save_live_iterate函數,如下:  
int qemu_savevm_state_iterate(QEMUFile *f, bool postcopy)
{SaveStateEntry *se;int ret = 1;QTAILQ_FOREACH(se, &savevm_state.handlers, entry) {if (!se->ops || !se->ops->save_live_iterate) {continue;}if (se->ops && se->ops->is_active) {if (!se->ops->is_active(se->opaque)) {continue;}}if (se->ops && se->ops->is_active_iterate) {if (!se->ops->is_active_iterate(se->opaque)) {continue;}}....../* 找到合適的SaveStateEntry,首先寫入part section的頭部*/save_section_header(f, se, QEMU_VM_SECTION_PART);	/* 調用save_live_iterate,如果是ram section,調用ram_save_iterate */ret = se->ops->save_live_iterate(f, se->opaque);/* 發送結束,標記section結束 */save_section_footer(f, se);......
}
 
ram_save_iterate會遍歷ram_list.blocks所有RAMBlock,根據位圖找出臟頁,然后遷移內存,如下:  
ram_save_iterateram_find_and_save_blockpss.block = rs->last_seen_block/* 取出ram_list維護的第一個RAMBlock */if (!pss.block) {pss.block = QLIST_FIRST_RCU(&ram_list.blocks);}/* 根據位圖查找臟的RAMBlock */find_dirty_block(rs, &pss, &again)/* 從位圖中找到下一個臟頁,如果找到臟頁的索引 */pss->page = migration_bitmap_find_dirty(rs, pss->block, pss->page)ram_save_host_page(rs, &pss, last_stage)/* 發送臟頁*/ram_save_target_page(rs, pss, last_stage)	ram_save_pagesave_normal_pagesave_page_headerqemu_put_buffer_async
 
save_normal_page函數實現以各內存頁的遷移,它首先發送描述內存頁的頭部信息,主要是內存頁在RAMBlock中的偏移:  
save_page_header(rs, rs->f, block, offset | RAM_SAVE_FLAG_PAGE)/*** save_page_header: write page header to wire** If this is the 1st block, it also writes the block identification* 如果發送的內存頁所屬的RAMBlock是一個新的RAMBlock,將RAMBlock的idstr一起發送* Returns the number of bytes written** @f: QEMUFile where to send the data* @block: block that contains the page we want to send* @offset: offset inside the block for the page*          in the lower bits, it contains flags*/
static size_t save_page_header(RAMState *rs, QEMUFile *f,  RAMBlock *block,ram_addr_t offset)
{   size_t size, len;if (block == rs->last_sent_block) {offset |= RAM_SAVE_FLAG_CONTINUE;}   qemu_put_be64(f, offset);	/* 發送內存頁在RAMBlock內存區域的偏移*/size = 8;if (!(offset & RAM_SAVE_FLAG_CONTINUE)) {len = strlen(block->idstr);qemu_put_byte(f, len);qemu_put_buffer(f, (uint8_t *)block->idstr, len);size += 1 + len;rs->last_sent_block = block;}return size;
}
 
頭部信息發送完之后,發送內存頁的內容,這是內存遷移的核心目的:  
/** directly send the page to the stream** Returns the number of pages written.** @rs: current RAM state* @block: block that contains the page we want to send* @offset: offset inside the block for the page* @buf: the page to be sent* @async: send to page asyncly*/
static int save_normal_page(RAMState *rs, RAMBlock *block, ram_addr_t offset,uint8_t *buf, bool async)
{ram_counters.transferred += save_page_header(rs, rs->f, block,offset | RAM_SAVE_FLAG_PAGE);if (async) {qemu_put_buffer_async(rs->f, buf, TARGET_PAGE_SIZE,migrate_release_ram() &migration_in_postcopy());} else {qemu_put_buffer(rs->f, buf, TARGET_PAGE_SIZE);}ram_counters.transferred += TARGET_PAGE_SIZE;ram_counters.normal++;return 1;
}   
 
part section發送的內容如下,以pc.ram RAMBlock為例,發送pc.ram的第一個內存頁,這時檢查到該頁所屬的RAMBlock是一個新的RAMBlock,page header除了填寫必須的偏移,還會附加上RAMBlock的idstr,之后如果再次發送pc.ram RAMBlock包含的內存頁,page header就只包含該頁在RAMBlock中的偏移,對于vga.vram,pc.bios等其它RAMBlock,在發送時也做同樣的處理。 end section  
遷移內存迭代一次后,下一次遷移前會計算剩余臟頁數,將其與水線比較,如果臟頁數大于水線,繼續遷移,如果小于水線,走migration_completion流程,migration_iteration_run是遷移迭代函數,如下:  
/** Return true if continue to the next iteration directly, false* otherwise.*/
static MigIterateState migration_iteration_run(MigrationState *s)
{uint64_t pending_size, pend_pre, pend_compat, pend_post;bool in_postcopy = s->state == MIGRATION_STATUS_POSTCOPY_ACTIVE;/* pending_size,剩余的臟頁總和 */qemu_savevm_state_pending(s->to_dst_file, s->threshold_size, &pend_pre,&pend_compat, &pend_post);pending_size = pend_pre + pend_compat + pend_post;trace_migrate_pending(pending_size, s->threshold_size,pend_pre, pend_compat, pend_post);/* 當剩余臟頁數大于水線時,繼續遷移 */if (pending_size && pending_size >= s->threshold_size) {/* Still a significant amount to transfer */if (migrate_postcopy() && !in_postcopy &&pend_pre <= s->threshold_size &&atomic_read(&s->start_postcopy)) {if (postcopy_start(s)) {error_report("%s: postcopy failed to start", __func__);}return MIG_ITERATE_SKIP;}/* Just another iteration step */qemu_savevm_state_iterate(s->to_dst_file,s->state == MIGRATION_STATUS_POSTCOPY_ACTIVE);} else {	/* 小于水線時,進入遷移完成階段 */trace_migration_thread_low_pending(pending_size);migration_completion(s);return MIG_ITERATE_BREAK;}return MIG_ITERATE_RESUME;
}
 
調用migration_completion函數進入遷移完成階段之后,會調用qemu_savevm_state_complete_precopy,該函數工作流程和qemu_savevm_state_iterate類似,迭代查找所有SaveStateEntry,找到ram SaveStateEntry之后,將里面所有RAMBlock的內存內容一次性發送,如下:  
int qemu_savevm_state_complete_precopy(QEMUFile *f, bool iterable_only,bool inactivate_disks)
{           QJSON *vmdesc;int vmdesc_len;SaveStateEntry *se;int ret;......QTAILQ_FOREACH(se, &savevm_state.handlers, entry) {if (!se->ops ||(in_postcopy && se->ops->has_postcopy &&se->ops->has_postcopy(se->opaque)) ||(in_postcopy && !iterable_only) ||!se->ops->save_live_complete_precopy) {continue;}if (se->ops && se->ops->is_active) {if (!se->ops->is_active(se->opaque)) {continue;}}trace_savevm_section_start(se->idstr, se->section_id);save_section_header(f, se, QEMU_VM_SECTION_END);ret = se->ops->save_live_complete_precopy(f, se->opaque);trace_savevm_section_end(se->idstr, se->section_id, ret);save_section_footer(f, se);......}......
}
 
end section發送的內存內容格式和part section類似,只是頭部的section type變成了0x3 full section  
qemu遷移一個設備狀態VMState使用的是full section,在qemu_savevm_state_complete_precopy中,遷移完剩余內存之后緊接著就遷移VMState,如下:  
int qemu_savevm_state_complete_precopy(QEMUFile *f, bool iterable_only,bool inactivate_disks)
{....../* json對象記錄所有遷移的VMState,如果需要,會在遷移結束后發送到目的端 */vmdesc = qjson_new();json_prop_int(vmdesc, "page_size", qemu_target_page_size());json_start_array(vmdesc, "devices");QTAILQ_FOREACH(se, &savevm_state.handlers, entry) {/* 如果SaveStateEntry的vmsd為空,說明它是一個內存section,跳過*/if ((!se->ops || !se->ops->save_state) && !se->vmsd) {continue;}if (se->vmsd && !vmstate_save_needed(se->vmsd, se->opaque)) {trace_savevm_section_skip(se->idstr, se->section_id);continue;}......json_start_object(vmdesc, NULL);json_prop_str(vmdesc, "name", se->idstr);json_prop_int(vmdesc, "instance_id", se->instance_id);/* 添加full類型的section header */save_section_header(f, se, QEMU_VM_SECTION_FULL);/* 遷移VMState*/ret = vmstate_save(f, se, vmdesc);....../* 添加頁尾 */save_section_footer(f, se);json_end_object(vmdesc);}......
}
 
VMStateDescription描述一個VMState,VMState簡單講,就是qemu設備模型中的設備狀態,每個設備狀態都有一個結構體,VMState就指代這些結構體的一個qemu術語。一個VMStateDescription的主要作用是在qemu遷移時,幫助qemu判斷,VMState結構體中哪些成員需要遷移,哪些成員不需要遷移,因為qemu的設備狀態數據結構需要向前兼容,因此這些判斷是必須而且有用的。 假設高版本qemu-3.0的一個VMState數據結構新增了成員A,遷移也需要發送A的值,同版本之間,源端發送A的信息,目的端接收A的信息,因為VMState數據結構是一樣的,因此A的收發都可以成功。但如果源端是qemu-2.0的低版本,它沒有增加這個新的成員A,它想讓自己的虛擬機遷移到高版本上去就不可能了,因為高版本目的端會檢查源端是否發送了A成員,如果沒有發送,不符合預期,遷移就失敗了。 為解決上面的問題,qemu為一個VMState數據結構增加了version_id字段,引入了subsections字段,每個field也增加了version_id字段,所有這些都是為了設備狀態信息可以遷移成功。要說清楚這個機制需要更多筆墨,之后有時間會另寫一篇專門分析VMState遷移原理的文章  TODO:VMState遷移原理 vmdescription section  
vmdescription字段是一個可選內容,它只可能在precopy中被發送,在遷移VMState時,qemu會把每個VMState的idstr,instance_id字段都記錄下來,組織成一個json字符串,在VMState遷移完成之后,發送到目的端 Q&A  
Q:遷移實現中的SaveStateEntry和section是什么關系,為什么說它倆都是遷移操作的基本單位?
                            總結 
                            
                                以上是生活随笔 為你收集整理的qemu内存迁移格式 的全部內容,希望文章能夠幫你解決所遇到的問題。
                            
                            
                                如果覺得生活随笔 網站內容還不錯,歡迎將生活随笔 推薦給好友。