在PHP RSHUTDOWN阶段调用zend_bailout导致zend_mm_heap corrupted问题

测试脚本如下:

1
2
3
<?php

date_default_timezone_set('Asia/Shanghai');

并且开启opcache

然后,我们编写如下代码:

1
2
3
4
5
PHP_RSHUTDOWN_FUNCTION(yasd) {
zend_bailout();

return SUCCESS;
}

接着,使用php-cgi来启动服务:

1
php-cgi -b 0.0.0.0:8000

然后,请求两次我们的脚本。第一次是正常的,第二次就会出现zend_mm_heap corrupted的问题。

并且,我发现,关闭opcache之后,这个错误就会消失。当然,我们还是不要在RSHUTDOWN阶段去调用zend_bailout

PHP扩展如何获取私有属性的名字

本篇文章基于PHP7.4.10

我们的测试脚本如下:

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

class Foo
{
public $a;

private $b;

public function __construct()
{
$this->a = 1;
$this->b = 1;
}
}

$foo = new Foo;

printAllAttributeKeys($foo);

其中printAllAttributeKeys是我们要编写的一个扩展函数,用来打印对象所有的属性名字。

因为PHP属性是存在一个哈希表里面的,所以我们可以进行如下操作:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
static PHP_FUNCTION(printAllAttributeKeys) {
zend_array *properties;
zval *zobj;
zend_ulong num;
zend_string *key;
zval *val;

ZEND_PARSE_PARAMETERS_START_EX(ZEND_PARSE_PARAMS_THROW, 1, 1)
Z_PARAM_OBJECT(zobj)
ZEND_PARSE_PARAMETERS_END_EX(RETURN_FALSE);

#if PHP_VERSION_ID >= 70400
properties = zend_get_properties_for(zobj, ZEND_PROP_PURPOSE_VAR_EXPORT);
#else
if (Z_OBJ_HANDLER_P(zobj, get_properties)) {
properties = Z_OBJPROP_P(zobj);
}
#endif

ZEND_HASH_FOREACH_KEY_VAL_IND(properties, num, key, val) {
printf("%s\n", ZSTR_VAL(key));
} ZEND_HASH_FOREACH_END();
}

执行结果如下:

1
2
3
php test.php
a

可以发现,只打印出了a。实际上,对于b这个属性,它在zend_string里的存储内容为:

1
\0Foo\0b

如果我们要打印出私有属性,我们可以作如下操作:

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
const char *get_property_name(zend_string *property_name) {
const char *class_name, *_property_name;
size_t _property_name_len;

zend_unmangle_property_name_ex(property_name, &class_name, &_property_name, &_property_name_len);

return _property_name;
}

static PHP_FUNCTION(printAllAttributeKeys) {
zend_array *properties;
zval *zobj;
zend_ulong num;
zend_string *key;
zval *val;

ZEND_PARSE_PARAMETERS_START_EX(ZEND_PARSE_PARAMS_THROW, 1, 1)
Z_PARAM_OBJECT(zobj)
ZEND_PARSE_PARAMETERS_END_EX(RETURN_FALSE);

#if PHP_VERSION_ID >= 70400
properties = zend_get_properties_for(zobj, ZEND_PROP_PURPOSE_VAR_EXPORT);
#else
if (Z_OBJ_HANDLER_P(zobj, get_properties)) {
properties = Z_OBJPROP_P(zobj);
}
#endif

ZEND_HASH_FOREACH_KEY_VAL_IND(properties, num, key, val) {
printf("%s\n", get_property_name(key));
} ZEND_HASH_FOREACH_END();
}

对属性key这个zend_string调用zend_unmangle_property_name_ex即可获取到私有属性的名字。

执行结果如下:

1
2
3
php test.php
a
b

PHP扩展如何去检查依赖的C++库是否存在

找了一圈,发现PHP没有去实现这样的一个宏来进行检测。并且,发现所有C++ wrapper扩展都没有去实现这个功能,只是在文档里面说了一下依赖了这个C++库。这样不太好,容易让开发者在编译的途中,发现漏了一个依赖库。

所以,我就写了一个简单的宏来实现这个功能:

1
2
3
4
5
6
7
8
9
10
11
12
AC_DEFUN([YASD_CHECK_CXX_LIB], [
AC_LANG_PUSH([C++])
LIBNAME=$1
AC_MSG_CHECKING([for boost])
AC_TRY_COMPILE(
[ #include $2 ],
[],
[ AC_MSG_RESULT(yes) ],
[ AC_MSG_ERROR([lib $LIBNAME not found. Try: install $LIBNAME library]) ]
)
AC_LANG_POP([C++])
])

那么怎么去用呢?我们只需要随便去找一个库的头文件就好了,例如:

1
YASD_CHECK_CXX_LIB([boost], [<boost/algorithm/string/constants.hpp>])

第一个参数填写依赖库的名字,第二个参数填头文件。

Swoole native curl协程化思路

这个还是有点复杂的,记录一下:

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
┌──────────────────────────┐                                                                                                                                                                      
│ │
│ │
│ co 1 │
│ │
│ 1. curl exec
│ │
│ │
└──────────────────────────┘



┌──────────────────────────┐
│ │
│ │
│ co 1 │
│ │
│ 2. add timer │
│ │
│ │
└──────────────────────────┘



┌──────────────────────────┐ ┌──────────────────────────────────┐ ┌──────────────────────────────────┐
│ │ │ │ │ │
│ │ │ │ │ │
│ co 1 │ │ event loop │ │ curl_multi_socket_action -> │
│ │────────────────────────────────────▶│ │──────────▶│ connect -> add write event -> │
│ 3. yield_m │ │ 4. timeout │ │ return to event loop │
│ │ │ │ │ │
│ │ │ │ │ │
└──────────────────────────┘ └──────────────────────────────────┘ └──────────────────────────────────┘




┌──────────────────────────────────┐ ┌──────────────────────────────────┐
│ │ │ │
│ │ │ │
│ event loop │ │ curl_multi_socket_action -> send │
│ │──────────▶│ request -> add read event -> │
│ 5. writeable │ │ return to event loop │
│ │ │ │
│ │ │ │
└──────────────────────────────────┘ └──────────────────────────────────┘






┌──────────────────────────────────┐ ┌──────────────────────────────────┐ ┌──────────────────────────────────┐
│ │ │ │ │ │
│ │ │ │ │ 6.3 curl_multi_socket_action -> │
│ event loop │ │ 6.1 curl_multi_socket_action -> │ │CURLOPT_WRITEFUNCTION func -> read
│ │──────────▶│ call CURLOPT_HEADERFUNCTION │─────────▶│body -> delete all event -> delete│
│ 6. readable │ │ │ │ timer -> resume_m │
│ │ │ │ │ │
│ │ │ │ │ │
└──────────────────────────────────┘ └──────────────────────────────────┘ └──────────────────────────────────┘
│ │
│ │
│ │
if set CURLOPT_HEADER│UNCTION in user code │
│ │
│ │
▼ ▼
┌──────────────────────────────────┐ ┌──────────────────────────────────┐
│ │ │ │
│ │ │ │
│6.2 set write_header for coroutine│ │ 6.4 read_info -> CURLMSG_DONE -> │
│ -> resume_m │ │ resume_m │
│ │ │ │
│ │ │ │
└──────────────────────────────────┘ │ │
│ └──────────────────────────────────┘
│ │
│ │
│ │
▼ │
┌────────────────────────────────────────────┐ │
│ │ │
│ │ │
│ co 1 │ │
│ │ │
│write_header func to get response header -> │ │
│ yield_m (Notice: Every time the │ │
│fn_write_header reads a row of headers in a │ │
│callback, a scheduler-to-coroutine switch is│ │
│ required) │ │
│ │ │
│ why we should call it in coroutine? │ │
│ Because this callback function may have │ │
│ blocking IO │ │
│ │ │
│ │ │
└────────────────────────────────────────────┘ │






┌──────────────────────────────────────────────┐ │
│ │ │
│ │ │
│ co 1 │ │
│ │◀─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘
│ 7. continue to run... │
│ │
│ │
└──────────────────────────────────────────────┘

暴力生成支配树

在编译器进行后端优化的时候,会使用支配树来构造SSA。支配树的生成算法有好几种,这里我们介绍一下最暴力的方法。(Opcache则是使用其他算法来实现,我们可以搜索论文A Simple, Fast Dominance Algorithm找到)

基本定义

支配

在一个图里面,有两个点uw,如果从图的源顶点出发,必须经过u才能到达w,那么我们称u支配w

如下图:

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
                        ┌────────────┐                        
│ 1 │
│ │
└────────────┘





┌────────────┐
│ 2 │
│ │
└────────────┘

┌───────────────────────┴───────────────────────┐
│ │
▼ ▼
┌────────────┐ ┌────────────┐
│ 3 │ │ 4 │
│ │ │ │
└────────────┘ └────────────┘
│ │
│ │
└────────────────────────┬───────────────────────┘


┌────────────┐
│ 5 │
│ │
└────────────┘

那么有如下支配关系:

1
2
1支配2,1支配3,1支配4,1支配5
2支配3,2支配4,2支配5

因为34都可以到达5,所以34不支配5

直接支配

如果u支配w,而w的其他支配者支配u,则节点u被认为是节点w的直接支配者,表示为idom (w)

如下图:

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
                        ┌────────────┐                        
│ 1 │
│ │
└────────────┘





┌────────────┐
│ 2 │
│ │
└────────────┘

┌───────────────────────┴───────────────────────┐
│ │
▼ ▼
┌────────────┐ ┌────────────┐
│ 3 │ │ 4 │
│ │ │ │
└────────────┘ └────────────┘
│ │
│ │
└────────────────────────┬───────────────────────┘


┌────────────┐
│ 5 │
│ │
└────────────┘

那么有如下直接支配关系:

1
2
1直接支配2
2直接支配3,2直接支配4,2直接支配5

我们发现,12都支配着345。但是,因为1支配了2,所以,按照直接支配的定义,2才是345的直接支配。

定理

1.除了图的源点外,其他点至少有一个点支配着它。

我们从上面的直接支配点可以看出,2345都被支配着。

2.除了图的源点外,其他点只有一个点直接支配着它。(我们可以结合上面的例子来理解)

支配树

我们可以通过edges {(idom(w),w)}来得到支配树。其中,有向图的源点就是支配树的根。

生成支配树的算法

DFS树

可以通过DFS来遍历有向图:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public function domDFS(Vertex $vertex)
{
if (!$this->isVisited($vertex)) {
$this->visitedVertexs[$vertex->name] = true;
foreach ($vertex->nexts as $next) {
if (!$this->isVisited($next)) $this->domDFS($next);
}
}
}

public function computeDominatorTree()
{
foreach ($this->predOrder as $parent) {
$this->visitedVertexs = [];
$this->visitedVertexs[$parent->name] = true;
$this->domDFS($this->predOrder[0]);
foreach ($this->predOrder as $child) {
if (!$this->isVisited($child)) {
$child->dominator = $parent;
}
}
}
}

实际上,这个算法很好理解,非常的直观暴力,但是我们还是来解释下。

前提,对图进行深度优先遍历,得到一组序列。

从第一个序列开始,每次取出一个序列,我们记作s。然后重新对图进行深度优先遍历,但是,如果遇到了当前这个点s,就停止往s这个点后面的点深度遍历了,开始回退,深度遍历其他的点。我们把每一个遍历到的点保存下来,比如放在一个visitedMap里面。最后,我们和所有的点进行对比,不在visitedMap里面的点,就是被当前s这个点支配的。一直重复下去,可以得到每一个点的直接支配点。最终,得到支配树。

在演算的过程中我们发现,对于这个算法,如果有两个点(记作v1v2)可以到达第三个点(记作v3)。那么,当选取v1或者v2进行深度遍历的时候,visitedMap会保存所有的顶点。也就意味着,v1v2不支配任何点。

Swoole AIO线程池实现协程化的思路

Swoole在实现一些不好Hook的函数的时候,采用了AIO线程池来完成协程化的工作。

它的基本工作思路如下:

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
┌──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐                                               
│ main thread │
│ │
│ │
│ ┌─────────┐ ┌─────────┐ ┌─────────┐ │
│ │ │ │ │ │ │ │◀─────────────────────────┐
│ │ co 1 │ │ co 2 │ │ co n │ │ │
│ │ │ │ │ │ │ │ │
│ └─────────┘ └─────────┘ └─────────┘ │ │
│ │ │ │ │ receive event and resume the coroutine
└───────────┼────────────────────────────────────────────────┼─────────────────────────────────────────────────┼───────────┘ │
│ │ │ │
dispatch task to pool::queue, dispatch task to pool::queue, dispatch task to pool::queue, │
then yield then yield then yield │
│ │ │ │
│ │ │ │
└────────────────────────────────────────────────┼─────────────────────────────────────────────────┘ │
│ │
│ │
│ ┌────────────────────┐
│ │ │
│ │ unix socket │
│ │ │
▼ │ │
┌────────────────────────────────────────┐ └────────────────────┘
│ │ ▲
│ ThreadPool::queue │ │
│ │ │
└────────────────────────────────────────┘ │
│ │
│ │
│ │
│ │
│ │
┌───────────────────────────────┬────────────────┴──────────────┬────────────────────────────────┐ │
│ │ │ │ │
│ │ │ │ │
pop task from pool::queue pop task from pool::queue pop task from pool::queue pop task from pool::queue │
│ │ │ │ │
│ │ │ │ │
▼ ▼ ▼ ▼ │
┌───────────────────────┐ ┌───────────────────────┐ ┌───────────────────────┐ ┌───────────────────────┐ │
│ │ │ │ │ │ │ │ │
│ AIO thread 1 │ │ AIO thread 2 │ │ AIO thread 3 │ │ AIO thread n │ │
│ │ │ │ │ │ │ │ │
│ │ │ │ │ │ │ │ │
└───────────────────────┘ └───────────────────────┘ └───────────────────────┘ └───────────────────────┘ │
│ │ │ │ │
send event send event send event send event │
└───────────────────────────────┴───────────────────────────────┴────────────────────────────────┴────────────────────────────────────────┘

大概讲一讲这个流程:

1.当一个协程执行一个不好协程化的任务的时候,就会创建一个任务,投递到线程池的queue里面,对应代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
AsyncEvent *dispatch(const AsyncEvent *request) {
if (SwooleTG.aio_schedule) {
schedule();
}
auto _event_copy = new AsyncEvent(*request);
_event_copy->task_id = current_task_id++;
_event_copy->timestamp = swoole_microtime();
_event_copy->pipe_socket = SwooleTG.aio_write_socket;
event_mutex.lock();
_queue.push(_event_copy);
_cv.notify_one();
event_mutex.unlock();
swDebug("push and notify one: %f", swoole_microtime());
return _event_copy;
}

2.投递完任务之后,挂起当前协程:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
bool async(const std::function<void(void)> &fn, double timeout) {
TimerNode *timer = nullptr;
AsyncEvent event{};
AsyncLambdaTask task{Coroutine::get_current_safe(), fn};

event.object = &task;
event.handler = async_lambda_handler;
event.callback = async_lambda_callback;

AsyncEvent *_ev = async::dispatch(&event);
if (_ev == nullptr) {
return false;
}
if (timeout > 0) {
timer = swoole_timer_add((long) (timeout * 1000), false, async_task_timeout, _ev);
}
task.co->yield();
// 省略其他代码
}

其他,async_lambda_handler会被AIO线程使用,async_lambda_callback被主线程的调度器使用。

3.当AIO线程抢到一个任务的时候,会调用async_lambda_handler,而async_lambda_handler就会去执行协程投递任务时设置的那个不好协程化的函数。

4.AIO线程执行完任务之后,通过unix socket通知主线程。此时,主线程就会执行async_lambda_callback,这个函数会resume这个任务对应的协程。然后,该协程继续往下运行。

这种方式很好用,但是,我们使用这种方式协程化的时候,需要注意一个问题,不要在任务里面去调用PHP的函数,因为这样就会让AIO线程操作ZendVM。因为主线程和AIO线程同时在修改同一个ZendVM上的数据,会导致一些内存错误。

编译器后端优化流程

用一个图来总结一下:

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
                                                   ┌───────────────────────────────┐                                                   
│ │
│ create IRGenerator │
┌────────▶│ │
│ │ │
│ └───────────────────────────────┘
│ │
│ │
│ ▼
│ ┌───────────────────────────────┐
│ │ │
│ │create cfg -- ControlFlowGraph │
│ │ │
┌───────────────────────────────┐ │ │ │
│ │ │ └───────────────────────────────┘
│ 1. init IRGenerator │ │ │
│ │────────┤ │
│ │ │ ▼
└───────────────────────────────┘ │ ┌───────────────────────────────┐
│ │ │ create entry basic block │
│ │ │ │
│ │ │ maybe we should set the block │
│ │ │ index and block name │
│ │ └───────────────────────────────┘
│ │ │
│ │ │
│ │ │
│ │ ▼
│ │ ┌───────────────────────────────┐
│ │ │ │
│ │ │ add the basic block to cfg │
│ └────────▶│ │
│ │ │
│ └───────────────────────────────┘



┌───────────────────────────────┐ ┌───────────────────────────────┐
│ │ │ │
│ 2. generate IR │ │ expressions │
│ │─────────────────▶│ │◀─┐
│ │ │ │ │
└───────────────────────────────┘ └───────────────────────────────┘ │
│ │ │
│ │ │
│ ▼ │
│ ┌───────────────────────────────┐ │
│ │ │ │
▼ │ get the current expression │ │
┌───────────────────────────────┐ │ │ │
│ │ │ │ │
│ 3. build inpred order and │ └───────────────────────────────┘ │
│ inpost order graph │ │ │
│ │ │ │
└───────────────────────────────┘ ▼ │
│ ┌───────────────────────────────┐ │ ┌───────────────────────────────┐
│ │ │ │ │ (maybe we should set up a │
│ │ change the expression to IR │ │ │ mapping between the variable │
▼ │ │──┼────▶│ and the block) │
┌───────────────────────────────┐ │ │ │ │ │
│ │ └───────────────────────────────┘ │ └───────────────────────────────┘
│ 4. build dominator tree │ │ │
│ │ │ │
│ │ │ │
└───────────────────────────────┘ ▼ │ ┌─────────────────────────────────────────┐
│ ┌───────────────────────────────┐ │ │ │
│ │ │ │ │ we should create new basic block for if
▼ │ add IR to basic block │ │ │ statement, loop condition, loop body, │
┌───────────────────────────────┐ │ │──┼────▶│then add basic block link and add to cfg.│
│ │ │ │ │ │(so we should create a branch statement) │
│ 5. insert PhiNodes │ └───────────────────────────────┘ │ │ │
│ │ │ │ │ │
│ │ │ │ └─────────────────────────────────────────┘
└───────────────────────────────┘ └──────────────────┘



┌───────────────────────────────┐
│ │
│ 6. build SSA │
│ │
│ │
└───────────────────────────────┘

什么是好的中间表示

摘抄自虎书

  1. 它必须便于语义分析阶段生成它
  2. 对于希望支持的所有目标机,它必须便于转变成真实的机器语言
  3. 便于对中间表示进行重写,因为后面可能会对中间表示进行优化

抽象语法中的复杂部分并不总是能正好与机器可以执行的复杂指令相对应。因此,中间表示中的个体成分应该只描述特别简单的事情:如单个取、存、加法或指令跳转等操作。这样,抽象语法中的任何复杂部分都可以用一组恰当的抽象机器指令来表示,而这些抽象机器指令则能形成“真正的”机器指令。

MacOS下pwrite无法O_APPEND的问题

这个问题来自Swoole的一个issue

有如下代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#include <sys/file.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <string.h>
#include <unistd.h>

int main(int argc, char const *argv[]) {
int flags = 0;
int fd;

flags = O_CREAT | O_WRONLY;
fd = open("test.txt", flags, 0644);
pwrite(fd, "first line\n", strlen("first line\n"), 0);

flags = O_APPEND | O_CREAT | O_WRONLY;
fd = open("test.txt", flags, 0644);
pwrite(fd, "second line\n", strlen("second line\n"), 0);

flags = O_APPEND | O_CREAT | O_WRONLY;
fd = open("test.txt", flags, 0644);
pwrite(fd, "third line\n", strlen("third line\n"), 0);

return 0;
}

此时,test.txt文件里面的内容是:

1
2
3
third line


我们发现,这实际上没有追加,而是覆盖了之前写入的内容。也就意味着pwriteoffsetO_APPEND没有一起起到作用。

我们换成write来测试追加:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#include <sys/file.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <string.h>
#include <unistd.h>

int main(int argc, char const *argv[]) {
int flags = 0;
int fd;

flags = O_CREAT | O_WRONLY;
fd = open("test.txt", flags, 0644);
write(fd, "first line\n", strlen("first line\n"));

flags = O_APPEND | O_CREAT | O_WRONLY;
fd = open("test.txt", flags, 0644);
write(fd, "second line\n", strlen("second line\n"));

flags = O_APPEND | O_CREAT | O_WRONLY;
fd = open("test.txt", flags, 0644);
write(fd, "third line\n", strlen("third line\n"));

return 0;
}

此时,test.txt文件里面的内容是:

1
2
3
4
first line
second line
third line

追加成功了。

Swoole Table内部结构

Swoole Table内部结构还是比较复杂的,这里做一个记录:

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
┌───────────────────┐                                                                                                                                                                
│ GlobalMemory │──────────────────▶┌────────────────────────────────────────────┐ ┌───▶───────────────────────────────┐
└───────────────────┘ │ │ │ │ TableColumn │
│ │ │ │ │
│ │ │ ├───────────────────────────────┤
┌───────────────────┐ │ │ │ │enum Type type - int, string...│
│ MemoryBlock │──────────────────▶├────────────────────────────────────────────┤ │ ├───────────────────────────────┤
├───────────────────┤ │ │ │ │ uint32_t size │
│ uint32_t size │─────────────────▶ │ size of memory │ │ ├───────────────────────────────┤
├───────────────────┤ │ │ │ │ std::string name │
│ char memory[0] │─────────┬────────▶├────────────────────────────────────────────┤ │ ├───────────────────────────────┤
└───────────────────┘ │ │ swoole::Table │ │ │ size_t index │
│ │ │ │ └───────────────────────────────┘
│ ├────────────────────────────────────────────┤ │
│ │ std::unordered_map *column_map │────┤ ┌───────────────────────────────┐
│ ├────────────────────────────────────────────┤ │ │ │
│ │ std::vector *column_list │────┘ │ memory_size = size * │
│ ├────────────────────────────────────────────┤ │ sizeof(TableRow *) │
│ │ size_t size - construct size │ │ │
│ ├────────────────────────────────────────────┤ │ memory_size += row_num * │
│ │ size_t memory_size │───────▶│(sizeof(TableRow) + item_size) │
│ ├────────────────────────────────────────────┤ │ │
│ │ size_t item_size - all column size of row │ │ │
│ ├────────────────────────────────────────────┤ │ │
│ │ sw_atomic_t row_num │ │ │
│ ├────────────────────────────────────────────┤ └───────────────────────────────┘
│ │ TableIterator *iterator │
│ ├────────────────────────────────────────────┤
│ │ Mutex *mutex │
│ ├────────────────────────────────────────────┤
│ │ void *memory - save the all rows │─────────▶───────────────────────────────┐
│ ├────────────────────────────────────────────┤ │ size * sizeof(TableRow *) │
│ │ TableRow **rows ├───┐ │ │
│ ├────────────────────────────────────────────┤ │ │ to foreach table │
│ │ │ │ ├───────────────────────────────┤
└─────────▶────────────────────────────────────────────┤ └────▶│ TableRow1 * │──┐ ┌─────────────────────────────────────────┐
│ │ ├───────────────────────────────┤ │ │ TableRow │
│ │ │ TableRow2 * │ │ │ │
│ │ ├───────────────────────────────┤ │ │ sw_atomic_t lock_ │
│ │ │ .... │ │ │ │
│ │ ├───────────────────────────────┤ │ │ uint8_t active - whether the row init │
│ │ │ row_num * (sizeof(TableRow) + │ │ │ │
│ │ │ item_size) │ │ │ uint8_t key_len │
│ │ │ │ │ │ │
│ │ ├───────────────────────────────┤ │ │ TableRow *next │
│ │ │ TableRow 1 │◀─┘ │ │
│ │ ├───────────────────────────────┤ │ char key[SW_TABLE_KEY_SIZE] │
│ │ │ TableRow 2 │ │ │
│ │ ├───────────────────────────────┤ │ char data[0] │
│ │ │ TableRow 3 │ │ │
│ │ ├───────────────────────────────┤ └─────────────────────────────────────────┘
│ │ │ TableRow ... │
│ │ └───────────────────────────────┘
│ │
└────────────────────────────────────────────┘