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開源項目創始人,學而思網校首席架構師。

相關文章
相關標籤/搜索