Использование разделяемой памяти в PHP
IPC ("Inter-Process Communication" - межпроцессное взаимодействие) - одна из важнейших особенностей ОС семейства UNIX. Она позволяет различным процессам взаимодействовать между собой. В этой статье речь пойдёт о двух технологиях System V IPC (System V - одна из ключевых версий ОС UNIX компании AT&T - прим. пер.): о семафорах и разделяемой памяти. System V IPC впервые появилась в SVR2 (System V Release 2 - прим. пер.). System V IPC, однако, была реализована многими разработчиками. Она также доступна в SVR4.
Концепция IPC слагается из нескольких компонентов. Термин подразумевает различные механизмы обмена данными между процессами, стартовавшими в одной системе. IPC позволяет избежать создания огромного приложения с большим набором функций всех различных назначений и заменить его на использование отдельных, малых приложений, способных обмениваться данными между собой. Традиционный подход Unix заключается в том, чтобы позволить многопроцессорным системам запускать приложения в отдельных процессах (threads) для сокращения времени, требуемого на выполнение специфических задач.
На высоком уровне мы можем разделить межпроцессное взаимодействие на следующие наиболее крупные и важные разделы:
- Сообщения: каналы (pipes) и очереди сообщений (pipes and message queues)
- Разделяемая память (Shared memory)
- Удаленный вызов процедур - RPC (remote procedure calls)
- Синхронизация: семафоры и любые виды блокирования
- Сетевое взаимодействие (API сокетов)
В этой статье речь идет о разделяемой памяти и синхронизации (семафорах). Детальное описание этих методов займет слишком много времени - существует огромное количество различного материала, посвященного этой теме. Если вы заинтересованы в подробном изучении, обратитесь ко множественным книгам по межпроцессному взаимодействию.
Идентификаторы IPC
Каждый объект IPC (очередь ли сообщений, семафор, или сегмент разделяемой памяти) обладает уникальным идентификатором (Id), который позволяет ядру ОС идентифицировать этот объект. К примеру, для того, чтобы сослаться на определенный сегмент разделяемой памяти, вам всего лишь необходимо знать уникальный идентификатор, назначенный этому сегменту.
Учтите, что идентификатор объекта IPC является уникальным только для одного типа объектов.
Другими словами, только одна очередь сообщений может иметь идентификатор 12345
, хотя номер 12345 может также использоваться семафорами и/или сегментами разделенной памяти.
Ключи IPC
Как создается идентификатор IPC? Для этого необходим ключ. Первым шагом при создании среды взаимодействия между приложениями является координирование использования ключей. Представьте себе это так: для того, чтобы позвонить кому-либо, вы должны знать его телефонный номер. Телефонная компания должна знать, как переслать ваш звонок абоненту. И только, когда он отвечает, происходит соединение.
В случае применения System V IPC "телефон" соединяет объекты одного типа. Телефонной компанией или способом маршрутизации является "ключ" IPC.
Приложения должны генерировать свои собственные ключи.
Функция ftok()
делает это и для клиента и для сервера.
Значение ключа, возвращаемое функцией ftok()
, зависит от значения inode и младшего значения идентификатора устройства для первого аргумента - файла - и символа - второго аргумента.
Это не обеспечивает уникальности, но приложения могут проводить проверки на предмет коллизий и генерировать новые ключи по мере необходимости.
key_t mykey;
mykey = ftok ("/tmp/myapp", 'a');
В примере, приведенном выше, директория /tmp/myapp
комбинируется с литеральным идентификатором a
для генерации ключа. Другим распространенным примером
является использование текущей директории:
key_t mykey;
mykey = ftok(".", 'a');
Обязанность проектирования алгоритма генерации ключей лежит на программисте прикладного приложения. Любой из этих методов должен учитывать конкурирующие состояния процессов и меры предотвращений "тупиковых" ситуаций. Для достижения наших демонстрационных целей мы ограничимся функцией ftok()
. Если мы условимся, что каждый процесс-клиент будет запущен из собственного уникального домашнего каталога, то условие уникальности будет соблюдено полностью.
Следующие системные вызовы IPC используют значение возвращаемого ключа для создания или изменения доступа к объектам IPC.
Команда ipcs
отображает состояние всех объектов System V IPC.
ipcs -q: показывать только очереди сообщений
ipcs -s: показывать только семафоры
ipcs -m: показывать только разделяемую память
ipcs --help: для любознательных
По умолчанию показываются все три категории объектов. Рассмотрим пример простейшего вывода команды ipcs
:
------ Shared Memory Segments --------
shmid owner perms bytes nattch status
------ Semaphore Arrays --------
^semid owner perms nsems status
------ Message Queues --------
msqid owner perms used-bytes messages
0 root 660 5 1
Мы видим единственную очередь сообщений с идентификатором
0
. Она принадлежит пользователю root и обладает правами доступа
660
, или -rw-rw---
. Очередь содержит одно 5-ти байтовое сообщение.
Команда ipcs
является мощным механизмом для мониторинга памяти ядра объектов IPC.
Команда ipcrm
.
ipcrm
удаляет объекты IPC из ядра. Однако, поскольку объекты IPC
могут быть удалены с помощью системных вызовов из прикладной программы,
необходимость удалять их вручную возникает редко. Команда очень простая:
ipcrm - type (тип) id (номер)
Необходимо обозначить тип удаляемого объекта параметром. Обратитесь за подробностями к справке man.
Идентификатор IPC можно определить с помощью команды ipcs
. Помните, что идентификатор уникален только для объектов одного типа.
Вот почему необходимо указывать тип объекта при его удалении.
Семафоры.
Лучше всего семафоры представить, как счетчик доступа к публичным ресурсам. Обычно они используются, как замки, не позволяя одному процессу получить доступ к чему-либо, уже используемому другим процессом. Семафоры также могут предоставить эксклюзивный доступ к ресурсам данной машины или ограничить количество процессоров, использующих ресурс одновременно.
Этот механизм также обеспечивает функции для работы с разделяемой памятью System V.
Разделяемая память обеспечивает доступ к глобальным переменным для различных процессов.
Демоны httpd
и даже другие программы (на Perl, C
и других языках) могут получить доступ к этим данным с целью глобального обмена данными.
Помните, однако, что разделяемая память не защищена от
одновременного доступа.
Таблица 1 демонстрирует переменные Unix, которые определяют параметры ограничений разделяемой памяти. Каждый подвид Unix обладает своей документацией по этим переменным. К примеру, во FreeBSD они являются частью конфигурации ядра. Вам потребуется перекомпилировать ядро для принятия изменений.
Table 1. Переменные разделяемой памяти Unix
SHMMAX |
Максимальное количество разделяемой памяти, обычно 131072 байт |
SHMMIN |
Минимальное количество разделяемой памяти, обычно 1 байт |
SHMMNI |
Максимальное количество сегментов разделяемой памяти в системе, обычно 100 |
SHMSEG |
Максимальное количество сегментов разделяемой памяти для одного процесса, обычно 6 |
Функции сообщений могут посылать сообщения одним процессам и принимать сообщения от других. Они являются простым и эффективным способом обмена данными между процессами без необходимости использования системы сокетов Unix.
Использование разделяемой памяти и семафоров
Изучая что-то новое, каждый разработчик хочет начать использовать эту технологию на практике, хотя бы написав простую программу "Hello, world!". Это нормально, поэтому приведем минимальные сведения, необходимые для того, чтобы вы могли создать этот простой тестовый пример.
Разделяемая память является быстрейшим видом IPC, но она требует синхронизации между сохранением и извлечением данных. Каков же тогда алгоритм использования разделяемой памяти? Попросту говоря, он заключается в следующем:
- Получить доступ к разделяемой памяти, используя семафор.
- Записать данные в сегмент разделяемой памяти.
- После завершения записи уведомить об этом другие программы, используя семафор.
Синхронизация с использованием семафора необходима, поскольку использование ресурса разделяемой памяти практически аналогично использованию файлового ресурса, где временное блокирование и разблокирование доступа позволяет избежать потери данных.
Теперь возникает вопрос, как использовать семафор. На самом деле это весьма просто и понятно из приведенного примера кода. Единственная тонкость заключается в том, что, поскольку семафоры могут блокировать и разблокировать ресурсы, они также могут блокировать друг друга. Без аккуратного проектирования процессы могут попытаться овладеть одним и тем же семафором одновременно, вызывая долгие задержки при ожидании доступа. И, даже при должном проектировании, всегда будет верхний предел производительности в мультипроцессорных системах. За подробностями обратитесь к любой книге, посвященной многопроцессорным системам.
Здесь приводится несколько примеров того, как следует использовать различные функции семафоров и разделяемой памяти. В конце статьи также прилагается более длинный пример кода. Вот эти функции в порядке их появления.
int sem_get (int key [, int max_acquire [, int perm]])
Эта функция возвращает id семафора. Он будет положительным в случае успеха и равен
FALSE
в случае ошибки. Используйте id для доступа к семафору V с помощью
ключа key.
Если это необходимо, семафор может обладать правами доступа, указанными в параметре
perm
. Значение по умолчанию: 0666
. Параметр
max_acquire
управляет количеством процессов, которые могут одновременно получить доступ к семафору.
По умолчанию он равен 1.
Вторичный вызов функции sem_get()
для того же ключа вернет другой id, но они оба
будут указывать на один и тот же семафор.
bool sem_acquire(int sem_identifier)
Эта функция возвращает TRUE
в случае успеха и FALSE
при неудаче.
Она заблокируется, если необходимо, до тех пор, пока не займет запрошенный семафор.
Процесс попытки занять запрошенный семафор будет длиться бесконечно, если он превысит значение параметра max_acquire
.
Все семафоры, используемые в процессе, но не освобожденные явно, будут закрыты автоматически. Однако, это породит предупреждение.
int shm_attach(int key [, int memsize [, int perm]])
Эта функция создает или открывает сегмент разделяемой памяти. shm_attach()
возвращает идентификатор для использования при получении доступа к сегменту с помощью заданного ключа.
Размер сегмента в байтах будет равен mem_size
. Значение по умолчанию равно sysvshm. init_mem
в файле конфигурации PHP (или 10,000 байт, если оно не установлено в файле).
Необязательный параметр perm
определяет права доступа и равен 0666
по умолчанию.
Первый вызов с заданным ключом создаст сегмент. Второй вызов с тем же ключом вернет другой идентификатор, игнорируя два других параметра. Оба идентификатора предоставляют доступ к одному и тому же сегменту разделяемой памяти.
mixed shm_get_var(int id, int variable_key)
Эта функция возвращает переменную, идентифицируемую параметром variable_key
,
из сегмента разделяемой памяти с идентификатором id
. Переменная остается в разделяемой памяти.
int shm_put_var(int shm_identifier, int variable_key, mixed variable)
Эта функция добавляет или обновляет значение переменной, заданной параметром variable_key
, в сегменте
разделяемой памяти, заданном параметром shm_identifier
. Она позволяет работать с переменными любых типов.
bool sem_release(int sem_identifier)
Эта функция освобождает семафор, если он занят текущим процессом. В противном случае выдает предупреждение (warning).
Возвращает TRUE
в случае успеха и
FALSE
в случае ошибки.
После освобождения семафора для того, чтобы его повторно использовать, вызовите sem_acquire()
.
int shm_remove(int shm_identifier)
Эта функция удаляет сегмент разделяемой памяти и все содержащиеся в нем данные.
bool sem_remove(int sem_identifier)
Эта функция удаляет семафор, заданный с помощью sem_identifier
, если он был создан с помощью
sem_get
. Возвращает TRUE
в случае успеха
и FALSE
в случае ошибки. Если семафора с указанным идентификатором не существует, она породит
предупреждение (warning). После удаления семафор недоступен.
Образец кода, использующего разделяемую память
Вот образец кода, использующего разделяемую память, с попутными комментариями:
MEMSIZE = 512; // объём выделяемой разделяемой памяти $SEMKEY = 1; // ключ семафора $SHMKEY = 2; // ключ разделяемой памяти echo "Старт.\n"; // Создаем семафор $sem_id = sem_get($SEMKEY, 1); if ($sem_id === false) { echo "Ошибка при создании семафора"; exit; } else echo "Создан семафор $sem_id.\n"; // Занимаем семафор if (! sem_acquire($sem_id)) { echo "Ошибка при попытке занять семафор $sem_id.\n"; sem_remove($sem_id); exit; } else echo "Успешно занят семафор $sem_id.\n"; // Подключаем разделяемую память $shm_id = shm_attach($SHMKEY, $MEMSIZE); if ($shm_id === false) { echo "Ошибка при подключении разделяемой памяти.\n"; sem_remove($sem_id); exit; } else echo "Успешное подключение разделяемой памяти: $shm_id.\n"; // Пишем переменную 1 if (!shm_put_var($shm_id, 1, "Переменная 1")) { echo "Ошибка при попытке записать переменную 1 в разделяемую память $shm_id.\n"; // Овобождаем ресурсы. sem_remove($sem_id); shm_remove($shm_id); exit; } else echo "Переменная 1 записана в разделяемую память.\n"; // Пишем переменную 2 if (!shm_put_var($shm_id, 2, "Переменная 2")) { echo "Ошибка при попытке записать переменную 2 в разделяемую память $shm_id.\n"; // Освобождаем ресурсы. sem_remove($sem_id); shm_remove ($shm_id); exit; } else echo "Переменная 2 записана в разделяемую память.\n"; // Читаем переменную 1 $var1 = shm_get_var($shm_id, 1); if ($var1 === false) { echo "Ошибка при попытке прочитать переменную 1 из разделяемой памяти $shm_id, " . "возвращенное значение=$var1.\n"; } else echo "Прочитана переменная 1=$var1.\n"; // Читаем переменную 2 $var2 = shm_get_var ($shm_id, 2); if ($var1 === false) { echo "Ошибка при попытке прочитать переменную 2 из разделяемой памяти $shm_id, " . "возвращенное значение=$var2.\n"; } else echo "Прочитана переменная 2=$var2.\n"; // Освобождаем семафор if (!sem_release($sem_id)) echo "Ошибка при попытке освободить семафор $sem_id.\n"; else echo "Семафор $sem_id освобожден.\n"; // Удаляем сегмент разделяемой памяти if (shm_remove ($shm_id)) echo "Сегмент разделяемой памяти успешно удален.\n"; else echo "Ошибка при попытке удалить сегмент разделяемой памяти $shm_id.\n"; // Удаляем семафор. if (sem_remove($sem_id)) echo "Семафор успешно удален.\n"; else echo "Ошибка при попытке удалить семафор $sem_id.\n"; echo "Конец.\n"; ?>
Вот пример кода, выполняющего различные операции с разделяемой памятью:
<?php // Создаем блок разделяемой памяти размером 100 байт и с идентификатором равным 0xff3 $shm_id = shmop_open(0xff3, "c", 0644, 100); if(!$shm_id) { echo "Ошибка при создании сегмента разделяемой памяти.\n"; } // Получаем размер сегмента разделяемой памяти $shm_size = shmop_size($shm_id); echo "Блок разделяемой памяти с размером: ". $shm_size . " создан.\n"; // Пишем тестовую строку в сегмент разделяемой памяти $shm_bytes_written = shmop_write($shm_id, "Мой блок разделяемой памяти", 0); if($shm_bytes_written != strlen("Мой блок разделяемой памяти")) { echo "Ошибка при попытке записать данные полностью\n"; } // Считываем записанную строку $my_string = shmop_read($shm_id, 0, $shm_size); if(!$my_string) { echo "Ошибка при попытке чтения разделяемой памяти\n"; } echo "Данные в разделяемой памяти равны: ".$my_string."\n"; // Удаляем блок и закрываем сегмент разделяемой памяти if(!shmop_delete($shm_id)) { echo "Ошибка при попытке пометить блок разделяемой памяти на удаление."; } shmop_close($shm_id); ?>
Для более подробной информации об этих или других функциях обращайтесь к официальной документации PHP.