Qemu中coroutine机制的实现

最近在看virtio的代码,看到virtio后端时发现在Qemu处理IO的时候使用了coroutine,之前对coroutine不了解,因此专门找了点资料学习并分析了下Qemu中的实现,于是做个笔记。Qemu貌似提供了好几种coroutine的实现方式(gthread、ucontext、sigalstack),我只看了利用ucontext实现的部分。

网上有一篇同主题的博文(http://www.cnblogs.com/VincentXu/p/3350389.html),里面有Qemu中coroutine机制相关的两个数据结构的介绍,所以就不写这两个结构体了;除此之外,还需要用到ucontext相关的和setjmp/longjmp,这些网上也有大把资料。

为了描述上的方便,将coroutine的执行函数(也就是传递给makecontext的第二个参数)称作“coroutine-function”,将在“coroutine-function”中会调用的真正进行用户期望的函数(也就是Coroutine结构中的entry函数指针)称作“user-code”。

Qemu中如何使用coroutine:

Qemu中主要提供了三个接口,使用的话应该要遵循qemu_coroutine_create-->qemu_coroutine_enter-->qemu_coroutine_yield这样的顺序。其中qemu_coroutine_yield可以不调用,这样的话Qemu将在“user-code”返回的时候主动去COROUTINE_TERMINATE。如果调用了qemu_coroutine_yield,比如说是在异步IO中,此协程会被“挂起”,等到IO完成时候在callback中调用qemu_coroutine_enter进入该协程运行,而此时刚好该协程运行的是“主动COROUTINE_TERMINATE”那句,从而完成了协程的推出

qemu_coroutine_create分析:

Qemu中有一个coroutine池,如果启用了coroutine池,那么会尝试从中取出一个,当然池中没有coroutine实例或者没启用coroutine池的话那就只能用qemu_coroutine_new分配一个实例:
Coroutine *qemu_coroutine_create(CoroutineEntry *entry)
{
    Coroutine *co = NULL;

    if (CONFIG_COROUTINE_POOL) {
        co = QSLIST_FIRST(&alloc_pool);
        if (!co) {
            ......
        }
        if (co) {
            QSLIST_REMOVE_HEAD(&alloc_pool, pool_next);
            alloc_pool_size--;
        }
    }

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

    co->entry = entry;
    QTAILQ_INIT(&co->co_queue_wakeup);
    return co;
}

  1. 调用qemu_coroutine_new函数分配一个实例
  2. Coroutine结构的entry域表示此coroutine实际的功能,也就是前面约定的“user-code”

qemu_coroutinue_new

Coroutine *qemu_coroutine_new(void)
{
    const size_t stack_size = 1 << 20;
    CoroutineUContext *co;
    ucontext_t old_uc, uc;
    sigjmp_buf old_env;
    union cc_arg arg = {0};
    ......
    if (getcontext(&uc) == -1) {
        abort();
    }

    co = g_malloc0(sizeof(*co));
    co->stack = g_malloc(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 = stack_size;
    uc.uc_stack.ss_flags = 0;
    ......
    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)) {
        swapcontext(&old_uc, &uc);
    }
    return &co->base;
}

  1. 调用getcontext函数用当前的上下文给uc赋一下
  2. 分配了一个CoroutineContext对象,并且为该对象分配了一个栈
  3. jmp_buf类型的变量是在setjmp/longjmp中使用的,这里将新的协程中的entry_arg域(后面会用到)指向old_env(第6步中会使用setjmp设置它)。
  4. 此函数中将新的协程的上下文存放于uc中、老上下文保存在old_uc中。将uc的uc_link设置为old_uc,这样的话在新的协程执行完时就会切换到uc_link指定的上下文。
  5. 设置uc的uc_stack域(sp指针、栈大小),它将作为新的coroutine运行时使用的栈
  6. 调用makecontext函数为新的协程对象创建上下文:将它的“coroutine-funciton”设置为coroutine_trampoline,并且该函数需要两个int型的参数(这是由union cc_arg的结构决定的,因为64bit系统上指针是8字节,而int是4字节,所以将指针分成两部分传递)
  7. 调用sigsetjmp(也就是setjmp函数)保存当前的调用环境(这个调用环境至少包括堆栈、指令指针,不同的体系有不同的要求。这里保存在old_env中,根据第2步,也可以理解是保存在了新协程的entry_arg中),这个函数在longjmp的配合下会返回两次:第一次是像这样直接调用,返回0;第二次是通过longjmp,返回longjmp函数的第二个参数值。
  8. 调用swapcontext函数切换上下文,它会把当前上下文保存在old_uc中、并将uc中的上下文“提拔”上来。此时上下文已经变成uc中指定的了,(根据第5步)紧接着就会去执行“coroutine-funciton”(这里对应的就是coroutine_trampoline函数)而不会去执行return &co->base

coroutine_trampoline

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

    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)) {
        siglongjmp(*(sigjmp_buf *)co->entry_arg, 1);
    }

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

  1. 调用setjmp将当前的调用环境在env域中
  2. 调用longjmp函数,这里看到它的第一个参数其实是上面提到的那个老的调用环境,第二个参数是1。因此执行流程会回到qemu_coroutine_new函数的第6步处,而此时该返回值是1,所以就会去执行return &co->base
在qemu_coroutine_new函数返回后(也就是回到了qemu_coroutine_create函数中,注意此时程序上下文已经切回到uc.uc_link中指定的了(切回来了)!)会根据传入的参数设置“user-code”。
至此,coroutine的创建就完成了。

qemu_coroutine_enter分析:

void qemu_coroutine_enter(Coroutine *co, void *opaque)
{
    Coroutine *self = qemu_coroutine_self();
    CoroutineAction ret;

    trace_qemu_coroutine_enter(self, co, opaque);

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

    co->caller = self;
    co->entry_arg = opaque;
    ret = qemu_coroutine_switch(self, co, COROUTINE_ENTER);

    qemu_co_queue_run_restart(co);

    switch (ret) {
    case COROUTINE_YIELD:
        return;
    case COROUTINE_TERMINATE:
        trace_qemu_coroutine_terminate(co);
        coroutine_delete(co);
        return;
    default:
        abort();
    }
}


第一个参数表示想进入的协程、第二个参数是该协程的“user-code”的参数。
  1. 调用qemu_coroutine_self函数获得当前协程的上下文。
  2. 设置caller,表示调用者吧。设置entry_arg域,这个将用作“user-code”的参数(可以看到entry_arg域有两个作用:1、在创建协程的时候用来保存老的调用环境(qemu_coroutine_new函数的低3步);2、在进入该协程的时候会作为该协程“user-code”的参数)。
  3. 调用qemu_coroutine_switch函数来进行写成之间的切换。

qemu_coroutine_switch

CoroutineAction 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;

    current = to_;

    ret = sigsetjmp(from->env, 0);
    if (ret == 0) {
        siglongjmp(to->env, action);
    }
    return ret;
}

  1. 调用setjmp保存当前协程的调用环境,第一次嘛,肯定是返回0的(前面提过了)
  2. 调用longjmp函数跳到目标协程去执行
跳过去的执行点一定是目标协程的“coroutine-function”(也就是coroutine_trampoline)函数的那个if(!sigsetjmp)处,而由于这是第二次,所以此时coroutine_trampoline函数中该setjmp的返回值是这个longjmp中指定的第二个参数,因为不是0,所以会执行coroutine_trampoline函数中那个while循环。在该循环中会调用entry回调函数(“user-code”)进行执行。
如果在“user-code”中没有显示调用qemu_coroutine_yield的话,则在“user-code”只完成之后又会调用qemu_coroutine_switch函数(并且是以COROUTINE_TERMINATE)回到此协程的caller的CoroutineUContext结构中保存的env环境处(对应的是qemu_coroutine_switch函数的 ret = sigsetjmp 处),而此时ret的只等于COROUTINE_TERMINATE,因此回去执行 return ret ,退到了qemu_coroutine_enter函数的  qemu_co_queue_run_restart(co) 处继续执行,在switch中就可以把它delete了,最后返回。









你可能感兴趣的:(虚拟化)