QEMU timer模块分析

qemu中所有的与时间相关的模块都基于timer.hqemu-timer.c实现,包括arm的计时器arm_timer.c以及通用的倒数计时器ptimer.c,本文分析timer.h文件,探究qemu中timer的机制和原理,再实现一个自己的加数计时器itimer.c

QEMUClock

QEMUClockType

QEMUClock一共有四种类型,分别是:QEMU_CLOCK_REALTIMEQEMU_CLOCK_VIRTUALQEMU_CLOCK_HOSTQEMU_CLOCK_VIRTUAL_RT,下面分别解释。

  • QEMU_CLOCK_REALTIME

The real time clock should be used only for stuff which does not change the virtual machine state, as it is run even if the virtual machine is stopped. The real time clock has a frequency of 1000 Hz.

real time clock可以理解为真实的(相对于虚拟的)时钟,即使虚拟机停止或者挂起了,这个时钟也会继续走,这就意味着这个时钟只能用在不涉及到虚拟机状态的地方,否则一旦挂起后恢复,虚拟机状态就会出问题。

它实际上调用的是clock_gettime()CLOCK_MONOTONIC,这是一个不可设定的恒定态时钟,从系统启动之后开始测量,并且不可修改,手动修改系统时间不会对其产生影响。

  • QEMU_CLOCK_VIRTUAL

The virtual clock is only run during the emulation. It is stopped when the virtual machine is stopped. Virtual timers use a high precision clock, usually cpu cycles (use ticks_per_sec).

virtual clock与real time clock相反,虚拟时钟只会在虚拟机运行时运行,当虚拟机停止了,它也会停止。因为这种特性,它会被用于处理虚拟机硬件的一些状态,例如一些外设的定时器。它使用的是高精度的时钟,通常就是通过CPU的cycle来计算的。

这其实很好理解,假设你在虚拟机上运行了一个定时程序,这个程序要求每隔60s打印一个“hello world”,如果你使用real time clock作为计时器,那么当你在程序运行到一半的时候将虚拟机挂起,等待一段时间后恢复,程序是无法从上一次停止的时刻开始继续倒计时。只有在使用virtual clock的情况下,虚拟机挂起时,会将程序的时间也冻结了,恢复时,程序会从上一次停止的时刻开始继续倒计时。

  • QEMU_CLOCK_HOST

The host clock should be use for device models that emulate accurate real time sources. It will continue to run when the virtual machine is suspended, and it will reflect system time changes the host may undergo (e.g. due to NTP). The host clock has the same precision as the virtual clock.

host clock 用于需要使用真实时间的设备,虚拟机挂起或者停止时它依然会运行,它反应的是系统时钟时间(你可以简单的理解为它用的就是date的时间),因此相比于real time clock它会收到系统时间的影响(例如,由于NTP时间同步导致的改变),host clock和virtual clock具有相同的精确度。

host clock实际上使用的是gettimeofday函数,这个函数返回的是一个日历时间,因此会因为宿主机系统的date改变而改变。real time clock在万不得已的情况下也会使用gettimeofday

  • QEMU_CLOCK_VIRTUAL_RT

Outside icount mode, this clock is the same as @QEMU_CLOCK_VIRTUAL. In icount mode, this clock counts nanoseconds while the virtual machine is running. It is used to increase @QEMU_CLOCK_VIRTUAL while the CPUs are sleeping and thus not executing instructions.

在非icount模式下,这个clock和virtual clock是一样的,不同的在于,当该clock处于icount模式下,它会以纳秒来计数。当cpu sleep时,它被用来增加virtual clock,这样就不需要运行额外的指令了。

要很好的理解virtual rt clock和virtual clock的关系和区别,需要对QEMU中的icount有一定的了解。

icount在QEMU中全称为TCG Instruction Counting。它是TCG用于指令计数的一个组件,当CPU在icount模式下sleep时,通过它来计算时间。

qemu_clock_get_ns

为了更好的理解前面提到的4中clock type的关系,可以直接看/qemu/util/qemu-timer.c文件下的qemu_clock_get_ns函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
/* get host real time in nanosecond */
static inline int64_t get_clock_realtime(void)
{
    struct timeval tv;

    gettimeofday(&tv, NULL);
    return tv.tv_sec * 1000000000LL + (tv.tv_usec * 1000);
}

/* Warning: don't insert tracepoints into these functions, they are
   also used by simpletrace backend and tracepoints would cause
   an infinite recursion! */
#ifdef _WIN32
extern int64_t clock_freq;

static inline int64_t get_clock(void)
{
    LARGE_INTEGER ti;
    QueryPerformanceCounter(&ti);
    return muldiv64(ti.QuadPart, NANOSECONDS_PER_SECOND, clock_freq);
}

#else

extern int use_rt_clock;

static inline int64_t get_clock(void)
{
    if (use_rt_clock) {
        struct timespec ts;
        clock_gettime(CLOCK_MONOTONIC, &ts);
        return ts.tv_sec * 1000000000LL + ts.tv_nsec;
    } else {
        /* XXX: using gettimeofday leads to problems if the date
           changes, so it should be avoided. */
        return get_clock_realtime();		// 实际上是gettimeofday(),不建议使用
    }
}
#endif

int64_t qemu_clock_get_ns(QEMUClockType type)
{
    switch (type) {
    case QEMU_CLOCK_REALTIME:
        return get_clock();		
    default:
    case QEMU_CLOCK_VIRTUAL:
        if (use_icount) {
            return cpu_get_icount();		// cpu cycle计数
        } else {
            return cpu_get_clock();			// cpu时钟
        }
    case QEMU_CLOCK_HOST:
        return REPLAY_CLOCK(REPLAY_CLOCK_HOST, get_clock_realtime()); 
    case QEMU_CLOCK_VIRTUAL_RT:
        return REPLAY_CLOCK(REPLAY_CLOCK_VIRTUAL_RT, cpu_get_clock());
    }
}

以autoconverge为例

migrate_auto_converge是QEMU热迁移支持的一个特性,它可以通过自动降频CPU的方式来减少写内存的频率,而降频的方法就是通过计算需要降频的时间和执行时间的比例来halt cpu。

启动虚拟机时,通过cpu_throttle_init->timer_new_ns注册收敛回调函数:

1
2
3
4
5
void cpu_throttle_init(void)
{
    throttle_timer = timer_new_ns(QEMU_CLOCK_VIRTUAL_RT,
                                  cpu_throttle_timer_tick, NULL);
}

入口函数mig_throttle_guest_down

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
static void mig_throttle_guest_down(uint64_t bytes_dirty_period,
                                    uint64_t bytes_dirty_threshold)
{
    /* ........ */
    /* We have not started throttling yet. Let's start it. */
    if (!cpu_throttle_active()) {
        cpu_throttle_set(pct_initial);
    } else {
        /* Throttling already on, just increase the rate */
        if (!pct_tailslow) {
            throttle_inc = pct_increment;
        } else {
            /* Compute the ideal CPU percentage used by Guest, which may
             * make the dirty rate match the dirty rate threshold. */
            cpu_now = 100 - throttle_now;
            cpu_ideal = cpu_now * (bytes_dirty_threshold * 1.0 /
                        bytes_dirty_period);
            throttle_inc = MIN(cpu_now - cpu_ideal, pct_increment);
        }
        /* 通过脏页率,计算想要的收敛时间 */
        cpu_throttle_set(MIN(throttle_now + throttle_inc, pct_max));
    }
}

调用cpu_throttle_set->timer_mod启动时钟:

1
2
3
4
5
6
void cpu_throttle_set(int new_throttle_pct)
{
    /* ........... */
    timer_mod(throttle_timer, qemu_clock_get_ns(QEMU_CLOCK_VIRTUAL_RT) +
                                       CPU_THROTTLE_TIMESLICE_NS);
}

当timer modify到预设的值,调用回调函数cpu_throttle_timer_tick

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
static void cpu_throttle_timer_tick(void *opaque)
{
    CPUState *cpu;
    double pct;

    /* Stop the timer if needed */
    if (!cpu_throttle_get_percentage()) {
        return;
    }
    CPU_FOREACH(cpu) {
        if (!atomic_xchg(&cpu->throttle_thread_scheduled, 1)) {
            async_run_on_cpu(cpu, cpu_throttle_thread,
                             RUN_ON_CPU_NULL);
        }
    }

    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执行cpu_throttle_thread线程,用于将一部分cpu时间设置为halt(通过pthread_cond_timedwait函数)。

QEMUClock初始化流程

QEMUClock

  1. qemu_init_main_loop中调用init_clocks初始化4种Clock类型:
  2. qemu_clock_init初始化4种Clock类型,并且每种Clock下都有一个TimerList,将TimerList加入到全局的TimerListGroup(main_loop_tlg)中。

QEMUClock执行流程

简化一下前面提到的auto-converge的例子:

1
timer_new_ns()->timer_mod()

本质上就只有两个调用,timer_new_nsmain_loop_tlg下对应的type中添加一个QEMUTimer。timer_mod修改当前的计时器,当current_time >= expire_time的时候,就会调用在timer_new_ns时注册的callback。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
void timer_mod_ns(QEMUTimer *ts, int64_t expire_time)
{
    QEMUTimerList *timer_list = ts->timer_list;
    bool rearm;

    qemu_mutex_lock(&timer_list->active_timers_lock);
    timer_del_locked(timer_list, ts);
    rearm = timer_mod_ns_locked(timer_list, ts, expire_time);
    qemu_mutex_unlock(&timer_list->active_timers_lock);

    if (rearm) {
        timerlist_rearm(timer_list);
    }
}

itimer设备实现

Reference

Prescaler除频器