前言
- qemu內存遷移主要遷移兩類信息,虛機內存和設備狀態。
- 對於虛機內存,qemu並不知道其具體內容,但它知道主機上爲虛機內存分配的虛機地址區間,這些區間由RAMBlock表示,組織成鏈表,qemu遷移內存,就是把這些RAMBlock一股腦兒遷移走。好比我想拿一瓶水,雖然水沒法移動,但裝水的瓶子可以移動,所以我手裏拿的是水瓶。
- 對於設備狀態,在qemu啓動的時候,它爲虛機提供設備的模擬,隨着虛機的運行,可能會對設備的某些內存空間進行配置或修改,比如pci設備,虛機內部的pci驅動會在使用過程中修改其配置空間,這樣的修改,反映到主機上,就是virtio設備某些字段的更新。再比如塊設備,如果有配置緩存的,那沒有落盤的緩存數據也是設備的一個狀態,在遷移時必須被遷移走。因此,qemu熱遷移時設備的某些狀態字段必須被遷移。
- 本文介紹的,就是qemu在設備遷移有關的開發過程中,遇到的問題以及爲解決這些問題引入的機制
設備狀態遷移
- 開源qemu在開發過程中,免不了對一些設備對應的數據結構進行修改,包括增加某個字段或者刪除某個字段,當這些字段與設備狀態相關需要被遷移時,就會涉及源端和目的端設備狀態的信息同步問題。
新增字段
- 問題:
如果一個設備對應的結構體需要增加一個字段,那麼在遷移源端的處理中,需要把這個字段加上併發送,在遷移目的端的處理中,也需要從源端發來的數據流中識別出該字段,接收該字段。這樣的修改引入之後,有一個問題,對於源端是未修改qemu,目的端是修改後的qemu,在遷移過程中,目的端會期望源端發送新增字段的信息,但源端是舊版本,不會發送這個信息,目的端會報錯,遷移失敗 - 解決方法:
引入版本號,每增加一個字段的修改就增大版本號。版本號當然不能是qemu的版本,因爲只是對設備狀態的字段修改,因此在描述設備狀態的結構體中引入版本號。同時每一個字段也引入一個版本號,表明該字段是在哪個版本引入的。當目的端接收到來自源端的遷移數據時,首先取出源端的設備狀態版本號,如果本地設備狀態的版本號大於等於對端版本號,說明對端是低版本,但允許遷移,本地接收解析字段時,首先檢查本地定義的字段版本,如果大於對端設備狀態的版本,跳過,反之解析。原理如下圖中1所示。 - 代碼舉例:
commit 3cda44f7bae5c9feddc11630ba6eecb2e3bed425
Author: Jens Freimann <[email protected]>
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 <[email protected]>
Signed-off-by: Jens Freimann <[email protected]>
Signed-off-by: Cornelia Huck <[email protected]>
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 <[email protected]>
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 <[email protected]>
Reviewed-by: Peter Maydell <[email protected]>
Message-id: [email protected]
Signed-off-by: Peter Maydell <[email protected]>
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
示意圖
引入的問題
- 通過引入三個版本號: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 <[email protected]>
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舉例
- 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()
}
};
- 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
條件的字段,這個判斷實際就是對本地設備字段中,版本號大於對端設備版本的,跳過接收,之後的流程,和發送端類似。