Swoole Partnership Tour

Author: Han Tianfeng's original address: Click to view

What is the consortium?

The first published explanation of the coroutine appeared later, in 1963. The concept of the coroutine is longer than that of the c language. The term coroutine is coined by Melvin Conway in 1958, after which it is applied to construction of an assembly program. The first published explanation of the coroutine appeared later, in 1963. In other words, program control can be transferred by yield. The relationship between programs is not the relationship between the caller and the callee, but symmetrical and equal to each other. It is completely controlled by user-mode programs, so it is also called user-mode threads. Collaboration is scheduled by the user in a non-preemptive manner, not by the operating system. Because of this, without the overhead of context switching in system scheduling, the collaboration has the characteristics of lightweight, efficient and fast. (Most of them are non-preemptive, but, for example, Golang also joined preemptive scheduling in 1.4. One of the cooperatives has a dead cycle, so that other cooperatives will not starve to death. The CPU needs to be released when necessary.

In recent years, Golang has become so popular, thanks in large part to the popularity and rapid development of Golang in China, which has been loved by many developers. There are many languages that support collaboration, such as Golang, Lua, Python, C#, JavaScript and so on. You can also use very short code with C/C++ to model the process. Of course, PHP also has its own co-implementation, that is, generators, which we will not discuss here.

Swoole 1.x

Swoole first came into our sight as a high-performance network communication engine. Swoole 1.x coding is mainly an asynchronous callback mode. Although the performance is very efficient, many development will find that, with the increasing complexity of project engineering, writing business code in an asynchronous callback mode is contrary to human normal thinking, especially when callback nested multi-layer, not only development. Maintenance costs have risen exponentially, and the probability of errors has increased dramatically. The ideal encoding method is synchronous encoding to achieve asynchronous non-blocking effect. So Swoole started exploring collaboration early on.

The original version of the collaboration is based on the way of the PHP generator Generators Field. You can refer to the introduction of the collaboration in the early blog of Nikita, the God of PHP. The event-driven combination of PHP and Swoole can refer to Tencent's open-source TSF framework, which is also used in many production projects. It really gives you the pleasure of writing asynchronous code in a synchronous programming way. However, the reality is always cruel. There are several fatal shortcomings in this way:

  • The yield keyword is required for all the logic of active concession. This leads to a great probability of programmers making mistakes, leading to a shift in understanding the principles of generators grammar.
  • Because the grammar can not be compatible with the old project, the reconstruction of the old project is too complex and costly.

This makes the new and old projects, use can not be handy.

Swoole 2.x

After 2.x, the coroutines are all based on the native coroutines of the kernel, without the need for the yield keyword. Version 2.0 is a very important milestone, which implements PHP stack management, and goes deep into zend kernel to operate PHP stack at the time of co-process creation, switching and termination.

2.x mainly uses setjmp/longjmp to implement the protocol. Many C projects mainly use this way to achieve try-catch-finally. You can also refer to the use of Zend kernel. The return value of the first call to setjmp is 0. When long JMP jumps, the return value of setjmp is the value passed to long jmp. Setjmp / longjmp has only the ability to control the flow jump. Although PC and stack pointer can be restored, stack frame can not be restored, so many problems will arise. For example, when long JMP is used, the scope of setjmp has exited and the stack frame has been destroyed. In this case, undefined behavior will occur. Suppose there is such a call chain:

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

Only in the case of setjmp in func{i}() and longjmp in func{i+k}(), can the behavior of the program be predictable.

Swoole 3.x

3.x is a short life cycle version. It mainly uses the VM interrupts mechanism of PHP7 for reference of the fiber-ext project. This mechanism can set tag bits in vm, check tag bits when executing some instructions (such as jump and function call) and execute corresponding hook functions to switch VM stacks if hit, so as to realize the collaboration.

Swoole 4.x

Starting with 4.x, Swoole implemented a dual stack-mode corecone. All IO and system operations are encapsulated to the bottom layer, and a thorough core collaboration is achieved. In addition, it also provides a new Runtime Hook module, which can make the existing old PHP synchronization code into a cooperative mode.

Swoole 4 Coprocess Analysis

Start with the simplest example of a collaboration:

<?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";
//The output is
coro 1 start
main flag
coro 2 start
main end
coro 1 exit
coro 2 exit

It can be found that a jump occurs inside the function, the control flow jumps from line 4 to line 7, then the go function is executed from line 8, then the go function is executed from line 10 to line 13, followed by line 9, and then the code of line 15. Why can Swoole's protocol be executed like this? We will analyze it step by step.

We know that PHP, as an interpretative language, needs to be compiled into intermediate bytecode before it can be executed. First, through lexical and grammatical analysis, the script is compiled into an opcode array, which is zend_op_array, and then executed by the vm engine. We're only focusing on the vm execution here. The execution part needs to focus on several important data structures

Opcodes

struct _zend_op {
    const void *handler;//c-processing function corresponding to each opcode
    znode_op op1;//Operand 1
    znode_op op2;//Operand 2
    znode_op result;//Return value
    uint32_t extended_value;
    uint32_t lineno;
    zend_uchar opcode;//opcode directive
    zend_uchar op1_type;//Operator 1 type
    zend_uchar op2_type;//Operator 2 type
    zend_uchar result_type;//return type
};

It is easy to find from the structure that opcodes is essentially a three-address code, where opcode is the type of instruction, with two input operands and one output operand. Each instruction may use all or part of these operands, such as add, subtract, multiply, divide and so on. Operations only use op1 and result; function calls involve whether there is a return value, etc.

Op Array

The main script of zend_op_array PHP generates a zend_op_array, and each function,eval, or even assert assertion of an expression generates a new op_array.

struct _zend_op_array {
    /* Common zend_function header here */
    /* ... */
    uint32_t last;//Number of opcode s in an array
    zend_op *opcodes;//opcode instruction array
    int last_var;// The number of CVs
    uint32_t T;//Number of IS_TMP_VAR and IS_VAR
    zend_string **vars;//Array of variable names
    /* ... */
    int last_literal;//Number of literal quantities
    zval *literals;//Literals array access is read by _zend_op_array-> literals + offset
    /* ... */
};

We are already familiar with the fact that php functions have their own separate scopes, thanks to the fact that each zend_op_array contains all the stack information in the current scope, and the call relationship between functions is also based on the handover of zend_op_array.

PHP stack frame

All the states required for PHP execution are stored in one VM stack associated with the linked list structure. Each stack is initialized to 256K by default. Swoole can customize the size of the stack individually (the default is 8K for the coprocess). When the stack capacity is insufficient, it will automatically expand and still associate each stack with the linked list relationship. Each time a function is called, a new stack frame is applied in the VM Stack space to accommodate the current scope execution. The memory layout of the stack frame structure is as follows:

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

The last structure to be introduced is the most important one.

struct _zend_execute_data {
    const zend_op       *opline;//The opcode currently executed starts with zend_op_array
    zend_execute_data   *call;//
    zval                *return_value;//Return value
    zend_function       *func;//Functions currently executed (non-function calls are null)
    zval                 This;/* this + call_info + num_args    */
    zend_class_entry    *called_scope;//Class of current call
    zend_execute_data   *prev_execute_data;
    zend_array          *symbol_table;//Global Variable Symbol Table
    void               **run_time_cache;   /* cache op_array->run_time_cache */
    zval                *literals;         /* cache op_array->literals       */
};

prev_execute_data represents the previous stack frame structure, and when the current stack is finished, the current execution pointer (analog PC) is pointed to the stack frame. The execution process of PHP is to load many zend_op_array s on the stack frame to execute. This process can be broken down into the following steps:

  • 1: Apply the current stack frame from the vm stack for the op_array that needs to be executed at present. The structure is as follows. Initialize the global variable symbol table, point the global pointer EG(current_execute_data) to the newly allocated zend_execute_data stack frame, and point EX(opline) to the starting position of op_array.
  • 2: Call the C handler of each opcode (i.e. _zend_op.handler) from EX(opline). After each opcode execution, EX(opline)++ will continue to execute the next one until all opcode is executed and a function or class member method call is encountered:

    • Remove the zend_op_array corresponding to this function from the EG(function_table) according to the function_name, then repeat step 1, assign EG(current_execute_data) to the prev_execute_data of the new structure, point EG(current_execute_data) to the new zend_execute_data stack frame, then start executing the new stack frame, start executing from the location zend_execute_data.opline, and finish executing EG (current_execute_data.opline). _ Excute_data redirects to EX(prev_execute_data), releases the allocated run stack frame, and returns to the next opline at the end of function execution.
  • 3: After all opcodes are executed, the stack frame allocated by 1 is released, and the execution phase ends.

With the above details of PHP execution, we can go back to the original example and find that what the process needs to do is to change the way PHP works, not to switch the stack frame at the end of the function running, but to switch to other stack frames flexibly at any time when the function is executing the current op_array (swoole internal control for IO waiting). Next we combine Zend VM with Swoole to analyze how to create the stack, how to encounter IO handover, how to restore the stack after IO completion, and how to destroy the stack frame when the process exits. Firstly, the main structure of the PHP part is introduced.

  • Coprocess php_coro_task
struct php_coro_task
{
    /* List only key structures*/
    /*...*/
    zval *vm_stack_top;//Top of stack
    zval *vm_stack_end;//Bottom of stack
    zend_vm_stack vm_stack;//Current Collaboration Stack Pointer
    /*...*/
    zend_execute_data *execute_data;//Current Coprocess Stack Frame
    /*...*/
    php_coro_task *origin_task;//Analog the function of prev_execute_data with the previous stack frame
};

Collaborative switching is mainly aimed at context preservation and recovery when the current stack execution interrupts. Combining the above VM execution process, we can know the functions of the above fields.

  • There is no doubt that the execute_data stack frame pointer needs to be saved and restored.
  • What does the vm_stack* series do? The reason is that PHP is a dynamic language. We have analyzed above that every time a new function enters and exits, it is necessary to create and release stack frames on the global stack. Therefore, it is necessary to save and restore the corresponding global stack pointers correctly to ensure that each stack frame is released without causing memory leaks. (When PHP is compiled in debug mode, each release checks if the global stack is legitimate)
  • origin_task is the previous stack frame that needs to be executed automatically after the current co-process is executed.

The main methods involved are as follows:

  • create a protocol and apply stack frames for the protocol on the global stack.

    • The creation of the protocol is to create a closure function that passes the function (op_array, which can be understood as an op_array to be executed) as a parameter into Swoole's built-in function go().
  • Collaboration Release, yield, encounter IO, save the context information of the current stack frame
  • Resume, resume,IO complete, restore the context information needed to be executed to the state before yield ing
  • Exit, exit and op_array are all executed. Release stack frame and swoole protocol related data.

After the introduction above, you should have a general understanding that Swoole's collaboration can jump within the function during its operation. Back to our original example and the php execution details above, we can know that this example will generate three op_array s, which are the main script, the collaboration 1 and the collaboration 2. We can use some tools to print out opcodes for intuitive observation. Usually we use the following two tools

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

//vld, third party extension
php -d vld.active=1 test.php

We use opcache to observe the opcodes that were not optimized, and we can clearly see the details of these three groups of op_array s.

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

When Swoole executes co::sleep(), it gives up the current control and jumps to the next op_array. Combining the above annotations, that is to say, when DO_FCALL is executed, it gives up and restores the execution stack of the co-process separately, so as to achieve the purpose of controlling the flow jump of the native co-process.

We analyze how INIT_FCALL DO_FCALL instructions are executed in the kernel. In order to better understand the function call stack switching relationship.

The internal instructions of VM are specialized into a c function based on the return value of the current operand. In our example, there are the following corresponding relationships

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); //Apply for the execution stack of the current function from the global stack
    call->prev_execute_data = EX(call); //Assign the executing stack to prev_execute_data of the function stack to be executed, and restore it to this place after the function is executed.
    EX(call) = call; //Assign the function stack to the global execution stack, the function stack to be executed
    ZEND_VM_NEXT_OPCODE();
}
ZEND_DO_FCALL_SPEC_RETVAL_UNUSED_HANDLER(ZEND_OPCODE_HANDLER_ARGS)
{
    USE_OPLINE
    zend_execute_data *call = EX(call);//Get the execution stack
    zend_function *fbc = call->func;//Current function
    zend_object *object;
    zval *ret;

    SAVE_OPLINE();//When there is a global register ((execute_data) - > opline) = opline
    EX(call) = call->prev_execute_data;//The current execution stack execute_data-> call = EX (call) -> prev_execute_data function restores to the tuned function after execution.
    /*...*/
    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);//Release local variables

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

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

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

Swoole can switch in the PHP layer according to the above way. As for IO waiting in the execution process, additional technology is needed to drive it. Our subsequent articles will introduce each version of the driver technology combined with Swoole's original event model, and describe how the Swoole protocol has evolved to the present.

Swoole 4 Co-op Dual Stack

Owing to the existence of C stack and PHP stack in our system, the agreed name is:

  • C protocol C stack management section,
  • PHP protocol PHP stack management section.

Adding C stack is the most important and critical part of the 4.x coroutine. Previous versions of PHP grammar can not be perfectly supported due to the lack of storage of C stack information. Next, we will analyze the C stack switching support. At first, we used Tencent's libco to support it, but there will be memory reading and writing errors through pressure measurement, and the open source community is not very active, there are problems can not be timely feedback processing. So, we stripped the assembly part of the c++ boost library, and now the driver of the co-process C stack is done on this basis.

System Architecture Diagram

It can be found that Swoole's role is to glue to the system API and PHP Zend VM to write high-performance code for PHPer user's deep interface; not only that, but also to support the development and use of C++/C users. Please refer to document C++ developer how to use Swoole in detail. The code of Part C is mainly divided into several parts:

  • Assembly ASM Driver
  • Conext context encapsulation
  • Socket Concurrent Socket Packaging
  • PHP Stream is packaged to seamlessly synergize PHP related functions.
  • Zend VM binding layer

Swoole's underlying system layers are more distinct. Socket will be the cornerstone of the whole network driver. In the original version, each client maintains the context based on asynchronous callbacks. Therefore, compared with the previous version, version 4.x has a qualitative leap, whether from the complexity of the project or the stability of the system. Code catalog hierarchy

$ tree swoole-src/src/coroutine/
swoole-src/src/coroutine/
├── base.cc //C protocol API, callback PHP protocol API
├── channel.cc //channel
├── context.cc //ASM make_fcontext jump_fcontext
├── hook.cc //hook
└── socket.cc //Network Operations Association Packaging
swoole-src/swoole_coroutine.cc //Zend VM related packaging, PHP consortium API

From user level to system level, we have PHP protocol API, C protocol API and ASM protocol API. Socket layer is the network encapsulation of compatible system API. We analyze it from the bottom up. ASM x86-64 architecture, for example, has 16 64-bit general purpose registers. The registers and their uses are as follows:

  • % rax is usually used to store the return results of function calls, as well as in multiplication and division instructions. In imul instructions, two 64-bit multiplications can produce 128-bit results at most. It requires% rax and% rdx to store multiplication results together. In div instructions, the divider is 128-bit. It also requires% rax and% rdx to store dividers together.

    • % RSP is the stack pointer register, which usually points to the top of the stack. The pop and push operations of the stack are realized by changing the value of% rsp, that is, moving the position of the stack pointer.
    • % rbp is a stack frame pointer that identifies the starting position of the current stack frame
    • % rdi,% rsi,% rdx,% rcx,% r8,% R9 six registers are used to store six parameters for function calls
  • % rbx,% r12,% r13,% 14,% 15 are used as data storage, following the rules of use by the callee

% r10,% r11 is used as data storage, following caller usage rules

That is to say, after entering the assembly function, the first parameter value has been put in the% rdi register, the second parameter value has been put in the% rsi register, and the location where the stack pointer% rsp points to is the return address x86-64 of the parent function stored on the top of the stack using swoole-src/thirdparty/boost/asm/make_x86_64_sysv_elf_gas.

//Create a context at the top of the current stack to execute the third parameter function fn and return the execution environment context after initialization is complete
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 * Return rax The pointer to the bottom of the stack, as context Return/
//Save the current context (including stack pointer, PC program counter and register) to * ofc, restore the context from nfc and start execution.
intptr_t jump_fcontext(fcontext_t *ofc, fcontext_t nfc, intptr_t vp, bool preserve_fpu = false);

jump_fcontext:
//Save the current register, stack
    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  Save the current stack top to rdi, that is, save the current stack top pointer to the first parameter% rdi ofc*/
    movq  %rsp, (%rdi)

    /* restore RSP (pointing to context-data) from RSI Modify the top address of the stack to be the address of the new protocol and rsi to be the second parameter address */
    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
// Register Recovery
    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  Put the return address in the r8 register */
    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 management, located in context.cc, is an encapsulation of ASM, providing two API s

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

The final protocol API is located in base.cc, and the main API is

//Create a c stack coordinator, provide an execution entry function, and enter the function start execution context
//For example, the PHP stack entry function Coroutine:: create (PHPCoroutine:: create_func, (void*) & php_coro_args);
long Coroutine::create(coroutine_func_t fn, void* args = nullptr); 
//Cut out from the current context and call hook functions such as the php stack switching function void PHPCoroutine::on_yield(void *arg)
void Coroutine::yield()
//Cut in from the current context and call hook functions such as the php stack switching function void PHPCoroutine::on_resume(void *arg)
void Coroutine::resume()
//C coroutine execution ends, and calls hook functions such as php stack to clean up void PHPCoroutine::on_close(void *arg)
void Coroutine::close()

Next is the bonding layer of Zend VM at swoole-src/swoole_coroutine.cc

PHPCoroutine Supply C Collaboration or underlying interface calls
//PHP coprocess creates entry functions with parameters of PHP functions
static long create(zend_fcall_info_cache *fci_cache, uint32_t argc, zval *argv);
//C Program Create API
static void create_func(void *arg);
//A part of base.cc's C-covariance on the C-covariance hook function is associated with the following three hook functions
static void on_yield(void *arg);
static void on_resume(void *arg);
static void on_close(void *arg);
//PHP stack management
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);
//Output cache management correlation
static inline void save_og(php_coro_task *task);
static inline void restore_og(php_coro_task *task);

With the construction of the above basic parts, combined with the PHP core execution stack management, we can drive the PHP protocol from the C protocol, and realize the original protocol of the C stack + PHP stack.

Author information

Han Tianfeng, founder of Swoole open source project and chief architect of XRS online school.

Keywords: C PHP socket network encoding

Added by rane500 on Mon, 14 Oct 2019 08:10:55 +0300