关于协程coroutine前面的文章已经介绍过了,本文总结对qemu中coroutine机制的分析,qemu 协程coroutine基于:setcontext函数族以及函数间跳转函数siglongjmp和sigsetjmp实现。使用setcontext函数族来实现用户态进程栈的切换,使用函数间跳转siglongjmp和sigsetjmp实现协程coroutine不退出以及屡次进入,即便coroutine执行的任务已经完成,这实现了协程池的功能,避免大量协程建立和销毁带来的系统开销。数据结构
qemu coroutine主要提供了5个接口,用于协程建立、协程进入、协程让出,下面首次介绍qemu 实现协程使用的主要数据结构,而后将依次介绍qemu coroutine 这5个接口的实现。函数
1.qemu协程实现使用的主要数据结构 coroutine和CoroutineUContext: ui
/* 协程coroutine */ struct Coroutine { CoroutineEntry *entry; /* 协程入口函数 */ void *entry_arg; /* 协程入口函数的参数 */ Coroutine *caller; QSLIST_ENTRY(Coroutine) pool_next; /* 协程池挂链 */ /* 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挂链 */ }; typedef struct { Coroutine base; /* 协程coroutine */ void *stack; /* 当前上下文的进程栈 */ sigjmp_buf env; #ifdef CONFIG_VALGRIND_H unsigned int valgrind_stack_id; #endif } CoroutineUContext; /* coroutine上下文 */
coroutine数据结构主要封装协程,coroutineUContext封装协程上下文,是对coroutine的进一步包装。spa
2. qemu协程建立函数 qemu_coroutine_create,其实现以下:指针
1 Coroutine *qemu_coroutine_create(CoroutineEntry *entry) 2 { 3 Coroutine *co = NULL; 4 5 if (CONFIG_COROUTINE_POOL) { /* 判断是否使用了coroutine池 */ 6 qemu_mutex_lock(&pool_lock); 7 co = QSLIST_FIRST(&pool); /* 从池子里取出第一个协程 */ 8 if (co) { 9 QSLIST_REMOVE_HEAD(&pool, pool_next); 10 pool_size--; 11 } 12 qemu_mutex_unlock(&pool_lock); 13 } 14 15 if (!co) { /* co为NULL,表示没有使用coroutine池或者池子已空 */ 16 co = qemu_coroutine_new(); /* 建立一个新的coroutine,这里只是一个空的协程 */ 17 } 18 19 co->entry = entry; /* 设置协程的入口函数 */ 20 QTAILQ_INIT(&co->co_queue_wakeup); /* 初始化协程线性队列 */ 21 return co; 22 }
qemu_coroutine_create首先尝试从coroutine池中取出一个coroutine,若是没有获取到,则经过qemu_coroutine_new函数建立一个新的coroutine,qemu_coroutine_new的实现以下:rest
1 Coroutine *qemu_coroutine_new(void) 2 { 3 const size_t stack_size = 1 << 20; /* ucontext_t使用的栈大小 */ 4 CoroutineUContext *co; /* 协程上下文 */ 5 ucontext_t old_uc, uc; /* 进程执行上下文 */ 6 sigjmp_buf old_env; /* 函数间跳转-环境 */ 7 union cc_arg arg = {0}; 8 9 /* The ucontext functions preserve signal masks which incurs a 10 * system call overhead. sigsetjmp(buf, 0)/siglongjmp() does not 11 * preserve signal masks but only works on the current stack. 12 * Since we need a way to create and switch to a new stack, use 13 * the ucontext functions for that but sigsetjmp()/siglongjmp() for 14 * everything else. 15 */ 16 17 if (getcontext(&uc) == -1) { 18 abort(); 19 } 20 /* 协程上下文CoroutineUContext初始化 */ 21 co = g_malloc0(sizeof(*co)); 22 co->stack = g_malloc(stack_size); 23 co->base.entry_arg = &old_env; /* stash away our jmp_buf */ 24 25 /* 进程执行上下文ucontext_t初始化 */ 26 uc.uc_link = &old_uc; 27 uc.uc_stack.ss_sp = co->stack; 28 uc.uc_stack.ss_size = stack_size; 29 uc.uc_stack.ss_flags = 0; 30 31 #ifdef CONFIG_VALGRIND_H 32 co->valgrind_stack_id = 33 VALGRIND_STACK_REGISTER(co->stack, co->stack + stack_size); 34 #endif 35 /* co的传递为何要以arg的方式?????? */ 36 arg.p = co; 37 /* 建立一个进程执行上下文uc,进程执行上下文的入口函数为coroutine_trampoline */ 38 makecontext(&uc, (void (*)(void))coroutine_trampoline, 39 2, arg.i[0], arg.i[1]); 40 41 /* swapcontext() in, siglongjmp() back out */ 42 if (!sigsetjmp(old_env, 0)) { /* 保存当前堆栈环境,sigsetjmp为一次调用屡次返回的函数 */ 43 swapcontext(&old_uc, &uc);/* 进入uc进程执行上下文,并保存当前上下文到old_uc */ 44 } 45 return &co->base; /* 返回coroutine */ 46 }
qemu_coroutine_new的主要动做:code
上面的注释提到了一个疑问:38行将协程上下文co做为参数传递给了新建立的协程uc,可是co的传递为何要转换成arg,并以两个int变量的形式传递?cc_arg联合体的定义给出了说明:协程
/* * va_args to makecontext() must be type 'int', so passing * the pointer we need may require several int args. This * union is a quick hack to let us do that */ union cc_arg { void *p; int i[2]; };
主要缘由是makecontext的va_args参数只接受int类型,所以做为指针传递的协程上下文co等价于两个int类型的变量,64位系统上int类型占用4个字节,指针类型占用8个字节。对象
上面qemu_coroutine_new函数43行的执行将致使进入coroutine_trampoline函数,下面分析coroutine_trampoline函数的实现:blog
1 /* 2 * qemu coroutine入口函数, 3 * 函数参数i0为协程上下文指针的低8位, 4 * i1为协程上下文指针的高八位。 5 */ 6 static void coroutine_trampoline(int i0, int i1) 7 { 8 union cc_arg arg; 9 CoroutineUContext *self; 10 Coroutine *co; 11 12 arg.i[0] = i0; 13 arg.i[1] = i1; 14 self = arg.p;/* 获取协程上下文对象指针 */ 15 co = &self->base;/* 获取协程上下文的协程对象指针 */ 16 17 /* Initialize longjmp environment and switch back the caller */ 18 if (!sigsetjmp(self->env, 0)) { /* 保存当前堆栈信息,为了再一次进入该协程上下文 */ 19 /* 函数间跳转,跳转到qemu_coroutine_new函数的42行 */ 20 siglongjmp(*(sigjmp_buf *)co->entry_arg, 1); 21 } 22 23 while (true) { 24 /* 执行协程的入口函数 */ 25 co->entry(co->entry_arg); 26 /* 协程入口函数退出,协程退出到调用者 */ 27 qemu_coroutine_switch(co, co->caller, COROUTINE_TERMINATE); 28 } 29 }
coroutine_trampoline的主要动做:
注意这里的co->caller将在进入该协程时被赋值,上面便是qemu中建立一个协程对象的过程,从上面的分析能够看出qemu中每一协程coroutine对象对应一个协程上下文对象,经过makecontext建立一个新的进程执行上下文,能够看作协程的主体,协程上下文对象的env成员保存了进入执行上下文的点,经过siglongjmp跳出该执行上下文,qemu协程的建立也即建立了一个新的进程执行上下文,而且保存了再次进入该执行上下文的堆栈信息,下面将分析协程进入函数qemu_coroutine_enter。
3. qemu协程进入函数 qemu_coroutine_enter,其实现以下:
1 /* 功能:切换到co执行上下文,也即开始执行co的入口函数,opaque为入口函数的参数 */ 2 void qemu_coroutine_enter(Coroutine *co, void *opaque) 3 { 4 Coroutine *self = qemu_coroutine_self(); /* 获取当前的进程执行上下文-当前协程 */ 5 6 trace_qemu_coroutine_enter(self, co, opaque); 7 8 if (co->caller) { /* qemu 协程不容许递归,也即协程内建立协程 */ 9 fprintf(stderr, "Co-routine re-entered recursively\n"); 10 abort(); 11 } 12 /* 调用co协程的协程,也即进入co上下文以前的进程上下文 */ 13 co->caller = self; 14 /* co协程入口函数的参数 */ 15 co->entry_arg = opaque; 16 /* 将进程上下文从self切换到co */ 17 coroutine_swap(self, co); 18 }
qemu_coroutine_enter函数的实现主要为:获取当前进程执行上下文并保存到co->caller中,而后设置co入口函数的参数,以后作上下文切换coroutine_swap()。coroutine_swap的实现以下:
1 /* 协程切换:从from切换到to */ 2 static void coroutine_swap(Coroutine *from, Coroutine *to) 3 { 4 CoroutineAction ret; 5 /* 协程切换,切换到to */ 6 ret = qemu_coroutine_switch(from, to, COROUTINE_YIELD); 7 /* to协程让出,依次唤醒co->co_queue_wakeup列表中排队的协程 */ 8 qemu_co_queue_run_restart(to); 9 /* 根据返回值,决定是否删除协程co仍是仅仅退出 */ 10 switch (ret) { 11 case COROUTINE_YIELD: 12 return; 13 case COROUTINE_TERMINATE: 14 trace_qemu_coroutine_terminate(to); 15 coroutine_delete(to); 16 return; 17 default: 18 abort(); 19 } 20 }
coroutine_swap的实现主要:首先切换到to协程上下文执行,当to协程让出后依次唤醒排队的协程,以后根据to协程退出的返回值来决定是否删除to,下面是qemu_coroutine_switch函数的实现:
1 CoroutineAction qemu_coroutine_switch(Coroutine *from_, Coroutine *to_, 2 CoroutineAction action) 3 { 4 CoroutineUContext *from = DO_UPCAST(CoroutineUContext, base, from_); 5 CoroutineUContext *to = DO_UPCAST(CoroutineUContext, base, to_); 6 CoroutineThreadState *s = coroutine_get_thread_state(); 7 int ret; 8 9 s->current = to_; /* s在这里起什么做用呢? */ 10 11 ret = sigsetjmp(from->env, 0); /* 保存当前堆栈到from->env,用于协程的让出 */ 12 if (ret == 0) { 13 siglongjmp(to->env, action);/* 跳转到coroutine_trampoline中第18行 */ 14 } 15 return ret; 16 }
qemu_coroutine_switch值得注意的两个地方:
有两种方式能够退出当前协程:协程入口函数返回、协程上下文主动执行qemu_coroutine_yield函数,前面已经说明了在coroutine_trampoline函数中协程入口函数返回时,将经过siglongjmp的方式来退出当前协程的执行上下文,下面介绍qemu_coroutine_yield的实现。
4. qemu协程让出函数 qemu_coroutine_yield,其实现以下