PHP 治理全局的办法
【相关学习引荐:php编程(视频)】
治理全局状态
在命令式说话中总是需要一些全局空间。在编程 PHP 或扩展时,我们将明白区分我们所称的恳求绑定全局变量和真正的全局变量。
恳求全局变量是处置恳求历程中需要携带和记忆信息的全局变量。一个简便的例子是,您要求会员在函数参数中供给一个值,并且但愿能够在其他函数中使用它。除了这条信息在几个 PHP 函数调取中 “保持其值” 之外,它只为当前恳求保存该值。下一个来的恳求应当什么都不知道。PHP 供给了一种机制来治理恳求全局变量,不管选中了什么样的多处置模型,我们将在本章后面具体介绍这一点。
真正的全局变量是跨恳求保存的信息片段。这些信息平常是只读的。假如您需要写入这样的全局变量作为恳求处置的一部分,那么 PHP 没法帮忙您。假如您使用 线程作为多处置模型, 您需要本人施行内存锁。假如你使用 进程作为多处置模型, 您需要使用本人的IPC(进程间通讯)。但是,在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 多进程模型会利用线程,也必需让它绑定到当前恳求中。
使用 TSRM 宏来庇护全局空间
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解决办法)。