源码分析:Swoole协程版curl源码剖析

一. 背景描述

2019年6月5日,Swoole作者宣布在4.4版本后开始初步支持协程版本的Curl。我在原来的研究分析中指出PHP内核对于curl部分的支持是建立在libcurl库上,swoole作者也表达因为PHP内核建立在libcurl基础上,造成了swoole无法直接对于libcurl内部的socket操作进行钩子化。那么,最新版的swoole是采用什么思路实现curl的协程化。
总体实现思路 : LibCurl的Socket操作不能Hook,那就Hook住PHP内核的curl函数组。

二. 知识背景

由于对于Swoole内核源码进行跟踪的过程中,会涉及到一些基础知识,如果在自行阅读源码中有阻碍的时候,可以补充一下相关知识:
  1. 函数指针
  2. 位运算
  3. 哈希表
  4. 函数跳转表

三. 源码跟踪

3.1 PHP样例代码

为了带大家更好的进入这部分内核,我先提供一份swoole作者提供的PHP样例代码:
<?php Swoole\Runtime::enableCoroutine(SWOOLE_HOOK_ALL); Swoole\Runtime::enableCoroutine(SWOOLE_HOOK_CURL); $n = 10; while($n) { go(function () { $ch = curl_init(); curl_setopt($ch, CURLOPT_URL, “http://www.xinhuanet.com/”); curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1); curl_setopt($ch, CURLOPT_HEADER, 0); $output = curl_exec($ch); if ($output === FALSE) { echo “CURL Error:” . curl_error($ch); } curl_close($ch); echo strlen($output) . ” bytes\n”; }); } ?>
PHP
上面的代码执行结果如下:
177173 bytes
177173 bytes
177173 bytes
177173 bytes
177173 bytes
177173 bytes
177173 bytes
177173 bytes
177173 bytes
177173 bytes
Shell
通过查看上面的样例代码,我们可以看出Swoole运行时通过设置SWOOLE_HOOK_CURL常量来启动curl部分的运行时协程化。

3.2 Runtime初始化跟踪

我们首先跟踪SwooleRuntime模块里的enableCoroutine部分,这个函数的主要功能就是设置swoole运行时内部哪些部分启动协程化,我先提供这部分的关键代码:
1268 static PHP_METHOD(swoole_runtime, enableCoroutine) 1269 { 1270 zval *zflags = nullptr; 1271 /*TODO: enable SW_HOOK_CURL by default after curl handler completed */ 1272 zend_long flags = SW_HOOK_ALL ^ SW_HOOK_CURL; 1273 1274 ZEND_PARSE_PARAMETERS_START(0, 2) 1275 Z_PARAM_OPTIONAL 1276 Z_PARAM_ZVAL(zflags) // or zenable 1277 Z_PARAM_LONG(flags) 1278 ZEND_PARSE_PARAMETERS_END_EX(RETURN_FALSE); 1279 1280 if (zflags) 1281 { 1282 if (Z_TYPE_P(zflags) == IS_LONG) 1283 { 1284 flags = SW_MAX(0, Z_LVAL_P(zflags)); 1285 } 1286 else if (ZVAL_IS_BOOL(zflags)) 1287 { 1288 if (!Z_BVAL_P(zflags)) 1289 { 1290 flags = 0; 1291 } 1292 } 1293 else 1294 { 1295 const char *space, *class_name = get_active_class_name(&space); 1296 zend_type_error(“%s%s%s() expects parameter %d to be %s, %s given”, class_name, space, get_active_function_name(), 1, “bool or long”, zend_zval_type_name(zflags)); 1297 } 1298 } 1299 1300 RETURN_BOOL(PHPCoroutine::enable_hook(flags)); 1301 }
C
这个函数的主要功能是准备需要被内核HOOK的模块常量,也就是代码中的flags参数,相关具体解释如下:
  • 1272行:从SW_HOOK_ALL常量中去除SW_HOOK_CURL常量所标志的bit位,代表默认不开启Curl部分
  • 1274-1278行:解析PHP代码中函数调用传进的参数,参数数量最小为0、最大为2,均为可选参数。
  • 1280-1298行:解析zflags类型,如果是布尔的false则把flags设置为0从而关闭所有协程模块。
  • 1300行:这是非常关键的一行,这一行是在准备好flags参数之后进行协程相关函数钩子的设置。

3.3 协程钩子跟踪

Swoole的协程是一个异步并发模型,但是PHP内核的很多函数是同步模式,而且不能在并发模型中达到线程安全,所以swoole底层设计了钩子机制来拦截这些内核函数,把这些内核函数做到无缝运行时替换,从而把一些同步操变成协程调度的异步IO。
这里,我先把关键的代码贴出来,目前只贴关于这次添加的Curl部分。
1209 if (flags & SW_HOOK_CURL) 1210 { 1211 if (!(hook_flags & SW_HOOK_CURL)) 1212 { 1213 replace_internal_function(ZEND_STRL(“curl_init”)); 1214 replace_internal_function(ZEND_STRL(“curl_setopt”)); 1215 replace_internal_function(ZEND_STRL(“curl_exec”)); 1216 replace_internal_function(ZEND_STRL(“curl_setopt_array”)); 1217 replace_internal_function(ZEND_STRL(“curl_error”)); 1218 replace_internal_function(ZEND_STRL(“curl_getinfo”)); 1219 replace_internal_function(ZEND_STRL(“curl_errno”)); 1220 replace_internal_function(ZEND_STRL(“curl_close”)); 1221 replace_internal_function(ZEND_STRL(“curl_reset”)); 1222 } 1223 }
C
上面的代码可以生动的展现,目前swoole针对了9种PHP内核的Curl函数做了内核替换,这些函数在运行时都将不再走PHP内核的函数模板,而是走Swoole内核自定义的内核函数。

3.4 内核钩子实现跟踪

在上一节中,我们已经看到swoole内核会把相关内核函数做runtime替换,接下来我来仔细分析这部分内核替换原理,我先贴出核心代码:
1665 static void replace_internal_function(const char *name, size_t l_name) 1666 { 1667 real_func *rf = (real_func *) zend_hash_str_find_ptr(function_table, name, l_name); 1668 if (rf) 1669 { 1670 rf->function->internal_function.handler = PHP_FN(_user_func_handler); 1671 return; 1672 } 1673 1674 zend_function *zf = (zend_function *) zend_hash_str_find_ptr(EG(function_table), name, l_name); 1675 if (zf == nullptr) 1676 { 1677 return; 1678 } 1679 1680 rf = (real_func *) emalloc(sizeof(real_func)); 1681 char func[128]; 1682 memcpy(func, ZEND_STRL(“swoole_”)); 1683 memcpy(func + 7, zf->common.function_name->val, zf->common.function_name->len); 1684 1685 ZVAL_STRINGL(&rf->name, func, zf->common.function_name->len + 7); 1686 1687 char *func_name; 1688 zend_fcall_info_cache *func_cache = (zend_fcall_info_cache *) emalloc(sizeof(zend_fcall_info_cache)); 1689 if (!sw_zend_is_callable_ex(&rf->name, NULL, 0, &func_name, NULL, func_cache, NULL)) 1690 { 1691 swoole_php_fatal_error(E_ERROR, “function ‘%s’ is not callable”, func_name); 1692 return; 1693 } 1694 efree(func_name); 1695 1696 rf->function = zf; 1697 rf->ori_handler = zf->internal_function.handler; 1698 zf->internal_function.handler = PHP_FN(_user_func_handler); 1699 rf->fci_cache = func_cache; 1700 1701 zend_hash_add_ptr(function_table, zf->common.function_name, rf); 1702 } 1779 static PHP_FUNCTION(_user_func_handler) 1780 { 1781 zend_fcall_info fci; 1782 fci.size = sizeof(fci); 1783 fci.object = NULL; 1784 fci.function_name = {{0}}; 1785 fci.retval = return_value; 1786 fci.param_count = ZEND_NUM_ARGS(); 1787 fci.params = ZEND_CALL_ARG(execute_data, 1); 1788 fci.no_separation = 1; 1789 1790 real_func *rf = (real_func *) zend_hash_find_ptr(function_table, execute_data->func->common.function_name); 1791 zend_call_function(&fci, rf->fci_cache); 1792 } 1793
C
这部分代码比较复杂,所以我主要给大家阐述这种内核替换的主要流程,具体代码阐述如下:
  • swoole在runtime内部维护了一个函数哈希表,叫做function_table。
  • 1667-1672行:swoole内核function_table如果已经存在函数,则构建函数句柄并返回。
  • 1674-1678行:从PHP内核全局函数表中查找是否已经存在函数,如果不存在则返回。
  • 1680-1700行:根据从内核函数表中查询到的函数名,构造函数模板,新的函数名已swoole_开头。
  • 1701行:向swoole内核的函数表中添加函数模板,函数名为内核函数名,函数栈模板使用swoole内核创建的。
通过上面的操作,就可以把php内核中curl相关的内核函数替换为swoole内部的函数,函数名是关键,swoole内核替换的函数名为swoole_开头。

四. 巧妙的内核函数替换

接下来,我们会讲解一下Swoole对于这部分的内核函数替换机制,并讲解Swoole利用eval巧妙加载字符串代码,通过这种机制达到函数的移花接木效果。

4.1 函数哈希表

Swoole内核定义了一个函数哈希表,这个哈希表被广泛运用于runtime部分,php内核函数hook都是借助这个哈希表,哈希表的关键代码如下:
static zend_array *function_table = nullptr; //定义模块全局变量 1244 bool PHPCoroutine::inject_function() 1245 { 1251 function_table = (zend_array*) emalloc(sizeof(zend_array)); //分配哈希表内存 1252 zend_hash_init(function_table, 8, NULL, NULL, 0);//哈希表初始化 1260 return true; 1261 }
C
关键点:PHPCoroutine::inject_function在runtime的create阶段被调用。

4.2 内核PHP代码存储

通过内核源码的分析,我们可以看到相关内核函数都已经替换为swoole_开头的函数,那么问题来了?这样打头的函数在哪真正定义的呢?
答案是:swoole通过在内核中存储PHP源代码,并通过zend::eval==>本质是PHP内核zend_eval_stringl进行运行时加载,PHP内核会把内核中存储的PHP源代码进行编译,并把相关函数名称注册进相关内核区域。
下面我们拿swoole_curl_error做样例,这函数替换了PHP内核的curl_error函数,这部分源码在php_swoole_library.h文件中
403 “\n” 404 “function swoole_curl_error(swoole_curl_handler $obj): string\n” 405 “{\n” 406 ” return $obj->errMsg;\n” 407 “}\n” 408 “\n”
C

4.3 内核PHP代码转化

上面那段PHP代码写在swoole内核字符串常量中,随后swoole内核调用类似eval函数编译并执行代码。
1583 1584 static void php_swoole_load_library() 1585 { 1588 zend::eval(swoole_library_source_ext_curl, “@swoole-src/library/ext/curl.php”); 1596 }
C
Swoole内核的 zend::eval实际上调用的是PHP内核的zend_eval_stringl函数,这个函数的关键点如下:
ZEND_API int zend_eval_stringl(char *str, size_t str_len, zval *retval_ptr, char *string_name) /* {{{ */ { ... new_op_array = zend_compile_string(&pv, string_name); // 这个是把php代码编译成为opcode的过程 ... zend_execute(new_op_array, &local_retval); // 这个是具体的执行过程,执行opcode,把结果存储到local_retval中 ... retval = SUCCESS; return retval; }
C

五. 总结

Swoole内核借助了PHP内核curl库对外暴露的函数接口,通过移花接木的手段,把函数的模板替换为 swoole_ 打头的函数。而这些待替换的内核函数都是通过swoole内核的eval把PHP代码块加载到内核运行时空间里,所以目前的协程版curl功能会比较少,而且仍需依赖php内核的curl库。
开源贡献:协助swoole社区修复了协程版本curl中的curl_error指针造成的内核崩溃问题。
素材猫为您提供网站源码,为中小站长服务。
素材猫 » 源码分析:Swoole协程版curl源码剖析

发表评论