<ruby id="bdb3f"></ruby>

    <p id="bdb3f"><cite id="bdb3f"></cite></p>

      <p id="bdb3f"><cite id="bdb3f"><th id="bdb3f"></th></cite></p><p id="bdb3f"></p>
        <p id="bdb3f"><cite id="bdb3f"></cite></p>

          <pre id="bdb3f"></pre>
          <pre id="bdb3f"><del id="bdb3f"><thead id="bdb3f"></thead></del></pre>

          <ruby id="bdb3f"><mark id="bdb3f"></mark></ruby><ruby id="bdb3f"></ruby>
          <pre id="bdb3f"><pre id="bdb3f"><mark id="bdb3f"></mark></pre></pre><output id="bdb3f"></output><p id="bdb3f"></p><p id="bdb3f"></p>

          <pre id="bdb3f"><del id="bdb3f"><progress id="bdb3f"></progress></del></pre>

                <ruby id="bdb3f"></ruby>

                # Swoole協程之旅 ## 1協程是什么? 概念其實很早就出現了,摘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) 協程近幾年如此火爆,很大一部分原因歸功于Golang在中國的流行和快速發展,受到很多開發者的喜愛。目前支持協程的語言有很多,例如:Golang、Lua、Python、C#、JavaScript等。大家也可以用很短的代碼用C/C++擼出協程的模型。當然PHP也有自己的協程實現,也就是生成器,我們這里不展開討論。 ### Swoole 1.x Swoole最初以高性能網絡通訊引擎的姿態進入大家視線,Swoole1.x的編碼主要是異步回調的方式,雖然性能非常高效,但很多開發都會發現,隨著項目工程的復雜程度增加,以異步回調的方式寫業務代碼是和人類正常思維相悖的,尤其是回調嵌套多層的時候,不僅開發維護成本指數級上升,而且出錯的幾率也大幅增加。 大家理想的編碼方式是:同步編碼得到異步非阻塞的效果。所以Swoole很早的時候就開始了協程的探索。最初的協程版本是基于PHP生成器Generators\Yield的方式實現的,可以參考PHP大神Nikita的早期博客關于協程的介紹。 PHP和Swoole的事件驅動的結合可以參考騰訊出團隊開源的TSF框架,我們也在很多生產項目中使用了該框架,確實讓大家感受到了,以同步編程的方式寫異步代碼的快感,然而,現實總是很殘酷,這種方式有幾個致命的缺點: * 所有主動讓出的邏輯都需要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 3.x 3.x是生命周期很短的一個版本,主要借鑒了`fiber-ext`項目,使用了PHP7的VM interrupts機制,該機制可以在vm中設置標記位,在執行一些指令的時候(例如:跳轉和函數調用等)檢查標記位,如果命中就可以執行相應的hook函數來切換vm的棧,進而實現協程。 ### Swoole 4.x 從4.x開始,Swoole實現了雙棧模式的協程內核。并且將所有IO和系統操作封裝了到了底層,實現了徹底的內核協程化。另外,還提供了全新的Runtime Hook模塊,可以使得已有的舊的PHP同步代碼變為協程模式。 ## 2 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行,緊接著執行第5行,然后執行第11行的代碼。為什么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空間上申請一塊新的棧幀來容納當前作用域執行所需。棧幀結構的內存布局如下所示: ![](https://img.kancloud.cn/bf/c5/bfc590684347d577db63a40c409f931a_831x435.png) 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的執行流程我們可以知道上面幾個字段的作用: 1.`execute_data`棧幀指針需要保存和恢復是毋容置疑的 2.`vm_stack*`系列是什么作用呢?原因是PHP是動態語言,我們上面分析到,每次有新函數進入執行和退出的時候,都需要在全局stack上創建和釋放棧幀,所以需要正確保存和恢復對應的全局棧指針,才能保障每個協程棧幀得到釋放,不會導致內存泄漏的問題。(當以debug模式編譯PHP后,每次釋放都會檢查當全局棧是否合法) 3.`origin_task`是當前協程執行結束后需要自動執行的前一個棧幀。 主要涉及到的方法有: 1.協程的創建create,在全局stack上為協程申請棧幀。 協程的創建是創建一個閉包函數,將函數(可以理解為需要執行的op_array)當作一個參數傳入Swoole的內建函數go()。 2.協程讓出yield,遇到IO,保存當前棧幀的上下文信息。 3.協程的恢復resume,IO完成,恢復需要執行的協程上下文信息到yield讓出前的狀態。 4.協程的退出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協程如何進化到現在。 ## 3 Swoole4協程雙棧 由于我們系統存在C棧和PHP棧兩部分,約定名字: * C協程C棧管理部分 * PHP協程PHP棧管理部分 增加C棧是4.x協程最重要也是最關鍵的部分,之前的版本種種無法完美支持PHP語法也是由于沒有保存C棧信息。接下來我們將展開分析,C棧切換的支持最初我們是使用騰訊出品libco來支持,但通過壓測會有內存讀寫錯誤而且開源社區很不活躍,有問題無法得到及時的反饋處理,所以,我們剝離的c++ boost庫的匯編部分,現在的協程C棧的驅動就是在這個基礎上做的。 系統架構圖 ![](https://img.kancloud.cn/a3/23/a323e44c910c029e4411d444f4589d2d_830x627.png) 可以發現,Swoole的角色是粘合在系統API和PHP ZendVM,給PHPer用戶深度接口編寫高性能的代碼;不僅如此,也支持給C++/C用戶開發使用,詳細請參考文檔C++開發者如何使用Swoole。C部分的代碼主要分為幾個部分: 1.匯編ASM驅動 2.Conext上下文封裝 3.Socket協程套接字封裝 4.PHP Stream系封裝,可以無縫協程化PHP相關函數 5.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位通用寄存器,各寄存器及用途如下: 1.%rax通常用于存儲函數調用的返回結果,同時也用于乘法和除法指令中。在imul指令中,兩個64位的乘法最多會產生128位的結果,需要%rax與%rdx共同存儲乘法結果,在div指令中被除數是128位的,同樣需要%rax與%rdx共同存儲被除數。 2.%rsp是堆棧指針寄存器,通常會指向棧頂位置,堆棧的pop和push操作就是通過改變%rsp的值即移動堆棧指針的位置來實現的。 3.%rbp是棧幀指針,用于標識當前棧幀的起始位置 4.%rdi, %rsi, %rdx, %rcx,%r8, %r9六個寄存器用于存儲函數調用時的6個參數 5.%rbx,%r12,%r13,%14,%15用作數據存儲,遵循被調用者使用規則 6.%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棧的雙棧的原生協程。 (文/韓天峰 編/韓冬輝)
                  <ruby id="bdb3f"></ruby>

                  <p id="bdb3f"><cite id="bdb3f"></cite></p>

                    <p id="bdb3f"><cite id="bdb3f"><th id="bdb3f"></th></cite></p><p id="bdb3f"></p>
                      <p id="bdb3f"><cite id="bdb3f"></cite></p>

                        <pre id="bdb3f"></pre>
                        <pre id="bdb3f"><del id="bdb3f"><thead id="bdb3f"></thead></del></pre>

                        <ruby id="bdb3f"><mark id="bdb3f"></mark></ruby><ruby id="bdb3f"></ruby>
                        <pre id="bdb3f"><pre id="bdb3f"><mark id="bdb3f"></mark></pre></pre><output id="bdb3f"></output><p id="bdb3f"></p><p id="bdb3f"></p>

                        <pre id="bdb3f"><del id="bdb3f"><progress id="bdb3f"></progress></del></pre>

                              <ruby id="bdb3f"></ruby>

                              哎呀哎呀视频在线观看