文章目录

  • 前言
  • 设备状态迁移
  • 新增字段
  • 删除字段
  • 示意图
  • 引入的问题
  • 数据结构
  • 设备状态
  • VMState举例
  • 发送设备状态
  • 接收设备状态


前言

  • qemu内存迁移主要迁移两类信息,虚机内存和设备状态。
  • 对于虚机内存,qemu并不知道其具体内容,但它知道主机上为虚机内存分配的虚机地址区间,这些区间由RAMBlock表示,组织成链表,qemu迁移内存,就是把这些RAMBlock一股脑儿迁移走。好比我想拿一瓶水,虽然水没法移动,但装水的瓶子可以移动,所以我手里拿的是水瓶。
  • 对于设备状态,在qemu启动的时候,它为虚机提供设备的模拟,随着虚机的运行,可能会对设备的某些内存空间进行配置或修改,比如pci设备,虚机内部的pci驱动会在使用过程中修改其配置空间,这样的修改,反映到主机上,就是virtio设备某些字段的更新。再比如块设备,如果有配置缓存的,那没有落盘的缓存数据也是设备的一个状态,在迁移时必须被迁移走。因此,qemu热迁移时设备的某些状态字段必须被迁移。
  • 本文介绍的,就是qemu在设备迁移有关的开发过程中,遇到的问题以及为解决这些问题引入的机制

设备状态迁移

  • 开源qemu在开发过程中,免不了对一些设备对应的数据结构进行修改,包括增加某个字段或者删除某个字段,当这些字段与设备状态相关需要被迁移时,就会涉及源端和目的端设备状态的信息同步问题。

新增字段

  • 问题:
    如果一个设备对应的结构体需要增加一个字段,那么在迁移源端的处理中,需要把这个字段加上并发送,在迁移目的端的处理中,也需要从源端发来的数据流中识别出该字段,接收该字段。这样的修改引入之后,有一个问题,对于源端是未修改qemu,目的端是修改后的qemu,在迁移过程中,目的端会期望源端发送新增字段的信息,但源端是旧版本,不会发送这个信息,目的端会报错,迁移失败
  • 解决方法:
    引入版本号,每增加一个字段的修改就增大版本号。版本号当然不能是qemu的版本,因为只是对设备状态的字段修改,因此在描述设备状态的结构体中引入版本号。同时每一个字段也引入一个版本号,表明该字段是在哪个版本引入的。当目的端接收到来自源端的迁移数据时,首先取出源端的设备状态版本号,如果本地设备状态的版本号大于等于对端版本号,说明对端是低版本,但允许迁移,本地接收解析字段时,首先检查本地定义的字段版本,如果大于对端设备状态的版本,跳过,反之解析。原理如下图中1所示。
  • 代码举例:
commit 3cda44f7bae5c9feddc11630ba6eecb2e3bed425
Author: Jens Freimann <jfrei@linux.vnet.ibm.com>
Date:   Mon Mar 2 17:44:24 2015 +0100

    s390x/kvm: migrate vcpu interrupt state
    
    This patch adds support to migrate vcpu interrupts.
    We use ioctl KVM_S390_GET_IRQ_STATE and _SET_IRQ_STATE
    to get/set the complete interrupt state for a vcpu.
    
    Reviewed-by: David Hildenbrand <dahi@linux.vnet.ibm.com>
    Signed-off-by: Jens Freimann <jfrei@linux.vnet.ibm.com>
    Signed-off-by: Cornelia Huck <cornelia.huck@de.ibm.com>

diff --git a/target-s390x/cpu-qom.h b/target-s390x/cpu-qom.h
index 8b376df..936ae21 100644
--- a/target-s390x/cpu-qom.h
+++ b/target-s390x/cpu-qom.h
@@ -66,6 +66,9 @@ typedef struct S390CPU {
     /*< public >*/
 
     CPUS390XState env;
+    /* needed for live migration */
+    void *irqstate;
+    uint32_t irqstate_saved_size;
 } S390CPU;
    
 const VMStateDescription vmstate_fpu = {
     .name = "cpu/fpu",
@@ -67,7 +76,8 @@ static inline bool fpu_needed(void *opaque)
 const VMStateDescription vmstate_s390_cpu = {
     .name = "cpu",
     .post_load = cpu_post_load,
-    .version_id = 3,
+    .pre_save = cpu_pre_save,
+    .version_id = 4,
     .minimum_version_id = 3,
     .fields      = (VMStateField[]) {
         VMSTATE_UINT64_ARRAY(env.regs, S390CPU, 16),
@@ -86,6 +96,9 @@ const VMStateDescription vmstate_s390_cpu = {
         VMSTATE_UINT64_ARRAY(env.cregs, S390CPU, 16),
         VMSTATE_UINT8(env.cpu_state, S390CPU),
         VMSTATE_UINT8(env.sigp_order, S390CPU),
+        VMSTATE_UINT32_V(irqstate_saved_size, S390CPU, 4),
+        VMSTATE_VBUFFER_UINT32(irqstate, S390CPU, 4, NULL, 0,
+                               irqstate_saved_size),
         VMSTATE_END_OF_LIST()
      },

上面的commit在S390CPU设备状态信息中增加了两个字段,irqstate和irqstate_saved_size,两个字段都需要迁移。S390CPU是个设备,它的基类是DeviceState。增加这两个字段后,不仅设备S390CPU的版本ID增大到4,两个字段的版本也声明为4,说明这是在设备版本号为4时引入的字段。

删除字段

  • 问题:
    如果一个设备对应的结构体要删除一个无用的字段,在修改完成后增大了设备状态的版本号。但在低版本向高版本迁移时仍然会有问题,因为低版本作为源端,字段未被删除,仍然会被发送,目的端解析源端发来的设备字段时,由于本地没有这个字段,因此解析不了,在迁移过程中就会报错。
  • 规避方法:
    对于要删除一个字段的情况,目的端因为没有定义该字段,所以当从对端发送的流中解析到该字段时,可能本地设备状态里面没有定义,目的端无法识别,在解析字段过程中会报错。为了将报错提前,在设备状态中引入一个最小版本ID((minimum_version_id),迁移设备状态时,首先检查源端的设备状态ID是否小于本地的最小版本ID,如果小于,说明不满足条,提前报错,如图中的3所示。从上面的分析看出,如果一个设备状态中某些需要迁移的字段被删除,qemu是无法将虚机从的版本迁移到高版本的,产品化的qemu中如果使用此方法无法实现向前兼容,即允许虚机从低版本qemu迁移到高版本qemu
  • 代码举例:
commit c3a86b35f2bae29278b2ebb3018c51ba69697db7
Author: Wei Huang <wei@redhat.com>
Date:   Thu Feb 18 14:16:17 2016 +0000

    ARM: PL061: Cleaning field of PL061 device state
    
    This patch removes the float_high field of PL061State, which doesn't
    seem to be used anywhere. Because this changes the device state, the
    version ID is also bumped up for the reason of compatiblity.
    
    Signed-off-by: Wei Huang <wei@redhat.com>
    Reviewed-by: Peter Maydell <peter.maydell@linaro.org>
    Message-id: 1455729552-28026-3-git-send-email-wei@redhat.com
    Signed-off-by: Peter Maydell <peter.maydell@linaro.org>

diff --git a/hw/gpio/pl061.c b/hw/gpio/pl061.c
index f9773b8..5ece8b0 100644
--- a/hw/gpio/pl061.c
+++ b/hw/gpio/pl061.c
@@ -56,7 +56,6 @@ typedef struct PL061State {
     uint32_t slr;
     uint32_t den;
     uint32_t cr;
-    uint32_t float_high;
     uint32_t amsel;
     qemu_irq irq;
     qemu_irq out[8];
@@ -65,8 +64,8 @@ typedef struct PL061State {
 
 static const VMStateDescription vmstate_pl061 = {
     .name = "pl061",
-    .version_id = 3,
-    .minimum_version_id = 3,
+    .version_id = 4,
+    .minimum_version_id = 4,
     .fields = (VMStateField[]) {
         VMSTATE_UINT32(locked, PL061State),
         VMSTATE_UINT32(data, PL061State),
@@ -88,7 +87,6 @@ static const VMStateDescription vmstate_pl061 = {
         VMSTATE_UINT32(slr, PL061State),
         VMSTATE_UINT32(den, PL061State),
         VMSTATE_UINT32(cr, PL061State),
-        VMSTATE_UINT32(float_high, PL061State),
         VMSTATE_UINT32_V(amsel, PL061State, 2),
         VMSTATE_END_OF_LIST()
     }

这个commit想删除PL061State设备的float_high字段,它做了两个工作,一是增加设备状态的版本号为4,二是设置目的端要求的源端设备状态最小版本号为4

示意图

qemu传文件 windows qemu文件交换_回调函数

引入的问题

  • 通过引入三个版本号:VMState version_id,Field version_id,VMState minimum_version_id,可以解决开发过程中的大多数问题。但还有一种场景需要考虑,假设当前一个设备的版本号是2,我们添加了一个新特性增加了设备的字段,版本号改为3,如果这时我们测试出一个bug是在版本号为2的阶段引入的,为了修改这个bug,我们需要增删某些字段,因此不得不把版本号增大为4,从而达到修复版本号2带来的bug。
  • 如果采用这种操作,当我们想把这个bug的修改同步到某些稳定的版本,如果版本引入了新特性的修改,版本号是3,那还好,我们可以直接在这个基础上同步代码。如果某些版本还处于版本号2的阶段,那么为了修复这个bug,我们不得不将新特性的修改和bug的修改一起,同步到这个版本上。这样是不合理的,因为有些稳定版本不想合入新特性,而且针对一个bug的修改,引入其它修改也是比较奇怪的。
  • 从上面可以看到,需要有一个机制,让我们可以增加设备状态的某些字段并在条件满足的情况下迁移这个字段,但同时不能增大版本号。这个机制就是subsection,当源端发送完必要的设备状态字段后,如果判断到某个subsection需要发送,则将该subsection发送,由于这个subsection是可选的,源端和目的端对发送哪些字段并没有事前约定,因此必须约定subsection发送的格式,发送subsection的元数据。包括subsection的版本号,名字长度,名字,以及subsection的版本号。这样目的端才能从接收流种实时解析发送过来的subsection。subsection格式如下:
  • 代码举例:
commit 59811a320d6b2a6db2646f908bb016dd8553df27
Author: Peter Maydell <peter.maydell@linaro.org>
Date:   Mon Oct 24 16:26:50 2016 +0100

    migration/savevm.c: migrate non-default page size
    
    Add a subsection to vmstate_configuration which is present
    only if the guest is using a target page size which is
    different from the default. This allows us to helpfully
    diagnose attempts to migrate between machines which
    are using different target page sizes.

diff --git a/migration/savevm.c b/migration/savevm.c
index a831ec2..cfcbbd0 100644
--- a/migration/savevm.c
+++ b/migration/savevm.c
@@ -265,6 +265,7 @@ typedef struct SaveState {
     bool skip_configuration;
     uint32_t len;
     const char *name;
+    uint32_t target_page_bits;
 } SaveState;

 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,
     .fields = (VMStateField[]) {
@@ -311,6 +356,10 @@ static const VMStateDescription vmstate_configuration = {
         VMSTATE_VBUFFER_ALLOC_UINT32(name, SaveState, 0, NULL, 0, len),
         VMSTATE_END_OF_LIST()
     },
+    .subsections = (const VMStateDescription*[]) {
+        &vmstate_target_page_bits,
+        NULL
+    }
 };

该commit的修改引入了target_page_bits结构体,当target_page_bits大小被虚机修改,和默认值不一样时,需要发送该结构体到目的端。

数据结构

设备状态

  • 设备状态说白了,就是一个结构体,这个结构体的各字段记录设备的状态信息,所有设备状态的基类都是DeviceState。迁移过程中设备状态的某些字段需要传送到目的端,如何描述哪些字段需要传送,哪些字段不需要传送,哪些可能需要判断之后才能传送,怎么获取这些字段的信息,这是VMStateDescription要提供的内容,VMStateDescription包含了一个设备要迁移所需的全部信息
struct VMStateDescription {
    const char *name;								/* 设备状态名 */
    int unmigratable;
    /* 版本ID,用于解决设备状态增删字段后的传输问题
     * 只有当源端的VMState版本ID小于等于目的端时
     * 设备状态信息才能传输成功
     */
    int version_id;					
    /* 目的端允许源端发送的VMState的最低版本
     * 如果源端发送的版本小于minimum_version_id 
     * 目的端报错
     * */		 		
    int minimum_version_id;							
    int minimum_version_id_old;
    MigrationPriority priority;
    LoadStateHandler *load_state_old;
    int (*pre_load)(void *opaque);					/* 接收VMState前的回调函数,非必须字段 */
    int (*post_load)(void *opaque, int version_id);	/* 接收VMState后的回调函数,非必须字段 */ 
    int (*pre_save)(void *opaque);					/* 发送VMState前的回调函数,非必须字段 */
    int (*post_save)(void *opaque);					/* 发送VMState后的回调函数,非必须字段 */
    bool (*needed)(void *opaque);					/* 当VMState为subsection时,用于判断哪些字段需要被发送,非必须字段 */
    const VMStateField *fields;						/* 必须发送的VMState的字段*/
    const VMStateDescription **subsections;			/* 可选的发送字段,发送subsections前需要先执行needed函数判断 */
};
  • 描述单独一个字段的结构体
struct VMStateField {
    const char *name;	/* 字段名字 */
    const char *err_hint;
    size_t offset;		/* 字段在VMState结构体中的偏移 */
    size_t size;		/* 字段长度 */
    size_t start;
    int num;
    size_t num_offset;	
    size_t size_offset;
    const VMStateInfo *info;	/* 收发该字段使用的函数 */
    enum VMStateFlags flags;		/* 描述设备状态字段的类型,包括指针,数组,结构体,缓存等等*/
    const VMStateDescription *vmsd;	/* 指向包含的子设备状态,非必须*/
    int version_id;				/* 当该字段的版本大于源端设备状态的版本时,不会被传输 */
    int struct_version_id;
    bool (*field_exists)(void *opaque, int version_id);
};

VMState举例

  1. vmstate_pl061
    fields域中定义的数组是,只要field的版本不大于设备状态的版本,都会被发送
static const VMStateDescription vmstate_pl061 = {
    .name = "pl061",
    .version_id = 4,
    .minimum_version_id = 4,
    .fields = (VMStateField[]) {
        VMSTATE_UINT32(locked, PL061State),
        VMSTATE_UINT32(data, PL061State),
		......
        VMSTATE_UINT32_V(amsel, PL061State, 2),
        VMSTATE_END_OF_LIST()
    }
};
  1. vmstate_configuration
    subsections中定义的数组,在发送之前需要调用needed函数判断,是否可以发送
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,		/* 发送端发送前的回调 */
    .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
    }   
};

发送设备状态

  • 对于precopy的迁移,设备状态的发送是在整个内存迁移完成之后,流程如下:
qemu_savevm_state_complete_precopy
	vmstate_save(f, se, vmdesc)
  		vmstate_save_state(f, se->vmsd, se->opaque, vmdesc)
  			vmstate_save_state_v(f, vmsd, opaque, vmdesc_id, vmsd->version_id)
  • 仔细分析vmstate_save_state_v函数,这个函数比较长,我们分段解释
int vmstate_save_state_v(QEMUFile *f, const VMStateDescription *vmsd,
                         void *opaque, QJSON *vmdesc, int version_id)
{
    int ret = 0;
    const VMStateField *field = vmsd->fields;

    trace_vmstate_save_state_top(vmsd->name);

    if (vmsd->pre_save) {
        ret = vmsd->pre_save(opaque);
        trace_vmstate_save_state_pre_save_res(vmsd->name, ret);
        if (ret) {
            error_report("pre-save failed: %s", vmsd->name);
            return ret;
        }       
    }           
            
    if (vmdesc) {
        json_prop_str(vmdesc, "vmsd_name", vmsd->name);
        json_prop_int(vmdesc, "version", version_id);
        json_start_array(vmdesc, "fields");
    }

    while (field->name) {
        if ((field->field_exists &&
             field->field_exists(opaque, version_id)) ||
            (!field->field_exists &&
             field->version_id <= version_id)) {
            void *first_elem = opaque + field->offset;
            int i, n_elems = vmstate_n_elems(opaque, field);
            int size = vmstate_size(opaque, field);
            int64_t old_offset, written_bytes;
            QJSON *vmdesc_loop = vmdesc;

            trace_vmstate_save_state_loop(vmsd->name, field->name, n_elems);
            if (field->flags & VMS_POINTER) {
                first_elem = *(void **)first_elem;
                assert(first_elem || !n_elems || !size);
            }
  • 函数首先从vmsd中取出本地定义的需要发送的设备状态字段vmsd->fields,参数opaque是设备状态的结构体,如果有prev_save,首先执行prev_save,搜集设备状态信息。参数vmdesc用于记录发送的设备字段名字。进入while循环,它的结束条件是field->name不为NULL,field数组都以VMSTATE_END_OF_LIST为结束标志,当field->name为NULL时,一定是遍历到了VMSTATE_END_OF_LIST。接下来的判断决定这设备的字段是否可以被发送,只有当字段版本不大于设备状态版本时(field->version_id <= version_id),该字段才能发送。first_elem = opaque + field->offset从设备状态信息中取对应的字段,逐个发送。
for (i = 0; i < n_elems; i++) {
                void *curr_elem = first_elem + size * i;
                ret = 0;

                vmsd_desc_field_start(vmsd, vmdesc_loop, field, i, n_elems);
                old_offset = qemu_ftell_fast(f);
                if (field->flags & VMS_ARRAY_OF_POINTER) {
                    assert(curr_elem);
                    curr_elem = *(void **)curr_elem;
                }
                if (!curr_elem && size) {
                    /* if null pointer write placeholder and do not follow */
                    assert(field->flags & VMS_ARRAY_OF_POINTER);
                    ret = vmstate_info_nullptr.put(f, curr_elem, size, NULL,
                                                   NULL);
                } else if (field->flags & VMS_STRUCT) {
                    ret = vmstate_save_state(f, field->vmsd, curr_elem,
                                             vmdesc_loop);
                } else if (field->flags & VMS_VSTRUCT) {
                    ret = vmstate_save_state_v(f, field->vmsd, curr_elem,
                                               vmdesc_loop,
                                               field->struct_version_id);
                } else {
                    ret = field->info->put(f, curr_elem, size, field,
                                     vmdesc_loop);
                }
                if (ret) {
                    error_report("Save of field %s/%s failed",
                                 vmsd->name, field->name);
                    if (vmsd->post_save) {
                        vmsd->post_save(opaque);
                    }
                    return ret;
                }

                written_bytes = qemu_ftell_fast(f) - old_offset;
                vmsd_desc_field_end(vmsd, vmdesc_loop, field, written_bytes, i);

                /* Compressed arrays only care about the first element */
                if (vmdesc_loop && vmsd_can_compress(field)) {
                    vmdesc_loop = NULL;
                }
            }
  • 发送field有两种情况,一种是filed包含一个复合的子VMState,这需要迭代发送,一种是普通的字段,直接调用field->info种定义的发送方法。如果要求记录vmdesc,vmsd_desc_field_end还会记录字段的长度{size, value}。
if (vmdesc) {
        json_end_array(vmdesc);
    }
        
    ret = vmstate_subsection_save(f, vmsd, opaque, vmdesc);

    if (vmsd->post_save) {
        int ps_ret = vmsd->post_save(opaque);
        if (!ret) {
            ret = ps_ret;
        }
    }
    return ret;
  • VMState的field数组逐个发送完成后,结束vmdesc的记录,开始发送subsection,之后是post_save操作。我们继续看一下subsection的发送操作。
static int vmstate_subsection_save(QEMUFile *f, const VMStateDescription *vmsd,
                                   void *opaque, QJSON *vmdesc)
{
    const VMStateDescription **sub = vmsd->subsections;
    bool subsection_found = false;
    int ret = 0;
        
    trace_vmstate_subsection_save_top(vmsd->name); 
    while (sub && *sub) {
        if (vmstate_save_needed(*sub, opaque)) {
            const VMStateDescription *vmsdsub = *sub;
            uint8_t len;
            
            trace_vmstate_subsection_save_loop(vmsd->name, vmsdsub->name);
            if (vmdesc) {
                /* Only create subsection array when we have any */
                if (!subsection_found) {
                    json_start_array(vmdesc, "subsections");
                    subsection_found = true;
                }

                json_start_object(vmdesc, NULL);
            }

            qemu_put_byte(f, QEMU_VM_SUBSECTION);
            len = strlen(vmsdsub->name);
            qemu_put_byte(f, len);
            qemu_put_buffer(f, (uint8_t *)vmsdsub->name, len);
            qemu_put_be32(f, vmsdsub->version_id);
            ret = vmstate_save_state(f, vmsdsub, opaque, vmdesc);
            if (ret) {
                return ret;
            }

            if (vmdesc) {
                json_end_object(vmdesc);
            }
        }
        sub++;
    }

    if (vmdesc && subsection_found) {
        json_end_array(vmdesc);
    }

    return ret;
}
  • subsection的发送,首先需要调用vmstate_save_needed判断该subsection是否需要发送,如果需要才继续,否则跳过。然后是subsection元数据的发送,包括版本,名字长度,名字等,最终vmstate_save_state才是subsection包含的VMState的发送

接收设备状态

  • 对于precopy的迁移,设备状态的接收流程如下:
qemu_loadvm_section_part_end
	vmstate_load(f, se)
		vmstate_load_state(f, se->vmsd, se->opaque, se->load_version_id);
  • 设备状态的接收端,基本流程和发送端类似,但有几个地方的判断需要着重说明一下:
int vmstate_load_state(QEMUFile *f, const VMStateDescription *vmsd,
                       void *opaque, int version_id)
{           
    const VMStateField *field = vmsd->fields;
    int ret = 0;
            
    trace_vmstate_load_state(vmsd->name, version_id);
    if (version_id > vmsd->version_id) {
        error_report("%s: incoming version_id %d is too new "
                     "for local version_id %d",
                     vmsd->name, version_id, vmsd->version_id);
        trace_vmstate_load_state_end(vmsd->name, "too new", -EINVAL);
        return -EINVAL; 
    }           
    if  (version_id < vmsd->minimum_version_id) {
        if (vmsd->load_state_old &&
            version_id >= vmsd->minimum_version_id_old) {
            ret = vmsd->load_state_old(f, opaque, version_id);
            trace_vmstate_load_state_end(vmsd->name, "old path", ret);
            return ret;
        }
        error_report("%s: incoming version_id %d is too old "
                     "for local minimum version_id  %d",
                     vmsd->name, version_id, vmsd->minimum_version_id);
        trace_vmstate_load_state_end(vmsd->name, "too old", -EINVAL);
        return -EINVAL;
    }
    if (vmsd->pre_load) {
        int ret = vmsd->pre_load(opaque);
        if (ret) {
            return ret;
        }
    }
  	while (field->name) {
        trace_vmstate_load_state_field(vmsd->name, field->name);
        if ((field->field_exists &&
             field->field_exists(opaque, version_id)) ||
            (!field->field_exists &&
             field->version_id <= version_id)) {
  • 参数vmsd是本地定义的VMState,参数opaque是VMState,参数version_id是源端发送过来的VMState版本ID,函数首先判断源端VMState版本是否高于目的端的,如果高,报错,不允许迁移。然后比较源端VMState版本是否比本地VMState要求的最低版本还低,如果是,也报错。接着进行接收字段前的预加载操作。然后进入解析设备状态字段的流程,首先跳过不满足field->version_id <= version_id条件的字段,这个判断实际就是对本地设备字段中,版本号大于对端设备版本的,跳过接收,之后的流程,和发送端类似。