Управление глобальным состоянием¶
В императивных языках всегда существует потребность в некотором глобальном пространстве. Работая с PHP или расширениями, мы будем проводить чёткое различие между тем, что мы называем глобальными переменными, привязанными к запросу, и истинными глобальными переменными.
Глобальные переменные запроса — это глобальные переменные, которые нужны, чтобы переносить и запоминать информацию в процессе обработки запроса. Простой пример — вы просите пользователя передать значение в качестве аргумента функции, и хотите иметь возможность использовать это значение позже в других функциях. Помимо того, что эта информация «сохраняет своё значение» на протяжении нескольких вызовов функций PHP, она сохраняет это значение только для текущего запроса. Следующий запрос ничего о ней не должен знать. PHP предоставляет механизм управления глобальными переменными запроса независимо от выбранной модели многопроцессности, и мы рассмотрим это подробнее далее в этой главе.
Истинные глобальные переменные — это информация, которая сохраняется между запросами. Как правило, эта информация доступна только для чтения. Если вам нужно записывать в такие глобальные переменные в процессе обработки запроса, PHP здесь не поможет. Если вы используете потоки как модель многопроцессности, вам нужно будет самостоятельно выполнять блокировки памяти. Если вы используете процессы как модель многопроцессности, вам нужно будет самостоятельно использовать свой собственный IPC (Inter Process Communication — межпроцессное взаимодействие). Однако такие случаи никогда не должны возникать при программировании расширений 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 используется в процессах в рамках модели многопроцессности, это недопустимо, если та использует потоки.
Note
Напомним, что вы не управляете тем, какая модель многопроцессности будет использоваться. При проектировании расширений вы должны быть готовы к обеим моделям.
Когда используются потоки, такая истинная глобальная переменная C является общей для каждого потока сервера. В нашем примере выше это означает, что все параллельные пользователи веб-сервера будут делить одно и то же число. Одни могут сбросить число в тот момент, когда другие пытаются его угадать. Короче говоря, здесь становится ясна принципиальная проблема потоков.
Нам нужно сохранять один фрагмент данных в рамках одного и того же запроса, но нам нужно, чтобы он был привязан к текущему запросу, даже если модель многопроцессности, в которой работает PHP, использует потоки.
Использование макросов TSRM для защиты глобального пространства¶
В PHP разработан слой, который помогает разработчикам расширений и ядра работать с глобальными переменными запроса. Этот слой называется TSRM (Thread Safe Resource Manager) и предоставляется в виде набора макросов, которые вы должны использовать каждый раз, когда вам нужен доступ к глобальной переменной, привязанной к запросу (доступ на чтение и на запись).
За кулисами эти макросы разворачиваются в нечто похожее на код, который мы показали выше, в случае, если модель многопроцессности использует процессы. Как мы видели, приведённый выше код полностью корректен, если потоки не используются. Таким образом, когда используются процессы, макросы, которые мы вскоре увидим, разворачиваются в нечто подобное.
Первое, что нужно сделать — объявить структуру, которая будет корнем всех ваших глобальных переменных:
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: Non-Thread-Safe), макрос просто разворачивается в данные, объявленные в вашей структуре. Отсюда следуют такие изменения:
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");
}
Note
При использовании модели процессов макросы TSRM просто разворачиваются в обращение к истинной глобальной переменной C.
Всё становится гораздо сложнее, когда используются потоки, то есть когда вы компилируете PHP с ZTS. Все рассмотренные нами макросы в этом случае разворачиваются в нечто совершенно иное, и объяснить это здесь довольно сложно. В общих чертах происходит следующее: TSRM выполняет сложную работу, используя TLS (Thread Local Storage), когда PHP скомпилирован с ZTS.
Note
Одним словом, при компиляции в ZTS глобальные переменные привязываются к текущему потоку, тогда как при компиляции в NTS глобальные переменные привязываются к текущему процессу. Макросы TSRM берут на себя всю сложную работу. Если вас интересует, как это работает внутри, загляните в каталог /TSRM исходного кода PHP, чтобы узнать больше о потокобезопасности в PHP.
Использование хуков глобальных переменных в расширениях¶
Иногда может понадобиться, чтобы ваши глобальные переменные инициализировались каким-то значением по умолчанию, обычно нулём. Система TSRM, с помощью движка, предоставляет хук для задания глобальным переменным значений по умолчанию — мы называем его GINIT.
Note
Полный обзор хуков 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 вам передаётся указатель на текущее место в памяти ваших глобальных
переменных. Вы используете его для инициализации глобальных переменных. Здесь мы записываем ноль
в наше случайное значение (не особо полезно, но пусть так).
Warning
Не используйте макрос PIB_G() в GINIT. Используйте переданный вам указатель.
Note
GINIT() запускается перед MINIT() для текущего процесса. В случае NTS это всё.
В случае ZTS GINIT() будет дополнительно вызываться для каждого нового потока,
порождённого библиотекой потоков.
Warning
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() очень дешёвая операция (читайте исходный код).
Когда мы возвращаем такой массив пользователю, мы не забываем увеличить его refcount (в
RETVAL_ZVAL), так как мы сохраняем ссылку на этот массив в нашем расширении.
Использование истинных глобальных переменных¶
Истинные глобальные переменные — это непотокозащищённые истинные глобальные переменные C. Иногда может понадобиться использовать некоторые из них. Однако помните главное правило: вы не можете безопасно записывать в такие глобальные переменные во время обработки запроса. Поэтому обычно в PHP такие переменные используются как переменные, доступные только для чтения.
Помните, что абсолютно безопасно записывать в истинные глобальные переменные, когда вы находитесь
на этапах MINIT() или MSHUTDOWN() жизненного цикла PHP. Но вы не можете записывать в них
во время обработки запроса (однако читать из них можно).
Простой пример — вы хотите прочитать значение переменной окружения, чтобы что-то с ним сделать. Также нередко инициализируют постоянные 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).