命令式风格符号表

我们一般会在语义分析阶段来生成符号表,而符号表的作用是将标志符映射到它们的类型和存储位置。然后,我们的变量是有作用域的。当语义分析到达每一个作用域的结束时,所有局部于此作用域的标识符都将被丢弃。

例如,我们有如下PHP代码:

1
2
3
4
5
6
7
8
9
10
$a = null;
{
$a = 1;
{
$a = '1';
{
$a = [1];
}
}
}

那么,我们的这个标识符号a在不同的作用域下的类型变化依次是:

1
null -> int -> string -> array -> string -> int -> null

可以看到,这个变化和栈中元素变化非常的类似。

那么,命令式风格的符号表的含义就是,有一个全局环境0。它在不同的时间变成环境1,环境2,环境3……每到一个新的环境,我们会更新这个环境下的标志符信息。因为我们退出环境的时候,需要恢复原来的环境,所以我们需要在进入新环境之前,保存旧环境的标志符信息。而这个恢复的过程,我们可以用栈来轻松的模拟出来。

以下是具体的实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
<?php

declare(strict_types=1);
/**
* This file is part of Library.
*/

class Binding
{
/**
* @var string
*/
public $type;

public $value;

public function __construct(string $type, $value)
{
$this->type = $type;
$this->value = $value;
}
}

class Bucket
{
/**
* @var string
*/
public $key;

/**
* @var Binding
*/
public $binding;

/**
* @var Bucket
*/
public $next;

public function __construct(string $key, Binding $binding, $next)
{
$this->key = $key;
$this->binding = $binding;
$this->next = $next;
}
}

class SymbolTable
{
public const SIZE = 256;

/**
* @var Bucket[]
*/
public $table = [];

public function insert(string $key, Binding $binding)
{
/** @var int $index */
$index = self::hash($key) % self::SIZE;
$this->table[$index] = $this->table[$index] ?? null;
$this->table[$index] = new Bucket($key, $binding, $this->table[$index]);
}

public function lookUp(string $key): ?Binding
{
$index = self::hash($key) % self::SIZE;

for ($target = $this->table[$index]; $target; $target = $target->next) {
if ($target->key === $key) {
return $target->binding;
}
}

return null;
}

public function pop(string $key)
{
$index = self::hash($key) % self::SIZE;

$this->table[$index] = $this->table[$index]->next;
}

public static function hash(string $key)
{
$h = 0;
for ($i = 0; $i < strlen($key); ++$i) {
$h = $h * 65599 + ord($key[$i]);
}
return $h;
}
}

$symbolTable = new SymbolTable;

echo "enter scope 0\n";
$symbolTable->insert('a', new Binding('int', 0));
echo "a: type -> " . $symbolTable->lookUp('a')->type . PHP_EOL;
echo "enter scope 1\n";
$symbolTable->insert('a', new Binding('string', '1'));
echo "a: type -> " . $symbolTable->lookUp('a')->type . PHP_EOL;
echo "enter scope 2\n";
$symbolTable->insert('a', new Binding('array', [1]));
echo "a: type -> " . $symbolTable->lookUp('a')->type . PHP_EOL;

echo "leave scope 2\n";
$symbolTable->pop('a');
echo "a: type -> " . $symbolTable->lookUp('a')->type . PHP_EOL;
echo "leave scope 1\n";
$symbolTable->pop('a');
echo "a: type -> " . $symbolTable->lookUp('a')->type . PHP_EOL;
echo "leave scope 0\n";
$symbolTable->pop('a');

执行结果如下:

1
2
3
4
5
6
7
8
9
10
11
enter scope 0
a: type -> int
enter scope 1
a: type -> string
enter scope 2
a: type -> array
leave scope 2
a: type -> string
leave scope 1
a: type -> int
leave scope 0

可以看到,结果是非常的对称的。

这个算法的思路类似于PHP哈希碰撞时候采取的拉链法,也是头插法。整个过程如下:

1
2
3
4
5
6
7
a: null
a: int -> null
a: string -> int -> null
a: array -> string -> int -> null
a: string -> int -> null
a: int -> null
a: null

一个简单的JIT例子

我的处理器是x86-64,操作系统是Mac OSX

这篇文章,会通过一个非常简单的例子,来讲解一下JIT的意思。

首先,假设我们有如下PHP代码:

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

function foo(int $a, int $b)
{
$ret = $a + $b;
$ret += $a - $b;

return $ret;
}

$ret = 0;

$ret = $ret + foo(1, 2);
echo $ret;

代码很简单,就是调用函数foo,然后打印结果。执行后,结果是2

现在,我们来写一个简单解释器,省去解析PHP代码的部分,直接来生成opcode

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/mman.h>

typedef int (*func_ptr)(int, int);

typedef struct _zend_op_array zend_op_array;
typedef struct _zend_op zend_op;

struct _zend_op {
int op1;
int op2;
char opcode;
func_ptr handler;
};

struct _zend_op_array
{
zend_op *opcodes;
};

int add_func(int a, int b)
{
return a + b;
}

int div_func(int a, int b)
{
return a - b;
}

int main(int argc, char const *argv[])
{
// generate opcode
zend_op_array op_array;

op_array.opcodes = malloc(2 * sizeof(zend_op));

int arg1 = 1;
int arg2 = 2;

op_array.opcodes[0].op1 = arg1;
op_array.opcodes[0].op2 = arg2;
op_array.opcodes[0].opcode = '+';
op_array.opcodes[0].handler = add_func;

op_array.opcodes[1].op1 = arg1;
op_array.opcodes[1].op2 = arg2;
op_array.opcodes[1].opcode = '-';
op_array.opcodes[1].handler = div_func;

// execute opcode
int ret = 0;

ret += op_array.opcodes[0].handler(op_array.opcodes[0].op1, op_array.opcodes[0].op2);
ret += op_array.opcodes[1].handler(op_array.opcodes[1].op1, op_array.opcodes[1].op2);
printf("%d\n", ret);
}

编译后,运行结果如下:

1
2
3
gcc jit.c
./a.out
2

其中,生成opcode的过程是一次性的。真正耗时间的是执行opcode的部分。参考PHP内核的实现我们会发现,每个opcode都会对应一个handler,例如这里的+-。而这些handler实际上是C函数,也就意味着,每执行一条opcode,我们就会调用一次C函数。换句话来说,对于我们的PHP脚本的那个foo函数来说,执行这两条语句至少需要2C层面的函数调用(实际上可能不止2次,我没有去打印真正的opcode)。对于我们写的这份C代码,查看a.out的汇编代码,我们会发现,除去调用mainprintf,可以看到还有2call指令的调用。

那么,如果我们对这个foo函数进行jit的话,会怎么样呢?我们来看看代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/mman.h>

typedef int (*func_ptr)(int, int);

typedef struct _zend_op_array zend_op_array;
typedef struct _zend_op zend_op;

struct _zend_op {
int op1;
int op2;
char opcode;
func_ptr handler;
};

struct _zend_op_array
{
zend_op *opcodes;
};

int add_func(int a, int b)
{
return a + b;
}

int div_func(int a, int b)
{
return a - b;
}

func_ptr foo;

void jit_code(zend_op_array *op_array)
{
unsigned char code[] = {
0x55, // push %rbp
0x48, 0x89, 0xe5, // mov %rsp,%rbp
0x89, 0x7d, 0xfc, // mov %edi,-0x4(%rbp)
0x89, 0x75, 0xf8, // mov %esi,-0x8(%rbp)
0xc7, 0x45, 0xf4, 0x00, 0x00, 0x00, 0x00, // movl $0x0,-0xc(%rbp)
0x8b, 0x45, 0xfc, // mov -0x4(%rbp),%eax
0x03, 0x45, 0xf8, // add -0x8(%rbp),%eax
0x89, 0x45, 0xf4, // mov %eax,-0xc(%rbp)
0x8b, 0x45, 0xfc, // mov -0x4(%rbp),%eax
0x2b, 0x45, 0xf8, // sub -0x8(%rbp),%eax
0x03, 0x45, 0xf4, // add -0xc(%rbp),%eax
0x89, 0x45, 0xf4, // mov %eax,-0xc(%rbp)
0x8b, 0x45, 0xf4, // mov -0xc(%rbp),%eax
0x5d, // pop %rbp
0xc3, // retq
};

void *mem = mmap(NULL, sizeof(code), PROT_WRITE | PROT_EXEC,
MAP_ANON | MAP_PRIVATE, -1, 0);
memcpy(mem, code, sizeof(code));
foo = mem;
}

int main(int argc, char const *argv[])
{
// generate opcode
zend_op_array op_array;

op_array.opcodes = malloc(2 * sizeof(zend_op));

int arg1 = 1;
int arg2 = 2;

op_array.opcodes[0].op1 = arg1;
op_array.opcodes[0].op2 = arg2;
op_array.opcodes[0].opcode = '+';
op_array.opcodes[0].handler = add_func;

op_array.opcodes[1].op1 = arg1;
op_array.opcodes[1].op2 = arg2;
op_array.opcodes[1].opcode = '-';
op_array.opcodes[1].handler = div_func;

// execute opcode
int ret = 0;

jit_code(&op_array);

ret = foo(arg1, arg2);
printf("%d\n", ret);
}

编译后执行结果如下:

1
2
3
4
gcc jit.c
./a.out

2

查看汇编代码我们会发现,汇编代码里面并没有foo函数,但是,因为解释器在运行脚本的过程中 ,我们已经把PHP脚本的foo函数编译成了对应的二进制代码,并且放在内存里面了。这种感觉就像是我们在用C代码。

所以,实际上,JIT是在对我们的opcode产生的指令进行精简,减少CPU指令的条数,从而达到速度的提升(我们一定要纠正一个误区就是“没有JIT前不是跑二进制,JIT后才是跑二进制”。实际上,无论JIT不JIT,都是跑二进制,只不过跑的CPU指令会有一些变化,也就是指令被优化了)。然而,JIT要完成的工作并不仅仅是我们这个例子那么简单,所以对应的优化也并不仅仅是省去一些函数调用。

那么,从这个例子,我们也可以看出,我们在JIT的时候,是需要消耗一部分时间去把opcode转化为二进制代码。如果后期执行这些精简后的二进制代码节约的时间,远远大于把opcode翻译成二进制代码的时间,那么收益是很明显的。但是,如果后续没怎么跑我们翻译好的二进制代码,那么,像PHP这种FPM模型,一个请求就JIT一次的话,性能反而会下降(然而,这种问题PHP官方肯定也想得到,就好比opcache缓存opcode一样)。除了这种函数调用之类的优化还有其他水很深的问题,不是想JIT就JIT,例如灵剑大佬指出的方方面面,这些问题我们后面会慢慢的去探索,所以,我们会看到,JIT是可以指定JIT哪一部分的。

像我们这个例子,我们是以函数为单位来进行JIT的,那如果我们函数的逻辑复杂一点的话,我们理论上甚至可以对某些路径进行JIT

后续,我们会使用LLVM来完成yaphpJIT工作。

PHP编译优化--常量折叠

本文基于的PHP8 commit为:14806e0824ecd598df74cac855868422e44aea53

有如下代码:

1
2
3
<?php

echo 1 + 2 + 3;

对应的opcode为:

1
2
L3    #0     ECHO                    6
L4 #1 RETURN<-1> 1

我们可以分析出,这个echo表达式对应的AST大概如下:

1
2
3
4
            ZEND_ECHO
+
+ 3
1 2

所以,可以看到,PHP代码生成的时候,很轻松的进行优化了:

1
2
3
        ZEND_ECHO
+
3 3

最后就会优化为:

1
2
    ZEND_ECHO
6

所以生成的opcode只有一条。

那么,我们再来看一个PHP目前没有优化的例子:

1
2
3
4
5
<?php

$x = 1;

echo $x + 2 + 3;

对应的opcode如下:

1
2
3
4
5
L3    #0     ASSIGN                  $x                   1
L5 #1 ADD $x 2 ~1
L5 #2 ADD ~1 3 ~2
L5 #3 ECHO ~2
L6 #4 RETURN<-1> 1

可以看到,这里没有进行优化,理论上来说,对常量进行折叠的话,可以减少一条opcode。那么为什么PHP内核它没有对这种情况进行优化呢?我们先来看一看这条语句对应的AST

1
2
3
4
            ZEND_ECHO
+
+ 3
x 2

可以发现,如果对AST进行深度遍历的话,是先进行x + 2,而x是一个变量,折叠不了,所以就没有优化到了,具体的代码是这样的(在函数zend_compile_binary_op里面):

1
2
3
4
5
6
7
8
9
10
if (left_node.op_type == IS_CONST && right_node.op_type == IS_CONST) {
if (zend_try_ct_eval_binary_op(&result->u.constant, opcode,
&left_node.u.constant, &right_node.u.constant)
) {
result->op_type = IS_CONST;
zval_ptr_dtor(&left_node.u.constant);
zval_ptr_dtor(&right_node.u.constant);
return;
}
}

我们发现,折叠的情况只有是当左右节点都为IS_CONST类型的时候,才会生效。

那么,面对这种情况,理论上我们可以怎么解决呢?我们可以对这个AST进行旋转,得到:

1
2
3
        ZEND_ECHO
+ +
x 2 3

然后,我们就可以优化为:

1
2
3
        ZEND_ECHO
+ 5
x

既然,PHP没有做这方面的优化工作,那么,我们写代码的时候,就可以稍微注意一下了。常量尽可能的往左边靠拢,例如1 + 2 + x这样。

后续我们的yaphp会使用LLVM来对这方面进行优化。

《手把手教你编写PHP编译器》-实现echo_expr

到目前为止,我们已经实现了yaphp源代码到AST的生成工作。但是,我们对echo语句的实现还是简单处理了,之前的实现如下:

1
2
3
statement:
T_ECHO expr ';' { $$ = $2; }
;

可以看到,这里实际上我们没有体现出T_ECHO的功能,我们仅仅处理了expr。所以,这里我们需要去处理一下。

有了前面的基础之后,我们可以非常轻松的来实现这个AST的生成。

我们修改后的规则如下:

1
2
3
4
5
6
7
8
9
10
statement:
T_ECHO echo_expr ';' { $$ = $2; }
;

echo_expr:
expr {
std::cout << "create echo zend_ast" << std::endl;
$$ = zend_ast_create_1(ZEND_AST_ECHO, 0, $1);
}
;

可以看到,我们加了一个echo_expr,用来创建一个ZEND_AST_ECHO类型的AST。这里,我们的语法和php-src的有点不同,php-src的语法功能更加的丰富一点,它支持echo后面跟一个ZEND_AST_STMT_LIST,这个的实现也是比较简单的,和我们之前创建top_statement_list的思路是一致的。这里小伙伴们可以自己去实现一下,我们的yaphp就不支持这种可有可无的语法了。

其中,zend_ast_create_1的实现如下:

1
2
3
4
5
6
7
8
9
10
zend_ast *zend_ast_create_1(zend_ast_kind kind, zend_ast_attr attr, zend_ast *child) {
zend_ast *ast;

ast = (zend_ast *) malloc(zend_ast_size(1));
ast->kind = kind;
ast->attr = attr;
ast->child[0] = child;

return ast;
}

然后,我们需要在_zend_ast_kind中增加一个ZEND_AST_ECHO

1
2
/* 1 child node */
ZEND_AST_ECHO,

最后,我们再修改一下我们的dump_compiler_globals函数:

1
2
3
4
5
6
else if (ast->kind > ZEND_AST_0_NODE_END && ast->kind < ZEND_AST_1_NODE_END) {
queue.push_back(ast->child[0]);
} else if (ast->kind > ZEND_AST_1_NODE_END && ast->kind < ZEND_AST_2_NODE_END) {
queue.push_back(ast->child[0]);
queue.push_back(ast->child[1]);
}

其中,ZEND_AST_0_NODE_END, ZEND_AST_1_NODE_END, ZEND_AST_2_NODE_END这三个zend_ast节点类型php-src是没有的,这里是为了方便调试给加上的,否则,我们每次增加一个zend_ast节点类型,就需要写一个if语句。

现在,让我们来编译一下yaphp,并且运行,结果如下:

1
2
3
4
5
6
7
8
9
10
create * zend_ast
create + zend_ast
create echo zend_ast
kind: 129, attr: 0
kind: 131, attr: 0
kind: 515, attr: 1
kind: 65, attr: 0, value: 1
kind: 515, attr: 3
kind: 65, attr: 0, value: 2
kind: 65, attr: 0, value: 3

符合我们的预期。

《手把手教你编写PHP编译器》-引入zend_ast

细心的小伙伴会发现,在我们实现算数表达式的时候,直接在语法分析的过程(也就是调用bison的阶段)对表达式进行了求值。现在,我们来引入抽象语法树的概念。在php-src里面,对应的就是zend_ast结构了。

我们先来编写一下zend_language_parser.y文件,以让我们对整个过程有一个清晰的认识。

首先,我们需要定义一个union

1
2
3
%union {
zend_ast *ast;
}

这个是什么呢?实际上,这个是YYSTYPE,也就是yylval。而yylval我们会在词法分析的时候用到。如果我们不定义这个union的话,那么YYSTYPE默认是int类型。那么,我们在词法分析的时候,就只能够使用yylval去接收int类型的token了。但是,我们现在在词法分析的时候,会去为T_LNUMBER生成一个zend_ast,并且赋值给yylval,以便在语法分析的阶段,直接使用这个这个zend_ast。(实际上,我们也可以把这一步放到语法分析来做,但是,没有必要。)

我们需要对token定义做一些修改:

1
2
%token <ident> T_ECHO    "'echo'"
%token <ast> T_LNUMBER "integer"

这里%token <ast> T_LNUMBER里面的ast就是说明了我们的T_LNUMBER这个token的值类型是ast

OK,除了声明token的类型之外,我们还需要对文法里面的非终结符进行声明:

1
2
3
4
%type <ast> top_statement statement
%type <ast> expr
%type <ast> scalar
%type <ast> top_statement_list

现在,让我们看看我们的语法规则:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
start:
top_statement_list { CG(ast) = $1; }
;

top_statement_list:
top_statement_list top_statement { $$ = zend_ast_list_add($1, $2); }
| %empty { $$ = zend_ast_create_list(0, ZEND_AST_STMT_LIST); }
;

top_statement:
statement { $$ = $1; }
;

statement:
T_ECHO expr ';' { $$ = $2; }
;

expr:
expr '+' expr { $$ = zend_ast_create_binary_op(ZEND_ADD, $1, $3); }
| expr '-' expr { $$ = zend_ast_create_binary_op(ZEND_SUB, $1, $3); }
| expr '*' expr { $$ = zend_ast_create_binary_op(ZEND_MUL, $1, $3); }
| expr '/' expr { $$ = zend_ast_create_binary_op(ZEND_DIV, $1, $3); }
| '(' expr ')' { $$ = $2; }
| scalar { $$ = $1; }
;

scalar:
T_LNUMBER { $$ = $1; }
;

这里的规则和php-src是保持一致的,但是可能会有一点不同(一些目前没有必要的东西被我删了)。

可以看到,我们在最顶层,也即是非终结符start的那条规则里,它的动作是把ast赋值给CG(ast)。熟悉PHP内核的小伙伴应该是能够立马知道这个CG(ast)是做什么的。这个CG(ast)保存的是,在编译PHP代码阶段的所有zend_ast

我们接着看:

1
2
3
4
top_statement_list:
top_statement_list top_statement { $$ = zend_ast_list_add($1, $2); }
| %empty { $$ = zend_ast_create_list(0, ZEND_AST_STMT_LIST); }
;

它表达的含义一句话来总结就是,定义了一个ZEND_AST_STMT_LIST类型的zend_ast,然后这个zend_ast下面有多个子zend_ast,这些一个个的子zend_ast对应的就是我们PHP代码里的一条条语句(其实也不一定是一条条,例如for循环啥的,这种我们就不太好形容为条。反正大概就是一个语句的意思)。

1
2
3
statement:
T_ECHO expr ';' { $$ = $2; }
;

表示我们的语句包含echo语句。

1
2
3
4
5
6
7
8
9
10
11
12
expr:
expr '+' expr { $$ = zend_ast_create_binary_op(ZEND_ADD, $1, $3); }
| expr '-' expr { $$ = zend_ast_create_binary_op(ZEND_SUB, $1, $3); }
| expr '*' expr { $$ = zend_ast_create_binary_op(ZEND_MUL, $1, $3); }
| expr '/' expr { $$ = zend_ast_create_binary_op(ZEND_DIV, $1, $3); }
| '(' expr ')' { $$ = $2; }
| scalar { $$ = $1; }
;

scalar:
T_LNUMBER { $$ = $1; }
;

这一段和我们前面的文章类似,但是,不同的地方在规则对应的动作,之前因为token的值都是int类型,所以,我们可以直接进行表达式的运算,但是,现在由于我们的token变成了ast类型,所以,我们就不能直接进行四则运算了(实际上,因为我们的语言是C++,所以我们可以对运算符进行重载,然而,这已经超出了我们教程的范围,所以我们就不去支持重载了,留给小伙伴们当作练习吧)。我们现在改为调用zend_ast_create_binary_op函数,而这个函数就是通过两个子zend_ast节点来生成一个四则运算的zend_ast,具体的函数实现我们后面会说。

改完了语法文件之后,我们就业需要改改词法文件:

1
2
3
4
5
[0-9]+ {
int64_t lnum = atoi(yytext);
yylval.ast = zend_ast_create_lnum(lnum);
return T_LNUMBER;
}

我们上面说了,yylval是一个union类型的了,所以,我们得使用yylval.ast来接收token的值。而zend_ast_create_lnum函数就是用来把数字变成zend_ast的。

OK,我们现在可以来看具体的函数实现了。不过在这之前,我们需要先设计好我们的zend_ast结构,我们和php-src的保持一致:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
typedef uint16_t zend_ast_kind;
typedef uint16_t zend_ast_attr;

struct _zend_ast {
zend_ast_kind kind;
zend_ast_attr attr;
zend_ast *child[1];
};

typedef struct _zend_ast_list {
zend_ast_kind kind;
zend_ast_attr attr;
uint32_t children;
zend_ast *child[1];
} zend_ast_list;

typedef struct _zend_ast_lnum {
zend_ast_kind kind;
zend_ast_attr attr;
int64_t lnum;
} zend_ast_lnum;

前面的_zend_ast_zend_ast_listphp-src里面的,小伙伴们可以在网上找到它们的区别。而_zend_ast_lnum是我自己引入的,表示这个zend_ast存了一个lnum的整数值。在php-src里面,这一块应该是zend_ast_zval,也就是存了一个zval。因为我们这篇文章还不想引入zval这个东西(因为我们的表达式都是整形值,所以没必要搞一个zval),所以我先简单处理了。

现在,让我们来看看函数实现了。

首先是zend_ast_create_list,我们在文件Zend/zend_ast.cc里面来进行定义:

1
2
3
4
5
6
7
8
9
10
11
zend_ast *zend_ast_create_list(uint32_t init_children, zend_ast_kind kind) {
zend_ast *ast;
zend_ast_list *list;

ast = (zend_ast *) malloc(zend_ast_list_size(4));
list = (zend_ast_list *) ast;
list->kind = kind;
list->children = 0;

return ast;
}

这个函数实现非常简单,首先,malloc出一块zend_ast的内存,然后,设置它的kindchildren。其中kind对应这个zend_ast的类型,children表示这个zend_ast有一个子zend_ast

然后是zend_ast_list_add函数:

1
2
3
4
5
zend_ast *zend_ast_list_add(zend_ast *ast, zend_ast *op) {
zend_ast_list *list = zend_ast_get_list(ast);
list->child[list->children++] = op;
return (zend_ast *) list;
}

这个函数就是设置zend_ast_create_list函数创建出来的zend_astchild。设置一个children加一。所以,有几条语句,我们的这个children就是几了。

最后,是我们的zend_ast_create_binary_op函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
static inline zend_ast *zend_ast_create_binary_op(uint32_t opcode, zend_ast *op0, zend_ast *op1) {
switch (opcode)
{
case ZEND_ADD:
std::cout << "create + zend_ast" << std::endl;
break;
case ZEND_SUB:
std::cout << "create - zend_ast" << std::endl;
break;
case ZEND_MUL:
std::cout << "create * zend_ast" << std::endl;
break;
case ZEND_DIV:
std::cout << "create / zend_ast" << std::endl;
break;
default:
std::cout << "unknow operator" << std::endl;
break;
}
return zend_ast_create_2(ZEND_AST_BINARY_OP, opcode, op0, op1);
}

zend_ast *zend_ast_create_2(zend_ast_kind kind, zend_ast_attr attr, zend_ast *child1, zend_ast *child2) {
zend_ast *ast;

ast = (zend_ast *) malloc(zend_ast_size(2));
ast->kind = kind;
ast->attr = attr;
ast->child[0] = child1;
ast->child[1] = child2;

return ast;
}

这个zend_ast_create_binary_op实际上做的一件事情就是创建一个包含两个子astzend_ast,然后设置zend_astkindZEND_AST_BINARY_OP,并且设置对应的运算类型(即加、减、乘、除),最后设置运算符操作的两个子ast

最后,我们编写一个打印ast的函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
void dump_ast(zend_ast *ast)
{
if (ast->kind == ZEND_AST_LNUM) {
zend_ast_lnum *ast_lnum = (zend_ast_lnum *) ast;
std::cout << "kind: " << ast_lnum->kind << ", attr: " << ast_lnum->attr << ", value: " << ast_lnum->lnum << std::endl;
} else {
std::cout << "kind: " << ast->kind << ", attr: " << ast->attr << std::endl;
}
}

void dump_compiler_globals()
{
zend_ast *ast;
std::deque<zend_ast *> queue;

queue.push_back(CG(ast));

while (!queue.empty())
{
ast = queue.front();

if (ast->kind == ZEND_AST_STMT_LIST) {
zend_ast_list *ast_list = (zend_ast_list *) ast;
for (size_t i = 0; i < ast_list->children; i++)
{
queue.push_back(ast_list->child[i]);
}
} else if (ast->kind == ZEND_AST_BINARY_OP) {
queue.push_back(ast->child[0]);
queue.push_back(ast->child[1]);
}
dump_ast(ast);
queue.pop_front();
}

return;
}

这个也很简单,实际上就是一个树的层次遍历。

OK,做完了这些工作之后,我们重新编译我们的yaphp(记得修改我们的CMakeLists)。

然后编写如下yaphp代码:

1
echo 1 + 2 * 3;

输出结果如下:

1
2
3
4
5
6
7
8
create * zend_ast
create + zend_ast
kind: 129, attr: 0
kind: 515, attr: 1
kind: 65, attr: 0, value: 1
kind: 515, attr: 3
kind: 65, attr: 0, value: 2
kind: 65, attr: 0, value: 3

这个ast的输出大概如下图:

1
2
3
4
        stmt_list
+
1 *
2 3

符合我们的预期。

《手把手教你编写PHP编译器》--实现算数表达式

实现算数表达式

这篇文章,我们来用flexbison实现算数表达式。

几乎所有的编译原理教程都会以这个为例子进行讲解,因为算数表达式的例子是比较复杂一点的,主要是因为它的语法会比其他语法难一点,这其中会涉及到递归,优先级等问题。而关于优先级问题,我们可以使用bison自带的功能来解决,但是,我们也会去讲如何自己手动来实现优先级。

首先,我们来写一下zend_language_scanner.l

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
%option noyywrap
%option nounput
%option noinput
%{
#include <iostream>
#include "zend_language_parser.h"
%}

%%
echo {
return T_ECHO;
}

[;(),+*/-] {
return *yytext;
}

[0-9]+ {
yylval = atoi(yytext);
return T_LNUMBER;
}

[#].* /* ignore comment */

[\t\n ]+ /* ignore \t, \n, whitespace */

. {
std::cerr << "Lexical error. Unrecognized character: '" << *yytext << "'" << std::endl;
exit(EXIT_FAILURE);
}
%%

我们逐一进行分析。

1
2
3
4
%{
#include <iostream>
#include "zend_language_parser.h"
%}

我们发现,这里有一对:

1
2
3
%{

%}

我们可以在这个地方去引入一些头文件。这里的重点是zend_language_parser.h这个文件,我们在之前的文章已经发现,zend_language_parser.h是由zend_language_parser.y通过bison生成的。那么我们为什么要引入这个头文件呢?因为我们会去使用zend_language_parser.h的一些东西(至于是什么,我们后面会讲)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
%%
echo {
return T_ECHO;
}

[;(),+*/-] {
return *yytext;
}

[0-9]+ {
yylval = atoi(yytext);
return T_LNUMBER;
}

[#].* /* ignore comment */

[\t\n ]+ /* ignore \t, \n, whitespace */

. {
std::cerr << "Lexical error. Unrecognized character: '" << *yytext << "'" << std::endl;
exit(EXIT_FAILURE);
}
%%

我们发现,这里有一对:

1
2
3
%%

%%

我们可以在这里面去定义我们需要解析的token。那么token是什么呢?我们来看这么一个例子。我们有如下php文件:

1
2
3
4
5
6
7
8
9
10
<?php
$tokens = token_get_all('<?php 1 + 2 + 3;');

foreach ($tokens as $token) {
if (is_array($token)) {
echo token_name($token[0]), " ('{$token[1]}')", PHP_EOL;
} else {
echo $token, PHP_EOL;
}
}

执行结果如下:

1
2
3
4
5
6
7
8
9
10
11
T_OPEN_TAG ('<?php ')
T_LNUMBER ('1')
T_WHITESPACE (' ')
+
T_WHITESPACE (' ')
T_LNUMBER ('2')
T_WHITESPACE (' ')
+
T_WHITESPACE (' ')
T_LNUMBER ('3')
;

可以发现,从

1
<?php 1 + 2 + 3;

代码里面得到的token包括了T_OPEN_TAGT_LNUMBERT_WHITESPACE+T_LNUMBER;

所以,我们的%% %%里面的一些列正则,实际上做的事情就和PHP类似了。它会根据这些正则表达式,对输入的代码(也就是字符串)进行解析,如果匹配到了某条正则,那么就拿到对应的token

其中:

1
2
3
[;(),+*/-] {
return *yytext;
}

表示,如果当前输入的是字符是;(),+*/-里面的某一个,那么,我们直接返回这个字符作为它的token。这和我们上面的PHP代码行为一致,PHP没有为它们单独写一个T_*

接着,我们来写一下我们的zend_language_parser.y文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
%{
#include <stdio.h>
#include <string.h>

extern int yylex(void);
extern int yyparse(void);
extern FILE *yyin;
extern int yylineno;

int yywrap()
{
return 1;
}

void yyerror(const char *s)
{
printf("[error] %s, in %d\n", s, yylineno);
}

int main(int argc, char const *argv[])
{
const char *file = argv[1];
FILE *fp = fopen(file, "r");

if(fp == NULL)
{
printf("cannot open %s\n", file);
return -1;
}

yyin = fp;
yyparse();

return 0;
}
%}

%token T_ECHO T_LNUMBER

%%

statement: %empty
| T_ECHO additive_expr { printf("%d\n", $2); }
;

additive_expr: %empty
| multiplicative_expr {$$ = $1;}
| additive_expr '+' multiplicative_expr {$$ = $1 + $3;}
| additive_expr '-' multiplicative_expr {$$ = $1 - $3;}
;

multiplicative_expr: %empty
| primary {$$ = $1;}
| multiplicative_expr '*' primary {$$ = $1 * $3;}
| multiplicative_expr '/' primary {$$ = $1 / $3;}

primary: %empty
| T_LNUMBER {$$ = $1;}
| '(' additive_expr ')' {$$ = $2;}

%%

我们发现,这个文件的结构和zend_language_scanner.l极其相似。在%{ %}中写一点cpp的东西。在%% %%中写一点语法(只不过zend_language_scanner.l是写一些词法)。我们逐一来看。

首先是:

1
2
3
4
int yywrap()
{
return 1;
}

这在多文件解析的时候会用到。如果返回1,表示我们解析文件完毕。如果返回0,说明我们还有文件需要解析。简单起见,我们先只支持单个文件的解析。

1
2
3
4
void yyerror(const char *s)
{
printf("[error] %s, in %d\n", s, yylineno);
}

当语法分析出错的时候,会调用这个函数。其中yylineno表示哪一行解析失败了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
int main(int argc, char const *argv[])
{
const char *file = argv[1];
FILE *fp = fopen(file, "r");

if(fp == NULL)
{
printf("cannot open %s\n", file);
return -1;
}

yyin = fp;
yyparse();

return 0;
}

表示我们输入一个yaphp文件,然后对这个文件进行解析。yyin里面存了当前要解析的文件。

1
%token T_ECHO T_LNUMBER

定义了两个token。这是提供给zend_language_scanner.l文件使用的。我们可以在文件zend_language_parser.h里面找到这两个token具体会被生成什么:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
/* Token kinds.  */
#ifndef YYTOKENTYPE
# define YYTOKENTYPE
enum yytokentype
{
YYEMPTY = -2,
YYEOF = 0, /* "end of file" */
YYerror = 256, /* error */
YYUNDEF = 257, /* "invalid token" */
T_ECHO = 258, /* T_ECHO */
T_LNUMBER = 259 /* T_LNUMBER */
};
typedef enum yytokentype yytoken_kind_t;
#endif

可以发现,实际上,我们在文件zend_language_parser.y里面定义的token,最终会被生成一个枚举类型的值。这也很好的解释了为什么我们需要在zend_language_scanner.l里面引入头文件zend_language_parser.h

接着,就是我们的重点了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
statement: %empty
| T_ECHO additive_expr { printf("%d\n", $2); }
;

additive_expr: %empty
| multiplicative_expr {$$ = $1;}
| additive_expr '+' multiplicative_expr {$$ = $1 + $3;}
| additive_expr '-' multiplicative_expr {$$ = $1 - $3;}
;

multiplicative_expr: %empty
| primary {$$ = $1;}
| multiplicative_expr '*' primary {$$ = $1 * $3;}
| multiplicative_expr '/' primary {$$ = $1 / $3;}

primary: %empty
| T_LNUMBER {$$ = $1;}
| '(' additive_expr ')' {$$ = $2;}

这就是我们算数表达式的语法了。这里,我们实现了优先级。其中括号()的优先级最高,乘法和除法的优先级其次,加法和减法的优先级最低。

那么,这个优先级是如何去实现的呢?我们可以看到,我们的加法表达式语法嵌套了乘法表达式语法;乘法表达式语法嵌套了我们的括号。

所以,我们通过文法的一个嵌套,实现了优先级。最下面的语法,它的优先级最高,最上面的语法,它的优先级最低。(如果对优先级这一块不太清楚的小伙伴,我建议自己去手写一遍优先级,然后就能够理解为什么了。我始终觉得,虽然我们是使用了bison做语法分析,但是,我们一定要具备没有这类工具,也可以去实现这些语法的能力,因为这是基本功)

好的,现在,让我们来运行一下我们的编译脚本:

1
./rebuild.sh

然后编写测试文件test1.php

1
echo 2 + 2 * 3 / 2

执行我们的yaphp

1
2
./build/yaphp tests/test1.php
5

我们发现,这里先计算了2 * 3 / 2,得到3之后,在进行2 + 3的运算,得到5

我们再来一个例子:

1
echo (2 + 2) * 3 / 2

执行我们的yaphp

1
2
./build/yaphp tests/test1.php
6

我们发现,这里先计算了(2 + 2)得到4之后,在进行4 * 3 / 2的运算,得到6

OK,我们算是实现了优先级。但是,这么去写语法规则是非常的难看的,一点也不优雅。

我们来借助bison的能力,实现一下优先级。我们修改一下我们的语法文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
%{
#include <stdio.h>
#include <string.h>

extern int yylex(void);
extern int yyparse(void);
extern FILE *yyin;
extern int yylineno;

int yywrap()
{
return 1;
}

void yyerror(const char *s)
{
printf("[error] %s, in line %d\n", s, yylineno);
}

int main(int argc, char const *argv[])
{
const char *file = argv[1];
FILE *fp = fopen(file, "r");

if(fp == NULL)
{
printf("cannot open %s\n", file);
return -1;
}

yyin = fp;
yyparse();

return 0;
}
%}

%token T_ECHO T_LNUMBER

%left '+' '-'
%left '*' '/'
%left '(' ')'

%%

statement: %empty
| T_ECHO expr { printf("%d\n", $2); }
;

expr: %empty
| expr '+' expr {$$ = $1 + $3;}
| expr '-' expr {$$ = $1 - $3;}
| expr '*' expr {$$ = $1 * $3;}
| expr '/' expr {$$ = $1 / $3;}
| '(' expr ')' {$$ = $2;}
| T_LNUMBER {$$ = $1;}
;

%%

可以发现,我们这里多了一部分东西:

1
2
3
%left '+' '-'
%left '*' '/'
%left '(' ')'

这实际上就是我们的优先级定义了。自上而下,它的优先级越来越高。我们现在重新编译一下我们的yaphp

1
./rebuild.sh

然后执行上面的两个例子,我们就可以得到和我们首先优先级一样的结果了。

《手把手教你编写PHP编译器》--准备工作

准备工作

在写代码之前,我们很有必要先把编译C++代码的工作做好。主要涉及到以下几个方面:

1
2
1. 编写CMakeLists
2. 编写一个编译的脚本

编写CMakeLists

因为CMakeLists.txt的内容比较简单,所以我直接贴出我们的CMakeLists.txt文件的内容:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
cmake_minimum_required(VERSION 3.4)
project(yaphp)

set(CMAKE_CXX_STANDARD 11)
set(CMAKE_CXX_COMPILER clang++)

find_package(FLEX REQUIRED)
set(FlexOutput ${CMAKE_SOURCE_DIR}/src/Zend/zend_language_scanner.cc)
if(FLEX_FOUND)
add_custom_command(
OUTPUT ${FlexOutput}
COMMAND ${FLEX_EXECUTABLE}
--outfile=${FlexOutput}
${CMAKE_SOURCE_DIR}/src/Zend/zend_language_scanner.l
COMMENT "Generating zend_language_scanner.cc"
)
endif()

find_package(BISON REQUIRED)
set(BisonOutput ${CMAKE_SOURCE_DIR}/src/Zend/zend_language_parser.cc)
if(BISON_FOUND)
add_custom_command(
OUTPUT ${BisonOutput}
COMMAND ${BISON_EXECUTABLE}
--defines=${CMAKE_SOURCE_DIR}/src/Zend/zend_language_parser.h
--output=${BisonOutput}
${CMAKE_SOURCE_DIR}/src/Zend/zend_language_parser.y
COMMENT "Generating zend_language_parser.cc"
)
endif()

add_executable(yaphp
${FlexOutput}
${BisonOutput}
)
include_directories(BEFORE ${CMAKE_CURRENT_SOURCE_DIR})
target_compile_options(yaphp PUBLIC ${CMAKE_CXX_FLAGS} -Wall -Wno-deprecated-register -O0 -g)

message(STATUS "summary of build options:
Install prefix: ${CMAKE_INSTALL_PREFIX}
Target system: ${CMAKE_SYSTEM_NAME}
Compiler:
CXX compiler: ${CMAKE_CXX_COMPILER}
")

我们来讲一下核心的东西,其他不清楚的地方,可以网上搜一下。

首先来看这段代码:

1
project(yaphp)

我们把我们的这个项目叫做yaphp,即表示Yet another php

然后是这段代码:

1
2
3
4
5
6
7
8
9
10
11
find_package(FLEX REQUIRED)
set(FlexOutput ${CMAKE_SOURCE_DIR}/src/Zend/zend_language_scanner.cc)
if(FLEX_FOUND)
add_custom_command(
OUTPUT ${FlexOutput}
COMMAND ${FLEX_EXECUTABLE}
--outfile=${FlexOutput}
${CMAKE_SOURCE_DIR}/src/Zend/zend_language_scanner.l
COMMENT "Generating zend_language_scanner.cc"
)
endif()

这段代码做的事情是,通过flex,让zend_language_scanner.l文件生成zend_language_scanner.cc文件(如果不清楚zend_language_scanner.l的小伙伴不用着急,我们后面会讲)。并且,我们可以看到,我们把zend_language_scanner.l文件放在了Zend目录下,这实际上是和php-src(即php解释器)的一致的。

然后是这段代码:

1
2
3
4
5
6
7
8
9
10
11
12
find_package(BISON REQUIRED)
set(BisonOutput ${CMAKE_SOURCE_DIR}/src/Zend/zend_language_parser.cc)
if(BISON_FOUND)
add_custom_command(
OUTPUT ${BisonOutput}
COMMAND ${BISON_EXECUTABLE}
--defines=${CMAKE_SOURCE_DIR}/src/Zend/zend_language_parser.h
--output=${BisonOutput}
${CMAKE_SOURCE_DIR}/src/Zend/zend_language_parser.y
COMMENT "Generating zend_language_parser.cc"
)
endif()

这段代码做的事情是,通过bison,让zend_language_parser.y文件生成zend_language_parser.cc文件和zend_language_parser.h文件(如果不清楚zend_language_parser.y的小伙伴不用着急,我们后面会讲)。

然后是这段代码:

1
2
3
4
add_executable(yaphp
${FlexOutput}
${BisonOutput}
)

表示,我们要把${FlexOutput}${BisonOutput}编译成yaphp可执行文件。

OK,按照这个CMakeLists.txt的意思,我们来创建对应的文件。首先是文件src/Zend/zend_language_scanner.l

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
%option noyywrap
%option nounput
%option noinput
%{
#include "zend_language_parser.h"
%}

%%
echo {
return T_ECHO;
}

[;(),+*/-] return *yytext;

[0-9]+ {
yylval = atoi(yytext);
return T_NUMBER;
}

[\t\n ]+ /* ignore \t, \n, whitespace */

%%

然后是文件src/Zend/zend_language_parser.y

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
%{
#include <stdio.h>
#include <string.h>

extern int yylex(void);
extern int yyparse(void);
extern FILE *yyin;
extern int yylineno;

int yywrap()
{
return 1;
}

void yyerror(const char *s)
{
printf("[error] %s, in line %d\n", s, yylineno);
}

int main(int argc, char const *argv[])
{
const char *file = argv[1];
FILE *fp = fopen(file, "r");

if(fp == nullptr)
{
printf("cannot open %s\n", file);
return -1;
}

yyin = fp;
yyparse();

return 0;
}
%}

%token T_ECHO T_NUMBER

%%

statement: %empty
| T_ECHO expr { printf("%d\n", $2); }
;

expr: %empty
| T_NUMBER {$$ = $1;}
;

%%

然后,我们创建文件tests/test1.php

1
echo 1

编写编译的脚本

我们创建文件rebuild.sh

1
2
3
4
#!/bin/bash
__DIR__=$(cd "$(dirname "$0")" || exit 1; pwd); [ -z "${__DIR__}" ] && exit 1

cd "${__DIR__}" && ./clean.sh && mkdir -p build && cd build && cmake .. && make

这段代码很简单,就是先调用clean.sh脚本做一些清理工作,然后调用cmake来生成Makefile,然后调用make来编译代码,生成yaphp

然后创建文件tools/cleaner.sh

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
#!/bin/bash
__DIR__=$(cd "$(dirname "$0")" || exit 1; pwd); [ -z "${__DIR__}" ] && exit 1

error(){ echo "[ERROR] $1"; exit 1; }
success(){ echo "[SUCCESS] $1"; exit 0; }
info(){ echo "[INFO] $1";}

workdir="$1"

if ! cd "${workdir}"; then
error "Cd to ${workdir} failed"
fi

info "Scanning dir \"${workdir}\" ..."

if [ ! -f "./Makefile" ] && [ ! -f "./CMakeLists.txt" ]; then
error "Non-project dir ${workdir}"
fi

info "CMake build dir will be removed:"

rm -rf -v ./build

info "Following files will be removed:"

find ${workdir}/src/Zend -name zend_language_scanner.cc -print0 | xargs -0 rm -f -v
find ${workdir}/src/Zend -name zend_language_parser.h -print0 | xargs -0 rm -f -v
find ${workdir}/src/Zend -name zend_language_parser.cc -print0 | xargs -0 rm -f -v

success "Clean '${workdir}' done"

这个脚本会清理掉cmake生成的一系列文件。

然后创建文件clean.sh

1
2
3
4
#!/bin/bash
__DIR__=$(cd "$(dirname "$0")" || exit 1; pwd); [ -z "${__DIR__}" ] && exit 1

"${__DIR__}"/tools/cleaner.sh "${__DIR__}"

OK,现在,所以的事情都做好了,我们只需要执行脚本rebuild.sh

1
2
3
4
5
./rebuild.sh

# 省略其他的输出
[100%] Linking CXX executable yaphp
[100%] Built target yaphp

现在,你将会在目录build下面看到编译好的yaphp。并且,细心的话,你会发现,在目录src/Zend下面,生成了文件zend_language_scanner.cczend_language_parser.hzend_language_parser.cc

现在,让我们执行这个yaphp

1
2
./build/yaphp tests/test1.php
1

我们将会看到,1被打印了出来。

Linux下Mutex的所有者线程先死亡造成其他线程获取锁阻塞的问题

Swoole最近有一个BUG,大概就是Mutex的所有者线程先死亡,造成其他线程获取锁阻塞的问题。具体的iseue在这里

我们可以用如下代码来对这个问题进行复现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
#include <pthread.h>
#include <iostream>
#include <unistd.h>

pthread_mutex_t mutex;

void *handler(void *)
{
std::cout << "child thread" << std::endl;
int ret = pthread_mutex_lock(&mutex);
std::cout << "child ret: " << ret << std::endl;
pthread_exit(NULL);
}

int main()
{
pthread_t tid;
pthread_mutexattr_t attr;
int ret;

pthread_mutexattr_init(&attr);
pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_ERRORCHECK);

pthread_mutex_init(&mutex, &attr);
pthread_mutexattr_destroy(&attr);

pthread_create(&tid, NULL, handler, NULL);

sleep(2);
std::cout << "father awake" << std::endl;

ret = pthread_mutex_lock(&mutex);
std::cout << "father ret: " << ret << std::endl;
return 0;
}

这段代码的意思是,主线程创建了子线程之后,立马调用sleep阻塞住。然后子线程去获取锁,然后子线程立马退出,导致锁没有被解开。然后当主线程sleep结束后,尝试获取锁的时候,就会阻塞住。(导致这个现象的原因是,子线程退出后,操作系统并不会把锁解开)

我们可以执行下上面这段代码,执行结果如下:

1
2
3
4
5
6
7
g++ lock.cc -lpthread
./a.out

child thread
child ret: 0
father awake

解决方式如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
#include <iostream>
#include <unistd.h>
#include <pthread.h>
#include <string.h>
#include <errno.h>

pthread_mutex_t lock;

void *dropped_thread(void*)
{
std::cout << "Setting lock..." << std::endl;
pthread_mutex_lock(&lock);
std::cout << "Lock set, now exiting without unlocking..." << std::endl;
pthread_exit(NULL);
}

int main(int argc, char *argv[])
{
int ret;
pthread_t lock_getter;
pthread_mutexattr_t attr;

pthread_mutexattr_init(&attr);
pthread_mutexattr_setrobust(&attr, PTHREAD_MUTEX_ROBUST);
pthread_mutex_init(&lock, &attr);
pthread_mutexattr_destroy(&attr);
pthread_create(&lock_getter, NULL, dropped_thread, NULL);
sleep(2);

std::cout << "Inside main" << std::endl;
std::cout << "Attempting to acquire mutex?" << std::endl;

ret = pthread_mutex_lock(&lock);
if (ret == EOWNERDEAD) {
std::cout << "errno: " << ret << ", error: " << strerror(ret) << std::endl;

std::cout << "consistent mutex" << std::endl;
pthread_mutex_consistent(&lock);

std::cout << "unlock mutex" << std::endl;
pthread_mutex_unlock(&lock);
} else {
std::cout << "errno: " << ret << ", error: " << strerror(ret) << std::endl;
}
std::cout << "Attempting to acquire mutex?" << std::endl;
ret = pthread_mutex_lock(&lock);
if (ret != 0) {
std::cout << "errno: " << ret << ", error: " << strerror(ret) << std::endl;
} else {
std::cout << "Successfully acquired lock!" << std::endl;
pthread_mutex_destroy(&lock);
}

return 0;
}

执行结果如下:

1
2
3
4
5
6
7
8
9
10
11
12
g++ lock.cc -lpthread
./a.out

Setting lock...
Lock set, now exiting without unlocking...
Inside main
Attempting to acquire mutex?
errno: 130, error: Owner died
consistent mutex
unlock mutex
Attempting to acquire mutex?
Successfully acquired lock!

这里的核心是,我们对这个锁设置了PTHREAD_MUTEX_ROBUST。这样的话,锁的拥有者退出后,其他线程去获取锁的时候,就不会阻塞住了,而是返回错误码EOWNERDEAD。并且,这里还有一个细节就是,在获得了错误码之后,我们需要设置锁的状态为consistent。这样,其他线程就能解开锁了。

《手把手教你编写PHP编译器》--开篇

从今天开始,我打算写一个全新的教程,手把手去实现一个五脏俱全的PHP,教程风格类似于去年我写的手把手编写PHP协程扩展那样,但是会有一点不同,这个教程可能就不会那么直接上那么多代码了,重点是讲解实现的思路。

这个编译器语法会尽可能的和PHP保持一致。并且我希望它是一门静态强类型的语言,你可以在定义一个变量的时候,不声明类型,但是我们会进行类型推导。

教程的知识点我希望和龙书的尽可能吻合,但是也不会和它完全一样,毕竟这本书前面有太多讲解编译器前端的东西了,很多手写解析源代码的,这有一些算法,一旦拿出来讲,估计会起到劝退的效果。这门教程我希望更多的是讲解编译器的后端优化,这一点也和大多数的PHP源码分析教程不同,目前来看,因为PHP的原因,编译器后端的优化除了JIT似乎就没了,而且大多数是去讲解AST生成的。

至于后端的优化,会讲解原理,但是,真正的去实现的时候,我们不会自己去手写,这太费劲了,我们直接使用LLVM,然后开优化,读IR,来验证优化的思路。所以,这门教程,会讲解LLVM的中间表示。但是我们不会去手写LLVMIR,而是使用LLVMAPI来自动生成中间表示。

这门教程是使用C++来开发的,构建工具是CMake,编译器前端工具是flexbison,编译器后端工具是LLVM

之前我打算使用PHP来写这门教程。试坑之后,我发现有以下几点问题:

1、如果直接使用PHP-Parser来生成AST,那么我们实现的语法就会受很大的限制了

2、PHPLLVM的绑定没有看到比较好的。我有想过去写扩展对LLVM包一层,但是这工作量太大了。也想过用FFI来直接搞,但是怕PHP的FFI有问题。所以就干脆直接用C++来完成我们的这门教程了。

我不是一个专门研究编译器的人,这个教程主要是对自己的一个阶段性学习的总结。就和PHP协程扩展开发教程一样,边写边学习,算是对自己这一年的一个总结吧。

CPU占用过高分析

今天遇到了一个hyperf死循环的bug,排查了很久,没有思路。后来峰哥指导,立马定位出了问题。

定位步骤,首先,通过perf top命令来查看系统的cpu占用情况:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
perf top -p 19732

Samples: 16K of event 'cpu-clock', 4000 Hz, Event count (approx.): 3364648111 lost: 0/0 drop: 0/0
Overhead Shared Object Symbol
9.02% [kernel] [k] _raw_spin_unlock_irqrestore
7.96% php [.] execute_ex
6.85% [kernel] [k] finish_task_switch
2.82% php [.] ZEND_FETCH_OBJ_R_SPEC_UNUSED_CONST_HANDLER
2.05% libpthread-2.17.so [.] __libc_recv
1.93% php [.] ZEND_INIT_METHOD_CALL_SPEC_UNUSED_CONST_HANDLER
1.55% libc-2.17.so [.] __memmove_ssse3_back
1.43% [vdso] [.] __vdso_gettimeofday
1.35% libc-2.17.so [.] __memcpy_ssse3_back
1.31% php [.] zend_leave_helper_SPEC

可以看到,execute_ex这个函数的Overhead非常的高。所以,可以大改猜测是PHP代码的问题。然后,我们找到PHP的进程,用strace看一下进程在做啥事情:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
strace -p 19732

sendto(20, "*3\r\n$5\r\nBRPOP\r\n$13\r\nqueue:waiting\r\n$1\r\n2\r\n", 42, 0, NULL, 0) = 42
recvfrom(20, "-", 1, MSG_PEEK, NULL, NULL) = 1
recvfrom(20, "-MISCONF Redis is configured to save RDB snapshots, but it is currently not able to persist on disk. Commandsthat may modify the data set are disabled, because this instance is configured to report errors during writes if RDB snapshotting fails (stop-writes-on-bgsave-error option). Please check the Redis logs for details about the RDB error.\r\n", 8192, 0, NULL, NULL) = 346
recvfrom(20, 0x7f08526509e0, 1, MSG_PEEK, NULL, NULL) = -1 EAGAIN (Resource temporarily unavailable)
sendto(20, "*7\r\n$16\r\nZREVRANGEBYSCORE\r\n$13\r\nqueue:delayed\r\n$10\r\n1599474415\r\n$4\r\n-inf\r\n$5\r\nLIMIT\r\n$1\r\n0\r\n$3\r\n100\r\n", 101, 0, NULL, 0) = 101
recvfrom(20, "*", 1, MSG_PEEK, NULL, NULL) = 1
recvfrom(20, "*0\r\n", 8192, 0, NULL, NULL) = 4
recvfrom(20, 0x7f08526509e0, 1, MSG_PEEK, NULL, NULL) = -1 EAGAIN (Resource temporarily unavailable)
sendto(20, "*7\r\n$16\r\nZREVRANGEBYSCORE\r\n$14\r\nqueue:reserved\r\n$10\r\n1599474415\r\n$4\r\n-inf\r\n$5\r\nLIMIT\r\n$1\r\n0\r\n$3\r\n100\r\n", 102, 0, NULL, 0) = 102
recvfrom(20, "*", 1, MSG_PEEK, NULL, NULL) = 1
recvfrom(20, "*0\r\n", 8192, 0, NULL, NULL) = 4
recvfrom(20, 0x7f08526509e0, 1, MSG_PEEK, NULL, NULL) = -1 EAGAIN (Resource temporarily unavailable)
sendto(20, "*3\r\n$5\r\nBRPOP\r\n$13\r\nqueue:waiting\r\n$1\r\n2\r\n", 42, 0, NULL, 0) = 42
recvfrom(20, "-", 1, MSG_PEEK, NULL, NULL) = 1
recvfrom(20, "-MISCONF Redis is configured to save RDB snapshots, but it is currently not able to persist on disk. Commandsthat may modify the data set are disabled, because this instance is configured to report errors during writes if RDB snapshotting fails (stop-writes-on-bgsave-error option). Please check the Redis logs for details about the RDB error.\r\n", 8192, 0, NULL, NULL) = 346
recvfrom(20, 0x7f08526509e0, 1, MSG_PEEK, NULL, NULL) = -1 EAGAIN (Resource temporarily unavailable)
sendto(20, "*7\r\n$16\r\nZREVRANGEBYSCORE\r\n$13\r\nqueue:delayed\r\n$10\r\n1599474415\r\n$4\r\n-inf\r\n$5\r\nLIMIT\r\n$1\r\n0\r\n$3\r\n100\r\n", 101, 0, NULL, 0) = 101
recvfrom(20, "*", 1, MSG_PEEK, NULL, NULL) = 1
recvfrom(20, "*0\r\n", 8192, 0, NULL, NULL) = 4
recvfrom(20, 0x7f08526509e0, 1, MSG_PEEK, NULL, NULL) = -1 EAGAIN (Resource temporarily unavailable)
sendto(20, "*7\r\n$16\r\nZREVRANGEBYSCORE\r\n$14\r\nqueue:reserved\r\n$10\r\n1599474415\r\n$4\r\n-inf\r\n$5\r\nLIMIT\r\n$1\r\n0\r\n$3\r\n100\r\n", 102, 0, NULL, 0) = 102
recvfrom(20, "*", 1, MSG_PEEK, NULL, NULL) = 1
recvfrom(20, "*0\r\n", 8192, 0, NULL, NULL) = 4
recvfrom(20, 0x7f08526509e0, 1, MSG_PEEK, NULL, NULL) = -1 EAGAIN (Resource temporarily unavailable)
sendto(20, "*3\r\n$5\r\nBRPOP\r\n$13\r\nqueue:waiting\r\n$1\r\n2\r\n", 42, 0, NULL, 0) = 42
recvfrom(20, "-", 1, MSG_PEEK, NULL, NULL) = 1
recvfrom(20, "-MISCONF Redis is configured to save RDB snapshots, but it is currently not able to persist on disk. Commandsthat may modify the data set are disabled, because this instance is configured to report errors during writes if RDB snapshotting fails (stop-writes-on-bgsave-error option). Please check the Redis logs for details about the RDB error.\r\n", 8192, 0, NULL, NULL) = 346
recvfrom(20, 0x7f08526509e0, 1, MSG_PEEK, NULL, NULL) = -1 EAGAIN (Resource temporarily unavailable)
sendto(20, "*7\r\n$16\r\nZREVRANGEBYSCORE\r\n$13\r\nqueue:delayed\r\n$10\r\n1599474415\r\n$4\r\n-inf\r\n$5\r\nLIMIT\r\n$1\r\n0\r\n$3\r\n100\r\n", 101, 0, NULL, 0) = 101
recvfrom(20, "*", 1, MSG_PEEK, NULL, NULL) = 1
recvfrom(20, "*0\r\n", 8192, 0, NULL, NULL) = 4
recvfrom(20, 0x7f08526509e0, 1, MSG_PEEK, NULL, NULL) = -1 EAGAIN (Resource temporarily unavailable)
sendto(20, "*7\r\n$16\r\nZREVRANGEBYSCORE\r\n$14\r\nqueue:reserved\r\n$10\r\n1599474415\r\n$4\r\n-inf\r\n$5\r\nLIMIT\r\n$1\r\n0\r\n$3\r\n100\r\n", 102, 0, NULL, 0) = 102
recvfrom(20, "*", 1, MSG_PEEK, NULL, NULL) = 1
recvfrom(20, "*0\r\n", 8192, 0, NULL, NULL) = 4
recvfrom(20, 0x7f08526509e0, 1, MSG_PEEK, NULL, NULL) = -1 EAGAIN (Resource temporarily unavailable)
sendto(20, "*3\r\n$5\r\nBRPOP\r\n$13\r\nqueue:waiting\r\n$1\r\n2\r\n", 42, 0, NULL, 0) = 42
recvfrom(20, "-", 1, MSG_PEEK, NULL, NULL) = 1
recvfrom(20, "-MISCONF Redis is configured to save RDB snapshots, but it is currently not able to persist on disk. Commandsthat may modify the data set are disabled, because this instance is configured to report errors during writes if RDB snapshotting fails (stop-writes-on-bgsave-error option). Please check the Redis logs for details about the RDB error.\r\n", 8192, 0, NULL, NULL) = 346
recvfrom(20, 0x7f08526509e0, 1, MSG_PEEK, NULL, NULL) = -1 EAGAIN (Resource temporarily unavailable)
sendto(20, "*7\r\n$16\r\nZREVRANGEBYSCORE\r\n$13\r\nqueue:delayed\r\n$10\r\n1599474415\r\n$4\r\n-inf\r\n$5\r\nLIMIT\r\n$1\r\n0\r\n$3\r\n100\r\n", 101, 0, NULL, 0) = 101
recvfrom(20, "*", 1, MSG_PEEK, NULL, NULL) = 1
recvfrom(20, "*0\r\n", 8192, 0, NULL, NULL) = 4
recvfrom(20, 0x7f08526509e0, 1, MSG_PEEK, NULL, NULL) = -1 EAGAIN (Resource temporarily unavailable)
sendto(20, "*7\r\n$16\r\nZREVRANGEBYSCORE\r\n$14\r\nqueue:reserved\r\n$10\r\n1599474415\r\n$4\r\n-inf\r\n$5\r\nLIMIT\r\n$1\r\n0\r\n$3\r\n100\r\n", 102, 0, NULL, 0) = 102
recvfrom(20, "*", 1, MSG_PEEK, NULL, NULL) = 1
recvfrom(20, "*0\r\n", 8192, 0, NULL, NULL) = 4
recvfrom(20, 0x7f08526509e0, 1, MSG_PEEK, NULL, NULL) = -1 EAGAIN (Resource temporarily unavailable)
sendto(20, "*3\r\n$5\r\nBRPOP\r\n$13\r\nqueue:waiting\r\n$1\r\n2\r\n", 42, 0, NULL, 0) = 42
recvfrom(20, "-", 1, MSG_PEEK, NULL, NULL) = 1
recvfrom(20, "-MISCONF Redis is configured to save RDB snapshots, but it is currently not able to persist on disk. Commandsthat may modify the data set are disabled, because this instance is configured to report errors during writes if RDB snapshotting fails (stop-writes-on-bgsave-error option). Please check the Redis logs for details about the RDB error.\r\n", 8192, 0, NULL, NULL) = 346
recvfrom(20, 0x7f08526509e0, 1, MSG_PEEK, NULL, NULL) = -1 EAGAIN (Resource temporarily unavailable)
sendto(20, "*7\r\n$16\r\nZREVRANGEBYSCORE\r\n$13\r\nqueue:delayed\r\n$10\r\n1599474415\r\n$4\r\n-inf\r\n$5\r\nLIMIT\r\n$1\r\n0\r\n$3\r\n100\r\n", 101, 0, NULL, 0) = 101
recvfrom(20, "*", 1, MSG_PEEK, NULL, NULL) = 1
recvfrom(20, "*0\r\n", 8192, 0, NULL, NULL) = 4
recvfrom(20, 0x7f08526509e0, 1, MSG_PEEK, NULL, NULL) = -1 EAGAIN (Resource temporarily unavailable)
sendto(20, "*7\r\n$16\r\nZREVRANGEBYSCORE\r\n$14\r\nqueue:reserved\r\n$10\r\n1599474415\r\n$4\r\n-inf\r\n$5\r\nLIMIT\r\n$1\r\n0\r\n$3\r\n100\r\n", 102, 0, NULL, 0) = 102
recvfrom(20, "*", 1, MSG_PEEK, NULL, NULL) = 1
recvfrom(20, "*0\r\n", 8192, 0, NULL, NULL) = 4
recvfrom(20, 0x7f08526509e0, 1, MSG_PEEK, NULL, NULL) = -1 EAGAIN (Resource temporarily unavailable)
sendto(20, "*3\r\n$5\r\nBRPOP\r\n$13\r\nqueue:waiting\r\n$1\r\n2\r\n", 42, 0, NULL, 0) = 42
recvfrom(20, "-", 1, MSG_PEEK, NULL, NULL) = 1
recvfrom(20, "-MISCONF Redis is configured to save RDB snapshots, but it is currently not able to persist on disk. Commandsthat may modify the data set are disabled, because this instance is configured to report errors during writes if RDB snapshotting fails (stop-writes-on-bgsave-error option). Please check the Redis logs for details about the RDB error.\r\n", 8192, 0, NULL, NULL) = 346
recvfrom(20, 0x7f08526509e0, 1, MSG_PEEK, NULL, NULL) = -1 EAGAIN (Resource temporarily unavailable)
sendto(20, "*7\r\n$16\r\nZREVRANGEBYSCORE\r\n$13\r\nqueue:delayed\r\n$10\r\n1599474415\r\n$4\r\n-inf\r\n$5\r\nLIMIT\r\n$1\r\n0\r\n$3\r\n100\r\n", 101, 0, NULL, 0) = 101
recvfrom(20, "*", 1, MSG_PEEK, NULL, NULL) = 1
recvfrom(20, "*0\r\n", 8192, 0, NULL, NULL) = 4
recvfrom(20, 0x7f08526509e0, 1, MSG_PEEK, NULL, NULL) = -1 EAGAIN (Resource temporarily unavailable)
sendto(20, "*7\r\n$16\r\nZREVRANGEBYSCORE\r\n$14\r\nqueue:reserved\r\n$10\r\n1599474415\r\n$4\r\n-inf\r\n$5\r\nLIMIT\r\n$1\r\n0\r\n$3\r\n100\r\n", 102, 0, NULL, 0) = 102
recvfrom(20, "*", 1, MSG_PEEK, NULL, NULL) = 1
recvfrom(20, "*0\r\n", 8192, 0, NULL, NULL) = 4
recvfrom(20, 0x7f08526509e0, 1, MSG_PEEK, NULL, NULL) = -1 EAGAIN (Resource temporarily unavailable)
sendto(20, "*3\r\n$5\r\nBRPOP\r\n$13\r\nqueue:waiting\r\n$1\r\n2\r\n", 42, 0, NULL, 0) = 42
recvfrom(20, "-", 1, MSG_PEEK, NULL, NULL) = 1
recvfrom(20, "-MISCONF Redis is configured to save RDB snapshots, but it is currently not able to persist on disk. Commandsthat may modify the data set are disabled, because this instance is configured to report errors during writes if RDB snapshotting fails (stop-writes-on-bgsave-error option). Please check the Redis logs for details about the RDB error.\r\n", 8192, 0, NULL, NULL) = 346
recvfrom(20, 0x7f08526509e0, 1, MSG_PEEK, NULL, NULL) = -1 EAGAIN (Resource temporarily unavailable)
sendto(20, "*7\r\n$16\r\nZREVRANGEBYSCORE\r\n$13\r\nqueue:delayed\r\n$10\r\n1599474415\r\n$4\r\n-inf\r\n$5\r\nLIMIT\r\n$1\r\n0\r\n$3\r\n100\r\n", 101, 0, NULL, 0) = 101
recvfrom(20, "*", 1, MSG_PEEK, NULL, NULL) = 1
recvfrom(20, "*0\r\n", 8192, 0, NULL, NULL) = 4
recvfrom(20, 0x7f08526509e0, 1, MSG_PEEK, NULL, NULL) = -1 EAGAIN (Resource temporarily unavailable)
sendto(20, "*7\r\n$16\r\nZREVRANGEBYSCORE\r\n$14\r\nqueue:reserved\r\n$10\r\n1599474415\r\n$4\r\n-inf\r\n$5\r\nLIMIT\r\n$1\r\n0\r\n$3\r\n100\r\n", 102, 0, NULL, 0) = 102
recvfrom(20, "*", 1, MSG_PEEK, NULL, NULL) = 1
recvfrom(20, "*0\r\n", 8192, 0, NULL, NULL) = 4
recvfrom(20, 0x7f08526509e0, 1, MSG_PEEK, NULL, NULL) = -1 EAGAIN (Resource temporarily unavailable)
sendto(20, "*3\r\n$5\r\nBRPOP\r\n$13\r\nqueue:waiting\r\n$1\r\n2\r\n", 42, 0, NULL, 0) = 42
recvfrom(20, "-", 1, MSG_PEEK, NULL, NULL) = 1
recvfrom(20, "-MISCONF Redis is configured to save RDB snapshots, but it is currently not able to persist on disk. Commandsthat may modify the data set are disabled, because this instance is configured to report errors during writes if RDB snapshotting fails (stop-writes-on-bgsave-error option). Please check the Redis logs for details about the RDB error.\r\n", 8192, 0, NULL, NULL) = 346
recvfrom(20, 0x7f08526509e0, 1, MSG_PEEK, NULL, NULL) = -1 EAGAIN (Resource temporarily unavailable)
sendto(20, "*7\r\n$16\r\nZREVRANGEBYSCORE\r\n$13\r\nqueue:delayed\r\n$10\r\n1599474415\r\n$4\r\n-inf\r\n$5\r\nLIMIT\r\n$1\r\n0\r\n$3\r\n100\r\n", 101, 0, NULL, 0) = 101
recvfrom(20, "*", 1, MSG_PEEK, NULL, NULL) = 1
recvfrom(20, "*0\r\n", 8192, 0, NULL, NULL) = 4
recvfrom(20, 0x7f08526509e0, 1, MSG_PEEK, NULL, NULL) = -1 EAGAIN (Resource temporarily unavailable)
sendto(20, "*7\r\n$16\r\nZREVRANGEBYSCORE\r\n$14\r\nqueue:reserved\r\n$10\r\n1599474415\r\n$4\r\n-inf\r\n$5\r\nLIMIT\r\n$1\r\n0\r\n$3\r\n100\r\n", 102, 0, NULL, 0) = 102
recvfrom(20, "*", 1, MSG_PEEK, NULL, NULL) = 1
recvfrom(20, "*0\r\n", 8192, 0, NULL, NULL) = 4
recvfrom(20, 0x7f08526509e0, 1, MSG_PEEK, NULL, NULL) = -1 EAGAIN (Resource temporarily unavailable)
sendto(20, "*3\r\n$5\r\nBRPOP\r\n$13\r\nqueue:waiting\r\n$1\r\n2\r\n", 42, 0, NULL, 0) = 42
recvfrom(20, "-", 1, MSG_PEEK, NULL, NULL) = 1
recvfrom(20, "-MISCONF Redis is configured to save RDB snapshots, but it is currently not able to persist on disk. Commandsthat may modify the data set are disabled, because this instance is configured to report errors during writes if RDB snapshotting fails (stop-writes-on-bgsave-error option). Please check the Redis logs for details about the RDB error.\r\n", 8192, 0, NULL, NULL) = 346
recvfrom(20, 0x7f08526509e0, 1, MSG_PEEK, NULL, NULL) = -1 EAGAIN (Resource temporarily unavailable)
sendto(20, "*7\r\n$16\r\nZREVRANGEBYSCORE\r\n$13\r\nqueue:delayed\r\n$10\r\n1599474415\r\n$4\r\n-inf\r\n$5\r\nLIMIT\r\n$1\r\n0\r\n$3\r\n100\r\n", 101, 0, NULL, 0) = 101
recvfrom(20, "*", 1, MSG_PEEK, NULL, NULL) = 1
recvfrom(20, "*0\r\n", 8192, 0, NULL, NULL) = 4
recvfrom(20, 0x7f08526509e0, 1, MSG_PEEK, NULL, NULL) = -1 EAGAIN (Resource temporarily unavailable)
sendto(20, "*7\r\n$16\r\nZREVRANGEBYSCORE\r\n$14\r\nqueue:reserved\r\n$10\r\n1599474415\r\n$4\r\n-inf\r\n$5\r\nLIMIT\r\n$1\r\n0\r\n$3\r\n100\r\n", 102, 0, NULL, 0) = 102
recvfrom(20, "*", 1, MSG_PEEK, NULL, NULL) = 1
recvfrom(20, "*0\r\n", 8192, 0, NULL, NULL) = 4
recvfrom(20, 0x7f08526509e0, 1, MSG_PEEK, NULL, NULL) = -1 EAGAIN (Resource temporarily unavailable)
sendto(20, "*3\r\n$5\r\nBRPOP\r\n$13\r\nqueue:waiting\r\n$1\r\n2\r\n", 42, 0, NULL, 0) = 42

可以看到,hyperf应该是没有去判断redis是否崩溃,即使redis崩溃了,也在一直循环的读取redis