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_restore2.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_MACRO2.3 简单流程
第一次创建时候,会保存一次上下文信息(拷贝具体执行过程),然后就直接返回给”用户态”,当st框架进行调度时,调度当这个协程的上下文时,会执行用户注册的回调函数。
当用户使用st封装的系统调用时,会出让调度权,流程如下:
流程沿途中红线开始到绿线结束。
假设coroutine 2已经存在,设置过第一个中断点,这时候执行coroutine 1,在其中又产生了一次上下文切换,这时候保存完上下文后,交换调度权,将当前调度权交给coroutine 2,coroutine 2执行完自己部分逻辑后,再次设置一个中断点2,也发生了上下文切换,保存了中断点2的上下文后再次让出调度权,如果coroutine可以执行,则进入coroutinue 1的中断点执行代码。
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