Swoole协程之旅

做者:韩天峰 原文地址:点击查看php

协程是什么?

概念其实很早就出现了,摘wiki一段:According to Donald Knuth, the term coroutine was coined by Melvin Conway in 1958, after he applied it to construction of an assembly program.The first published explanation of the coroutine appeared later, in 1963. 协程要比c语言的历史还要悠久,究其概念,协程是子程序的一种, 能够经过yield的方式转移程序控制权,协程之间不是调用者与被调用者的关系,而是彼此对称、平等的。协程彻底有用户态程序控制,因此也被成为用户态的线程。协程由用户以非抢占的方式调度,而不是操做系统。正由于如此,没有系统调度上下文切换的开销,协程有了轻量,高效,快速等特色。(大部分为非抢占式,可是,好比Golang在1.4也加入了抢占式调度,其中一个协程发生死循环,不至于其余协程被饿死。须要在必要的时刻让出CPU)node

协程近几年如此火爆,很大一部分缘由归功与Golang在中国的流行和快速发展,受到不少开发的喜好。目前支持协程的语言有不少,例如: Golang、Lua、Python、C#、JavaScript等。你们也能够用很短的代码用C/C++撸出协程的模型。固然PHP也有本身的协程实现,也就是生成器,咱们这里不展开讨论。c++

Swoole 1.x

Swoole最初以高性能网络通信引擎的姿态进入你们视线,Swoole1.x的编码主要是异步回调的方式,虽然性能很是高效,但不少开发都会发现,随着项目工程的复杂程度增长,以异步回调的方式写业务代码是和人类正常思惟相悖的,尤为是回调嵌套多层的时候,不只开发维护成本指数级上升,并且出错的概率也大幅增长。你们理想的编码方式是:同步编码获得异步非阻塞的效果。因此Swoole很早的时候就开始了协程的探索。程序员

最初的协程版本是基于PHP生成器GeneratorsYield的方式实现的,能够参考PHP大神Nikita的早期博客的关于协程介绍。PHP和Swoole的事件驱动的结合能够参考腾讯出团队开源的TSF框架,咱们也在不少生产项目中使用了该框架,确实让你们感觉到了,以同步编程的方式写异步代码的快感,然而,现实老是很残酷,这种方式有几个致命的缺点:shell

  • 全部主动让出的逻辑都须要yield关键字。这会给程序员带来极大的几率犯错,致使你们对协程的理解转移到了对generators语法的原理的理解。
  • 因为语法没法兼容老的项目,改造老的项目工程复杂度巨大,成本过高。

这样使得不管新老项目,使用都没法驾轻就熟。编程

Swoole 2.x

  2.x以后的协程都是基于内核原生的协程,无需yield关键字。2.0的版本是一个很是重要的里程碑,实现了php的栈管理,深刻zend内核在协程建立,切换以及结束的时候操做PHP栈。数组

   2.x主要使用了setjmp/longjmp的方式实现协程,不少C项目主要采用这种方式实现try-catch-finally,你们也能够参考Zend内核的用法。setjmp的首次调用返回值是0,longjmp跳转时,setjmp的返回值是传给longjmp的value。 setjmp/longjmp因为只有控制流跳转的能力。虽然能够还原PC和栈指针,可是没法还原栈帧,所以会出现不少问题。好比longjmp的时候,setjmp的做用域已经退出,当时的栈帧已经销毁。这时就会出现未定义行为。假设有这样一个调用链:缓存

func0() -> func1() -> ... -> funcN()

只有在func{i}()中setjmp,在func{i+k}()中longjmp的状况下,程序的行为才是可预期的。swoole

Swoole 3.x

3.x 是生命周期很短的一个版本,主要借鉴了fiber-ext项目,使用了PHP7的VM interrupts机制,该机制能够在vm中设置标记位,在执行一些指令的时候(例如:跳转和函数调用等)检查标记位,若是命中就能够执行相应的hook函数来切换vm的栈,进而实现协程。网络

Swoole 4.x

从 4.x 开始,Swoole 实现了双栈模式的协程内核。而且将全部IO和系统操做封装了到了底层,实现了完全的内核协程化。另外,还提供了全新的Runtime Hook模块,可使得已有的旧的PHP同步代码变为协程模式。

Swoole4 协程分析

先从一个协程最简单的例子入手:

<?php
go(function(){
    echo "coro 1 start\n";
    co::sleep(1);
    echo "coro 1 exit";
});
echo "main flag\n";
go(function(){
    echo "coro 2 start\n";
    co::sleep(1);
    echo "coro 2 exit\n";
});
echo "main end\n";
//输出内容为
coro 1 start
main flag
coro 2 start
main end
coro 1 exit
coro 2 exit

能够发现,原生协程是在函数内部发生了跳转,控制流从第4行跳转到第7行,接着执行从第8行开始执行go函数,到第10行跳转到了第13行,紧接着执行第9行,而后执行第15行的代码。为何Swoole的协程能够这样执行呢?咱们下面将一步一步进行分析。

  咱们知道PHP做为一门解释型的语言,须要通过编译为中间字节码才能够执行,首先会通过词法和语法分析,将脚本编译为opcode数组,成为zend_op_array,而后通过vm引擎来执行。咱们这里只关注vm执行部分。执行的部分须要关注几个重要的数据结构

Opcodes

struct _zend_op {
    const void *handler;//每一个opcode对应的c处理函数
    znode_op op1;//操做数1
    znode_op op2;//操做数2
    znode_op result;//返回值
    uint32_t extended_value;
    uint32_t lineno;
    zend_uchar opcode;//opcode指令
    zend_uchar op1_type;//操做数1类型
    zend_uchar op2_type;//操做数2类型
    zend_uchar result_type;//返回值类型
};

从结构中很容易发现opcodes本质上是一个三地址码,这里opcode是指令的类型,有两个输入的操做数数和一个表示输出的操做数。每一个指令可能所有或者部分使用这些操做数,好比加、减、乘、除等会用到所有三个; !操做只用到op1和result两个;函数调用会涉及到是否有返回值等。

Op Array

zend_op_array PHP的主脚本会生成一个zend_op_array,每一个function,eval,甚至是assert断言一个表达式等都会生成一个新得op_array。

struct _zend_op_array {
    /* Common zend_function header here */
    /* ... */
    uint32_t last;//数组中opcode的数量
    zend_op *opcodes;//opcode指令数组
    int last_var;// CVs的数量
    uint32_t T;//IS_TMP_VAR、IS_VAR的数量
    zend_string **vars;//变量名数组
    /* ... */
    int last_literal;//字面量数量
    zval *literals;//字面量数组 访问时经过_zend_op_array->literals + 偏移量读取
    /* ... */
};

咱们已经熟知php的函数内部有本身的单独的做用域,这归功于每一个zend_op_array包含有当前做用域下全部的堆栈信息,函数之间的调用关系也是基于zend_op_array的切换来实现。

PHP栈帧

PHP执行须要的全部状态都保存在一个个经过链表结构关联的VM栈里,每一个栈默认会初始化为256K,Swoole能够单独定制这个栈的大小(协程默认为8k),当栈容量不足的时候,会自动扩容,仍然以链表的关系关联每一个栈。在每次函数调用的时候,都会在VM Stack空间上申请一块新的栈帧来容纳当前做用域执行所需。栈帧结构的内存布局以下所示:

+----------------------------------------+
| zend_execute_data                      |
+----------------------------------------+
| VAR[0]                =         ARG[1] | arguments
| ...                                    |
| VAR[num_args-1]       =         ARG[N] |
| VAR[num_args]         =   CV[num_args] | remaining CVs
| ...                                    |
| VAR[last_var-1]       = CV[last_var-1] |
| VAR[last_var]         =         TMP[0] | TMP/VARs
| ...                                    |
| VAR[last_var+T-1]     =         TMP[T] |
| ARG[N+1] (extra_args)                  | extra arguments
| ...                                    |
+----------------------------------------+

zend_execute_data 最后要介绍的一个结构,也是最重要的一个。

struct _zend_execute_data {
    const zend_op       *opline;//当前执行的opcode,初始化会zend_op_array起始
    zend_execute_data   *call;//
    zval                *return_value;//返回值
    zend_function       *func;//当前执行的函数(非函数调用时为空)
    zval                 This;/* this + call_info + num_args    */
    zend_class_entry    *called_scope;//当前call的类
    zend_execute_data   *prev_execute_data;
    zend_array          *symbol_table;//全局变量符号表
    void               **run_time_cache;   /* cache op_array->run_time_cache */
    zval                *literals;         /* cache op_array->literals       */
};

prev_execute_data 表示前一个栈帧结构,当前栈执行结束之后,会把当前执行指针(类比PC)指向这个栈帧。PHP的执行流程正是将不少个zend_op_array依次装载在栈帧上执行。这个过程能够分解为如下几个步骤:

  • 1: 为当前须要执行的op_array从vm stack上申请当前栈帧,结构如上。初始化全局变量符号表,将全局指针EG(current_execute_data)指向新分配的zend_execute_data栈帧,EX(opline)指向op_array起始位置。
  • 2: 从EX(opline)开始调用各opcode的C处理handler(即_zend_op.handler),每执行完一条opcode将EX(opline)++继续执行下一条,直到执行彻底部opcode,遇到函数或者类成员方法调用:

    • 从EG(function_table)中根据function_name取出此function对应的zend_op_array,而后重复步骤1,将EG(current_execute_data)赋值给新结构的prev_execute_data,再将EG(current_execute_data)指向新的zend_execute_data栈帧,而后开始执行新栈帧,从位置zend_execute_data.opline开始执行,函数执行完将EG(current_execute_data)从新指向EX(prev_execute_data),释放分配的运行栈帧,执行位置回到函数执行结束的下一条opline。
  • 3: 所有opcodes执行完成后将1分配的栈帧释放,执行阶段结束。

有了以上php执行的细节,咱们回到最初的例子,能够发现协程须要作的是,改变本来php的运行方式,不是在函数运行结束切换栈帧,而是在函数执行当前op_array中间任意时候(swoole内部控制为遇到IO等待),能够灵活切换到其余栈帧。接下来咱们将Zend VM和Swoole结合分析,如何建立协程栈,遇到IO切换,IO完成后栈恢复,以及协程退出时栈帧的销毁等细节。先介绍协程PHP部分的主要结构:

  • 协程 php_coro_task
struct php_coro_task
{
    /* 只列出关键结构*/
    /*...*/
    zval *vm_stack_top;//栈顶
    zval *vm_stack_end;//栈底
    zend_vm_stack vm_stack;//当前协程栈指针
    /*...*/
    zend_execute_data *execute_data;//当前协程栈帧
    /*...*/
    php_coro_task *origin_task;//上一个协程栈帧,类比prev_execute_data的做用
};

协程切换主要是针对当前栈执行发生中断时对上下文保存,和恢复。结合上面VM的执行流程咱们能够知道上面几个字段的做用。

  • execute_data 栈帧指针须要保存和恢复是毋容置疑的
  • vm_stack* 系列是什么做用呢?缘由是PHP是动态语言,咱们上面分析到,每次有新函数进入执行和退出的时候,都须要在全局stack上建立和释放栈帧,因此须要正确保存和恢复对应的全局栈指针,才能保障每一个协程栈帧获得释放,不会致使内存泄漏的问题。(当以debug模式编译PHP后,每次释放都会检查当全局栈是否合法)
  • origin_task 是当前协程执行结束后须要自动执行的前一个栈帧。

主要涉及到的方法有

  • 协程的建立 create,在全局stack上为协程申请栈帧。

    • 协程的建立是建立一个闭包函数,将函数(能够理解为须要执行的op_array)看成一个参数传入Swoole的内建函数go();
  • 协程让出,yield,遇到IO,保存当前栈帧的上下文信息
  • 协程的恢复,resume,IO完成,恢复须要执行的协程上下文信息到yield让出前的状态
  • 协程的退出,exit,协程op_array所有执行完毕,释放栈帧和swoole协程的相关数据。

通过上面的介绍你们应该对Swoole协程在运行过程当中能够在函数内部实现跳转有一个大概了解,回到最初咱们例子结合上面php执行细节,咱们可以知道,该例子会生成3个op_array,分别为 主脚本,协程1,协程2。咱们能够利用一些工具打印出opcodes来直观的观察一下。一般咱们会使用下面两个工具

//Opcache, version >= PHP 7.1
php -d opcache.opt_debug_level=0x10000 test.php

//vld, 第三方扩展
php -d vld.active=1 test.php

咱们用opcache来观察没有被优化前的opcodes,咱们能够很清晰的看到这三组op_array的详细信息。

php -dopcache.enable_cli=1 -d opcache.opt_debug_level=0x10000 test.php
$_main: ; (lines=11, args=0, vars=0, tmps=4)
    ; (before optimizer)
    ; /path-to/test.php:2-6
L0 (2):     INIT_FCALL 1 96 string("go")
L1 (2):     T0 = DECLARE_LAMBDA_FUNCTION string("")
L2 (6):     SEND_VAL T0 1
L3 (6):     DO_ICALL
L4 (7):     ECHO string("main flag
")
L5 (8):     INIT_FCALL 1 96 string("go")
L6 (8):     T2 = DECLARE_LAMBDA_FUNCTION string("")
L7 (12):    SEND_VAL T2 1
L8 (12):    DO_ICALL
L9 (13):    ECHO string("main end
")
L10 (14):   RETURN int(1)

{closure}: ; (lines=6, args=0, vars=0, tmps=1)
    ; (before optimizer)
    ; /path-to/test.php:2-6
L0 (9):     ECHO string("coro 2 start
")
L1 (10):    INIT_STATIC_METHOD_CALL 1 string("co") string("sleep")
L2 (10):    SEND_VAL_EX int(1) 1
L3 (10):    DO_FCALL//yiled from 当前op_array [coro 1] ; resume
L4 (11):    ECHO string("coro 2 exit
")
L5 (12):    RETURN null

{closure}: ; (lines=6, args=0, vars=0, tmps=1)
    ; (before optimizer)
    ; /path-to/test.php:2-6
L0 (3):     ECHO string("coro 1 start
")
L1 (4):     INIT_STATIC_METHOD_CALL 1 string("co") string("sleep")
L2 (4):     SEND_VAL_EX int(1) 1
L3 (4):     DO_FCALL//yiled from 当前op_array [coro 2];resume
L4 (5):     ECHO string("coro 1 exit
")
L5 (6):     RETURN null
coro 1 start
main flag
coro 2 start
main end
coro 1 exit
coro 2 exit

Swoole在执行co::sleep()的时候让出当前控制权,跳转到下一个op_array,结合以上注释,也就是在DO_FCALL的时候分别让出和恢复协程执行栈,达到原生协程控制流跳转的目的。

咱们分析下 INIT_FCALL DO_FCALL指令在内核中如何执行。以便于更好理解函数调用栈切换的关系。

VM内部指令会根据当前的操做数返回值等特殊化为一个c函数,咱们这个例子中 有如下对应关系

INIT_FCALL => ZEND_INIT_FCALL_SPEC_CONST_HANDLER

DO_FCALL => ZEND_DO_FCALL_SPEC_RETVAL_UNUSED_HANDLER

ZEND_INIT_FCALL_SPEC_CONST_HANDLER(ZEND_OPCODE_HANDLER_ARGS)
{
    USE_OPLINE

    zval *fname = EX_CONSTANT(opline->op2);
    zval *func;
    zend_function *fbc;
    zend_execute_data *call;

    fbc = CACHED_PTR(Z_CACHE_SLOT_P(fname));
    if (UNEXPECTED(fbc == NULL)) {
        func = zend_hash_find(EG(function_table), Z_STR_P(fname));
        if (UNEXPECTED(func == NULL)) {
            SAVE_OPLINE();
            zend_throw_error(NULL, "Call to undefined function %s()", Z_STRVAL_P(fname));
            HANDLE_EXCEPTION();
        }
        fbc = Z_FUNC_P(func);
        CACHE_PTR(Z_CACHE_SLOT_P(fname), fbc);
        if (EXPECTED(fbc->type == ZEND_USER_FUNCTION) && UNEXPECTED(!fbc->op_array.run_time_cache)) {
            init_func_run_time_cache(&fbc->op_array);
        }
    }

    call = zend_vm_stack_push_call_frame_ex(
        opline->op1.num, ZEND_CALL_NESTED_FUNCTION,
        fbc, opline->extended_value, NULL, NULL); //从全局stack上申请当前函数的执行栈
    call->prev_execute_data = EX(call); //将正在执行的栈赋值给将要执行函数栈的prev_execute_data,函数执行结束后恢复到此处
    EX(call) = call; //将函数栈赋值到全局执行栈,即将要执行的函数栈
    ZEND_VM_NEXT_OPCODE();
}
ZEND_DO_FCALL_SPEC_RETVAL_UNUSED_HANDLER(ZEND_OPCODE_HANDLER_ARGS)
{
    USE_OPLINE
    zend_execute_data *call = EX(call);//获取到执行栈
    zend_function *fbc = call->func;//当前函数
    zend_object *object;
    zval *ret;

    SAVE_OPLINE();//有全局寄存器的时候 ((execute_data)->opline) = opline
    EX(call) = call->prev_execute_data;//当前执行栈execute_data->call = EX(call)->prev_execute_data 函数执行结束后恢复到被调函数
    /*...*/
    LOAD_OPLINE();

    if (EXPECTED(fbc->type == ZEND_USER_FUNCTION)) {
        ret = NULL;
        if (0) {
            ret = EX_VAR(opline->result.var);
            ZVAL_NULL(ret);
        }

        call->prev_execute_data = execute_data;
        i_init_func_execute_data(call, &fbc->op_array, ret);

        if (EXPECTED(zend_execute_ex == execute_ex)) {
            ZEND_VM_ENTER();
        } else {
            ZEND_ADD_CALL_FLAG(call, ZEND_CALL_TOP);
            zend_execute_ex(call);
        }
    } else if (EXPECTED(fbc->type < ZEND_USER_FUNCTION)) {
        zval retval;

        call->prev_execute_data = execute_data;
        EG(current_execute_data) = call;
        /*...*/
        ret = 0 ? EX_VAR(opline->result.var) : &retval;
        ZVAL_NULL(ret);

        if (!zend_execute_internal) {
            /* saves one function call if zend_execute_internal is not used */
            fbc->internal_function.handler(call, ret);
        } else {
            zend_execute_internal(call, ret);
        }

        EG(current_execute_data) = execute_data;
        zend_vm_stack_free_args(call);//释放局部变量

        if (!0) {
            zval_ptr_dtor(ret);
        }

    } else { /* ZEND_OVERLOADED_FUNCTION */
        /*...*/
    }

fcall_end:
        /*...*/
    }
    zend_vm_stack_free_call_frame(call);//释放栈
    if (UNEXPECTED(EG(exception) != NULL)) {
        zend_rethrow_exception(execute_data);
        HANDLE_EXCEPTION();
    }
    ZEND_VM_SET_OPCODE(opline + 1);
    ZEND_VM_CONTINUE();
}

Swoole在PHP层能够按照以上方式来进行切换,至于执行过程当中有IO等待发生,须要额外的技术来驱动,咱们后续的文章将会介绍每一个版本的驱动技术结合Swoole原有的事件模型,讲述Swoole协程如何进化到如今。

Swoole4 协程双栈

 因为咱们系统存在C栈和PHP栈两部分,约定名字:

  • C协程 C栈管理部分,
  • PHP协程 PHP栈管理部分。

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

系统架构图

能够发现,Swoole的角色是粘合在系统API和php ZendVM,给PHPer用户深度接口编写高性能的代码;不只如此,也支持给C++/C用户开发使用,详细请参考文档C++开发者如何使用Swoole。C部分的代码主要分为几个部分:

  • 汇编ASM驱动
  • Conext 上下文封装
  • Socket协程套接字封装
  • PHP Stream系封装,能够无缝协程化PHP相关函数
  • ZendVM结合层

Swoole底层系统层次更加分明,Socket将做为整个网络驱动的基石,原来的版本中,每一个客户端都要基于异步回调的方式维护上下文,因此4.x版本较以前版本比较,不管是从项目的复杂程度,仍是系统的稳定性,能够说都有一个质的飞跃。代码目录层级

$ tree swoole-src/src/coroutine/
swoole-src/src/coroutine/
├── base.cc //C协程API,可回调PHP协程API
├── channel.cc //channel
├── context.cc //协程实现 基于ASM make_fcontext jump_fcontext
├── hook.cc //hook
└── socket.cc //网络操做协程封装
swoole-src/swoole_coroutine.cc //ZendVM相关封装,PHP协程API

咱们从用户层到系统至上而下有 PHP协程API, C协程API, ASM协程API。其中Socket层是兼容系统API的网络封装。咱们至下而上进行分析。ASM x86-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

//在当前栈顶建立一个上下文,用来执行执行第三个参数函数fn,返回初始化完成后的执行环境上下文
fcontext_t make_fcontext(void *sp, size_t size, void (*fn)(intptr_t));
make_fcontext:
    /* first arg of make_fcontext() == top of context-stack */
    movq  %rdi, %rax

    /* shift address in RAX to lower 16 byte boundary */
    andq  $-16, %rax

    /* reserve space for context-data on context-stack */
    /* size for fc_mxcsr .. RIP + return-address for context-function */
    /* on context-function entry: (RSP -0x8) % 16 == 0 */
    leaq  -0x48(%rax), %rax

    /* third arg of make_fcontext() == address of context-function */
    movq  %rdx, 0x38(%rax)

    /* save MMX control- and status-word */
    stmxcsr  (%rax)
    /* save x87 control-word */
    fnstcw   0x4(%rax)

    /* compute abs address of label finish */
    leaq  finish(%rip), %rcx
    /* save address of finish as return-address for context-function */
    /* will be entered after context-function returns */
    movq  %rcx, 0x40(%rax)

    ret /* return pointer to context-data * 返回rax指向的栈底指针,做为context返回/
//将当前上下文(包括栈指针,PC程序计数器以及寄存器)保存至*ofc,从nfc恢复上下文并开始执行。
intptr_t jump_fcontext(fcontext_t *ofc, fcontext_t nfc, intptr_t vp, bool preserve_fpu = false);

jump_fcontext:
//保存当前寄存器,压栈
    pushq  %rbp  /* save RBP */
    pushq  %rbx  /* save RBX */
    pushq  %r15  /* save R15 */
    pushq  %r14  /* save R14 */
    pushq  %r13  /* save R13 */
    pushq  %r12  /* save R12 */

    /* prepare stack for FPU */
    leaq  -0x8(%rsp), %rsp

    /* test for flag preserve_fpu */
    cmp  $0, %rcx
    je  1f

    /* save MMX control- and status-word */
    stmxcsr  (%rsp)
    /* save x87 control-word */
    fnstcw   0x4(%rsp)

1:
    /* store RSP (pointing to context-data) in RDI  保存当前栈顶到rdi 即:将当前栈顶指针保存到第一个参数%rdi ofc中*/
    movq  %rsp, (%rdi)

    /* restore RSP (pointing to context-data) from RSI 修改栈顶地址,为新协程的地址 ,rsi为第二个参数地址 */
    movq  %rsi, %rsp

    /* test for flag preserve_fpu */
    cmp  $0, %rcx
    je  2f

    /* restore MMX control- and status-word */
    ldmxcsr  (%rsp)
    /* restore x87 control-word */
    fldcw  0x4(%rsp)

2:
    /* prepare stack for FPU */
    leaq  0x8(%rsp), %rsp
// 寄存器恢复
    popq  %r12  /* restrore R12 */
    popq  %r13  /* restrore R13 */
    popq  %r14  /* restrore R14 */
    popq  %r15  /* restrore R15 */
    popq  %rbx  /* restrore RBX */
    popq  %rbp  /* restrore RBP */

    /* restore return-address  将返回地址放到 r8 寄存器中 */
    popq  %r8

    /* use third arg as return-value after jump*/
    movq  %rdx, %rax
    /* use third arg as first arg in context function */
    movq  %rdx, %rdi

    /* indirect jump to context */
    jmp  *%r8

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

bool Context::SwapIn()
bool Context::SwapOut()

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

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

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

PHPCoroutine 供C协程或者底层接口调用
//PHP协程建立入口函数,参数为php函数
static long create(zend_fcall_info_cache *fci_cache, uint32_t argc, zval *argv);
//C协程建立API
static void create_func(void *arg);
//C协程钩子函数 上一部分base.cc的C协程会关联到如下三个钩子函数
static void on_yield(void *arg);
static void on_resume(void *arg);
static void on_close(void *arg);
//PHP栈管理
static inline void vm_stack_init(void);
static inline void vm_stack_destroy(void);
static inline void save_vm_stack(php_coro_task *task);
static inline void restore_vm_stack(php_coro_task *task);
//输出缓存管理相关
static inline void save_og(php_coro_task *task);
static inline void restore_og(php_coro_task *task);

有了以上基础部分的建设,结合PHP内核执行栈管理,就能够从C协程驱动PHP协程,实现C栈+PHP栈的双栈的原生协程。

做者信息

韩天峰,Swoole开源项目创始人,学而思网校首席架构师。

相关文章
相关标签/搜索