makecontext理解与使用
#include <ucontext.h> int getcontext(ucontext_t *ucp); int setcontext(const ucontext_t *ucp); void makecontext(ucontext_t *ucp, void (*func)(), int argc, ...); int swapcontext(ucontext_t *oucp, ucontext_t *ucp);
其中,ucontext 定义如下:
typedef struct ucontext { unsigned long int uc_flags; struct ucontext *uc_link; stack_t uc_stack; mcontext_t uc_mcontext; __sigset_t uc_sigmask; struct _fpstate __fpregs_mem; } ucontext_t;
getcontext(2) gets the current context of the calling process, storing it in the ucontext struct pointed to by ucp. setcontext(2) sets the context of the calling process to the state stored in the ucontext struct pointed to by ucp. The struct must either have been created by getcontext(2) or have been passed as the third parameter of the sigaction(2) signal handler. The makecontext() function modifies the context pointed to by ucp (which was obtained from a call to getcontext(2)). Before invoking makecontext(), the caller must allocate a new stack for this context and assign its address to ucp->uc_stack, and define a successor context and assign its address to ucp->uc_link. swapcontext swaps out context old_ctx with new context new_ctx. The int r# values have no place in the system call functionality. The regs value indicates the current user register values from the user stack.
man page 给出的例子:
#include <ucontext.h> #include <stdio.h> #include <stdlib.h> static ucontext_t uctx_main, uctx_func1, uctx_func2; #define handle_error(msg) do { perror(msg); exit(EXIT_FAILURE); } while (0) static void func1(void) { printf("func1: started "); printf("func1: swapcontext(&uctx_func1, &uctx_func2) "); // swap 跳转uctx_func2 if (swapcontext(&uctx_func1, &uctx_func2) == -1) handle_error("swapcontext"); printf("func1: returning "); // 执行完 跳转uctx_main } static void func2(void) { printf("func2: started "); printf("func2: swapcontext(&uctx_func2, &uctx_func1) "); // swap 跳转uctx_func1 if (swapcontext(&uctx_func2, &uctx_func1) == -1) handle_error("swapcontext"); printf("func2: returning "); // 执行完 跳转uctx_func1 } int main(int argc, char *argv[]) { char func1_stack[16384]; char func2_stack[16384]; if (getcontext(&uctx_func1) == -1) handle_error("getcontext"); // 预分配的栈空间 uctx_func1.uc_stack.ss_sp = func1_stack; uctx_func1.uc_stack.ss_size = sizeof(func1_stack); // 后继上下文环境是uctx_main uctx_func1.uc_link = &uctx_main; // uctx_func1的上下文环境:func1 makecontext(&uctx_func1, func1, 0); if (getcontext(&uctx_func2) == -1) handle_error("getcontext"); // 预分配的栈空间 uctx_func2.uc_stack.ss_sp = func2_stack; uctx_func2.uc_stack.ss_size = sizeof(func2_stack); // 后继上下文环境是uctx_func1(argc = 0的时候) /* Successor context is f1(), unless argc > 1 */ uctx_func2.uc_link = (argc > 1) ? NULL : &uctx_func1; // uctx_func2的上下文环境:func2 makecontext(&uctx_func2, func2, 0); // swap 跳转uctx_func2 printf("main: swapcontext(&uctx_main, &uctx_func2) "); if (swapcontext(&uctx_main, &uctx_func2) == -1) handle_error("swapcontext"); printf("main: exiting "); exit(EXIT_SUCCESS); }
执行结果:
$ ./a.out main: swapcontext(&uctx_main, &uctx_func2) func2: started func2: swapcontext(&uctx_func2, &uctx_func1) func1: started func1: swapcontext(&uctx_func1, &uctx_func2) func2: returning func1: returning main: exiting
云风利用 context 函数镞实现的C Coroutine
部分核心代码阅读理解:
void coroutine_resume(struct schedule * S, int id) { assert(S->running == -1); assert(id >=0 && id < S->cap); struct coroutine *C = S->co[id]; if (C == NULL) return; int status = C->status; switch(status) { // 刚初始化的coroutine case COROUTINE_READY: // 初始化设置当前coroutine的context(mainfunc) getcontext(&C->ctx); C->ctx.uc_stack.ss_sp = S->stack; C->ctx.uc_stack.ss_size = STACK_SIZE; C->ctx.uc_link = &S->main; S->running = id; C->status = COROUTINE_RUNNING; uintptr_t ptr = (uintptr_t)S; makecontext(&C->ctx, (void (*)(void)) mainfunc, 2, (uint32_t)ptr, (uint32_t)(ptr>>32)); // swap后执行mainfunc,实际上是执行当前的coroutine,即自己 swapcontext(&S->main, &C->ctx); break; // 被yield的coroutine case COROUTINE_SUSPEND: // 复原堆栈(S->main),将yield保存下来的堆栈拷贝回来 memcpy(S->stack + STACK_SIZE - C->size, C->stack, C->size); // 设置当前coroutine为running,swap后到mainfunc执行当前coroutine S->running = id; C->status = COROUTINE_RUNNING; swapcontext(&S->main, &C->ctx); break; default: assert(0); } } // 这里是相当于将top和当前栈之间的数据,都保存到C->stack内去了 static void _save_stack(struct coroutine *C, char *top) { char dummy = 0; assert(top - &dummy <= STACK_SIZE); if (C->cap < top - &dummy) { free(C->stack); C->cap = top-&dummy; C->stack = malloc(C->cap); } C->size = top - &dummy; memcpy(C->stack, &dummy, C->size); } void coroutine_yield(struct schedule * S) { int id = S->running; assert(id >= 0); struct coroutine * C = S->co[id]; assert((char *)&C > S->stack); // 保存堆栈(S->main),第二个参数我觉得为什么不写成&S->main呢? _save_stack(C,S->stack + STACK_SIZE); C->status = COROUTINE_SUSPEND; S->running = -1; // swap后交回main去执行 swapcontext(&C->ctx , &S->main); }
需要注意的一点是:上面的代码在切换时做了栈拷贝,coroutine 在 yield 时将 S->main 的栈拷贝到 coroutine->stack,在 resume 时再拷贝回来。
考虑到将 coroutine 应用到游戏服务中,假设这么一个场景:每个 coroutine 负责一个玩家的逻辑,那么在高并发下,上面这种栈拷贝的方式个人觉得不是一个很好的方式。
借鉴云风大大的实现,并做了一点调整:
测试结果良好,但是也并非完美的:
实验代码在这里。