内存是整个虚拟机中重要的部分,也是迁移过程中迁移量最大的部分。
首先我们将从迁移框架的流程上看看整个过程都有哪些函数参与其中,了解内存迁移的流程结构。
接着我们要从数据结构层面看看我们在这些流程中究竟是要处理哪些数据。
内存迁移流程
发送流程
要说流程,还是要先回到总体架构那一节中的总体流程。
migration_thread()
qemu_savevm_state_header
qemu_put_be32(f, QEMU_VM_FILE_MAGIC);
qemu_put_be32(f, QEMU_VM_FILE_VERSION);
qemu_put_be32(f, QEMU_VM_CONFIGURATION);
vmstate_save_state(f, &vmstate_configuration, &savevm_state, 0);
qemu_savevm_send_open_return_path(s->to_dst_file);
qemu_savevm_send_ping(s->to_dst_file, 1);
qemu_savevm_command_send(f, MIG_CMD_PING, , (uint8_t *)&buf)
; iterate savevm_state and call save_setup
qemu_savevm_state_setup(s->to_dst_file);
save_section_header(f, se, QEMU_VM_SECTION_START)
se->ops->save_setup(f, se->opaque)
save_section_footer(f, se)
precopy_notify(PRECOPY_NOTIFY_SETUP, &local_err)
migrate_set_state(&s->state, MIGRATION_STATUS_SETUP, MIGRATION_STATUS_ACTIVE);
migration_iteration_run
; iterate savevm_state and call save_live_pending
qemu_savevm_state_pending(pend_pre/compat/post)
se->ops->save_live_pending()
; iterate savevm_state and call save_live_iterate
qemu_savevm_state_iterate()
save_section_header(f, se, QEMU_VM_SECTION_PART)
se->ops->save_live_iterate(f, se->opaque)
save_section_footer(f, se)
migration_completion()
qemu_system_wakeup_request(QEMU_WAKEUP_REASON_OTHER, NULL);
vm_stop_force_state(RUN_STATE_FINISH_MIGRATE)
qemu_savevm_state_complete_precopy(s->to_dst_file, false, inactivate);
; iterate savevm_state and call save_live_complete_precopy
cpu_synchronize_all_states();
save_section_header(f, se, QEMU_VM_SECTION_END);
se->ops->save_live_complete_precopy(f, se->opaque)
save_section_footer(f, se);
; iterate savevm_state and call vmstate_save
save_section_header(f, se, QEMU_VM_SECTION_FULL);
vmstate_save(f, se, vmdesc)
save_section_footer(f, se);
migration_detect_error
migration_update_counters
migration_iteration_finish
在这个基础上我们进一步抽取和简化,能看到整个过程中主要是这么几个回调函数在起作用。
se->ops->save_live_pending
se->ops->save_live_iterate
se->ops->save_live_complete_precopy
那对应到内存,这个se就是savevm_ram_handlers,其对应的函数们就是
虽然里面做了很多事儿,也有很多细节上的优化,不过总的流程可以总结成两句话:
接收流程
看完了发送,还得来看看接收。同样,看具体内存之前,先看一看总体架构。
qemu_loadvm_state()
qemu_get_be32, QEMU_VM_FILE_MAGIC
qemu_get_be32, QEMU_VM_FILE_VERSION
qemu_loadvm_state_setup
se->ops->load_setup
vmstate_load_state(f, &vmstate_configuration, &savevm_state, 0)
cpu_synchronize_all_pre_loadvm
cpu_synchronize_pre_loadvm(cpu)
qemu_loadvm_state_main
section_type = qemu_get_byte(f)
QEMU_VM_SECTION_START | QEMU_VM_SECTION_FULL
qemu_loadvm_section_start_full
section_id = qemu_get_be32
vmstate_load
QEMU_VM_SECTION_PART | QEMU_VM_SECTION_END
qemu_loadvm_section_part_end
section_id = qemu_get_be32
vmstate_load
QEMU_VM_COMMAND
loadvm_process_command
QEMU_VM_EOF
qemu_loadvm_state_cleanup
se->ops->load_cleanup
cpu_synchronize_all_post_init
cpu_synchronize_post_init(cpu);
进一步打开,我们可以看到有这么几个重要的函数。
对应到内存,分别是
发送接收对应关系
source destination
+------------------------+ +-------------------------+
| | | |
SETUP | ram_save_setup | | ram_load_setup |
| | | |
+------------------------+ +-------------------------+
sync dirty bit to Setup RAMBlock->receivedmap
RAMBlock->bmap
+------------------------+ +-------------------------+
| | | |
ITER | ram_save_pending | | ram_load |
| ram_save_iterate | | |
| | | |
+------------------------+ +-------------------------+
sync dirty bit Receive page
and send page
+------------------------+ +-------------------------+
| | | |
COMP | ram_save_pending | | ram_load |
| ram_save_complete | | |
| | | |
+------------------------+ +-------------------------+
sync dirty bit Receive page
and send page
脏页同步
在整个内存迁移的过程中脏页同步是重中之重了,这一小节我们来看看qemu是如何获得这一段时间中的虚拟机脏页的。
代码流程
qemu发起获得脏页的地方有几处,比如每次迭代开始,或者最后要结束的时候。这个动作都通过统一的函数migration_bitmap_sync()。
migration_bitmap_sync
memory_global_dirty_log_sync (1)
memory_region_sync_dirty_bitmap(NULL);
listener->log_sync(listener, &mrs) -> kvm_log_sync
kvm_physical_sync_dirty_bitmap
migration_bitmap_sync_range; called on each RAMBlock
cpu_physical_memory_sync_dirty_bitmap (2)
其中主要工作分成两步:
通过KVM_GET_DIRTY_LOG获得脏页到kvm_dirty_log.dirty_bitmap,并复制到ram_list.dirty_memory
再将ram_list.dirty_memory的脏页拷贝到RAMBlock->bmap
至于KVM_GET_DIRTY_LOG是怎么得到的脏页这个要看kvm的代码,其中一部分的功劳在vmx_flush_pml_buffer()。
相关数据结构
在内存迁移过程中重要的数据结构就是跟踪脏页的bitmap了。
其中一共用到了两个bitmap:
ram_list.dirty_memory[DIRTY_MEMORY_MIGRATION]
如果要画一个图来解释的画,那么这个可能会像一点。
ram_list.dirty_memory[]
+----------------------+---------------+--------------+----------------+
| | | | |
+----------------------+---------------+--------------+----------------+
^ ^ ^ ^
| | | |
RAMBlock.bmap RAMBlock.bmap
+----------------------+ +--------------+
| | | |
+----------------------+ +--------------+
ram_list.dirty_memroy[]是一整个虚拟机地址空间的bitmap。虽然虚拟机的地址空间可能有空洞,但是这个bitmap是连续的。 RAMBlock.bmap是每个RAMBlock表示的地址空间的bitmap。
所以同步的时候分成两步:
虚拟机对内存发生读写时会更新ram_list.dirty_memroy[]
每次内存迁移迭代开始,将ram_list.dirty_memory[]更新到RAMBlock.bmap
这样两个bitmap各司其职可以同步进行。
进化
理解了总体的流程和重要的数据结构,我们来看看内存迁移部分是如何进化的。如何从最开始的找到脏页迁移脏页,变成现在这么复杂的逻辑的。
零页
迁移的时候,如果知道这个页面的内容都是0,那么从信息的角度看,其实信息量很小。那是不是有更好的方法去传送这些信息呢?
函数save_zero_page_to_file(),就做了这件事。如果这个页面是零页,我们只传送一些标示符号。
压缩
这个想法也比较直接,就是在传送前先压缩一下。这个工作交给了do_compress_ram_page()。
多线程
这也是一个比较直观的想法,一个人干活慢,那就多叫几个人。多线程的设置在compress_threads_save_setup()。
Free Page
这个想法相对来说就比较隐晦了。意思是系统中总有部分的内存没有使用是free的,那么这些内存在最开始的时候就没有必要传送。(默认第一遍是都传送的。)
当然要做到这点,需要在虚拟机中有一个内应。当前这个内应是virtio-balloon。在启动虚拟机时需要加上参数。
-object iothread,id=iothread1 --device virtio-balloon,free-page-hint=true,iothread=iothread1
multifd
接下来这个东西略微有点复杂。也不知道为什么名字叫multifd,好像很有来头的样子。那就先来看看其中重要的数据结构。
第一个叫multifd_send_state。这个结构在函数multifd_save_setup中初始化。样子大概长这样。
multifd_send_state
+-------------------------------+
|packet_num | global
| (uint64_t) |
|sem_sync |
|channels_ready |
| (QemuSemaphore) |
| |
|pages |
| (MultiFDPages_t*) |
| |
|params | [migrate_multifd_channels()] each channel has one MultiFDSendParams
| (MultiFDSendParams*) |
| | MultiFDSendParams MultiFDSendParams MultiFDSendParams
+-------------------------------+ +-------------------+ +-------------------+ +-------------------+
|name | |name | |name |
| multifdsend_0 | | multifdsend_1 | | multifdsend_2 |
|pages | |pages | |pages |
| (MultiFDPages_t*) | | (MultiFDPages_t*) | | (MultiFDPages_t*) |
|packet | |packet | |packet |
| (MultiFDPacket_t*)| | (MultiFDPacket_t*)| | (MultiFDPacket_t*)|
| | | | | |
| | | | | |
+-------------------+ +-------------------+ +-------------------+
几点说明:
multifd是一个多线程的结构,一共有migrate_multifd_channels个线程,这个可以通过参数设置
所以也很好理解的是对应有migrate_multifd_channels()个params,每个线程人手一个不要打架
每个params中重要的两个成员是pages/packet,packet相当于控制信息,pages就是数据
接着我们来看看page和packet的样子。
pages
(MultiFDPages_t*)
+----------------------+
|packet_num | global
| (uint64_t) |
|block |
| (RAMBlock*) |
|allocated |
|used |
| (uint32_t) |
|offset | [allocated]
| (ram_addr_t) |
|iov | [allocated]
| (struct iovec*) |
| +------------------+
| |iov_base | = block->host + offset
| |iov_len | = TARGET_PAGE_SIZE
| +------------------+
| |
+----------------------+
看到这个大家或许能够明白一点,page其实是一个iov的集合。一共分配有allocated个iov的结构,用于发送数据。不过要注意的一点是,发送的所有数据都属于同一个RAMBlock。
packet
(MultiFDPacket_t*)
+----------------------+
|magic | = MULTIFD_MAGIC
|version | = MULTIFD_VERSION
|flags | = params->flags
| |
|pages_alloc | = page_count
|pages_used | = pages->used
|next_packet_size | = params->next_packet_size
|packet_num | = params->packet_num
| |
|offset[] | [page_count]
| (ram_addr_t) |
+----------------------+
这个相当于每次发送时的元数据了。而且因为iov的传送只有base/len,所以这里还要传送在RAMBlock中的offset。
xbzrle
这个东西也很高端,全称叫 XOR Based Zero run lenght encoding. 其中使用到的编码是LEB128.
这个东西很有意思,说起来其实也简单。
不直接传送每个页的内容,而是传送每个页的差值
怎么样,是不是很有意思?好了,这个思想的所有内容就这么写,其他的就剩下具体的实现细节了。