热迁移架构分析(postcoy)
最近分析了qemu关于热迁移方面的实现,从qemu-2.12.1版本在热迁移方面的实现来看,其支持的迁移方式并不单一,而且可利用的特性也比较多,这可以执行virsh help migrate命令查看,比如,支持precopy、postcopy、postcopy-after-precopy等迁移模式,支持页的压缩及压缩算法、压缩level、多线程压缩、指定超时等等。这里简单说一下迁移的几种方式:
Precopy:precopy策略在迁移开始后, 在将内存状态拷贝到目的节点的同时, 源物理主机上的虚拟机仍在继续运行. 如果一个内存在拷贝之后再次被修改(dirtied), 那么将会在后续迁移过程中再次发送到目的节点. 整个内存拷贝是个迭代的过程. 直到最后剩余的脏页内存数量可以在一个短时间内迁移到目的主机时,才停止源虚拟机,迁移剩余内存页和虚拟机状态,启动目的虚拟机;
Postcopy:与precopy不同, postcopy首先把CPU状态和设备状态等复制到目的主机上,在目的主机上开启虚拟机, 然后源主机继续将剩余的内存页推送到目的主机上的虚拟机中. 与此同时, 目的主机上的虚拟机运行时可能会访问到不存在页面(还未被推送)而发生缺页异常, 虚拟机将向源主机发起请求传输相应的页,目的端在获取到相关页后继续pagefault的后续处理. 可以看到postcopy方式对每个内存页面只会传送一次, 降低了预拷贝中冗余拷贝的开销。
Postcopy-after-precopy:根据以上介绍, 我们知道Pre-Copy适用于内存read-intensive的虚拟机热迁移, 对于write-intensive的虚拟机, 在Pre-Copy的每轮迭代中都会有大量新的内存页被修改, 导致迁移的效率低下(当然也可以通过某种手段来控制脏页产生的速度,比如控制vcpu执行的时间,这个在本文后续章节会单独介绍). 对于Post-Copy则相反, 对于read-intensive的虚拟机, Post-Copy将会产生大量的缺页异常, 导致虚拟机内应用性能的降低. Post-Copy更适用于内存比较大, 内存write-intensive的虚拟机热迁移。为了使热迁移更具有一般性, 通用性, 一种自然的想法是将Pre-Copy和Post-Copy结合起来.具体过程如下:
发送端(src):
发送端核心线程主要有两个(不包括页压缩线程):
hmp_migrate->
qmp_migrate->
tcp_start_outgoing_migration->
socket_start_outgoing_migration->
socket_outgoing_migration->
migration_channel_connect->
migrate_fd_connect
在migrate_fd_connect中会注册迁移完成后的清理函数migrate_fd_cleanup以及起两个线程:
live_migration:负责主体迁移工作;
return path:负责处理dst对src发送的信息的响应信息和src的请求信息(如缺页异常等);
Live_migration线程的主体如下(省略了部分不重要的代码)
static void *migration_thread(void *opaque)
{
s->iteration_start_time = qemu_clock_get_ms(QEMU_CLOCK_REALTIME);
qemu_savevm_state_header(s->to_dst_file);
if (s->rp_state.from_dst_file) {
qemu_savevm_send_open_return_path(s->to_dst_file);
}
if (migrate_postcopy()) {
qemu_savevm_send_postcopy_advise(s->to_dst_file);
}
/遍历savevm_state中涉及的设备,调用对应的save_setup回调函数在dst建立对应设备基 本的信息,比如ram:向dst发送内存ram使用量、各个block的描述和使用量、block中* 的页大小等,如果具备MIGRATION_CAPABILITY_COMPRESS,则创建压缩内存页的线程(线 * 程函数:do_data_compress)
*/
qemu_savevm_state_setup(s->to_dst_file);
s->setup_time = qemu_clock_get_ms(QEMU_CLOCK_HOST) - setup_start;
/迁移进入MIGRATION_STATUS_ACTIVE状态/
migrate_set_state(&s->state, MIGRATION_STATUS_SETUP,
MIGRATION_STATUS_ACTIVE);
/*以下是迁移的核心处理部分:
在遵守迁移带宽限制的情况下迁移虚拟机
*/
while (s->state == MIGRATION_STATUS_ACTIVE ||
s->state == MIGRATION_STATUS_POSTCOPY_ACTIVE) {
int64_t current_time;
if (!qemu_file_rate_limit(s->to_dst_file)) {
MigIterateState iter_state = migration_iteration_run(s);
if (iter_state == MIG_ITERATE_SKIP) {
continue;
} else if (iter_state == MIG_ITERATE_BREAK) {
break;
}
}
current_time = qemu_clock_get_ms(QEMU_CLOCK_REALTIME);
/这里动态的估算了当前迁移带宽大小及如果是precopy模式,根据指定的最小停机时间计 算the final stage需要传输的阈值,如果超过该值则不能停止当前虚拟机,否则停止虚拟 * 机进行最后的迁移*/
migration_update_counters(s, current_time);
if (qemu_file_rate_limit(s->to_dst_file)) {
/* usleep expects microseconds */
g_usleep((s->iteration_start_time + BUFFER_DELAY -
current_time) * 1000);
}
}
}
可以看到,迁移时支持限速功能,当BUFFER_DELAY时间内的流量值超过了阈值,则需要根据当前在BUFFER_DELAY内所占用的情况休眠剩余的时间值。而在正常的迁移流程下会进入migration_iteration_run做迭代迁移操作:
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;
/*获取剩下需要迁移的dirty page数量,根据是pre、非pre迁移分别保存到pend_pre、
pend_compat中*/
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;
/dirty page还没有小于阈值,继续正常的迁移流程/
if (pending_size && pending_size >= s->threshold_size) {
/*对于postcopy,由于此时还处在MIGRATION_STATUS_ACTIVE状态,首次执行的时
候触发该流程 */
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;
}
/正常的迁移迭代/
qemu_savevm_state_iterate(s->to_dst_file,
s->state == MIGRATION_STATUS_POSTCOPY_ACTIVE);
} else {
/最后一小部分内容的迁移/
migration_completion(s);
return MIG_ITERATE_BREAK;
}
return MIG_ITERATE_RESUME;
}
在migration_iteration_run中每次进入迭代之前,总是调用qemu_savevm_state_pending获取当前需要迁移的page数量(如果涉及块设备的迁移,还会包括块设备需要迁移的大小)。剩余需要迁移的量小于在migration_update_counters中根据当前传输带宽情况预估的阈值时,则调用migration_completion完成最终的迁移。这里首先分析postcopy_start部分,该部分是将迁移转换到postcopy模式,由于postcopy_start代码较长,但逻辑设计并不复杂,所以这里只给出其实现的基本流程:
1、如果虚拟机不是处于running状态,则需要先唤醒;
2、暂停当前虚拟机,因为postcopy模式会立刻在dst端启动运行虚拟机,不需要src端虚拟机继续运行;
3、inactive块设备;
4、同步内核态所有的vcpu状态到qemu中各vcpu的CPUState中保存,对于不支持postcopy的设备(如在开启了block设备迁移的情况下),调用其save_live_complete_precopy方法将其相关头部元数据先发送到dst;
5、对于postcopy,在precopy后需要告知dst哪些页需要被discard;
6、通过wrap设备状态信息到一个bioc buffer,并一次性发送到dst,其中包括MIG_CMD_POSTCOPY_LISTEN、MIG_CMD_POSTCOPY_RUN命令,需要特别注意的是这里将MIG_CMD_POSTCOPY_LISTEN、MIG_CMD_POSTCOPY_RUN捆绑在一起作为一条信息发送到dst,这一点很重要,会为dst端qemu主线程退出热迁移逻辑,创建单独线程接替qemu主线程继续热迁移后续工作创造条件;
7、通知spice做相应的状态切换;
8、通过madvice和fallocate释放RAMBlock bmap中bit位为0对应的page,释放部分内存,这主要是考虑src端内存紧张的情况;
接下来qemu_savevm_state_iterate进入正式的热迁移迭代,其实该函数可简化为如下流程:
int qemu_savevm_state_iterate(QEMUFile *f, bool postcopy)
{
。。。。。。。。。。。。。。
QTAILQ_FOREACH(se, &savevm_state.handlers, entry) {
/在设备支持迁移迭代且active、active_iterate的情况下/
if (postcopy &&
!(se->ops->has_postcopy && se->ops->has_postcopy(se->opaque))) {
continue;
}
if (qemu_file_rate_limit(f)) {
return 0;
}
save_section_header(f, se, QEMU_VM_SECTION_PART);
ret = se->ops->save_live_iterate(f, se->opaque);
trace_savevm_section_end(se->idstr, se->section_id, ret);
save_section_footer(f, se);
/*这里遵循的原则是先迁移完一个vmstate,再继续下一个,这样做的好处是避免一些
接下来我们再看看src端热迁移的另一个线程:return path,其创建流程为:
hmp_migrate->
qmp_migrate->
tcp_start_outgoing_migration->
socket_start_outgoing_migration->
socket_outgoing_migration->
migration_channel_connect->
migrate_fd_connect->
open_return_path_on_source
Return path线程主要负责接收来自dst的pagefault请求或相应返回值,其线程主体函数为:source_return_path_thread,主要处理dst对src发送的信息的响应信息和src的请求信息(如缺页异常的页请求),Return path线程大体内容如下:
static void *source_return_path_thread(void *opaque)
{
。。。。。。。。。。。。
while (!ms->rp_state.error && !qemu_file_get_error(rp) &&
migration_is_setup_or_active(ms->state)) {
header_type = qemu_get_be16(rp);/来自dst的信息/
header_len = qemu_get_be16(rp);
/*接收指定大小的data*/
res = qemu_get_buffer(rp, buf, header_len);
switch (header_type) {
/*dst告知src不会再发送消息,src可以结束当前线程*/
case MIG_RP_MSG_SHUT:
sibling_error = ldl_be_p(buf);
goto out;
/*对src ping dst的回应,主要用于调试*/
case MIG_RP_MSG_PONG:
tmp32 = ldl_be_p(buf);
break;
/*请求页(start: be64, len: be32)*/
case MIG_RP_MSG_REQ_PAGES:
start = ldq_be_p(buf);
len = ldl_be_p(buf + 8);
/*这里的rbname参数为NULL,表示Reuse last RAMBlock*/
migrate_handle_rp_req_pages(ms, NULL, start, len);
break;
/*请求页,这里不同的是请求指定RAMBlock的对应页*/
case MIG_RP_MSG_REQ_PAGES_ID:
expected_len = 12 + 1; /* header + termination */
if (header_len >= expected_len) {
start = ldq_be_p(buf);
len = ldl_be_p(buf + 8);
/* Now we expect an idstr */
tmp32 = buf[12]; /* Length of the following idstr */
buf[13 + tmp32] = '\0';
expected_len += tmp32;
}
migrate_handle_rp_req_pages(ms, (char *)&buf[13], start, len);
break;
default:
break;
}
}
out:
ms->rp_state.from_dst_file = NULL;
qemu_fclose(rp);
return NULL;
}
可以看到,return path线程所做的工作很清晰和简单,主要就是对四种类型信息的响应:
MIG_RP_MSG_PONG:由src migration线程ping dst,dst对ping请求的回应,主要用于调试;
MIG_RP_MSG_SHUT:在分析migration线程的时候已经提到,在迁移完成后,dst端会发送 shutdown命令给src端,也就是这里由return path线程,接收到该 命令后会退出线程;
MIG_RP_MSG_REQ_PAGES_ID:请求指定RAMBlock中的页;
MIG_RP_MSG_REQ_PAGES:请求页(start: be64, len: be32),这里没有指定RAMBlock id,表示 Reuse last RAMBlock;
这里关于特定页请求的处理和在dst端是如何发起缺页请求的整个流程,放在后续分析postcopy模式下的缺页异常时统一分析。
到现在为止,src端迁移的大概流程已经梳理完,涉及的页压缩线程等相关知识点,暂不分析,接下来分析dst端接收线程的处理逻辑。
接收端(dst):
这里以走tcp协议进行热迁移为例,在迁移之前,dst端将以migration-listen模式启动一个参数值和src端完全相同的qemu进程,例如:
-incoming tcp:0:4444 (or other PORT))
这样src端利用virsh或libvirt接口便可触发迁移:
migrate -d tcp:$(dst ip):4444 (or other PORT)
结合qemu的incoming参数可知,接收端的逻辑处理为:
Main->
qemu_start_incoming_migration->
tcp_start_incoming_migration->
socket_start_incoming_migration->
socket_accept_incoming_migration->
migration_channel_process_incoming->
migration_ioc_process_incoming->
migration_fd_process_incoming->
migration_incoming_process->
process_incoming_migration_co
以上便是从qemu的main函数接收incoming参数出发,到处理热迁移接收的主要流程,这之间的主要工作是和src端建立连接,以协程的方式启动热迁移接收。
在不考虑colo和错误处理等情况下,Process_incoming_migration_co可简化为:
static void process_incoming_migration_co(void *opaque)
{
MigrationIncomingState *mis = migration_incoming_get_current();
PostcopyState ps;
int ret;
mis->largest_page_size = qemu_ram_pagesize_largest();
postcopy_state_set(POSTCOPY_INCOMING_NONE);
migrate_set_state(&mis->state, MIGRATION_STATUS_NONE,
MIGRATION_STATUS_ACTIVE);/*状态切换*/
/接收端的主体部分/
ret = qemu_loadvm_state(mis->from_src_file);
/迁移后接收端的清理工作/
ps = postcopy_state_get();
if (ps != POSTCOPY_INCOMING_NONE) {
if (ps == POSTCOPY_INCOMING_ADVISE) {
/*
* Where a migration had postcopy enabled (and thus went to advise)
* but managed to complete within the precopy period, we can use
* the normal exit.
/
postcopy_ram_incoming_cleanup(mis);
} else if (ret >= 0) {
/ 对于postcopy来讲,会走该分支,由创建的”postcopy/listen”线程来继续
/* Now an eventfd we use to tell the fault-thread to quit */
/利用该eventfd通知fault-thread线程的退出/
mis->userfault_event_fd = eventfd(0, EFD_CLOEXEC);
qemu_sem_init(&mis->fault_thread_sem, 0);
/创建”postcopy/fault”线程处理pagefault/
qemu_thread_create(&mis->fault_thread, “postcopy/fault”,
postcopy_ram_fault_thread, mis, QEMU_THREAD_JOINABLE);
/等待”postcopy/fault线程运行”/
qemu_sem_wait(&mis->fault_thread_sem);
qemu_sem_destroy(&mis->fault_thread_sem);
mis->have_fault_thread = true;/标识存在”postcopy/fault”线程/
/* Mark so that we get notified of accesses to unwritten areas */
/*向内核态注册各个RAMBlock地址区间,这样在发生pagefault时根据地址得到正确的
页请求*/
if (qemu_ram_foreach_block(ram_block_enable_notify, mis)) {
return -1;
}
/*
禁止balloon驱动,因为balloon驱动会导致page的缺失,这会导致postcopy处理
pagefault时的异常
*/
qemu_balloon_inhibit(true);
return 0;
}
在page fault的处理方面,利用了内核态的userfault机制,通过userfault系统调用创建的文件和qmeu的mm_struct关联,这样也就和整个虚拟机内存关联在了一起。紧接着调用ram_block_enable_notify函数将各个RAMBlock的地址空间注册到userfault fd所负责监听的地址范围,注册的模式为UFFDIO_REGISTER_MODE_MISSING:
reg_struct.range.start = (uintptr_t)host_addr;
reg_struct.range.len = length;
reg_struct.mode = UFFDIO_REGISTER_MODE_MISSING;
ioctl(mis->userfault_fd, UFFDIO_REGISTER, ®_struct)
接下来看看内核态的实现,内核根据ioctl操作调用到userfaultfd_register,该函数的实现很长,这里就不贴出其代码实现了,该函数实现的注册逻辑为:
1、校验注册地址的有效性;
2、如果是hugepage,则地址是否满足页对其;
3、对qemu进程中的mm_struct中查找和RAMBlock地址空间有交集的vm_area_struct,并检查其兼容性,包括:vma是否支持userfault,VM_MAYWRITE校验,地址对齐等;
4、对RAMBlock地址空间与mm_struct中涉及的vm_area_struct的合并与拆分,加入RAMBlock地址区间后,如果可让相邻的区间连接且属性一致,则可合并为一个vm_area_struct;而如果RAMBlock阻断了相关的vm_area_struct,其属于不同,则需要对其进行拆分;
但这里能够和userfault_fd相关联,最终能因为pagefault通知到”postcopy/listen”的关联点为:
static int userfaultfd_register(struct userfaultfd_ctx *ctx,
unsigned long arg)
{
。。。。。。
vma->vm_flags = new_flags;
vma->vm_userfaultfd_ctx.ctx = ctx;
。。。。。。
}
这样在发生pagefault时可利用ctx与userfault_fd关联,具体往下看:
再来看看虚拟机因发生pagefault会导致怎样的一种响应流程(比如):
do_page_fault->
__do_page_fault->
handle_mm_fault->
__handle_mm_fault->
handle_pte_fault->
do_anonymous_page->
handle_userfault:
int handle_userfault(struct vm_fault *vmf, unsigned long reason)
{
。。。。。。。。。。。
/*将当前线程挂载到等待队列,这是在请求的page由src传递给dst,
加载成功后处理了pagefault异常再唤醒当前线程,以便继续后续的操作*/
init_waitqueue_func_entry(&uwq.wq, userfaultfd_wake_function);
uwq.wq.private = current;
uwq.msg = userfault_msg(vmf->address, vmf->flags, reason,
ctx->features);
uwq.ctx = ctx;
uwq.waken = false;
__add_wait_queue(&ctx->fault_pending_wqh, &uwq.wq);
。。。。。。。。。。。。。
/*这里的wake_up_poll是在发生pagefault后需要唤醒”postcopy/fault”线程
进行处理:主要是向src发送页请求*/
if (likely(must_wait && !ACCESS_ONCE(ctx->released) &&
(return_to_userland ? !signal_pending(current) :
!fatal_signal_pending(current)))) {
wake_up_poll(&ctx->fd_wqh, POLLIN);
schedule();
ret |= VM_FAULT_MAJOR;
/这里等待pagefault处理后被唤醒/
while (!READ_ONCE(uwq.waken)) {
set_current_state(blocking_state);
if (READ_ONCE(uwq.waken) ||
READ_ONCE(ctx->released) ||
(return_to_userland ? signal_pending(current) :
fatal_signal_pending(current)))
break;
schedule();
}
}
__set_current_state(TASK_RUNNING);
。。。。。。。。。。。。。
}
可见这里进程因pagefault休眠,又因pagefault处理完成而被唤醒,涉及了多个任务的参与,接下来我再将”postcopy/listen”线程的处理逻辑和处理完pagefault后唤醒因pagefault而等待的线程逻辑做一下说明,然后给出总的pagefault处理逻辑图示。
在分析”postcopy/listen”之前,需要明确的是其考虑了vhost-user等基于其他线程因共享虚拟机内存而出现的pagefault情况,所以”postcopy/listen”线程的实现逻辑如下(因其实现代码太长,仅做说明):总的来说“postcopy/listen”会以poll方式监听多个文件,然后针对不同的文件做相应的处理,包括:
userfault_fd:负责监听虚拟机运行缺页时请求”postcopy/listen”的处理,根据请求的地址转换为对应RAMBlock的偏移,最后向src的”return path”线程发送请求;
userfault_event_fd:迁移完成后由postcopy_ram_incoming_cleanup通过该eventfd通知”postcopy/listen”退出;
postcopy_remote_fds:vhost-user模式下的缺页请求;根据pagefault地址计算其在RAMBlock中的偏移,再向src的”return path”线程发送请求;
以上便是”postcopy/listen”线程的主要完成的任务,接下来看看当”postcopy/listen”收到对应的pagefault页后是如何唤醒等待线程的,这是在”postcopy/listen”线程处理接收到page进行load的过程中:
ram_load->
ram_load_postcopy->
postcopy_place_page:
int postcopy_place_page(MigrationIncomingState *mis, void *host, void *from,
RAMBlock *rb)
{
size_t pagesize = qemu_ram_pagesize(rb);
qemu_ufd_copy_ioctl(mis->userfault_fd, host, from, pagesize, rb)
return postcopy_notify_shared_wake(rb,
qemu_ram_block_host_offset(rb, host));
}
postcopy_place_page实际上负责了三部分工作:
1、调用qemu_ufd_copy_ioctl加载内存页,建立页映射;
2、如果虚拟机本身有线程由于pagefault而挂起,此时也会唤醒该线程;
3、如果存在vhost-user的情况,则调用postcopy_notify_shared_wake唤醒vhost-user后端进程,这里唤醒的方式是vhost-user提供了waker回调函数vhost_user_postcopy_waker。
不管是以上步骤中的2还是3中提到的唤醒方式,其实都是利用userfault_fd陷入到内核态,最终走到wake_userfault:
static __always_inline void wake_userfault(struct userfaultfd_ctx *ctx,
struct userfaultfd_wake_range *range)
{
unsigned seq;
bool need_wakeup;
。。。。。。。。。。。。
do {
seq = read_seqcount_begin(&ctx->refile_seq);
need_wakeup = waitqueue_active(&ctx->fault_pending_wqh) ||
waitqueue_active(&ctx->fault_wqh);
cond_resched();
} while (read_seqcount_retry(&ctx->refile_seq, seq));
if (need_wakeup)
__wake_userfault(ctx, range);
}
这里也就和do_page_fault处挂载线程等待pagefault处理相呼应,即唤醒等待的线程。
通过以上的分析,可将postcopy中pagefault的处理流程表示如下图(pagefault下考虑到响应速度不再使用压缩、解压程序):
Precopy下vcpu的抑制
该需求主要是针对precopy模式下传输dirty page的时候由于src端写内存过快,导致新产生的脏页的速率比起传输脏页的速率来讲过快,导致需要过长时间才能迁移完成,或者极端情况下产生脏页的速度大于脏页传输速率,这样热迁移永远不可能收敛。因此这种情况下需要增加一种机制,抑制vcpu的执行,让脏页生成速度变慢。那么什么时候qemu知道产生了多少脏页呢,这就需要在发送的时候与内核交互,获取当前系统的脏页信息,而内核是如何知道哪些页被写脏了呢,简单来说就是在迁移的时候认为虚拟机所有的页都为脏页,在相关的页被迁移后,需要将该页置为只读,当再次写该页面时,由于权限异常而将页变为可写状态并对其进行修改,同时记录下该页为dirty page。同理,当该页被迁移后,仍然需要再次将该页置为只读,以免该页被再次修改。
static void migration_bitmap_sync(RAMState rs)
{
。。。。。。
/同步脏页信息/
memory_global_dirty_log_sync();
qemu_mutex_lock(&rs->bitmap_mutex);
rcu_read_lock();
RAMBLOCK_FOREACH(block) {
migration_bitmap_sync_range(rs, block, 0, block->used_length);
}
。。。。。。。。。
/如果满足条件则抑制vcpu/
if ((rs->num_dirty_pages_period * TARGET_PAGE_SIZE >
(bytes_xfer_now - rs->bytes_xfer_prev) / 2) &&
(++rs->dirty_rate_high_cnt >= 2)) {
trace_migration_throttle();
rs->dirty_rate_high_cnt = 0;
mig_throttle_guest_down();
}
。。。。。。
}
这里抑制的条件是在传输num_dirty_pages_period的时间内,如果新产生的脏页数量大于这段时间内传输的脏页数量的50%,那么需要考虑抑制vcpu以减少脏页产生的速率,这里抑制的程度会随着没能抑制住脏页产生的速率而逐渐加大抑制力度。
Mig_throttle_guest_down->
Cpu_throttle_set:
void cpu_throttle_set(int new_throttle_pct)
{
/ Ensure throttle percentage is within valid range */
new_throttle_pct = MIN(new_throttle_pct, CPU_THROTTLE_PCT_MAX);
new_throttle_pct = MAX(new_throttle_pct, CPU_THROTTLE_PCT_MIN);
atomic_set(&throttle_percentage, new_throttle_pct);
timer_mod(throttle_timer, qemu_clock_get_ns(QEMU_CLOCK_VIRTUAL_RT) +
CPU_THROTTLE_TIMESLICE_NS);
}
通过设置throttle_percentage百分比,然后触发throttle_timer定时器,该定时器的回调函数为cpu_throttle_timer_tick:
static void cpu_throttle_timer_tick(void *opaque)
{
CPUState *cpu;
double pct;
/* Stop the timer if needed */
if (!cpu_throttle_get_percentage()) {
return;
}
/将cpu_throttle_thread函数挂载到cpu->queued_work_first队列/
CPU_FOREACH(cpu) {
if (!atomic_xchg(&cpu->throttle_thread_scheduled, 1)) {
async_run_on_cpu(cpu, cpu_throttle_thread,
RUN_ON_CPU_NULL);
}
}
/每CPU_THROTTLE_TIMESLICE_NS / (1-pct)时间触发timer,设置throttle/
pct = (double)cpu_throttle_get_percentage()/100;
timer_mod(throttle_timer, qemu_clock_get_ns(QEMU_CLOCK_VIRTUAL_RT) +
CPU_THROTTLE_TIMESLICE_NS / (1-pct));
}
针对每个vcpu,将cpu_throttle_thread函数挂载到cpu->queued_work_first队列,然后调用qemu_cpu_kick函数向vcpu发送SIG_IPI信号,然后vcpu会exit guest,发现在内核态无法处理SIG_IPI信号而退出到用户态:从qemu对SIG_IPI信号的注册函数可以看到:
static void kvm_ipi_signal(int sig)
{
if (current_cpu) {
assert(kvm_immediate_exit);
kvm_cpu_kick(current_cpu);
}
}
void kvm_init_cpu_signals(CPUState *cpu)
{
。。。。。。。
memset(&sigact, 0, sizeof(sigact));
sigact.sa_handler = kvm_ipi_signal;
sigaction(SIG_IPI, &sigact, NULL);
。。。。。。。。。
}
Qemu的SIG_IPI信号的响应函数kvm_cpu_kick只做了一件事:
atomic_set(&cpu->kvm_run->immediate_exit, 1)
这里就很明显了,当vcpu收到SIG_IPI信号后,guest VMexit,然后执行信号处理函数,之后继续在内核态运行,此时在内核态的执行上下文为:
int kvm_arch_vcpu_ioctl_run(struct kvm_vcpu *vcpu, struct kvm_run *kvm_run)
{
if (kvm_run->immediate_exit)
r = -EINTR;
else
r = vcpu_run(vcpu);
out:
kvm_put_guest_fpu(vcpu);
post_kvm_run_save(vcpu);
kvm_sigset_deactivate(vcpu);
return r;
}
这里因返回-EINTR导致qemu回到用户态,然后在用户态qemu有如下调用关系:
qemu_kvm_cpu_thread_fn->
qemu_wait_io_event->
qemu_wait_io_event_common->
process_queued_cpu_work
在process_queued_cpu_work中遍历cpu->queued_work_first队列并执行,这里也就调用到了cpu_throttle_thread函数,该函数抑制vcpu执行的原理是根据throttle_percentage计算出vcpu需要被抑制执行的时间值,然后休眠该时间长度,以这种方式来减少vcpu的执行,从而降低dirty page的产生速率。
后记:热迁移涉及的内容众多,这里只梳理了主体架构流程,其中很多细节和优化点都值得再去研究,但时间有限,有空再慢慢积累。