文章目录
基本原理数据结构QEMUTimerqemu_work_item宏实现流程定时器注册启动cpu throttlevcpu睡眠停止cpu throttle基本原理
cpu throttle主要目的是限制虚机vcpu的运行,降低虚机的脏页速率。在热迁移长时间无法完成的情况下,可以使用这个手段降低脏页速率,从而促使迁移收敛。cpu throttle核心思想非常简单:让处于运行状态的vcpu退出到kvm,然后返回用户态qemu,睡眠一段时间后再进入guest。cpu throttle的输入参数是vcpu睡眠时间与总时间的百分比。公式如下:这里我们将sleep_time+run_time
称为一个throttle周期,qemu在限制vcpu运行时,保证了vcpu在一个throttle周期内的运行时间固定,因此,不同百分比(throttle_percentage
)会导致throttle周期不同,throttle_percentage
越大,vcpu睡眠的时间越多,throttle周期越长。下面我们以throttle周期内运行时间10ms为例(这是qemu的默认实现),介绍不同迁移过程中设置不同cpu throttle时qemu的工作流程:throttle percentage: 50%、run_time: 10ms、sleep_time:10ms、throttle period: 20ms
下图描述的是cpu throttle设置为50的情况下每个vcpu的运行情况,由于每个throttle周期内run_time相同,都是10ms,根据比例计算得到throttle周期为20ms。因此qemu主线程中设置定时器每20ms触发一次回调,回调函数中会kick vcpu。vcpu被kick后退回到用户态,休眠10ms后会重新进入guest态运行,10ms后再次被kick,周而复始。
throttle percentage: 60%、run_time: 10ms、sleep_time:15ms、throttle period: 25ms
数据结构
QEMUTimer
QEMUTimer用来实现定时,周期性触发回调函数,kick处于运行态的vcpu:struct QEMUTimer {int64_t expire_time; /* 超时时间,单位为ns */QEMUTimerList *timer_list;QEMUTimerCB *cb;/* 定时器关联的回调函数 */void *opaque;QEMUTimer *next;int attributes;int scale;};
qemu_work_item
qemu_work_item用来实现vcpu运行任务的封装,所有需要vcpu线程在用户态执行的任务会被链表链接起来,放在CPUState结构体的work_list成员中,一旦vcpu退回到用户态,vcpu线程会遍历该链表的所有任务并依次执行struct qemu_work_item {QSIMPLEQ_ENTRY(qemu_work_item) node;/* 需要vcpu线程在用户态执行的任务通过该成员链接到work_list链表 */run_on_cpu_func func;/* 任务执行时调用的具体函数 */run_on_cpu_data data;/* 任务执行时调用的具体函数参数 */bool free, exclusive, done;};struct CPUState {......QSIMPLEQ_HEAD(, qemu_work_item) work_list;/* vcpu上所有待执行的任务有work_list维护 */......}
宏
#define CPU_THROTTLE_PCT_MIN 1/* 允许cpu睡眠的最小占比 */#define CPU_THROTTLE_PCT_MAX 99/* 允许cpu睡眠的最大占比 */#define CPU_THROTTLE_TIMESLICE_NS 10000000/* 每个throttle周期固定运行10ms */
实现流程
定时器注册
cpu throttle通过定时器周期性kick vcpu实现,需要注册qemu定时器,这个动作包含在qemu主线程启动过程中,流程如下:qemu_initcpu_timers_initcpu_throttle_init/* 注册throttle timer* 超时单位:ns* timer回调:cpu_throttle_timer_tick* 超时时间:无* 超时时间类型:虚拟机运行时间* */throttle_timer = timer_new_ns(QEMU_CLOCK_VIRTUAL_RT, cpu_throttle_timer_tick, NULL);
启动cpu throttle
迁移线程通过调用cpu_throttle_set
函数触发cpu throttle的定时器,参数为vcpu睡眠时间与throttle周期的百分比,qemu定义全局静态变量throttle_percentage
来控制throttle的运行,throttle_percentage
大于0时触发cpu throttle,等于0时停止cpu throttle,流程如下:void cpu_throttle_set(int new_throttle_pct){/** boolean to store whether throttle is already active or not,* before modifying throttle_percentage* 取出全局变量throttle_percentage判断throttle是否启动*/bool throttle_active = cpu_throttle_active();/* 确保睡眠百分比在1-99范围内 *//* 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);qatomic_set(&throttle_percentage, new_throttle_pct);/* 如果throttle没有启动,触发 */if (!throttle_active) {cpu_throttle_timer_tick(NULL);}}
cpu_throttle_set
对输入进行了检查并查看throttle是否启动,如果没有启动则触发,启动cpu throttle的真正工作在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()) {/* 检查throttle是否有必要停止 */return;}CPU_FOREACH(cpu) {/* 遍历虚机每个vcpu */if (!qatomic_xchg(&cpu->throttle_thread_scheduled, 1)) {/* 如果vcpu启动throttle任务 */async_run_on_cpu(cpu, cpu_throttle_thread,/* 将cpu_throttle_thread任务添加到vcpu的work_list链表中 */RUN_ON_CPU_NULL);}}/* 根据睡眠百分比计算throttle周期,将其设置为throttle timer的超时时间 * 从这里可以看出,throttle timer的超时时间与睡眠百分比有关**/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));}
将cpu_throttle_thread
函数封装成任务并挂到vcpu的work_list链表的工作由async_run_on_cpu
完成,继续分析:
void async_run_on_cpu(CPUState *cpu, run_on_cpu_func func, run_on_cpu_data data){struct qemu_work_item *wi;/* 将cpu_throttle_thread封装成qemu_work_item */wi = g_malloc0(sizeof(struct qemu_work_item));wi->func = func;wi->data = data;wi->free = true;/* 挂入work_list链表 */queue_work_on_cpu(cpu, wi);}
完成work挂入work_list链表的操作后,queue_work_on_cpu
最后会kick vcpu,让vcpu从guest态返回到用户态,如下:
static void queue_work_on_cpu(CPUState *cpu, struct qemu_work_item *wi){qemu_mutex_lock(&cpu->work_mutex);QSIMPLEQ_INSERT_TAIL(&cpu->work_list, wi, node);/* 任务添加到链表 */wi->done = false;qemu_mutex_unlock(&cpu->work_mutex);/* 使vcpu从guest态退出到用户态,处理work_list上的任务 */qemu_cpu_kick(cpu);}
vcpu睡眠
在分析vcpu如何执行睡眠任务前,我们先追溯一下vcpu线程工作流程:x86_cpu_realizefnqemu_init_vcpucpus_accel->create_vcpu_thread(cpu)<=>kvm_start_vcpu_threadqemu_thread_create(cpu->thread, thread_name, kvm_vcpu_thread_fn, cpu, QEMU_THREAD_JOINABLE);
qemu主线在实例化vcpu时会创建vcpu线程kvm_vcpu_thread_fn
,此后vcpu线程会一直运行直到虚机被销毁,vcpu线程工作流程如下:
static void *kvm_vcpu_thread_fn(void *arg){......do {if (cpu_can_run(cpu)) {/* 下发KVM_RUN命令字让vcpu进入guest态运行* 用户态vcpu线程睡眠 * 迁移线程kick vcpu线程时,内核发送IPI核间中断到vcpu所在物理核* 硬件会让vcpu从guest态退出,返回到用户态* 然后用户态vcpu线程被唤醒,kvm_cpu_exec函数返回* */kvm_cpu_exec(cpu);......}/* 用户态vcpu线程被唤醒后,调用qemu_wait_io_event处理相关工作* 其中就包括执行work_list上的任务* */qemu_wait_io_event(cpu);} while (!cpu->unplug || cpu_can_run(cpu));......}
从上面分析可知,qemu_wait_io_event
是vcpu处理work_list上任务的入口,继续分析:
qemu_wait_io_eventqemu_wait_io_event_commonprocess_queued_cpu_workwi->func(cpu, wi->data)<=>cpu_throttle_thread
process_queued_cpu_work
函数会遍历work_list上的work并逐一执行work注册的任务,包括实现cpu throttle注册的cpu_throttle_thread
任务,我们分析cpu_throttle_thread
函数的具体实现:
static void cpu_throttle_thread(CPUState *cpu, run_on_cpu_data opaque){double pct;double throttle_ratio;int64_t sleeptime_ns, endtime_ns;/* 取出全局变量throttle_percentage判断是否继续睡眠 */if (!cpu_throttle_get_percentage()) {return;}/* 根据睡眠百分比计算得到线程要睡眠的时间 */pct = (double)cpu_throttle_get_percentage() / 100;throttle_ratio = pct / (1 - pct);/* Add 1ns to fix double's rounding error (like 0.9999999...) *//* 算出线程要睡眠的时间 */sleeptime_ns = (int64_t)(throttle_ratio * CPU_THROTTLE_TIMESLICE_NS + 1);endtime_ns = qemu_clock_get_ns(QEMU_CLOCK_REALTIME) + sleeptime_ns;/* 如果线程睡眠时间大于0并且cpu没有终止运行,进入睡眠 */while (sleeptime_ns > 0 && !cpu->stop) {if (sleeptime_ns > SCALE_MS) {/* 如果睡眠时间大于1ms,我们将以ns为单位的sleeptime_ns转化为ms * 让线程在halt_cond信号量上休眠等待sleeptime_ns,休眠过程中如果* 信号量halt_cond由信号发来,线程会被唤醒* */qemu_cond_timedwait_iothread(cpu->halt_cond,sleeptime_ns / SCALE_MS);} else {/* 如果睡眠的时间小于1ms,直接睡眠 */qemu_mutex_unlock_iothread();g_usleep(sleeptime_ns / SCALE_US);qemu_mutex_lock_iothread();}/* 更新睡眠时间 */sleeptime_ns = endtime_ns - qemu_clock_get_ns(QEMU_CLOCK_REALTIME);}/* 完成睡眠,设置throttle_thread_scheduled为0表示vcpu的throttle线程终止 */qatomic_set(&cpu->throttle_thread_scheduled, 0);}
这里有个疑问,为什么睡眠时间大于1ms和小于1ms要分开处理,个人猜测是为了处理cpu终止的情况,由于睡眠时间大于1ms,时间较长,这个过程中有可能qemu主线程想让vcpu线程终止,qemu主线程会设置cpu->stop为true,并在cpu->halt_cond广播,此时throttle线程睡在cpu->halt_cond上就会被唤醒,然后更新睡眠时间,如果检查到睡眠时间还是大于0但cpu->stop为true,那么throttle线程就不会再进入循环继续睡眠而直接终止。从而达到终止throttle线程的目的。对于睡眠时间小于1ms,qemu可能认为时间较短影响很小。
停止cpu throttle
停止cpu throttle通过cpu_throttle_stop
完成,非常简单,直接设置全局变量throttle_percentage
为0,定时器的回调函数和throttle线程都会检查这个变量,一旦为0,就会直接返回void cpu_throttle_stop(void){qatomic_set(&throttle_percentage, 0);}
如果觉得《cpu throttle原理浅析》对你有帮助,请点赞、收藏,并留下你的观点哦!