makecontext 理解与使用

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;

man page 的说明

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\n");
       printf("func1: swapcontext(&uctx_func1, &uctx_func2)\n");

       // swap 跳转uctx_func2
       if (swapcontext(&uctx_func1, &uctx_func2) == -1)
           handle_error("swapcontext");
       printf("func1: returning\n");

       // 执行完 跳转uctx_main
   }

   static void
   func2(void)
   {
       printf("func2: started\n");
       printf("func2: swapcontext(&uctx_func2, &uctx_func1)\n");

       // swap 跳转uctx_func1
       if (swapcontext(&uctx_func2, &uctx_func1) == -1)
           handle_error("swapcontext");
       printf("func2: returning\n");

       // 执行完 跳转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)\n");
       if (swapcontext(&uctx_main, &uctx_func2) == -1)
           handle_error("swapcontext");

       printf("main: exiting\n");
       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

makecontex 函数簇的应用

云风利用 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 负责一个玩家的逻辑,那么在高并发下,上面这种栈拷贝的方式个人觉得不是一个很好的方式。

借鉴云风大大的实现,并做了一点调整:

  1. 每个 coroutine 对象有自己的栈空间,swapcontext 之后跑在自己的栈空间上.
  2. 每个 coroutine 对象不会太小,加一个 freelist 来做回收和分配,减少内存压力.
  3. 为了保证栈不溢出,用了 mprotect 对栈底做了保护, 在预设的栈空间之外再增加了一个 page,当写到这个 page 时就会给出 segment fault 信号;

测试结果良好,但是也并非完美的:

  1. 基于栈的协议,这就意味在 gdb bt 时看到的栈是不完整的,只有当前跑的 coroutine 中的一部分.
  2. 一些工具的使用受到限制, 例如 valgrind 在这种场景下就无能为力.
  3. 目前只支持 Linux,跨平台需要做额外不少工作.

实验代码在这里

参考文章