qemu內存遷移格式

前言

  • 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內存遷移的有三個階段:
  1. 標髒所有的內存頁
  2. 迭代遷移所有髒頁,直到剩餘髒頁降低到一定水線
  3. 暫停虛擬機,一次性遷移剩餘髒頁,然後遷移設備狀態,啓動目的端虛擬機
  • 遷移第一階段會把所有頁標髒,首次遷移肯定會傳輸所有內存頁,第二次遷移前如果計算得到的剩餘髒頁降低到水線以下,可以暫停虛擬機剩餘髒頁一次性遷移完,因此遷移最理想的狀態是迭代兩次;當虛擬機內存變化大時,會不斷有髒頁產生,遲遲不能降到水線以下,內存變化越大遷移越難收斂,最糟糕的情況是內存髒頁永遠無法降到水線以下,遷移永遠無法完成
  • 針對上述問題,qemu提出postcopy遷移模式,把傳統遷移模式稱爲precopy,兩種模型的不同點在於第二次及其之後的內存髒頁拷貝時機不同。precopy模型的髒頁拷貝在目的端虛擬機啓動之前必須完成;postcopy模型的髒頁拷貝在啓動之後還會繼續。
  • postcopy的內存遷移也有三個階段:
  1. 遷移設備狀態
  2. 標髒所有內存頁,將源端所有內存頁拷貝到目的端,啓動虛擬機
  3. 當目的端虛機訪問到內存髒頁時,會觸發缺頁異常,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進程的所有虛擬機內存空間,查找到以下內存區域:
  1. pc.ram
    在這裏插入圖片描述
  2. vga.vram
    在這裏插入圖片描述
  3. 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);
    }
	......
}
  • 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遷移完成之後,發送到目的端
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章