管理全局状态

命令式语言总是需要一些全局空间。在编写 PHP 或扩展程序时,我们将明确区分所谓的 请求绑定全局变量真正的全局变量

请求全局变量 是您在处理请求的过程中需要携带和记忆信息的全局变量。一个简单的例子是,您要求用户在函数参数中提供一个值,并且您希望能够在其他函数中使用它。除了这条信息在多个 PHP 函数调用中 “保持其值” 这一事实之外,它只为当前请求保留该值。下一个请求应该对此一无所知。无论选择了哪种多处理模型,PHP 都提供了一种管理 请求全局变量 的机制,我们将在本章后面详细介绍。

真正的全局变量 是跨请求保留的信息。这些信息通常是只读的。如果您需要在请求处理过程中写入此类全局变量,那么 PHP 无法帮助您。如果您使用 线程作为多处理模型,则需要在您这边执行内存锁定。如果您使用 进程作为多处理模型,则需要在您这边使用您自己的 IPC(进程间通信)。然而,在 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() 确定全局的大小,然后是 GINITGSHUTDOWN 钩子。然后,为了关闭结构,我们使用了 STANDARD_MODULE_PROPERTIES_EX 而不是 STANDARD_MODULE_PROPERTIES。只需以正确的方式完成结构,请参见 ?:

#define STANDARD_MODULE_PROPERTIES \
    NO_MODULE_GLOBALS, NULL, STANDARD_MODULE_PROPERTIES_EX

GINIT 函数中,你会得到一个指向全局文件当前内存位置的指针。你可以用它来初始化你的全局项。在这里,我们将随机值设为 0(其实没什么用,但还是接受它吧)。

不要在 GINIT 中使用 PIB_G() 宏。使用给定的指针。

对于当前进程,GINIT()MINIT() 之前启动。如果是 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)
{
    /* 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 变量 moreless。这些字符串不需要像之前那样,在使用时随时创建和销毁。它们是不可变的字符串,只要保持不可变(又称:只读),就可以分配一次,并在需要时随时重复使用。我们在 MINIT() 中使用 zend_string_init() 中的持久化分配来初始化这两个字符串,我们现在就预先计算它们的哈希值(而不是让第一个请求来计算),我们告诉 zval 垃圾收集器这些字符串是内部的,这样它就永远不会试图销毁它们(不过,如果它们被用作写操作的一部分,比如连接,它可能需要复制它们)。显然,我们不会忘记在 MSHUTDOWN() 中销毁这些字符串。

然后在 MINIT() 中,我们探测 PIB_RAND_MAX 环境并将其用作随机数选择的最大范围值。由于我们使用无符号整数,并且我们知道 strtoull() 不会抱怨负数(因此会因为符号不匹配而绕过整数边界),我们只是避免使用负数(经典的 libc 解决方法)。