• Swoole协程之旅-后篇

    Swoole协程之旅-后篇

     本篇我们开始深入PHP来分析Swoole协程的驱动部分,也就是C栈部分。

     由于我们系统存在C栈和PHP栈两部分,约定名字:

    • C协程 C栈管理部分,
    • PHP协程 PHP栈管理部分。
       增加C栈是4.x协程最重要也是最关键的部分,之前的版本种种无法完美支持PHP语法也是由于没有保存C栈信息。接下来我们将展开分析,C栈切换的支持最初我们是使用腾讯出品libco来支持,但通过压测会有内存读写错误而且开源社区很不活跃,有问题无法得到及时的反馈处理,所以,我们剥离的c++ boost库的汇编部分,现在的协程C栈的驱动就是在这个基础上做的。

     先来一张简单的系统架构图。Swoole4.x架构图可以发现,Swoole的角色是粘合在系统API和php ZendVM,给PHPer用户深度接口编写高性能的代码;不仅如此,也支持给C++/C用户开发使用,详细请参考文档C++开发者如何使用Swoole。C部分的代码主要分为几个部分

    • 汇编ASM驱动
    • Conext 上下文封装
    • Socket协程套接字封装
    • PHP Stream系封装,可以无缝协程化PHP相关函数
    • ZendVM结合层
      Swoole底层系统层次更加分明,Socket将作为整个网络驱动的基石,原来的版本中,每个客户端都要基于异步回调的方式维护上下文,所以4.x版本较之前版本比较,无论是从项目的复杂程度,还是系统的稳定性,可以说都有一个质的飞跃。代码目录层级
    1. $ tree swoole-src/src/coroutine/
    2. swoole-src/src/coroutine/
    3. ├── base.cc //C协程API,可回调PHP协程API
    4. ├── channel.cc //channel
    5. ├── context.cc //协程实现 基于ASM make_fcontext jump_fcontext
    6. ├── hook.cc //hook
    7. └── socket.cc //网络操作协程封装
    8. swoole-src/swoole_coroutine.cc //ZendVM相关封装,PHP协程API

    我们从用户层到系统至上而下有 PHP协程API, C协程API, ASM协程API。其中Socket层是兼容系统API的网络封装。我们至下而上进行分析。ASMx86-64架构为例,共有16个64位通用寄存器,各寄存器及用途如下

    • %rax 通常用于存储函数调用的返回结果,同时也用于乘法和除法指令中。在imul 指令中,两个64位的乘法最多会产生128位的结果,需要 %rax 与 %rdx 共同存储乘法结果,在div 指令中被除数是128 位的,同样需要%rax 与 %rdx 共同存储被除数。
    • %rsp 是堆栈指针寄存器,通常会指向栈顶位置,堆栈的 pop 和push 操作就是通过改变 %rsp 的值即移动堆栈指针的位置来实现的。
    • %rbp 是栈帧指针,用于标识当前栈帧的起始位置
    • %rdi, %rsi, %rdx, %rcx,%r8, %r9 六个寄存器用于存储函数调用时的6个参数
    • %rbx,%r12,%r13,%14,%15 用作数据存储,遵循被调用者使用规则
    • %r10,%r11 用作数据存储,遵循调用者使用规则
      也就是说在进入汇编函数后,第一个参数值已经放到了 %rdi 寄存器中,第二个参数值已经放到了 %rsi 寄存器中,并且栈指针 %rsp 指向的位置即栈顶中存储的是父函数的返回地址x86-64使用swoole-src/thirdparty/boost/asm/make_x86_64_sysv_elf_gas.S
    1. //在当前栈顶创建一个上下文,用来执行执行第三个参数函数fn,返回初始化完成后的执行环境上下文
    2. fcontext_t make_fcontext(void *sp, size_t size, void (*fn)(intptr_t));
    3. make_fcontext:
    4. /* first arg of make_fcontext() == top of context-stack */
    5. movq %rdi, %rax
    6. /* shift address in RAX to lower 16 byte boundary */
    7. andq $-16, %rax
    8. /* reserve space for context-data on context-stack */
    9. /* size for fc_mxcsr .. RIP + return-address for context-function */
    10. /* on context-function entry: (RSP -0x8) % 16 == 0 */
    11. leaq -0x48(%rax), %rax
    12. /* third arg of make_fcontext() == address of context-function */
    13. movq %rdx, 0x38(%rax)
    14. /* save MMX control- and status-word */
    15. stmxcsr (%rax)
    16. /* save x87 control-word */
    17. fnstcw 0x4(%rax)
    18. /* compute abs address of label finish */
    19. leaq finish(%rip), %rcx
    20. /* save address of finish as return-address for context-function */
    21. /* will be entered after context-function returns */
    22. movq %rcx, 0x40(%rax)
    23. ret /* return pointer to context-data * 返回rax指向的栈底指针,作为context返回/
    1. //将当前上下文(包括栈指针,PC程序计数器以及寄存器)保存至*ofc,从nfc恢复上下文并开始执行。
    2. intptr_t jump_fcontext(fcontext_t *ofc, fcontext_t nfc, intptr_t vp, bool preserve_fpu = false);
    3. jump_fcontext:
    4. //保存当前寄存器,压栈
    5. pushq %rbp /* save RBP */
    6. pushq %rbx /* save RBX */
    7. pushq %r15 /* save R15 */
    8. pushq %r14 /* save R14 */
    9. pushq %r13 /* save R13 */
    10. pushq %r12 /* save R12 */
    11. /* prepare stack for FPU */
    12. leaq -0x8(%rsp), %rsp
    13. /* test for flag preserve_fpu */
    14. cmp $0, %rcx
    15. je 1f
    16. /* save MMX control- and status-word */
    17. stmxcsr (%rsp)
    18. /* save x87 control-word */
    19. fnstcw 0x4(%rsp)
    20. 1:
    21. /* store RSP (pointing to context-data) in RDI 保存当前栈顶到rdi 即:将当前栈顶指针保存到第一个参数%rdi ofc中*/
    22. movq %rsp, (%rdi)
    23. /* restore RSP (pointing to context-data) from RSI 修改栈顶地址,为新协程的地址 ,rsi为第二个参数地址 */
    24. movq %rsi, %rsp
    25. /* test for flag preserve_fpu */
    26. cmp $0, %rcx
    27. je 2f
    28. /* restore MMX control- and status-word */
    29. ldmxcsr (%rsp)
    30. /* restore x87 control-word */
    31. fldcw 0x4(%rsp)
    32. 2:
    33. /* prepare stack for FPU */
    34. leaq 0x8(%rsp), %rsp
    35. // 寄存器恢复
    36. popq %r12 /* restrore R12 */
    37. popq %r13 /* restrore R13 */
    38. popq %r14 /* restrore R14 */
    39. popq %r15 /* restrore R15 */
    40. popq %rbx /* restrore RBX */
    41. popq %rbp /* restrore RBP */
    42. /* restore return-address 将返回地址放到 r8 寄存器中 */
    43. popq %r8
    44. /* use third arg as return-value after jump*/
    45. movq %rdx, %rax
    46. /* use third arg as first arg in context function */
    47. movq %rdx, %rdi
    48. /* indirect jump to context */
    49. jmp *%r8

    context管理位于context.cc,是对ASM的封装,提供两个API

    1. bool Context::SwapIn()
    2. bool Context::SwapOut()

    最终的协程API位于base.cc,最主要的API为

    1. //创建一个c栈协程,并提供一个执行入口函数,并进入函数开始执行上下文
    2. //例如PHP栈的入口函数Coroutine::create(PHPCoroutine::create_func, (void*) &php_coro_args);
    3. long Coroutine::create(coroutine_func_t fn, void* args = nullptr);
    4. //从当前上下文中切出,并且调用钩子函数 例如php栈切换函数 void PHPCoroutine::on_yield(void *arg)
    5. void Coroutine::yield()
    6. //从当前上下文中切入,并且调用钩子函数 例如php栈切换函数 void PHPCoroutine::on_resume(void *arg)
    7. void Coroutine::resume()
    8. //C协程执行结束,并且调用钩子函数 例如php栈清理 void PHPCoroutine::on_close(void *arg)
    9. void Coroutine::close()

    接下来是ZendVM的粘合层 位于swoole-src/swoole_coroutine.cc

    1. PHPCoroutine C协程或者底层接口调用
    2. //PHP协程创建入口函数,参数为php函数
    3. static long create(zend_fcall_info_cache *fci_cache, uint32_t argc, zval *argv);
    4. //C协程创建API
    5. static void create_func(void *arg);
    6. //C协程钩子函数 上一部分base.cc的C协程会关联到以下三个钩子函数
    7. static void on_yield(void *arg);
    8. static void on_resume(void *arg);
    9. static void on_close(void *arg);
    10. //PHP栈管理
    11. static inline void vm_stack_init(void);
    12. static inline void vm_stack_destroy(void);
    13. static inline void save_vm_stack(php_coro_task *task);
    14. static inline void restore_vm_stack(php_coro_task *task);
    15. //输出缓存管理相关
    16. static inline void save_og(php_coro_task *task);
    17. static inline void restore_og(php_coro_task *task);

    有了以上基础部分的建设,结合我们上一篇文章中PHP内核执行栈管理,就可以从C协程驱动PHP协程,实现C栈+PHP栈的双栈的原生协程。

    下一篇文章,我们将挑一个客户端实现分析socket层,把协程和Swoole事件驱动结合来分析C协程以及PHP协程在底层网络库的应用和实践。