Swoole PHP协程入口函数实战分析(二)

上一篇文章,我们分析了Swoole PHP协程入口函数swoole::PHPCoroutine::main_funczend_first_tryzend_catch,明白了这对结构解决了什么问题。这篇文章,我们继续分析协程入口函数。

我们使用的PHP版本是7.3.12Swoole的版本是v4.4.16。我们把commit切到v4.4.16

1
git checkout v4.4.16

我们继续读代码:

1
2
3
4
5
6
7
8
9
int i;
php_coro_args *php_arg = (php_coro_args *) arg;
zend_fcall_info_cache fci_cache = *php_arg->fci_cache;
zend_function *func = fci_cache.function_handler;
zval *argv = php_arg->argv;
int argc = php_arg->argc;
php_coro_task *task;
zend_execute_data *call;
zval _retval, *retval = &_retval;

其中:

1
2
3
4
5
php_coro_args *php_arg = (php_coro_args *) arg;
zend_fcall_info_cache fci_cache = *php_arg->fci_cache;
zend_function *func = fci_cache.function_handler;
zval *argv = php_arg->argv;
int argc = php_arg->argc;

其中,

fci_cache这个结构是由我们在函数Coroutine::create中传递进去的函数生成的。例如:

1
2
3
Coroutine::create(function () {
echo 'hello swoole' . PHP_EOL;
});

就是由这个匿名函数生成的。

func则是对应这个匿名函数本体。

argv则对应着我们传递给函数的参数,argc则是参数的个数。例如:

1
2
3
4
5
6
7
<?php

use Swoole\Coroutine;

Coroutine::create(function ($arg1, $arg2) {
echo 'hello swoole' . PHP_EOL;
}, 1, 'arg2');

那么argv[0]存储的就是这里的整形参数1argv[1]存储的就是这里的字符串参数arg2。对应的,argc就等于2

_retval则是用来保存函数的返回值。注意,这里保存的不是Coroutine::create这个函数的返回值,而是传递给Coroutine::create的函数的返回值(我们把传递进去的函数叫做协程任务函数吧)。

举个例子:

1
2
3
4
5
6
7
8
<?php

use Swoole\Coroutine;

Coroutine::create(function ($arg1, $arg2) {
echo 'hello swoole' . PHP_EOL;
return 'ret';
}, 1, 'arg2');

此时,_retval存储的就是字符串ret

我们继续读代码:

1
2
3
4
if (fci_cache.object)
{
GC_ADDREF(fci_cache.object);
}

这段代码解决了什么问题呢?

协程任务函数是属于某个对象的话,那么需要给这个对象加引用计数,不然协程发生切换时,PHP会默认释放掉这个对象,导致下次协程切换回来发生错误。

我们编写一下测试脚本:

1
2
3
4
5
6
7
8
9
10
11
12
13
<?php

use Swoole\Coroutine;

class Test
{
public function func1()
{
echo 'hello swoole' . PHP_EOL;
}
}

Coroutine::create([new Test, 'func1']);

此时就会进入if (fci_cache.object)的逻辑了。

我们可以注释掉main_func的以下代码:

1
2
3
4
if (fci_cache.object)
{
GC_ADDREF(fci_cache.object);
}
1
2
3
4
if (fci_cache.object)
{
OBJ_RELEASE(fci_cache.object);
}

然后重新编译、安装扩展。接着执行这个测试脚本:

1
2
3
4
[root@592b0366acbf coroutine]# php test.php
*RECURSION*
[root@592b0366acbf coroutine]# php test.php
Segmentation fault

不出意外,会得到这两个错误。我们来跟踪代码的执行流程。

首先,程序会进入main_func函数里面,并且调用zend_execute_ex(EG(current_execute_data));来执行我们的协程任务函数。zend_execute_ex对应的就是PHP内核的execute_ex这个函数(在文件zend_vm_execute.h里面)。接着,执行ZEND_DO_ICALL_SPEC_RETVAL_UNUSED_HANDLER这个PHPhandler。然后,在这个handler里面调用fbc->internal_function.handler(call, ret);方法,而这个handler实际上就是我们的var_dump函数了。函数如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
PHP_FUNCTION(var_dump)
{
zval *args;
int argc;
int i;

ZEND_PARSE_PARAMETERS_START(1, -1)
Z_PARAM_VARIADIC('+', args, argc)
ZEND_PARSE_PARAMETERS_END();

for (i = 0; i < argc; i++) {
php_var_dump(&args[i], 1);
}
}

args就是我们传递给var_dump函数的参数,argc则是我们传递给var_dump函数的参数个数。如果我们去调试的话,会发现args[0]的类型是object,也就是我们的Test类对象。

然后,调用php_var_dump来打印变量信息。此时会进入这个case分支:

1
2
3
4
5
case IS_OBJECT:
if (Z_IS_RECURSIVE_P(struc)) {
PUTS("*RECURSION*\n");
return;
}