<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>

                ThinkChat2.0新版上線,更智能更精彩,支持會話、畫圖、視頻、閱讀、搜索等,送10W Token,即刻開啟你的AI之旅 廣告
                # Swoole協程之旅-中篇 ?本篇我們開始深入PHP來分析Swoole協程的PHP部分。 ?先從一個協程最簡單的例子入手: ~~~ <?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本質上是一個[三地址碼](https://zh.wikipedia.org/wiki/%E4%B8%89%E4%BD%8D%E5%9D%80%E7%A2%BC "三地址碼"),這里opcode是指令的類型,有兩個輸入的操作數數和一個表示輸出的操作數。每個指令可能全部或者部分使用這些操作數,比如加、減、乘、除等會用到全部三個;`!`操作只用到op1和result兩個;函數調用會涉及到是否有返回值等。 * Op arrays `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協程如何進化到現在。
                  <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>

                              哎呀哎呀视频在线观看