协程(Coroutine)是一种程序组件。
相对子例程而言,协程更为一般和灵活,但在实践中使用没有子例程那样广泛。协程源自Simula和Modula-2语言,但也有其他语言支持。协程更适合于用来实现彼此熟悉的程序组件,如合作式多任务,迭代器,无限列表和管道。
子例程的起始处是唯一的入口点,一旦退出就完成了子例程的执行,子例程的一个实例只会返回一次。协程有多个入口和出口点。协程可以通过yield来调用其他的协程,通过yield方式(这种方式指的是协程主动退出的方式)转移执行权的协程之间不是调用者和被调用者的关系,而是彼此对称的、平等的。协程的起始处是第一个入口点,在协程返回后,会进入下一个协程的入口点。子例程的生命期遵循后进先出,而协程的生命期是由它们的使用需要决定的。
qemu的开发里使用了很多异步处理,异步处理的代码大致方法就是发送请求,设置回调函数,然后返回处理其他事项,等请求处理完毕,在调用回调函数完成最后一步。但是异步处理带来的问题是复杂化了代码,原本在同步代码里顺序执行的一块代码被强行拆分成了一片片的代码,而且为了在回调函数里传递上下文,通常还需要包装一个结构体进行传递,所有的这一切就是增加了代码的复杂度,导致qemu的许多模块在开发的时候倾向于选择同步代码。所以这时候就需要引入协程。协程在这里最大的好处在于保持了代码的完整性,因为协程有自己独立的栈空间,因此可以在协程里维护一个完整的上下文环境。
在QEMU的block操作的实现中,coroutine得到了大量的应用。因此,如果需要对QEMU的block操作的阅读学习,需要对coroutine有一定的理解。QEMU对coroutine的实现代码位于include/qemu/coroutine.h、include/qemu/coroutine_int.h,相关的函数实现都在util文件夹下的qemu-coroutine.c和coroutine-ucontext.c中。
QEMU的coroutine实现基于setcontext函数,以及函数间跳转函数siglongjmp和sigsetjmp。使用setcontext函数族来实现用户态进程栈的创建,以及切换到新的堆栈中。使用siglongjmp和sigsetjmp实现coroutine的多次进入。
struct Coroutine {
CoroutineEntry *entry; /*协程的入口函数*/
void *entry_arg; /*协程入口函数的参数*/
Coroutine *caller; /*调用当前协程的协程*/
QSLIST_ENTRY(Coroutine) pool_next; /*全局协程池的entry*/
/* Coroutines that should be woken up when we yield or terminate */
QTAILQ_HEAD(, Coroutine) co_queue_wakeup; /*当前协程主动推出或停止时,需要进入的协程队列*/
QTAILQ_ENTRY(Coroutine) co_queue_next; /*co_queue_wakeup的entry*/
};
typedef struct {
Coroutine base; /*直接相关的Coroutine*/
void *stack; /*保存了Coroutine的上下文堆栈,专门为setcontext函数族准备的*/
sigjmp_buf env; /*sigsetjmp保存的Coroutine的上下文信息,作为参数传递到siglongjmp函数中,实现跳转*/
#ifdef CONFIG_VALGRIND_H
unsigned int valgrind_stack_id;
#endif
} CoroutineUContext;
/*以下是对所有线程的全局变量*/
static QSLIST_HEAD(, Coroutine) release_pool = QSLIST_HEAD_INITIALIZER(pool); /*全局的协程池*/
static unsigned int release_pool_size;
/*以下是一个线程内部的全局变量*/
static __thread QSLIST_HEAD(, Coroutine) alloc_pool = QSLIST_HEAD_INITIALIZER(pool); /*当前线程被分配的协程池*/
static __thread unsigned int alloc_pool_size;
static __thread Notifier coroutine_pool_cleanup_notifier; /*当前线程协程池被清空时的notifier*/
/*以下变量都是每个线程内部的全局变量*/
static __thread CoroutineUContext leader; /*每个线程的根coroutine*/
static __thread Coroutine *current; /*线程的当前的coroutine*/
从全局变量的介绍,我们知道QEMU对所有线程维持了一个名为release_pool的协程池,并且对每个线程都维持了自己被分配的协程池。这样当我们需要创建一个新的协程时,会先从线程自身的协程池中找到一个协程,或者从全局的release_pool中找到一个协程,如果在这两者中才会创建一个新的协程。从下面协程创建的代码分析,我们就可以知道,创建新的协程代价很高,协程池的策略,可以尽量避免创建新的协程。
qemu_coroutine_create函数是向外暴露的,其他的代码可以直接调用这一函数创建协程。
Coroutine *qemu_coroutine_create(CoroutineEntry *entry)
{
Coroutine *co = NULL;
/*先从协程池中查找,查找是否有被释放的协程*/
if (CONFIG_COROUTINE_POOL) {
co = QSLIST_FIRST(&alloc_pool);
if (!co) {
if (release_pool_size > POOL_BATCH_SIZE) {
/* Slow path; a good place to register the destructor, too. */
if (!coroutine_pool_cleanup_notifier.notify) {
coroutine_pool_cleanup_notifier.notify = coroutine_pool_cleanup;
qemu_thread_atexit_add(&coroutine_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.
*/
alloc_pool_size = atomic_xchg(&release_pool_size, 0);
QSLIST_MOVE_ATOMIC(&alloc_pool, &release_pool);
co = QSLIST_FIRST(&alloc_pool);
}
}
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;
}
我们重点看一下,如果协程池中没有被释放的协程时,qemu如何创建新的协程。在这个函数中,使用了一个联合体:
union cc_arg {
void *p; //64位系统中,一个指针会占用8bytes
int i[2]; //64位系统中,一个int型占用4bytes
};
union的重要特性是:union维护足够的空间来置放多个成员中的一种,所有的数据成员都共用一个地址空间,都共用一个起始地址。我们可以利用union的这一特性,向一些具有固定参数类型的函数传递其参数类型的参数值。
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};
/* 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.
*/
/* 以上的注释写得很明白:
* qemu在创建一个新的堆栈并且切换到这个新堆栈的时候,
* qemu使用的是ucontext函数族,使用makecontext创建一个新的堆栈,使用swapcontext切换到新的堆栈。
* 而Coroutine中涉及的其他Coroutine跳转使用的都是sigsetjmp()和siglongjmp()函数。
*/
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;
#ifdef CONFIG_VALGRIND_H
co->valgrind_stack_id =
VALGRIND_STACK_REGISTER(co->stack, co->stack + stack_size);
#endif
arg.p = co;
/*创建一个新堆栈,堆栈入口是coroutine_trampoline函数,并传递arg.i[0]和arg.i[1]这两个参数*/
makecontext(&uc, (void (*)(void))coroutine_trampoline,
2, arg.i[0], arg.i[1]);
/*利用sigsetjmp()将当前的上下文信息保存到old_env中*/
/*利用swapcontext跳转到之前创建的堆栈uc中,即coroutine_trampoline这个函数中*/
if (!sigsetjmp(old_env, 0)) {
swapcontext(&old_uc, &uc);
}
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; /*利用union的特性获得了对应的CoroutineUContext的指针*/
co = &self->base;
/* 利用sigsetjmp函数保存当前的堆栈信息到coroutine的env属性中,此时sigsetjmp的返回值为0
* 因此调用siglongjmp返回到qemu_coroutine_new函数中
* 当其他的coroutine通过siglongjmp函数跳转到这个coroutine时,
* sigsetjmp函数的返回值不为0.因此能够继续执行coroutine的entry
*/
if (!sigsetjmp(self->env, 0)) {
siglongjmp(*(sigjmp_buf *)co->entry_arg, 1);
}
while (true) {
co->entry(co->entry_arg); //执行coroutine的入口函数
qemu_coroutine_switch(co, co->caller, COROUTINE_TERMINATE); //coroutine结束,跳转到coroutine的调用函数
}
}
这个函数可以得到当前线程正在执行的coroutine。由于我们对当前线程执行的coroutine维持了全局变量current,因此这一操作的实现是简单的。
Coroutine *qemu_coroutine_self(void)
{
if (!current) {
current = &leader.base;
}
return current;
}
进入一个coroutine的实现代码如下:
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); //在协程yield或者terminate之后,需要执行协程co_wake_up队列中的协程
switch (ret) {
case COROUTINE_YIELD:
return;
case COROUTINE_TERMINATE:
trace_qemu_coroutine_terminate(co);
coroutine_delete(co);
return;
default:
abort();
}
}
我们重点看一下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;
current = to_;
//利用sigsetjmp讲协程的上下文保存
ret = sigsetjmp(from->env, 0);
if (ret == 0) {
siglongjmp(to->env, action); //利用siglongjmp跳转到目标协程中
}
return ret;
}
协程调用这个函数可以跳转回调用它的函数中
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);
}
该函数仅仅将要删除的协程插入到全局的协程池中,只有当全局的协程池中协程池数量满时,才会真正删除这个协程。
static void coroutine_delete(Coroutine *co)
{
co->caller = NULL;
if (CONFIG_COROUTINE_POOL) {
###Coroutine的应用
使用Coroutine,首先需要创建一个Coroutine,需要一个Coroutine的入口函数
if (release_pool_size < POOL_BATCH_SIZE * 2) {
QSLIST_INSERT_HEAD_ATOMIC(&release_pool, co, pool_next);
atomic_inc(&release_pool_size);
return;
}
if (alloc_pool_size < POOL_BATCH_SIZE) {
QSLIST_INSERT_HEAD(&alloc_pool, co, pool_next);
alloc_pool_size++;
return;
}
}
qemu_coroutine_delete(co);
}
qemu_coroutine_delete的实现代码如下:
void qemu_coroutine_delete(Coroutine *co_)
{
CoroutineUContext *co = DO_UPCAST(CoroutineUContext, base, co_);
#ifdef CONFIG_VALGRIND_H
valgrind_stack_deregister(co);
#endif
g_free(co->stack);
g_free(co);
}