前言
qemu內存遷移功能基於savevm和loadvm接口實現,savevm可以保存一個運行態虛擬機所有內存和設備狀態到鏡像文件,loadvm可以實現從鏡像狀態文件讀取信息,恢復虛擬機。
qemu虛機內存的遷出通過savevm功能實現,遷入通過loadvm實現。libvirt使用此接口不僅實現了內存遷移,還實現了內存快照。內存遷移,將一個節點上運行的虛擬機,動態遷移到另一個節點上;內存快照,將運行虛擬機的內存快照保存到鏡像文件中,通過快照還原,可以將虛擬機還原到做內存快照時刻的狀態。內存遷移和快照都基於savevm/loadvm接口實現,因此分析快照鏡像相當於於分析內存遷移的靜態數據。本文基於這個原理,通過分析libvirt save命令保存的鏡像文件,來間接分析qemu內存遷移的內存和格式。
內存鏡像格式
我們分析的內存鏡像使用下面這條命令產生,c75_test是測試虛擬機,後面內存鏡像文件的輸出路徑:
virsh save c75_test /home/data/c75_test_save_cmd.mem
下面這條命令可以啓動並將虛擬機還原到快照時刻的狀態:
virsh restore /home/data/c75_test_save_cmd.mem
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(0XB10),如下:
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內存遷移以section爲單位,每個SaveStateEntry就是一個section,precopy模式下會先迭代傳輸內存,當剩餘內存髒頁數降低到水線以下,一次性傳輸所有剩餘內存,然後傳輸設備狀態。簡單講,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 0x00000003
void qemu_savevm_state_header(QEMUFile *f)
{
trace_savevm_state_header();
qemu_put_be32(f, QEMU_VM_FILE_MAGIC); // 發送"QEVM" magic
qemu_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) { // 判斷magic
error_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_state
if (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區域,首先通過命令
cat /proc/qemu_pic/maps | less
獲取qemu進程的所有虛擬機內存空間,查找到以下內存區域:
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_iterate
ram_find_and_save_block
pss.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_page
save_normal_page
save_page_header
qemu_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);
}
......
}
vmdescription section
vmdescription字段是一個可選內容,它只可能在precopy中被髮送,在遷移VMState時,qemu會把每個VMState的idstr,instance_id字段都記錄下來,組織成一個json字符串,在VMState遷移完成之後,發送到目的端