qemu协程分析

我写了个简化版的https://github.com/TangGee/coroutine

要介绍qemu协程前先来说一下协程是什么?

要说协程先从线程说起,在我们传统的编程环境中,要实现多任务一般使用多线程,线程的调度完全靠操作系统。当一个线程的时间片用完了之后,或者进入io阻塞等条件,线程将会被挂起,进入休眠状态,从而运行其他线程。线程切换是一件开销比较大的事(涉及到页表,tbl切换,会造成大量cache 不命中),协程的主要目的就是减少这种由于线程切换造成的开销。
为了减少多线程协作陷入内核,线程上下文切换。把线程上下文切换放到用户空间,这就是协程的基本原理,一般内核管理线程切换都会进行保留现场,将线程当前的寄存器保存在栈中,如果在用户空间进行切换(这里只能说函数切换),则需要用户空间自己开辟内存保护和回复现场。
这里需要说明下,切换是指的切换成么? 其实仔细想一个,无论是线程还是协程,切换的都是函数。
这里我们可以看出,如果携程的函数调用了一个阻塞函数,还是会引起休眠,怎么办,所以协程要大量配合非阻塞io使用,当我们检测到io没有准备好时,主动切换其他携程函数来运行,这样就不会造成线程休眠和线程切换。

在分析qemu协程之前先来看看它的api

Coroutine *qemu_coroutine_create(CoroutineEntry *entry, void *opaque)
static void coroutine_delete(Coroutine *co)
void coroutine_fn qemu_coroutine_yield(void)
void qemu_aio_coroutine_enter(AioContext *ctx, Coroutine *co)
static void coroutine_pool_cleanup(Notifier *n, void *value)

qemu协程主要的api就是上面的五个函数,
qemu_coroutine_create 用于创建一个协程,其中参数CoroutineEntry为在协程中要调用的函数, opaque为参数,返回值为Coroutine,Coroutine就是代表一个协程,可想而知Coroutine必须要维护调用的函数,协程的运行时现场(后面我们回看到如何保留现场)

coroutine_delete 用于删除协程
qemu_aio_coroutine_enter 用于恢复协程执行
qemu_coroutine_yield 用于切换协程
coroutine_pool_cleanup 用于线程退出的时候清理协程

看到api之后我们对协程可能有了初步的认识,我们来分析携程的代码

首先从创建开始

Coroutine *qemu_coroutine_create(CoroutineEntry *entry, void *opaque)
{
    Coroutine *co = NULL;

    if (
    ) {
        CoroutinePool* pool = QEMU_THREAD_LOCAL_GET_PTR(co_alloc_pool);
        co = QSLIST_FIRST(&pool->pool);
        if (!co) {
            if (release_pool_size > POOL_BATCH_SIZE) {
                /* Slow path; a good place to register the destructor, too.  */
                if (!pool->cleanup_notifier.notify) {
                    pool->cleanup_notifier.notify = coroutine_pool_cleanup;
                    qemu_thread_atexit_add(&pool->cleanup_notifier);
                }

                /* This is not exact; there could be a little skew between
                 * release_pool_size and the actual size of release_pool.  But
                 * it is just a heuristic, it does not need to be perfect.
                 */
                pool->size = atomic_xchg(&release_pool_size, 0);
                QSLIST_MOVE_ATOMIC(&pool->pool, &release_pool);
                co = QSLIST_FIRST(&pool->pool);
            }
        }
        if (co) {
            QSLIST_REMOVE_HEAD(&pool->pool, pool_next);
            pool->size--;
        }
    }

    if (!co) {
        co = qemu_coroutine_new();
    }

    co->entry = entry;
    co->entry_arg = opaque;
    QSIMPLEQ_INIT(&co->co_queue_wakeup);
    return co;
}

首先协程创建这里,由于协程创建开销比较大,qemu弄了两层协程缓存池,用于复用协程。一层是线程级别的,一层则是全局的(无非就是个连标),全局的那层叫做release_pool,从release_pool中获取元素的时候耍了个小花招,先把release_pool_size设置成0 ,防止其他线程同时获取。另外查找的优先级是先从当前线程的缓存池(threadlocal的)去找,如果没有才去全局缓存池去找。
当然如果没有缓存的携程就要自己创建了,这才是关键的部分,创建完成后给携程设置工作的函数,也就是entry。

ok来看下如何真正创建协程(注意携程的两个要素,运行现场和要运行的函数),函数entry是在协程创建完设置的,那么下面要分析的qemu_coroutine_new函数则主要创建协程的工作环境。

Coroutine *qemu_coroutine_new(void)
{
    CoroutineUContext *co;
    ucontext_t old_uc, uc;
    sigjmp_buf old_env;
    union cc_arg arg = {0};
    void *fake_stack_save = NULL;

    /* The ucontext functions preserve signal masks which incurs a
     * system call overhead.  sigsetjmp(buf, 0)/siglongjmp() does not
     * preserve signal masks but only works on the current stack.
     * Since we need a way to create and switch to a new stack, use
     * the ucontext functions for that but sigsetjmp()/siglongjmp() for
     * everything else.
     */

    if (getcontext(&uc) == -1) {
        abort();
    }

    co = g_malloc0(sizeof(*co));
    co->stack_size = COROUTINE_STACK_SIZE;
    co->stack = qemu_alloc_stack(&co->stack_size);
    co->base.entry_arg = &old_env; /* stash away our jmp_buf */

    uc.uc_link = &old_uc;
    uc.uc_stack.ss_sp = co->stack;
    uc.uc_stack.ss_size = co->stack_size;
    uc.uc_stack.ss_flags = 0;

#ifdef CONFIG_VALGRIND_H
    co->valgrind_stack_id =
        VALGRIND_STACK_REGISTER(co->stack, co->stack + co->stack_size);
#endif

    arg.p = co;

    makecontext(&uc, (void (*)(void))coroutine_trampoline,
                2, arg.i[0], arg.i[1]);

    /* swapcontext() in, siglongjmp() back out */
    if (!sigsetjmp(old_env, 0)) {
        start_switch_fiber(&fake_stack_save, co->stack, co->stack_size);
        swapcontext(&old_uc, &uc);
    }

    finish_switch_fiber(fake_stack_save);

    return &co->base;
}

这里采用ucontext函数,主要是为了给携程创建一个工作现场(其实最主要的是栈,因为协程的工作环境都要保存在栈上)。 所以这个函数最开始调用的就是创建栈,之后调用makecontext的时候就建立了新的运行环境,通过swapcontext切换的新的运行环境上去。
另外这里还使用了sigsetjmp 函数执行远跳转,如何理解sigsetjmp函数呢,sigsetjmp函数其实也是用于保留现场的作用,sigsetjmp和sigsetjmp配合使用,sigsetjmp会将现场保留在它的第一个参数sigjmp_buf中,通过siglongjmp恢复sigjmp_buf保存的现场(就是重新跳转到sigsetjmp处执行),siglongjmp第二个参数则sigsetjmp的返回值。
关于ucontext可以参考文章ucontext-人人都可以实现的简单协程库
关于
sitsetjmp可以参考文章sigsetjmp用法

所以qemu创建协程保存现场的工作是由sigsetjmp做的,而ucontext的作用则是创建一个栈(方便协程的复用)。
在qemu_coroutine_new这个函数里面,第一次调用sigsetjmp返回0 ,所以调用 swapcontext(&old_uc, &uc) 切到coroutine_trampoline函数中执行,这也是ucontext的一项工作,就是等待给新创建的协程设置好运行环境后再将结果返回给调用函数。

所以我们来看coroutine_trampoline函数

static void coroutine_trampoline(int i0, int i1)
{
    union cc_arg arg;
    CoroutineUContext *self;
    Coroutine *co;
    void *fake_stack_save = NULL;

    finish_switch_fiber(NULL);

    arg.i[0] = i0;
    arg.i[1] = i1;
    self = arg.p;
    co = &self->base;

    /* Initialize longjmp environment and switch back the caller */
    if (!sigsetjmp(self->env, 0)) {
        start_switch_fiber(&fake_stack_save,
                           leader.stack, leader.stack_size);
        siglongjmp(*(sigjmp_buf *)co->entry_arg, 1);
    }

    // 协程恢复现场后执行后面函数
    finish_switch_fiber(fake_stack_save);

    while (true) {
        co->entry(co->entry_arg);
        qemu_coroutine_switch(co, co->caller, COROUTINE_TERMINATE);
    }
}

注意刚才qemu_coroutine_new函数中sigsetjump主要的作用是等待我们初始化好协程环境后延迟返回给调用者。所以coroutine_trampoline中调用的sigsetjmp函数才是用于保存协程的工作环境。 发哦村好环境后调用siglongjmp恢复qemu_coroutine_new执行,将结果返回给qemu_coroutine_new的调用者。所以siglongjmp函数后续的函数这里都不会执行,直到有人调用siglongjmp(self->env, xxx) xxx大于0的时候,才会执行后续函数,后续函数其实解释调用协程的工作函数,当协程的工作函数完成后就调用qemu_coroutine_switch切换现场(qemu_coroutine_switch)后边分析。

这样qemu_coroutine_new函数的调用者拿到了一个可以使用的协程 。 那么接下来就是使用协程了,qemu_aio_coroutine_enter函数正式这个作用

void qemu_aio_coroutine_enter(AioContext *ctx, Coroutine *co)
{
    QSIMPLEQ_HEAD(, Coroutine) pending = QSIMPLEQ_HEAD_INITIALIZER(pending);
    Coroutine *from = qemu_coroutine_self();

    QSIMPLEQ_INSERT_TAIL(&pending, co, co_queue_next);

    /* Run co and any queued coroutines */
    while (!QSIMPLEQ_EMPTY(&pending)) {
        Coroutine *to = QSIMPLEQ_FIRST(&pending);
        CoroutineAction ret;

        /* Cannot rely on the read barrier for to in aio_co_wake(), as there are
         * callers outside of aio_co_wake() */
        const char *scheduled = atomic_mb_read(&to->scheduled);

        QSIMPLEQ_REMOVE_HEAD(&pending, co_queue_next);

        trace_qemu_aio_coroutine_enter(ctx, from, to, to->entry_arg);

        /* if the Coroutine has already been scheduled, entering it again will
         * cause us to enter it twice, potentially even after the coroutine has
         * been deleted */
        if (scheduled) {
            fprintf(stderr,
                    "%s: Co-routine was already scheduled in '%s'\n",
                    __func__, scheduled);
            abort();
        }

        if (to->caller) {
            fprintf(stderr, "Co-routine re-entered recursively\n");
            abort();
        }

        to->caller = from;
        to->ctx = ctx;

        /* Store to->ctx before anything that stores to.  Matches
         * barrier in aio_co_wake and qemu_co_mutex_wake.
         */
        smp_wmb();

        ret = qemu_coroutine_switch(from, to, COROUTINE_ENTER);

        /* Queued coroutines are run depth-first; previously pending coroutines
         * run after those queued more recently.
         */
        QSIMPLEQ_PREPEND(&pending, &to->co_queue_wakeup);

        switch (ret) {
        case COROUTINE_YIELD:
            break;
        case COROUTINE_TERMINATE:
            assert(!to->locks_held);
            trace_qemu_coroutine_terminate(to);
            coroutine_delete(to);
            break;
        default:
            abort();
        }
    }
}

这里协程的co_queue_wakeup和scheduled都是用于协程的休眠唤醒机制我们后面分析,主提函数还是调用qemu_coroutine_switch 切换协程,注意这里有三个参数都是至关重要的,from表达调用者协程,to表示要调用的协程, 而COROUTINE_ENTER表示有新协程要运行,另外新的协程运行完成或者要主动切换要返回到哪里去?就是from协程,一个新加入的协程的调用者就是当前协程。 (返回值部分后面说明)

Coroutine *qemu_coroutine_self(void)
{
    if (!current) {
        current = &leader.base;
    }
    return current;
}

其实就是返回current的值,这个值是在switch的时候设置的后面我们会看到。另外leader的作用就是总要有第一个协程来引导其他协程,所以leader就是一个线程中的这样一个协程,后面我们也会分析。
这里还要思考的一点就是为什么from是current? 这主要是因为调用qemu_coroutine_switch的函数肯定是在from协程的entry函数里面。

先来看qemu_coroutine_switch

CoroutineAction __attribute__((noinline))
qemu_coroutine_switch(Coroutine *from_, Coroutine *to_,
                      CoroutineAction action)
{
    CoroutineUContext *from = DO_UPCAST(CoroutineUContext, base, from_);
    CoroutineUContext *to = DO_UPCAST(CoroutineUContext, base, to_);
    int ret;
    void *fake_stack_save = NULL;

    current = to_;

    ret = sigsetjmp(from->env, 0);
    if (ret == 0) {
        start_switch_fiber(action == COROUTINE_TERMINATE ?
                           NULL : &fake_stack_save, to->stack, to->stack_size);
        siglongjmp(to->env, action);
    }

    finish_switch_fiber(fake_stack_save);

    return ret;
}

其实很简单,就是保留调from的现场,恢复to的现场。
还记得to的现场吗, 在coroutine_trampoline中,另外to的现场也可能在qemu_coroutine_switch函数中,也就是from自己切出去的时候。另外值得注意的是siglongjmp函数恢复to协程现场的第二个参数,这里是COROUTINE_ENTER,表示进入协程执行,另外调用qemu_coroutine_switch的函数还有协程执行完成后调用 qemu_coroutine_switch(co, co->caller, COROUTINE_TERMINATE), 这里caller就是qemu_coroutine_switch中调用to而被切出去的from。所以一个协程完成调用后会切换到它的caller执行,也就是qemu_coroutine_switch中的return ret这部分。 这部分返回后就又到了qemu_aio_coroutine_enter函数的返回值处理部分。 这里有两种可能要调用的协程返回,一种是它执行完了,一种则是它不想执行了,调用 yield函数把自己切出去了.

好吧来看看yield怎么处理的

void coroutine_fn qemu_coroutine_yield(void)
{
    Coroutine *self = qemu_coroutine_self();
    Coroutine *to = self->caller;

    trace_qemu_coroutine_yield(self, to);

    if (!to) {
        fprintf(stderr, "Co-routine is yielding to no one\n");
        abort();
    }

    self->caller = NULL;
    qemu_coroutine_switch(self, to, COROUTINE_YIELD);
}

这里比较重要的一点把自己的caller设置为null了,只有这样才可以通过
qemu_aio_coroutine_enter函数再次运行。然后调用qemu_coroutine_switch参数为COROUTINE_YIELD, 要switch到的协程为调用者协程序, 所以最终还是把结果返回给qemu_aio_coroutine_enter 函数。着实有点抽象。

到这里我们就可以分析qemu_aio_coroutine_enter返回值的处理了

 switch (ret) {
        case COROUTINE_YIELD:
            break;
        case COROUTINE_TERMINATE:
            assert(!to->locks_held);
            trace_qemu_coroutine_terminate(to);
            coroutine_delete(to);
            break;
        default:
            abort();
        }

COROUTINE_TERMINATE说明协程执行完成了,咋办?善后呗,执行coroutine_delete删除。COROUTINE_YIELD则直接啥都不做,这样就又到了调用者执行。

好了到这里整个协程的框架我们也基本搞清楚了,前边还有连个问题没有分析:
1 delete问题
2 leader问题
3 schedule问题
4 swapcontext返回后咋处理

1 delete

static void coroutine_delete(Coroutine *co)
{
    co->caller = NULL;

    if (CONFIG_COROUTINE_POOL) {
        if (release_pool_size < POOL_BATCH_SIZE * 2) {
            QSLIST_INSERT_HEAD_ATOMIC(&release_pool, co, pool_next);
            atomic_inc(&release_pool_size);
            return;
        }
        CoroutinePool* pool = QEMU_THREAD_LOCAL_GET_PTR(co_alloc_pool);
        if (pool->size < POOL_BATCH_SIZE) {
            QSLIST_INSERT_HEAD(&pool->pool, co, pool_next);
            pool->size++;
            return;
        }
    }

    qemu_coroutine_delete(co);
}

void qemu_coroutine_delete(Coroutine *co_)
{
    CoroutineUContext *co = DO_UPCAST(CoroutineUContext, base, co_);

#ifdef CONFIG_VALGRIND_H
    valgrind_stack_deregister(co);
#endif

    qemu_free_stack(co->stack, co->stack_size);
    g_free(co);
}

这里如果缓存数量没有达到上限就加到缓存里面,release_pool的添加明显有线程安全问题,就不深究了。

2 leader问题,leader只是个象征,不会返回的leader了,只会返回到最开始的调用者, 因为第一次调用 qemu_aio_coroutine_enter函数保存的leader现场其实就是调用者的现场

3 scheduled

void coroutine_fn qemu_co_sleep_ns(QEMUClockType type, int64_t ns)
{
    AioContext *ctx = qemu_get_current_aio_context();
    CoSleepCB sleep_cb = {
        .co = qemu_coroutine_self(),
    };

    const char *scheduled = atomic_cmpxchg(&sleep_cb.co->scheduled, NULL,
                                           __func__);
    if (scheduled) {
        fprintf(stderr,
                "%s: Co-routine was already scheduled in '%s'\n",
                __func__, scheduled);
        abort();
    }
    sleep_cb.ts = aio_timer_new(ctx, type, SCALE_NS, co_sleep_cb, &sleep_cb);
    timer_mod(sleep_cb.ts, qemu_clock_get_ns(type) + ns);
    qemu_coroutine_yield();
    timer_del(sleep_cb.ts);
    timer_free(sleep_cb.ts);
}

实际上是添加到timer队列上,然后把自己切出去。
切换出去后定时器到期后回调co_sleep_cb函数

static void co_sleep_cb(void *opaque)
{
    CoSleepCB *sleep_cb = opaque;

    /* Write of schedule protected by barrier write in aio_co_schedule */
    atomic_set(&sleep_cb->co->scheduled, NULL);
    aio_co_wake(sleep_cb->co);
}

void aio_co_wake(struct Coroutine *co)
{
    AioContext *ctx;

    /* Read coroutine before co->ctx.  Matches smp_wmb in
     * qemu_coroutine_enter.
     */
    smp_read_barrier_depends();
    ctx = atomic_read(&co->ctx);

    aio_co_enter(ctx, co);
}

void aio_co_enter(AioContext *ctx, struct Coroutine *co)
{
    if (ctx != qemu_get_current_aio_context()) {
        aio_co_schedule(ctx, co);
        return;
    }

    if (qemu_in_coroutine()) {
        Coroutine *self = qemu_coroutine_self();
        assert(self != co);
        QSIMPLEQ_INSERT_TAIL(&self->co_queue_wakeup, co, co_queue_next);
    } else {
        aio_context_acquire(ctx);
        qemu_aio_coroutine_enter(ctx, co);
        aio_context_release(ctx);
    }
}

这里如果当前aio上下文不是给定的aio上下文,使用aio_co_schedule把协程切到当前上下文执行。

如果当前线程运行在协程上下文,则添加到当前进行运行的协程队列中
否则使用qemu_aio_coroutine_enter直接调用协程

为什么这样呢,如果当前是协程上下文可以把要唤醒的协程添加到唤醒队列中,来进行执行。 不是携程上下文的话可以直接执行这个协程。也就是在aio上下文执行协程。其实类似个函数调用,但是感觉这不可行啊,不会破坏aio的效率吗????

感觉qemu这个代码很脏啊。

按照这个思路我自己实现了一个简化版的,参考 https://github.com/TangGee/coroutine

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

猜你喜欢

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