Объекты SVID IPC
Статья из серии "Программирование для Linux", журнал Linux Format
,
Я должен отметить, что основная цель компьютерных наук, – устранение неразберихи, так и не была достигнута | ||
Эсгар Дейкстра |
Мы продолжаем изучение механизмов взаимодействия между процессами в Linux. Каналы различных типов, рассмотренные в предыдущей статье, существовали в Unix практически с самого начала. Позже к ним были добавлены и другие механизмы межпроцессного взаимодействия. Мы остановимся на трех механизмах, которые появились в Unix System V и были описаны в System V Interface Definition (SVID). В настоящее время эти механизмы поддерживаются почти всеми Unix-системами (очереди сообщений не поддерживаются в Mac OS X 10.3 [].
Интерфейсы трех механизмов SVID IPC подобны. Для того, чтобы разные процессы могли получить доступ к одному объекту системы, они должны «договориться» об идентификации этого объекта. Роль идентификатора для всех объектов System V IPC выполняет ключ - уникальное число-идентификатор объекта. Для того, чтобы использовать один и тот же объект, программы должны использовать один и тот же ключ. Для каждого объекта IPC предусмотрены специальные функции чтения и записи, а также управляющая функция. Сообщения
Механизм сообщений Linux похож на механизм сообщений, используемый в графических многооконных средах. Сообщения накапливаются в очередях и могут изыматься из очередей последовательно или в произвольном порядке. Каждая группа процессов может создать одну или несколько очередей для обмена сообщениями, а одна очередь сообщений может использоваться совместно более чем двумя процессами. Сообщение определяется как «последовательность байтов, передаваемая от одного процесса другому». Система сообщений SVID обладает следующими свойствами:
- Возможность накопления сообщений в очереди. Приложения, использующие сообщения для обмена данными, создают свою собственную очередь сообщений, которая может (и должна) быть удалена приложением-владельцем в момент завершения его работы.
- Возможность произвольного выбора сообщений из очереди на основе назначенных им идентификаторов. Эта возможность позволяет организовать приоритетную обработку сообщений, а также идентифицировать сообщения, посылаемые разными приложениями, участвующими в обмене данными.
- Произвольная структура и размер сообщения.
Последний пункт требует уточнения. Максимальный размер сообщения и максимальное количество сообщений в очереди ограничены, причем не существует единого для всех Unix-систем способа определить эти ограничения. В Linux максимальная длина сообщения в байтах задана константой MSGMAX, определенной в файле <linux/msg.h>, а максимальное число сообщений - константой MSGMNG из того же файла. На платформе IA32 размер сообщения не может превышать 8 килобайт, а длина очереди – 16384 (16K) сообщений. Структура данных, использующаяся для передачи сообщений, может быть определена следующим образом:
struct msgp { long mtype; ... // Любые другие поля };
Поле mtype является единственным обязательным полем в приведенной структуре. В этом поле хранится произвольный идентификатор сообщения, который может интерпретироваться как тип передаваемых данных. Кроме поля mtype структура данных сообщения может содержать любое количество других полей любых типов.
В качестве примера использования очередей рассмотрим совместную работу двух программ – клиента и сервера. Исходные тексты вы найдете (файлы msgcli.c и msgserv.c соответственно). Для того, чтобы программы могли обмениваться сообщениями, они должны использовать один и тот же формат сообщений и идентификатор очереди. Эти данные, общие для клиента и сервера, удобно вынести в отдельный заголовочный файл (мы назовем его msgtypes.h). Наша структура данных выглядит так:
#define MAXLEN 512 struct msg_t { long mtype; int snd_pid; char body[MAXLEN]; };
Помимо поля mtype мы вводим поле snd_pid, которое будет содержать идентификатор процесса-отправителя сообщения, и поле body, которое предназначено для текста сообщения. Мы могли бы определить несколько разных структур для сообщений разных типов. Значение поля mtype указывало бы, с какой структурой мы имеем дело. Поле mtype может быть не только идентификатором типа сообщения. С его помощью можно указать, например, приоритет сообщения. Используя функцию произвольной выборки сообщений, приложение может считывать в первую очередь сообщения с более высоким приоритетом.
Помимо структуры сообщения нам следует определить ключ очереди. Для получения уникального ключа можно использовать функцию ftok(3), однако руководство по работе с функциями SVID рекомендует выбирать значения самостоятельно, поэтому в нашем примере мы определим ключ как константу в файле msgtypes.h: #define KEY 1174
Маловероятно, что в системе уже существует другая очередь сообщений с ключом 1174. В принципе, программа, создающая объект IPC, может узнать, существует ли уже такой объект (см. ниже использование флага IPC_EXCL), однако толку от этого не много. Допустим, процесс установил, что объект с указанным идентификатором существует, но что ему делать? Процесс может выбрать другой идентификатор из какого-нибудь пула, однако о новом идентификаторе нужно как- то оповестить другие процессы. Для оповещения можно использовать именованные каналы, для которых, в свою очередь, необходим уникальный идентификатор... Уникальность идентификатора файловых каналов основана на уникальности имен файловой системы (имеются в виду полные имена, начиная с корневого слеша). Функция ftok(), которую мы рассмотрим ниже, тоже пытается генерировать идентификаторы, основываясь на уникальности имен файловой системы. Кроме того, проверка существования объекта IPC может «обмануть» процесс, если существующий объект был создан предыдущим экземпляром того же процесса, выгруженным из системы в результате серьезной ошибки. Рассмотрим теперь работу сервера. Сервер получает сообщение, переданное клиентом, распечатывает сообщение на экране терминала, возвращает клиенту сообщение “Ok!”, ждет подтверждения, что клиент получил ответ, затем удаляет очередь и завершает работу. Программу-сервер следует запустить до запуска программы-клиента.
Текст программы (msgserv.c), как всегда, начинается с заголовочных файлов. Все типы, константы и функции, использующиеся при работе с сообщениями, становятся доступны при включении в текст программы файлов <sys/ipc.h> и <sys/msg.h>. Очередь сообщений создается при помощи функции msgget(2): msgid = msgget(KEY, 0666 | IPC_CREAT);
Первый параметр msgget() – ключ, гарантирующий уникальность очереди. Ключ очереди представлен числом, поэтому его можно спутать с другим числом – идентификатором очереди, который присваивает система. Помните, что ключ нужен только для открытия очереди, а для работы с ней используется идентификатор. Второй параметр функции msgget() представляет собой комбинацию маски прав доступа к создаваемой очереди (аналогичной маске прав доступа к именованным каналам) и нескольких дополнительных флагов: кажется, программист, писавший функции SVID IPC, сильно экономил на переменных- параметрах. Флаг IPC_CREATE указывает, что в результате вызова msgget() должна быть создана новая очередь. При установке флага IPC_EXCL, функция msgget() вернет сообщение об ошибке, если очередь с указанным ключом уже существует. В случае успеха msgget() возвращает положительное значение – идентификатор созданной очереди.
Передача и получение сообщений выполняется при помощи функций msgsnd(2) и msgrcv(2) соответственно. Первым параметром обеих функций является идентификатор очереди, возвращенный функцией msgget(). Во втором параметре передается размер структуры сообщения. Как было сказано выше, программа, читающая сообщения из очереди, должна указать размер сообщения, соответствующий ожидаемому идентификатору и может читать сообщения разного размера (речь идет о ситуации, когда программа ждет сообщений определенного типа). На диске есть пример polymsgserv/polymsgcli, демонстрирующий этот подход. Третьим параметром функции msgrcv() является идентификатор сообщения. Если значение этого параметра больше нуля, из очереди будет извлечено сообщение с соответствующим значением поля mtype. Если этот параметр равен нулю, из очереди будет извлечено первое по порядку сообщение, а если параметр отрицательный, из очереди будет извлечено первое сообщение, чей идентификатор меньше либо равен абсолютному значению параметра. Последний параметр в функциях msgsnd() и msgrcv() и позволяет задать дополнительные флаги. Обычно функция, читающая сообщение из очереди, приостанавливает выполнение программы до тех пор, пока извлечение сообщения не будет выполнено, то есть пока в очереди не появится сообщение ожидаемого типа. Именно так работает эта функция в наших программах. При указании флага IPC_NOWAIT msgrcv() вернет сообщение об ошибке, если на момент вызова в очереди отсутствует подходящее сообщение.
В нашем примере сервер и клиент используют разные идентификаторы для посылаемых сообщений. Это сделано для того, чтобы программа, вызывающая последовательно msgsnd() и msgrcv(), не извлекала из очереди свои собственные сообщения. Наш сервер записывает в очередь сообщения со значением mtype, равным 1, а считывает – со значением, равным 2 (у программы-клиента все наоборот).
Для удаления очереди используется функция msgctl(2), которая, как и все функции *ctl(), может выполнять множество разных действий (например, получение данных о состоянии очереди). Первый параметр этой функции, как всегда, идентификатор очереди, второй параметр – команда (IPC_STAT, IPC_SET или IPC_RMID). Третий параметр используется в вызовах-запросах (то есть, когда второй параметр равен IPC_STAT), а также для конфигурации очереди (команда IPC_SET). В нем передается указатель на структуру msgid_ds, поля которой содержат значения различных параметров очереди. Функция возвращает статус выполнения команды. Вызов msgctl(msgid, IPC_RMID, 0);
удаляет очередь с идентификатором msgid.
Рассмотрим теперь программу-клиент. Первым делом программа-клиент должна получить идентификатор очереди. Для этого используется функция msgget() с тем же ключом очереди, что и у сервера, с маской прав доступа, но без дополнительных флагов. В этом варианте функция возвращает идентификатор уже существующей очереди с данным ключом и -1, если очередь не существует:
msgid = msgget(KEY, 0666); if (msgid == -1) { printf("Server is not running!\n", msgid); return EXIT_FAILURE; }
Далее клиент считывает строку, вводимую пользователем, формирует сообщение, записывая в поле mtype значение 2, отправляет сообщение, и ждет ответ сервера – сообщения с идентификатором 1. Скомпилируйте обе программы (можете просто скомандовать make msgdemo), запустите сначала сервер, потом, в другом окне терминала, клиент. Напечатайте в окне клиента строку и нажмите ввод.