state-threads的协程切换

1. 简介

        state-threads是一个C语言实现的轻量级协程库,基于setjmp和longjmp来对不同协程进行切换。本文将先分析其保存上下文的setjmp和longjmp的汇编代码,简单分析其上下文切换的过程。

2. 代码分析

2.1 保存恢复上下文

        保存恢复上下文可以使用系统中的setjmp和longjmp,glibc中也有,st中自己也有实现部分架构下的setjmp和longjmp(md.S)。

        首先看汇编上保存上下文的处理代码,在x86下,主要根据调用约定保存了rbx、rbp(帧指针,基址寄存器)、r12-r15 [2]、rsp(栈指针)、PC(为该函数的调用者callq的下一条指令,会保存在栈上),保存在了thread->context,是一个__jmp_buf结构体,保存上下文时返回值总是0,恢复时根据传入的第二个参数进行判断,返回非零值。这里两个返回值不同主要是为了处理保存环境和再次调度的区别,具体差异见下c代码。

/*
 * Internal __jmp_buf layout
 */
#define JB_RBX  0
#define JB_RBP  1
#define JB_R12  2
#define JB_R13  3
#define JB_R14  4
#define JB_R15  5
#define JB_RSP  6
#define JB_PC   7

        .file "md.S"
        .text

        /* _st_md_cxt_save(__jmp_buf env) */
.globl _st_md_cxt_save
        .type _st_md_cxt_save, @function
        .align 16
_st_md_cxt_save:
        /*
         * Save registers.
         */
        /* rdi为第一个入参,即jmp_buf env.  */
        /* jmp_buf为一个长度为8的8字节的数组.  */
        /* 存放顺序为:
         * [rdi + 0] = rbx;    --->  env[0]
         * [rdi + 8] = rbp;    --->  env[1]
         * [rdi + 16] = r12;   --->  env[2]
         * [rdi + 24] = r13;   --->  env[3]
         * [rdi + 32] = r14;   --->  env[4]
         * [rdi + 40] = r15;   --->  env[5]
         * rbx, rbp, r12, r13, r14, r15需要由被调用者维护。
         */
        movq %rbx, (JB_RBX*8)(%rdi)
        movq %rbp, (JB_RBP*8)(%rdi)
        movq %r12, (JB_R12*8)(%rdi)
        movq %r13, (JB_R13*8)(%rdi)
        movq %r14, (JB_R14*8)(%rdi)
        movq %r15, (JB_R15*8)(%rdi)
        /* Save SP */
        /* rdx = rsp + 8;  */
        leaq 8(%rsp), %rdx
        /* [rdi + 48] = rsp + 8;---> env[6]  */
        movq %rdx, (JB_RSP*8)(%rdi)
        /* Save PC we are returning to */
        /* [rdi + 56] = [rsp]; --->  env[7]  */
        movq (%rsp), %rax
        movq %rax, (JB_PC*8)(%rdi)
        /* 保存现场时候总是返回0.
         * 恢复现场时候会返回1.
         */
        xorq %rax, %rax
        ret
        .size _st_md_cxt_save, .-_st_md_cxt_save


/****************************************************************/

        /* _st_md_cxt_restore(__jmp_buf env, int val) */
.globl _st_md_cxt_restore
        .type _st_md_cxt_restore, @function
        .align 16
_st_md_cxt_restore:
        /*
         * Restore registers.
         */
        /* 恢复现场。  */
        movq (JB_RBX*8)(%rdi), %rbx
        movq (JB_RBP*8)(%rdi), %rbp
        movq (JB_R12*8)(%rdi), %r12
        movq (JB_R13*8)(%rdi), %r13
        movq (JB_R14*8)(%rdi), %r14
        movq (JB_R15*8)(%rdi), %r15
        /* Set return value */
        /* if (esi == 0)
         *   eax = 1;
         * else
         *   eax = esi;
         */
        test %esi, %esi
        mov $01, %eax
        cmove %eax, %esi
        mov %esi, %eax
        movq (JB_PC*8)(%rdi), %rdx
        movq (JB_RSP*8)(%rdi), %rsp
        /* Jump to saved PC */
        jmpq *%rdx
        .size _st_md_cxt_restore, .-_st_md_cxt_restore
2.2 中断点

        如[1]中描述,对于中断点的使用,通常为:

if(setjmp(x))
{
  _handle_funcion ()
}
        在setjmp的时候保存了上下文,但是返回值为0,不执行具体的函数。当通过longjmp调度进来的时候,汇编会直接跳到if的判断汇编上,但是这时候返回值非零,从而走入具体的执行函数中进行相应的处理。

        st中具体的代码为三个宏。

        st中主要中断点有两个:

        第一个为初始化协程时候为了非阻塞的创建,而将协程回调的执行函数包在_st_thread_main中,作为第一个中断点并返回至"创建协程的用户态"。

        第二个中断点为主动出让的中断点,执行宏_ST_SWITCH_CONTEXT,会保存当前上下文,并通过_st_vp_schedule,出让本次调度权,并选择下一个可执行的协程,通过_ST_RESTORE_CONTEXT来恢复所选择的协程。主动设置的中断点这个宏主要用于一些系统调用的封装,如IO操作,或者sleep函数,当调用st封装过的系统调用函数时,会在这个协程中出让调度权限。

/*
 * Switch away from the current thread context by saving its state and
 * calling the thread scheduler
 */
/* 中断点2:
 * 主动设置中断点,保存完中断点的上下文信息后,通过函数_st_vp_schedule将调度权限出让,执行其他协程函数
 * 当函数再次被调用时,判断条件FALSE,继续执行函数剩余部分。
 */
#define _ST_SWITCH_CONTEXT(_thread)       \
    ST_BEGIN_MACRO                        \
    ST_SWITCH_OUT_CB(_thread);            \
    if (!MD_SETJMP((_thread)->context)) { \
      _st_vp_schedule();                  \
    }                                     \
    ST_DEBUG_ITERATE_THREADS();           \
    ST_SWITCH_IN_CB(_thread);             \
    ST_END_MACRO

/*
 * Restore a thread context that was saved by _ST_SWITCH_CONTEXT or
 * initialized by _ST_INIT_CONTEXT
 */
/* 恢复某个协程的上下文信息,主要调用点在_st_vp_schedule函数中,出让协程后获取下一个协程,通过
 * _ST_RESTORE_CONTEXT来恢复对应协程的上下文,MD_LONGJMP最后一句汇编会跳转至该协程保存的上下文环境中执行。
 */
#define _ST_RESTORE_CONTEXT(_thread)   \
    ST_BEGIN_MACRO                     \
    _ST_SET_CURRENT_THREAD(_thread);   \
    MD_LONGJMP((_thread)->context, 1); \
    ST_END_MACRO

/* 中断点1:
 * 上下文初始化,这里重置了sp,使用的是st中自己分配的stack。
 * _main函数为st框架的协程主函数,第一次创建协程时候执行MD_INIT_CONTEXT。
 * 这里主要是拷贝了一份用户注册给协程的回调函数的执行环境,
 * 具体执行由下一次st框架调度(即longjmp返回非零时)(非阻塞创建协程)。
 */
#define MD_INIT_CONTEXT(_thread, _sp, _main) \
  ST_BEGIN_MACRO                             \
  if (MD_SETJMP((_thread)->context))         \
    _main();                                 \
  MD_GET_SP(_thread) = (long) (_sp);         \
  ST_END_MACRO
2.3 简单流程
        创建协程时候的非阻塞创建简单示意图如下:

        第一次创建时候,会保存一次上下文信息(拷贝具体执行过程),然后就直接返回给”用户态”,当st框架进行调度时,调度当这个协程的上下文时,会执行用户注册的回调函数。

state-threads的协程切换_第1张图片

        当用户使用st封装的系统调用时,会出让调度权,流程如下:

        流程沿途中红线开始到绿线结束。

        假设coroutine 2已经存在,设置过第一个中断点,这时候执行coroutine 1,在其中又产生了一次上下文切换,这时候保存完上下文后,交换调度权,将当前调度权交给coroutine 2,coroutine 2执行完自己部分逻辑后,再次设置一个中断点2,也发生了上下文切换,保存了中断点2的上下文后再次让出调度权,如果coroutine可以执行,则进入coroutinue 1的中断点执行代码。

state-threads的协程切换_第2张图片

3. 参考文档

[1] https://en.wikipedia.org/wiki/Setjmp.h

[2] http://refspecs.linuxbase.org/elf/x86_64-abi-0.95.pdf

[3] https://cs.brown.edu/courses/cs033/docs/guides/x64_cheatsheet.pdf

[4] http://www.searchtb.com/2013/03/x86-64_register_and_function_frame.html

你可能感兴趣的:(state-threads的协程切换)