#在指令式語言中總是需要一些全域空間。在編程 PHP 或擴充時,我們將明確區分我們所稱的請求綁定全域變數和真正的全域變數。
請求全域變數是處理請求過程中需要攜帶和記憶訊息的全域變數。一個簡單的例子是,您要求使用者在函數參數中提供一個值,並且希望能夠在其他函數中使用它。除了這條資訊在幾個 PHP 函數呼叫中 “保持其值” 之外,它只為當前請求保留該值。下一個來的請求應該什麼都不知道。 PHP 提供了一種機制來管理請求全域變量,不管選擇了什麼樣的多處理模型,我們將在本章後面詳細介紹這一點。
真正的全域變數是跨請求保留的資訊片段。這些資訊通常是唯讀的。如果您需要寫入這樣的全域變數作為請求處理的一部分,那麼 PHP 無法幫助您。如果您使用 線程作為多處理模型, 您需要自己執行記憶體鎖定。如果你使用 進程作為多處理模型, 您需要使用自己的IPC(進程間通訊)。但是,在PHP擴展編程中不應該出現這種情況。
相關學習推薦:PHP程式設計從入門到精通
下面是一個使用請求全域的簡單擴充範例:
/* 真正的 C 全局 */ static zend_long rnd = 0; static void pib_rnd_init(void) { /* 在 0 到 100 之间随机一个数字 */ php_random_int(0, 100, &rnd, 0); } PHP_RINIT_FUNCTION(pib) { pib_rnd_init(); return SUCCESS; } PHP_FUNCTION(pib_guess) { zend_long r; if (zend_parse_parameters(ZEND_NUM_ARGS(), "l", &r) == FAILURE) { return; } if (r == rnd) { /* 将数字重置以进行猜测 */ pib_rnd_init(); RETURN_TRUE; } if (r < rnd) { RETURN_STRING("more"); } RETURN_STRING("less"); } PHP_FUNCTION(pib_reset) { if (zend_parse_parameters_none() == FAILURE) { return; } pib_rnd_init(); }
如你所見,這個擴充在請求開始時挑選一個隨機整數數,之後透過pib_guess()
可以試著猜到這個陣列。一旦猜到,該數字將重置。如果使用者想要手動重置數字,它也可以自己手動呼叫pib_reset()
去重置數值。
該隨機數以一個 C 全域變數實作。如果 PHP 在進程中作為多進程模型的一部分使用不再是個問題,如果之後使用線程,這是不行的。
注意
作為提醒,你無需掌握將要使用哪種多進程模型。當你設計擴充時,你必須為這兩種模型做好準備。
當使用線程,會針對伺服器中的每個線程共享一個 C 全域變數。例如我們上面的例子,網路伺服器的每個並行用戶將共享同一個數值。有些可能會一開始就重置數值,而有些則嘗試去猜測它。簡而言之,你清楚地了解了線程的關鍵問題。
我們必須持久化資料到同一請求,即使運行 PHP 多進程模型會利用線程,也必須讓它綁定到目前請求中。
PHP 設計了可以幫助擴充功能和核心開發人員處理全域請求的層。該層稱為TSRM (線程安全資源管理) ,並且作為一組巨集公開,你必須在任何需要存取請求綁定全域(讀取和寫入)的時候使用該巨集。
在多進程模型使用流程的情況下,在後台,這些巨集將解析為類似我們上面顯示的程式碼。如我們所見,如果不適用線程,上面的程式碼是完全有效的。所以,當使用進程時,這些巨集將會擴展為類似的巨集。
首先你要要做的就是宣告一個結構,它將是你所有全域變數的根:
ZEND_BEGIN_MODULE_GLOBALS(pib) zend_long rnd; ZEND_END_MODULE_GLOBALS(pib) /* 解析为 : * * typedef struct _zend_pib_globals { * zend_long rnd; * } zend_pib_globals; */
然後,建立一個這樣的全域變數:
ZEND_DECLARE_MODULE_GLOBALS(pib) /* 解析为 zend_pib_globals pib_globals; */
現在,你可以使用全域巨集存取器存取資料。這個巨集是由框架創建的,它應該在你的 php_pib.h 頭檔定義。這看起來是這樣的:
#ifdef ZTS #define PIB_G(v) ZEND_MODULE_GLOBALS_ACCESSOR(pib, v) #else #define PIB_G(v) (pib_globals.v) #endif
如你所見,如果沒有啟用ZTS 模式,即編譯非線程安全的PHP 和擴展(我們稱之為NTS模式:非線程安全),巨集只是解析到結構中聲明的資料。因此,有以下變更:
static void pib_rnd_init(void) { php_random_int(0, 100, &PIB_G(rnd), 0); } PHP_FUNCTION(pib_guess) { zend_long r; if (zend_parse_parameters(ZEND_NUM_ARGS(), "l", &r) == FAILURE) { return; } if (r == PIB_G(rnd)) { pib_rnd_init(); RETURN_TRUE; } if (r < PIB_G(rnd)) { RETURN_STRING("more"); } RETURN_STRING("less"); }
注意
當使用一個行程模型,TSRM 巨集解析為對 C 全域變數的存取。
當使用執行緒時,也就是當你編譯 ZTS PHP,事情變得更複雜。然後,我們看到的所有巨集都解析為一些完全不同的東西,在這裡很難解釋。基本上,當使用 ZTS 編譯時,TSRM 使用 TLS(執行緒本地儲存)執行了一項艱難的工作。
注意
簡而言之,當在 ZTS 編譯時,全域變數將綁定到目前執行緒。而在 NTS 編譯時,全域變數將綁定到目前進程上。 TSRM 巨集處理這項艱難的工作。你可能對運作方式感興趣,瀏覽 PHP 原始碼的/TSRM 目錄以了解更多關於 PHP 線程安全。
有时,可能需要将全局变量初始化为一些默认值,通常为零。引擎帮助下的TSRM系统提供了一个钩子来为您的全局变量提供默认值,我们称之为GINIT。
注意
关于 PHP 挂钩的完整信息,请参考 PHP 生命周期章节。
让我们将随机值设为零:
PHP_GSHUTDOWN_FUNCTION(pib) { } PHP_GINIT_FUNCTION(pib) { pib_globals->rnd = 0; } zend_module_entry pib_module_entry = { STANDARD_MODULE_HEADER, "pib", NULL, NULL, NULL, NULL, NULL, NULL, "0.1", PHP_MODULE_GLOBALS(pib), PHP_GINIT(pib), PHP_GSHUTDOWN(pib), NULL, /* PRSHUTDOWN() */ STANDARD_MODULE_PROPERTIES_EX };
我们选择仅显示 zend_module_entry
(和其他 NULL
)的相关部分。如你所见,全局管理挂钩发生在结构的中间。首先是PHP_MODULE_GLOBALS()
来确定全局变量的大小,然后是我们的 GINIT
和 GSHUTDOWN
钩子。然后我们使用了STANDARD_MODULE_PROPERTIES_EX
关闭结构,而不是STANDARD_MODULE_PROPERTIES
。只需以正确的方式完成结构即可,请参阅?:
#define STANDARD_MODULE_PROPERTIES NO_MODULE_GLOBALS, NULL, STANDARD_MODULE_PROPERTIES_EX
在GINIT
函数中,你传递了一个指向全局变量当前存储位置的指针。你可以使用它来初始化全局变量。在这里,我们将零放入随机值(虽然不是很有用,但我们接受它)。
警告
不要在 GINIT 中使用
PIB_G()
宏。使用你得到的指针。注意
对于当前进程,在
MINIT()
之前启动了GINIT()
。如果是 NTS,就这样而已。 如果是 ZTS,线程库产生的每个新线程都会额外调用GINIT()
。警告
GINIT()
不作为RINIT()
的一部分被调用。如果你需要在每次新请求时清除全局变量,则需要像在本章所示示例中所做的那样手动进行。
这是一个更高级的完整示例。如果玩家获胜,则将其得分(尝试次数)添加到可以从用户区获取的得分数组中。没什么难的,得分数组在请求启动时初始化,然后在玩家获胜时使用,并在当前请求结束时清除:
ZEND_BEGIN_MODULE_GLOBALS(pib) zend_long rnd; zend_ulong cur_score; zval scores; ZEND_END_MODULE_GLOBALS(pib) ZEND_DECLARE_MODULE_GLOBALS(pib) static void pib_rnd_init(void) { /* 重置当前分数 */ PIB_G(cur_score) = 0; php_random_int(0, 100, &PIB_G(rnd), 0); } PHP_GINIT_FUNCTION(pib) { /* ZEND_SECURE_ZERO 是 memset(0)。也可以解析为 bzero() */ ZEND_SECURE_ZERO(pib_globals, sizeof(*pib_globals)); } ZEND_BEGIN_ARG_INFO_EX(arginfo_guess, 0, 0, 1) ZEND_ARG_INFO(0, num) ZEND_END_ARG_INFO() PHP_RINIT_FUNCTION(pib) { array_init(&PIB_G(scores)); pib_rnd_init(); return SUCCESS; } PHP_RSHUTDOWN_FUNCTION(pib) { zval_dtor(&PIB_G(scores)); return SUCCESS; } PHP_FUNCTION(pib_guess) { zend_long r; if (zend_parse_parameters(ZEND_NUM_ARGS(), "l", &r) == FAILURE) { return; } if (r == PIB_G(rnd)) { add_next_index_long(&PIB_G(scores), PIB_G(cur_score)); pib_rnd_init(); RETURN_TRUE; } PIB_G(cur_score)++; if (r < PIB_G(rnd)) { RETURN_STRING("more"); } RETURN_STRING("less"); } PHP_FUNCTION(pib_get_scores) { if (zend_parse_parameters_none() == FAILURE) { return; } RETVAL_ZVAL(&PIB_G(scores), 1, 0); } PHP_FUNCTION(pib_reset) { if (zend_parse_parameters_none() == FAILURE) { return; } pib_rnd_init(); } static const zend_function_entry func[] = { PHP_FE(pib_reset, NULL) PHP_FE(pib_get_scores, NULL) PHP_FE(pib_guess, arginfo_guess) PHP_FE_END }; zend_module_entry pib_module_entry = { STANDARD_MODULE_HEADER, "pib", func, /* 函数入口 */ NULL, /* 模块初始化 */ NULL, /* 模块关闭 */ PHP_RINIT(pib), /* 请求初始化 */ PHP_RSHUTDOWN(pib), /* 请求关闭 */ NULL, /* 模块信息 */ "0.1", /* 替换为扩展的版本号 */ PHP_MODULE_GLOBALS(pib), PHP_GINIT(pib), NULL, NULL, STANDARD_MODULE_PROPERTIES_EX };
这里必须要注意的是,如果你希望在请求之间保持分数,PHP 不提供任何便利。而是需要一个持久的共享存储,例如文件,数据库,某些内存区域等。PHP 的设计目的不是将信息持久存储在其内部的请求,因此它不提供这么做,但它提供了实用程序来访问请求绑定的全局空间,如我们所示。
然后,很容易地在RINIT()
中初始化一个数组,然后在RSHUTDOWN()
中销毁它。请记住,array_init
创建一个zend_array 并放入一个 zval。但这是免分配的,不要担心分配用户无法使用的数组(因此浪费分配),array_init()
非常廉价 (阅读源代码)。
当我们将这样的数组返回给用户时,我们不会忘记增加其引用计数(在 RETVAL_ZVAL
中),因为我们在扩展中保留了对此类数组的引用。
真实全局变量是非线程保护的真实C全局变量。有时可能会需要它们。但是请记住主要规则:在处理请求时,不能安全地写入此类全局变量。因此,通常在 PHP 中,我们需要此类变量并将其用作只读变量。
请记住,在 PHP 生命周期的MINIT()
或MSHUTDOWN()
步骤中编写真实全局变量是绝对安全的。但是不能在处理请求时给他们写入值(但可以从他们那里读取)。
因此,一个简单的示例是你想要读取环境值以对其进行处理。此外,初始化持久性的 zend_string并在之后处理某些请求时加以利用是很常见的。
这是介绍真实全局变量的修补示例,我们仅显示与先前代码的差异,而不显示完整代码:
static zend_string *more, *less; static zend_ulong max = 100; static void register_persistent_string(char *str, zend_string **result) { *result = zend_string_init(str, strlen(str), 1); zend_string_hash_val(*result); GC_FLAGS(*result) |= IS_INTERNED; } static void pib_rnd_init(void) { /* 重置当前分数 */ PIB_G(cur_score) = 0; php_random_int(0, max, &PIB_G(rnd), 0); } PHP_MINIT_FUNCTION(pib) { char *pib_max; register_persistent_string("more", &more); register_persistent_string("less", &less); if (pib_max = getenv("PIB_RAND_MAX")) { if (!strchr(pib_max, '-')) { max = ZEND_STRTOUL(pib_max, NULL, 10); } } return SUCCESS; } PHP_MSHUTDOWN_FUNCTION(pib) { zend_string_release(more); zend_string_release(less); return SUCCESS; } PHP_FUNCTION(pib_guess) { zend_long r; if (zend_parse_parameters(ZEND_NUM_ARGS(), "l", &r) == FAILURE) { return; } if (r == PIB_G(rnd)) { add_next_index_long(&PIB_G(scores), PIB_G(cur_score)); pib_rnd_init(); RETURN_TRUE; } PIB_G(cur_score)++; if (r < PIB_G(rnd)) { RETURN_STR(more); } RETURN_STR(less); }
在这里我们创建了两个 zend_string 变量 more
和 less
。这些字符串不需要像以前一样在使用时立即创建和销毁。这些是不可变的字符串,只要保持不变,就可以分配一次并在需要的任何时间重复使用(即只读)。我们在zend_string_init()
中使用持久分配,在MINIT()
中初始化这两个字符串,我们现在预先计算其哈希值(而不是先执行第一个请求),并且我们告诉 zval 垃圾收集器,这些字符串已被扣留,因此它将永远不会尝试销毁它们(但是,如果将它们用作写操作(例如连接)的一部分,则可能需要复制它们)。显然我们不会忘记在MSHUTDOWN()
中销毁这些字符串。
然後在MINIT()
中我們探查一個PIB_RAND_MAX
環境,並將其用作隨機數選擇的最大範圍值。由於我們使用無符號整數,並且我們知道strtoull()
不會抱怨負數(因此將整數範圍包裹為符號不匹配),我們只是避免使用負數(經典的libc解決方法)。
以上是php之管理全域狀態的詳細內容。更多資訊請關注PHP中文網其他相關文章!