qemu2 时钟系统分析

我们都知道程序能够执行,cpu能够运转,全都是靠时钟系统去驱动,我们今天来分析下qemu时钟系统如何工作
time系统的文章主要在include/qemu/timer.h 中,我们主要分析它是如何实现的
首先分析下时钟的功能
1 记录时间
2 处理定时任务

qemu支持四种时钟

/**
 1. QEMUClockType:
 2.  3. The following clock types are available:
 4.  5. @QEMU_CLOCK_REALTIME: Real time clock
 6.  7. The real time clock should be used only for stuff which does not
 8. change the virtual machine state, as it runs even if the virtual
 9. machine is stopped.
 10.  11. @QEMU_CLOCK_VIRTUAL: virtual clock
 12.  13. The virtual clock only runs during the emulation. It stops
 14. when the virtual machine is stopped.
 15.  16. @QEMU_CLOCK_HOST: host clock
 17.  18. The host clock should be used for device models that emulate accurate
 19. real time sources. It will continue to run when the virtual machine
 20. is suspended, and it will reflect system time changes the host may
 21. undergo (e.g. due to NTP).
 22.  23. @QEMU_CLOCK_VIRTUAL_RT: realtime clock used for icount warp
 24.  25. Outside icount mode, this clock is the same as @QEMU_CLOCK_VIRTUAL.
 26. In icount mode, this clock counts nanoseconds while the virtual
 27. machine is running.  It is used to increase @QEMU_CLOCK_VIRTUAL
 28. while the CPUs are sleeping and thus not executing instructions.
 */

上面有四种时钟

  • QEMU_CLOCK_REALTIME 不受虚拟系统的影响,随时间流逝而累加计数
  • QEMU_CLOCK_VIRTUAL 虚拟时钟,记录虚拟系统的时间滴答
  • QEMU_CLOCK_HOST 这个类似墙上时钟,修改宿主机系统时间会改变这个时间
  • QEMU_CLOCK_VIRTUAL_RT 在非icount模式下和QEMU_CLOCK_VIRTUAL,在icount模式下于QEMU_CLOCK_VIRTUAL不同的是在虚拟cpu休眠的时候该值也会累加

下面我们来重点看下qemu2 时钟系统的实现

初始化

  init_clocks(qemu_timer_notify_cb);

void init_clocks(QEMUTimerListNotifyCB *notify_cb)
{
    QEMUClockType type;
    for (type = 0; type < QEMU_CLOCK_MAX; type++) {
        qemu_clock_init(type, notify_cb);
    }

#ifdef CONFIG_PRCTL_PR_SET_TIMERSLACK
    prctl(PR_SET_TIMERSLACK, 1, 0, 0, 0);
#endif
}

两步操作
1 初始化4个时钟
2 设置 线程的定时器计数器为1

static void qemu_clock_init(QEMUClockType type, QEMUTimerListNotifyCB *notify_cb)
{
    QEMUClock *clock = qemu_clock_ptr(type);

    /* Assert that the clock of type TYPE has not been initialized yet. */
    assert(main_loop_tlg.tl[type] == NULL);

    clock->type = type;
    clock->enabled = (type == QEMU_CLOCK_VIRTUAL ? false : true);
    clock->last = INT64_MIN;
    QLIST_INIT(&clock->timerlists);
    notifier_list_init(&clock->reset_notifiers);
    main_loop_tlg.tl[type] = timerlist_new(type, notify_cb, NULL);
}

static inline QEMUClock *qemu_clock_ptr(QEMUClockType type)
{
    return &qemu_clocks[type];
}

static QEMUClock qemu_clocks[QEMU_CLOCK_MAX];

四个时钟使用类型作为索引放在qemu_clocks数组中,描述时钟的数据结构为QEMUClock。
QEMUClock数据结构如下


typedef struct QEMUClock {
    /* We rely on BQL to protect the timerlists */
    QLIST_HEAD(, QEMUTimerList) timerlists;

    NotifierList reset_notifiers;
    int64_t last;

    QEMUClockType type;
    bool enabled;
} QEMUClock;

QEMUTimerList用于存放定时器链表, 注意定时器为每个滴答都会回调
NotifierList 用于时钟被重置时调用
type表示时钟类型
enabled表示时钟是否禁用
last 最后一次查询的时间

在初始化过程中,由于虚拟机还未启动所以QEMU_CLOCK_VIRTUAL类型的时钟被禁用,默认计数器值设置为INT64_MIN.
main_loop_tlg.tl[type] = timerlist_new(type, notify_cb, NULL); 函数用于初始化工作在主线程的定时器链表。
timerlist_new函数用于初始化时钟的定时器链表

QEMUTimerList *timerlist_new(QEMUClockType type,
                             QEMUTimerListNotifyCB *cb,
                             void *opaque)
{
    QEMUTimerList *timer_list;
    QEMUClock *clock = qemu_clock_ptr(type);

    timer_list = g_malloc0(sizeof(QEMUTimerList));
    qemu_event_init(&timer_list->timers_done_ev, true);
    timer_list->clock = clock;
    timer_list->notify_cb = cb;
    timer_list->notify_opaque = opaque;
    qemu_mutex_init(&timer_list->active_timers_lock);
    QLIST_INSERT_HEAD(&clock->timerlists, timer_list, list);
    return timer_list;
}

这个函数其实就是把一个定时器回调函数注册到了这个时钟的timer队列里面,由此可见主线程的定时器回调函数为qemu_timer_notify_cb。另外主线程的定时器不当可以从定时器结构QEMUClock.timer_list 中索引,还可以从main_loop_tlg中索引。
这样定时器就初始化完成了.
我们来看下timer是如何计算计算时间的
int64_t qemu_clock_get_ns(QEMUClockType type) 函数是用来获取时钟上的时间的,我们来看看它的实现

int64_t qemu_clock_get_ns(QEMUClockType type)
{
    int64_t now, last;
    QEMUClock *clock = qemu_clock_ptr(type);

    switch (type) {
    case QEMU_CLOCK_REALTIME:
        return get_clock();
    default:
    case QEMU_CLOCK_VIRTUAL:
        if (use_icount) {
            return cpu_get_icount();
        } else {
            return cpu_get_clock();
        }
    case QEMU_CLOCK_HOST:
        now = REPLAY_CLOCK(REPLAY_CLOCK_HOST, get_clock_realtime());
        last = clock->last;
        clock->last = now;
        if (now < last || now > (last + get_max_clock_jump())) {
            notifier_list_notify(&clock->reset_notifiers, &now);
        }
        return now;
    case QEMU_CLOCK_VIRTUAL_RT:
        return REPLAY_CLOCK(REPLAY_CLOCK_VIRTUAL_RT, cpu_get_clock());
    }
}

对于没用时钟使用不同时间获取函数,先来看看QEMU_CLOCK_REALTIME, 这是一个不受vcpu影响的累加计数器,get_clock其实是使用clock_gettime(CLOCK_MONOTONIC, &ts) 实现的,所以是获取的主机开机后经过的相对时间。

QEMU_CLOCK_VIRTUAL 则是从vcpu获取的时间或者icount,这里不做展开了。
QEMU_CLOCK_HOST 则使用timeofday函数获取的真实时间,这是一个绝对时间
QEMU_CLOCK_VIRTUAL_RT 获取vcpu时间

然后我们来看下如何注册一个timer
我们前边已经知道timer的结构
一个QEMUClock下面有一个链表 ,叫做timerlists,类型为QEMUTimerList, QEMUTimerList里面又有个链表,叫做active_timers,链表的元素类型为QEMUTimer,active_timers里面的元素是按照时间顺序排序的,也就是最先到期的元素在最前面。我们来看下QEMUTimerList和QEMUTimer的定义

//定时器链表
struct QEMUTimerList {
    QEMUClock *clock;    //所属的QEMUClock
    QemuMutex active_timers_lock;  //用于修改active_timers链表的锁
    QEMUTimer *active_timers;   // timer链表
    QLIST_ENTRY(QEMUTimerList) list;  //用于挂载到QEMUClock
    QEMUTimerListNotifyCB *notify_cb; //该队列有定时器到期后的回调
    void *notify_opaque; //回调的参数

    /* lightweight method to mark the end of timerlist's running */
    QemuEvent timers_done_ev; //后面分析
};

//定时器
struct QEMUTimer {
    int64_t expire_time;        /* in nanoseconds */  //到期时间,绝对时间,单位ns
    QEMUTimerList *timer_list; // 所属的QEMUTimerList
    QEMUTimerCB *cb;  //定时器到期回调
    void *opaque;  //回调参数
    QEMUTimer *next; //下一个元素
    int scale;  //用于归一时间单位的参数,这个设计容易引起混乱
};

了解了定时器链表和定时器后我们就可以分析如何使用定时器了。
注册一个定时器的方法为timer_mod_xxx和timer_mod_anticipate_xxx,
后面的xxx表示时间单位不同。拿timer_mod_ns和timer_mod_anticipate_xxx举例,二者的签名如下

void timer_mod_ns(QEMUTimer *ts, int64_t expire_time);
void timer_mod_anticipate_ns(QEMUTimer *ts, int64_t expire_time);

都是两个参数,第一个参数为定时器对象,第二个参数则为到期时间。
二者的区别是timer_mod_ns注册的到期时间如果小于当前时间则

/* modify the current timer so that it will be fired when current_time
   >= expire_time or the current deadline, whichever comes earlier.
   The corresponding callback will be called. */
void timer_mod_anticipate_ns(QEMUTimer *ts, int64_t expire_time)
{
    QEMUTimerList *timer_list = ts->timer_list;
    bool rearm;

    qemu_mutex_lock(&timer_list->active_timers_lock);
    if (ts->expire_time == -1 || ts->expire_time > expire_time) {
        if (ts->expire_time != -1) {
            timer_del_locked(timer_list, ts);
        }
        rearm = timer_mod_ns_locked(timer_list, ts, expire_time);
    } else {
        rearm = false;
    }
    qemu_mutex_unlock(&timer_list->active_timers_lock);

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

/* modify the current timer so that it will be fired when current_time
   >= expire_time. The corresponding callback will be called. */
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);
    }
}

两个函数非常接近,只不过前者不允许修改向后修改定时时间,后者则随意修改。另外这两个函数不但可以修改定时器,还可以添加定时器。

我们以timer_mod_ns进行分析,timer_mod_ns 首先从所属的QEMUTimer删除定时器,然后再调用timer_mod_ns_locked添加定时器,我们来看下timer_mod_ns_locked 函数


static bool timer_mod_ns_locked(QEMUTimerList *timer_list,
                                QEMUTimer *ts, int64_t expire_time)
{
    QEMUTimer **pt, *t;

    /* add the timer in the sorted list */
    pt = &timer_list->active_timers;
    for (;;) {
        t = *pt;
        if (!timer_expired_ns(t, expire_time)) {
            break;
        }
        pt = &t->next;
    }
    ts->expire_time = MAX(expire_time, 0);
    ts->next = *pt;
    atomic_set(pt, ts);

    return pt == &timer_list->active_timers;
}

如我们之前说的,expire_time进行排序,放入链表,并返回定时器链表最近到期时间是否跟新。 在timer_mod_ns 这个函数中如果链表最近到期时间被更新则会触发调用timerlist_rearm函数,代表链表重新装配
我们开看下它的实现

static void timerlist_rearm(QEMUTimerList *timer_list)
{
    /* Interrupt execution to force deadline recalculation.  */
    if (timer_list->clock->type == QEMU_CLOCK_VIRTUAL) {
        qemu_start_warp_timer();
    }
    timerlist_notify(timer_list);
}

首先如果是虚拟时钟则会调用qemu_start_warp_timer更新时钟
另外所有时钟类型都会调用timerlist_notify通知重新QEMUTimer重新装配。


void timerlist_notify(QEMUTimerList *timer_list)
{
    if (timer_list->notify_cb) {
        timer_list->notify_cb(timer_list->notify_opaque, timer_list->clock->type);
    } else {
        qemu_notify_event();
    }
}

这里出现了分支,为什么会这样呢,对于QEMUTimerList重新装配,也就是最近到期时间更新的处理有两种情况,一种是QEMUTimerList 设置了QEMUTimerListNotifyCB *notify_cb 函数,则表示QEMUTimerList注册要自己处理链表重新装配时间,则调用回调函数自行处理。 否则的话就是默认机制调用qemu_notify_event() 进行通知。我们稍后展开qemu_notify_event()函数,先以主线程处理重新装配的回调函数qemu_timer_notify_cb

void qemu_timer_notify_cb(void *opaque, QEMUClockType type)
{
    if (!use_icount || type != QEMU_CLOCK_VIRTUAL) {
        qemu_notify_event();
        return;
    }

    if (qemu_in_vcpu_thread()) {
        /* A CPU is currently running; kick it back out to the
         * tcg_cpu_exec() loop so it will recalculate its
         * icount deadline immediately.
         */
        qemu_cpu_kick(current_cpu);
    } else if (first_cpu) {
        /* qemu_cpu_kick is not enough to kick a halted CPU out of
         * qemu_tcg_wait_io_event.  async_run_on_cpu, instead,
         * causes cpu_thread_is_idle to return false.  This way,
         * handle_icount_deadline can run.
         * If we have no CPUs at all for some reason, we don't
         * need to do anything.
         */
        async_run_on_cpu(first_cpu, do_nothing, RUN_ON_CPU_NULL);
    }
}

我们不去管icount模式,发现qemu_timer_notify_cb函数也是调用了qemu_notify_event函数。 好了我们可以分析qemu_notify_event函数了,到底是什么推进动timer前进。

void qemu_notify_event(void)
{
    if (!qemu_aio_context) {
        return;
    }
    qemu_bh_schedule(qemu_notify_bh);
}

到这里有引入了新的变量qemu_aio_context(看来这个timer是使用aio实现的),我们不得不去分析qemu_aio_context。
这还要回到clock初始化的时候

int qemu_init_main_loop(Error **errp)
{
    int ret;
    GSource *src;
    Error *local_error = NULL;

    init_clocks(qemu_timer_notify_cb);

    ret = qemu_signal_init();
    if (ret) {
        return ret;
    }

    qemu_aio_context = aio_context_new(&local_error);
    if (!qemu_aio_context) {
        error_propagate(errp, local_error);
        return -EMFILE;
    }
    qemu_notify_bh = qemu_bh_new(notify_event_cb, NULL);
    gpollfds = g_array_new(FALSE, FALSE, sizeof(GPollFD));
    src = aio_get_g_source(qemu_aio_context);
    g_source_set_name(src, "aio-context");
    g_source_attach(src, NULL);
    g_source_unref(src);
    src = iohandler_get_g_source();
    g_source_set_name(src, "io-handler");
    g_source_attach(src, NULL);
    g_source_unref(src);
    return 0;
}

在init_clocks 执行完之后紧接着调用了aio相关的一些列函数,包括aio_context_new,qemu_bh_new ,aio_get_g_source等函数,我们逐一分析

AioContext *aio_context_new(Error **errp)
{
    int ret;
    AioContext *ctx;

    ctx = (AioContext *) g_source_new(&aio_source_funcs, sizeof(AioContext));
    aio_context_setup(ctx);

    ret = event_notifier_init(&ctx->notifier, false);
    if (ret < 0) {
        error_setg_errno(errp, -ret, "Failed to initialize event notifier");
        goto fail;
    }
    g_source_set_can_recurse(&ctx->source, true);
    qemu_lockcnt_init(&ctx->list_lock);

    ctx->co_schedule_bh = aio_bh_new(ctx, co_schedule_bh_cb, ctx);
    QSLIST_INIT(&ctx->scheduled_coroutines);

    aio_set_event_notifier(ctx, &ctx->notifier,
                           false,
                           (EventNotifierHandler *)
                           event_notifier_dummy_cb,
                           event_notifier_poll);
#ifdef CONFIG_LINUX_AIO
    ctx->linux_aio = NULL;
#endif
    ctx->thread_pool = NULL;
    qemu_rec_mutex_init(&ctx->lock);
    timerlistgroup_init(&ctx->tlg, aio_timerlist_notify, ctx);

    ctx->poll_ns = 0;
    ctx->poll_max_ns = 0;
    ctx->poll_grow = 0;
    ctx->poll_shrink = 0;

    return ctx;
fail:
    g_source_destroy(&ctx->source);
    return NULL;
}

这里设计到了glibc的事件处理机制,可以参考Qemu事件处理机制简介这篇
这里主要注册了一个事件循环,

发布了113 篇原创文章 · 获赞 22 · 访问量 9万+

猜你喜欢

转载自blog.csdn.net/woai110120130/article/details/99689645