管理全局状态
命令式语言总是需要一些全局空间。在编写 PHP 或扩展程序时,我们将明确区分所谓的 请求绑定全局变量 和 真正的全局变量。
请求全局变量 是您在处理请求的过程中需要携带和记忆信息的全局变量。一个简单的例子是,您要求用户在函数参数中提供一个值,并且您希望能够在其他函数中使用它。除了这条信息在多个 PHP 函数调用中 “保持其值” 这一事实之外,它只为当前请求保留该值。下一个请求应该对此一无所知。无论选择了哪种多处理模型,PHP 都提供了一种管理 请求全局变量 的机制,我们将在本章后面详细介绍。
管理请求全局
这是一个使用请求全局的简单扩展的简单示例:
/* true C global */
static zend_long rnd = 0;
static void pib_rnd_init(void)
{
/* Pick a number to guess between 0 and 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) {
/* Reset the number to guess */
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 全局变量将在服务器的每个线程之间共享。对于我们上面的例子,将会发生的情况是,Web 服务器的每个并行用户将共享相同的数字。有些人可能会在执行过程中重置该数字,而其他人则会尝试猜测它。简而言之,您在这里清楚地了解了线程的竞争问题。
我们需要将一个数据持久化到同一个请求中,但我们需要将其绑定到当前请求,即使 PHP 运行的多处理模型使用线程。
使用 TSRM 宏来保护全局空间
PHP 设计了一个层来帮助扩展和核心开发人员处理请求全局变量。该层称为 TSRM(线程安全资源管理器),并以一组宏的形式公开,您必须在需要访问请求绑定全局变量(读写访问)时使用这些宏。
在后台,如果多处理模型使用进程,这些宏将解析为类似于我们上面显示的代码。正如我们所见,如果没有使用线程,则上述代码完全有效。因此,当使用进程时,我们稍后将看到的宏将扩展为类似的内容。
您首先需要做的是声明一个结构,该结构将成为所有全局变量的根:
ZEND_BEGIN_MODULE_GLOBALS(pib)
zend_long rnd;
ZEND_END_MODULE_GLOBALS(pib)
/* Resolved as :
*
* typedef struct _zend_pib_globals {
* zend_long rnd;
* } zend_pib_globals;
*/
然后,就可以创建这样一个真正的全局变量:
ZEND_DECLARE_MODULE_GLOBALS(pib)
/* Resolved as 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
函数中,你会得到一个指向全局文件当前内存位置的指针。你可以用它来初始化你的全局项。在这里,我们将随机值设为 0(其实没什么用,但还是接受它吧)。
不要在 GINIT 中使用 |
对于当前进程, |
|
完整例子
以下是更高级的完整示例。如果玩家获胜,其得分(尝试次数)将添加到可从用户空间获取的得分数组中。没什么难的,得分数组在请求启动时初始化,然后在玩家获胜时使用,并在当前请求结束时清除:
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)
{
/* reset current score as well */
PIB_G(cur_score) = 0;
php_random_int(0, 100, &PIB_G(rnd), 0);
}
PHP_GINIT_FUNCTION(pib)
{
/* ZEND_SECURE_ZERO is a memset(0). Could resolve to bzero() as well */
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, /* Function entries */
NULL, /* Module init */
NULL, /* Module shutdown */
PHP_RINIT(pib), /* Request init */
PHP_RSHUTDOWN(pib), /* Request shutdown */
NULL, /* Module information */
"0.1", /* Replace with version number for your extension */
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_ADD_FLAGS(*result, IS_STR_INTERNED);
}
static void pib_rnd_init(void)
{
/* reset current score as well */
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
。这些字符串不需要像之前那样,在使用时随时创建和销毁。它们是不可变的字符串,只要保持不可变(又称:只读),就可以分配一次,并在需要时随时重复使用。我们在 MINIT()
中使用 zend_string_init()
中的持久化分配来初始化这两个字符串,我们现在就预先计算它们的哈希值(而不是让第一个请求来计算),我们告诉 zval
垃圾收集器这些字符串是内部的,这样它就永远不会试图销毁它们(不过,如果它们被用作写操作的一部分,比如连接,它可能需要复制它们)。显然,我们不会忘记在 MSHUTDOWN()
中销毁这些字符串。
然后在 MINIT()
中,我们探测 PIB_RAND_MAX
环境并将其用作随机数选择的最大范围值。由于我们使用无符号整数,并且我们知道 strtoull()
不会抱怨负数(因此会因为符号不匹配而绕过整数边界),我们只是避免使用负数(经典的 libc
解决方法)。