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框架进行调度时,调度当这个协程的上下文时,会执行用户注册的回调函数。
当用户使用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