天天看點

php協程curl,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核心源碼進行跟蹤的過程中,會涉及到一些基礎知識,如果在自行閱讀源碼中有阻礙的時候,可以補充一下相關知識:函數指針

位運算

哈希表

函數跳轉表

三. 源碼跟蹤

3.1 PHP樣例代碼

為了帶大家更好的進入這部分核心,我先提供一份swoole作者提供的PHP樣例代碼:

PHP

Copy

上面的代碼執行結果如下:

177173 bytes

177173 bytes

177173 bytes

177173 bytes

177173 bytes

177173 bytes

177173 bytes

177173 bytes

177173 bytes

177173 bytes

Shell

Copy

通過檢視上面的樣例代碼,我們可以看出Swoole運作時通過設定SWOOLE_HOOK_CURL常量來啟動curl部分的運作時協程化。

3.2 Runtime初始化跟蹤

我們首先跟蹤SwooleRuntime子產品裡的enableCoroutine部分,這個函數的主要功能就是設定swoole運作時内部哪些部分啟動協程化,我先提供這部分的關鍵代碼:

1268 static PHP_METHOD(swoole_runtime, enableCoroutine)

1269 {

1270 zval *zflags = nullptr;

1271

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

Copy

這個函數的主要功能是準備需要被核心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

Copy

上面的代碼可以生動的展現,目前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

Copy

這部分代碼比較複雜,是以我主要給大家闡述這種核心替換的主要流程,具體代碼闡述如下:

swoole在runtime内部維護了一個函數哈希表,叫做function_table。

1667-1672行:swoole核心function_table如果已經存在函數,則建構函數句柄并傳回。

1674-1678行:從PHP核心全局函數表中查找是否已經存在函數,如果不存在則傳回。

1680-1700行:根據從核心函數表中查詢到的函數名,構造函數模闆,新的函數名已swoole_開頭。

1701行:向swoole核心的函數表中添加函數模闆,函數名為核心函數名,函數棧模闆使用swoole核心建立的。