实现协程最核心的部分就是栈切换了,其他的和非阻塞io的编程方式没什么区别。
栈切换,libc中有一个实现,swapcontext,但是已经被标准移除了,未来是否可用不得而知,自己实现需要写汇编代码,这是一个很困难的任务,因为既要熟悉不同cpu指令集又要熟悉不同平台的标准,好在从boost library的协程实现中找到了已经写好了的栈切换汇编代码,利用这些汇编代码可以在c语言中实现栈切换。
这段代码是在s_task协程库中发现的,s_task很好,还可以和libuv结合,如果没有特殊要求,可以直接使用了,但是如果想根据自己工作中的业务逻辑做定制,还是需要掌握原理,并且不清楚原理,可能不能用最恰当的方式使用,实现最好的设计,出了问题也不知道该怎么查和用什么办法查。
附上s_task 和 boost library的地址,感兴趣的可以去研究一下。
https://github.com/xhawk18/s_task
https://github.com/boostorg/context
栈切换代码在源码的asm目录中,实际上在c语言中对应两个函数,
typedef void* fcontext_t; typedef struct { fcontext_t fctx; void* data; } transfer_t; extern transfer_t jump_fcontext( fcontext_t const to, void * vp); extern fcontext_t make_fcontext( void * sp, size_t size, void (* fn)( transfer_t) );
这两个函数是什么意思,怎么用,看了s_task中的代码,但是开始的时候还是没看懂,于是想从汇编的角度入手,最终通过x86_64的汇编代码(make_x86_64_sysv_elf_gas.S jump_x86_64_sysv_elf_gas.S)弄清楚了这两个函数的用法,结论在本文最末尾,如果只想看结果,可以跳到最后面。
直接看代码注释吧。
make_x86_64_sysv_elf_gas.S
1 /* 2 Copyright Oliver Kowalke 2009. 3 Distributed under the Boost Software License, Version 1.0. 4 (See accompanying file LICENSE_1_0.txt or copy at 5 http://www.boost.org/LICENSE_1_0.txt) 6 */ 7 8 // 栈空间图,栈顶在低地址,注意和jump_x86_64_sysv_elf_gas.S对比着看,里面代表的是内存(栈,运行现场)中的数据 9 // 不代表寄存器,途中标的寄存器的意思是这些寄存器在保存现场的时候会保存到对应的内存地址中 10 /**************************************************************************************** 11 * * 12 * ---------------------------------------------------------------------------------- * 13 * | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | * 14 * ---------------------------------------------------------------------------------- * 15 * | 0x0 | 0x4 | 0x8 | 0xc | 0x10 | 0x14 | 0x18 | 0x1c | * 16 * ---------------------------------------------------------------------------------- * 17 * | fc_mxcsr|fc_x87_cw| R12 | R13 | R14 | * 18 * ---------------------------------------------------------------------------------- * 19 * ---------------------------------------------------------------------------------- * 20 * | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | * 21 * ---------------------------------------------------------------------------------- * 22 * | 0x20 | 0x24 | 0x28 | 0x2c | 0x30 | 0x34 | 0x38 | 0x3c | * 23 * ---------------------------------------------------------------------------------- * 24 * | R15 | RBX | RBP | RIP | * 25 * ---------------------------------------------------------------------------------- * 26 * * 27 ****************************************************************************************/ 28 29 .file "make_x86_64_sysv_elf_gas.S" 30 .text 31 .globl make_fcontext 32 .type make_fcontext,@function 33 .align 16 34 make_fcontext: 35 // make_fcontext的第一个参数保存到rax中,rax现在开始代表了这个运行环境的栈顶, 36 // rax也是本函数的返回值, 37 /* first arg of make_fcontext() == top of context-stack */ 38 movq %rdi, %rax 39 40 // 16对齐, 规定的 41 /* shift address in RAX to lower 16 byte boundary */ 42 andq $-16, %rax 43 44 // -0x40就是将栈顶指针(栈顶寄存器,这里不是rsp,是rax,rsp是当前运行环境使用的)移动到图中的0位置,即分配栈空间 45 // 递减栈是向下分配的 46 /* reserve space for context-data on context-stack */ 47 /* on context-function entry: (RSP -0x8) % 16 == 0 */ 48 leaq -0x40(%rax), %rax 49 50 // size那个参数这里没有使用,第三个参数是fn,要执行的函数 51 // 这里把fn的地址放到了0x28的位置,恢复到寄存器就是rbx 52 /* third arg of make_fcontext() == address of context-function */ 53 /* stored in RBX */ 54 movq %rdx, 0x28(%rax) 55 56 // 这两个寄存器不清楚 57 /* save MMX control- and status-word */ 58 stmxcsr (%rax) 59 /* save x87 control-word */ 60 fnstcw 0x4(%rax) 61 62 // 从英文注释看,实现的是将trampoline的地址保存在0x38的位置,即rip保存的地方,返回之后接着运行的地址 63 // 所以这个地方实际上是设置了跳转过来之后执行的指令的位置,jump_fcontext认为自己跳到了 64 // 一个暂停过的地方,即保存过现场的地方,恢复现场继续执行,而这里是首次运行,所以要模拟这种场景 65 // 也就是说启动一个任务之后会先运行trampoline那里。 66 /* compute abs address of label trampoline */ 67 leaq trampoline(%rip), %rcx 68 /* save address of trampoline as return-address for context-function */ 69 /* will be entered after calling jump_fcontext() first time */ 70 movq %rcx, 0x38(%rax) 71 72 // 这里是把finish处的地址放到0x30中, 是一个技巧,见trampoline处 73 /* compute abs address of label finish */ 74 leaq finish(%rip), %rcx 75 /* save address of finish as return-address for context-function */ 76 /* will be entered after context-function returns */ 77 movq %rcx, 0x30(%rax) 78 79 // make_fcontext函数返回 80 ret /* return pointer to context-data */ 81 82 trampoline: 83 /* store return address on stack */ 84 /* fix stack alignment */ 85 // 从jump_x86_64_sysv_elf_gas.S中可以看出,跳到这里之前已经从栈空间中恢复了rbp,也就是说 86 // 现在rbp保存的是finish的地址,因为rbp是从0x30恢复的,0x30前面已经保存了finish的地址 87 // 现在的栈顶rsp呢,是在图中0x40的位置,见jump_x86_64_sysv_elf_gas.S中的 leaq 0x40(%rsp), %rsp 88 // 现在push rbp是把fish的地址push到了0x38的位置,也就是本应该保存返回地址rip的地方, 89 // 也就是说fn运行结束返回到make_fcontext的finish处继续运行,就好像是make_fcontext调用 90 // 的fn一样,当然只是好像而已。可见fn返回了整个进程就退出了,这是make_fcontext指定的。 91 push %rbp 92 93 // rbx是设置的fn的地址,跳到fn运行,首次运行fn任务才会走到这里,中间暂停了 94 // 是恢复从暂停的地方的下一个指令开始执行。 95 /* jump to context-function */ 96 jmp *%rbx 97 98 finish: 99 // 这里的代码就是退出进程了 100 /* exit code is zero */ 101 xorq %rdi, %rdi 102 /* exit application */ 103 call _exit@PLT 104 hlt 105 .size make_fcontext,.-make_fcontext 106 107 /* Mark that we don't need executable stack. */ 108 .section .note.GNU-stack,"",%progbits
jump_x86_64_sysv_elf_gas.S
1 /* 2 Copyright Oliver Kowalke 2009. 3 Distributed under the Boost Software License, Version 1.0. 4 (See accompanying file LICENSE_1_0.txt or copy at 5 http://www.boost.org/LICENSE_1_0.txt) 6 */ 7 8 // 栈空间图,栈顶在低地址,注意和make_x86_64_sysv_elf_gas.S对比着看,里面代表的是内存(栈,运行现场)中的数据 9 // 不代表寄存器,途中标的寄存器的意思是这些寄存器在保存现场的时候会保存到对应的内存地址中 10 /**************************************************************************************** 11 * * 12 * ---------------------------------------------------------------------------------- * 13 * | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | * 14 * ---------------------------------------------------------------------------------- * 15 * | 0x0 | 0x4 | 0x8 | 0xc | 0x10 | 0x14 | 0x18 | 0x1c | * 16 * ---------------------------------------------------------------------------------- * 17 * | fc_mxcsr|fc_x87_cw| R12 | R13 | R14 | * 18 * ---------------------------------------------------------------------------------- * 19 * ---------------------------------------------------------------------------------- * 20 * | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | * 21 * ---------------------------------------------------------------------------------- * 22 * | 0x20 | 0x24 | 0x28 | 0x2c | 0x30 | 0x34 | 0x38 | 0x3c | * 23 * ---------------------------------------------------------------------------------- * 24 * | R15 | RBX | RBP | RIP | * 25 * ---------------------------------------------------------------------------------- * 26 * * 27 ****************************************************************************************/ 28 29 .file "jump_x86_64_sysv_elf_gas.S" 30 .text 31 .globl jump_fcontext 32 .type jump_fcontext,@function 33 .align 16 34 jump_fcontext: 35 // 保存当前的运行现场,caller-saved registers调用者保存,这里只保存callee-saved registers 36 leaq -0x38(%rsp), %rsp /* prepare stack */ 37 38 #if !defined(BOOST_USE_TSX) 39 stmxcsr (%rsp) /* save MMX control- and status-word */ 40 fnstcw 0x4(%rsp) /* save x87 control-word */ 41 #endif 42 43 movq %r12, 0x8(%rsp) /* save R12 */ 44 movq %r13, 0x10(%rsp) /* save R13 */ 45 movq %r14, 0x18(%rsp) /* save R14 */ 46 movq %r15, 0x20(%rsp) /* save R15 */ 47 movq %rbx, 0x28(%rsp) /* save RBX */ 48 movq %rbp, 0x30(%rsp) /* save RBP */ 49 // 这里为什么没有使用0x38位置的8个字节,因为这里面存的是返回地址,即本函数返回的时候 50 // 要继续运行的指令的地址,在调用jump_fcontext的时候已经保存了,所以这里不用保存 51 // 看make_fcontext可以看到,那里使用了0x38,因为那个fcontext不是通过调用jump_fcontext