Марк Митчелл - Программирование для Linux. Профессиональный подход
■ Сокеты — соединяют несвязанные процессы, работающие на разных компьютерах.
Различия между способами взаимодействия определяются следующими критериями:
■ ограничено ли взаимодействие рамками связанных процессов (имеющих общего предка) или же соединяются процессы, выполняющиеся в одной файловой системе либо на разных компьютерах:
■ ограничен ли процесс только чтением либо только записью данных;
■ число взаимодействующих процессов;
■ синхронизируются ли взаимодействующие процессы (например, должен ли читающий процесс перейти в режим ожидания при отсутствии данных на входе).
5.1. Совместно используемая память
Простейшим способом взаимодействия процессов является совместный доступ к общей области памяти. Это выглядит так, как если бы два или более процесса вызвали функцию malloc() и получили указатели на один и тот же блок памяти. Когда один из процессов меняет содержимое памяти, другие процессы замечают это изменение.
5.1.1. Быстрое локальное взаимодействие
Совместное использование памяти — самый быстрый способ взаимодействия. Процесс обращается к общей памяти с той же скоростью, что и к своей собственной памяти, и никаких системных вызовов или обращений к ядру не требуется. Устраняется также ненужное копирование данных.
Ядро не синхронизирует доступ процессов к общей памяти — об этом следует позаботиться программисту. Например, процесс не должен читать данные из совместно используемой памяти, пока в нее осуществляется запись, и два процесса не должны одновременно записывать данные в одну и ту же область памяти. Стандартная стратегия предотвращения подобной конкуренции заключается в использовании семафоров, о которых пойдет речь в раздаче 5.2, "Семафоры для процессов". Тем не менее в приводимом далее примере программы доступ к памяти осуществляет только один процесс: просто мы хотим сконцентрировать внимание читателей на механизме совместного использования памяти и не перегружать программу кодом синхронизации.
5.1.2. Модель памяти
При совместном использовании сегмента памяти один процесс должен сначала выделить память. Затем все остальные процессы, которые хотят получить доступ к ней, должны подключить сегмент. По окончании работы с сегментом каждый процесс отключает его. Последний процесс освобождает память.
Для того чтобы понять принципы выделения и подключения сегментов памяти, необходимо разобраться в модели памяти Linux. В Linux виртуальная память (ВП) каждого процесса разбита на страницы. Все процессы хранят таблицу соответствий между своими адресами памяти и страницами ВП, содержащими реальные данные. Несмотря на то что за каждым процессом закреплены свои адреса, разным процессам разрешается ссылаться на одни и те же страницы. Это и есть совместное использование памяти.
При выделении совместно используемого сегмента памяти создаются страницы ВП. Это действие должно выполняться только одни раз, так как все остальные процессы будут обращаться к этому же сегменту. Если запрашивается выделение существующего сегмента, новые страницы не создаются; вместо этого возвращается идентификатор существующих страниц. Чтобы сделать сегмент общедоступным, процесс подключает его, при этом создаются адресные ссылки на страницы сегмента. По окончании работы с сегментом адресные ссылки удаляются. Когда все процессы завершили работу с сегментом, один (и только один) из них должен освободить страницы виртуальной памяти.
Размер совместно используемого сегмента кратен размеру страницы ВП. В Linux последняя величина обычно равна 4 Кбайт, но никогда не помешает это проверить с помощью функции getpagesize().
5.1.3. Выделение сегментов памяти
Процесс выделяет сегмент памяти с помощью функции shmget(). Первым аргументом функции является целочисленный ключ, идентифицирующий создаваемый сегмент. Если несвязанные процессы хотят получить доступ к одному и тому же сегменту, они должны указать одинаковый ключ. К сожалению, ничто не мешает посторонним процессам выбрать тот же самый ключ сегмента, а это приведет к системному конфликту. Указание специальной константы IPC_PRIVATE в качестве ключа позволяет гарантировать, что будет создан совершенно новый сегмент.
Во втором аргументе функции задается размер сегмента в байтах. Это значение округляется, чтобы быть кратным размеру страницы ВП.
Третий параметр содержит набор битовых флагов. Перечислим наиболее важные из них.
■ IPC_CREAT. Указывает на то, что создается новый сегмент, которому присваивается заданный ключ.
■ IPC_EXCL. Всегда используется совместно с флагом IPC_CREAT и заставляет функцию shmget() выдать ошибку в случае, когда сегмент с указанным ключом уже существует. Если флаг не указан и возникает описанная ситуация, функция shmget() возвращает идентификатор существующего сегмента, не создавая новый сегмент.
■ Флаги режима. В эту группу входят 9 флагов, задающих права доступа к сегменту для владельца, группы и остальных пользователей. Биты выполнения игнорируются. Проще всего задавать права доступа с помощью констант, определенных в файле <sys/stat.h> (они описаны на man-странице функции stat()).[15] Например, флаги S_IRUSR и S_IWUSR предоставляют право чтения и записи владельцу сегмента, а флаги S_IROTH и S_IWOTH предоставляют аналогичные права остальным пользователям.
В следующем фрагменте программы функция shmget() создает новый совместно используемый сегмент памяти (или возвращает идентификатор существующего, если значение shm_key уже зарегистрировано в системе), доступный для чтения/записи только его владельцу:
int segment_id = shmget(shm_key, getpagesize(),
IPC_CREAT | S_IRUSR | S_IWUSR);
В случае успешного завершения функция возвращает идентификатор сегмента. Если сегмент уже существует, проверяются нрава доступа к нему.
5.1.4. Подключение и отключение сегментов
Чтобы сделать сегмент памяти общедоступным, процесс должен подключить его с помощью функции shmat(). В первом ее аргументе передается идентификатор сегмента, возвращенный функцией shmget(). Второй аргумент — это указатель, определяющий, где в адресном пространстве процесса необходимо создать привязку на совместно используемую область памяти. Если задать значение NULL, ОС Linux выберет первый доступный адрес. Третий аргумент может содержать следующие флаги.
■ SHM_RND. Указывает на то, что адрес, заданный во втором параметре, должен быть округлен, чтобы стать кратным размеру страницы. Если этот флаг не указан, необходимо самостоятельно позаботиться о выравнивании сегмента по границе страницы.
■ SHM_RDONLY. Указывает на то. что сегмент доступен только для чтения, но не для записи.
В случае успешного завершения функция возвращает адрес подключенного сегмента. Дочерний процесс, созданный функцией fork(), унаследует этот адрес и в случае необходимости сможет отключить сегмент.
По завершении работы с сегментом его необходимо отключить с помощью функции shmdt(). Ей следует передать адрес, возвращаемый функцией shmat(). Если текущий процесс был последним, кто ссылался на сегмент, сегмент удаляется из памяти. Функции exit() и exec() автоматически отключают сегменты.
5.1.5. Контроль и освобождение совместно используемой памяти
Функция shmctl() возвращает информацию о совместно используемом сегменте и способна модифицировать его. Первым параметром является идентификатор сегмента.
Чтобы получить информацию о сегменте, укажите в качестве второго параметра константу IPC_STAT, а в третьем параметре передайте указатель на структуру shmid_ds.
Чтобы удалить сегмент, передайте во втором параметре константу IPC_RMID, а в третьем параметре — NULL. Сегмент удаляется, когда последний подключивший его процесс отключает сегмент.
Каждый совместно используемый сегмент должен явно освобождаться с помощью функции shmctl(), чтобы случайно не был превышен системный лимит на общее число таких сегментов. Функции exit() и exec() отключают сегменты, но не освобождают их.
Описание других операций, выполняемых над совместно используемыми сегментами памяти, можно найти на man-странице функции shmctl().
5.1.6. Пример программы
Программа, приведенная в листинге 5.1, иллюстрирует методику совместного использования памяти.
Листинг 5.1. (shm.c) Пример совместного использования памяти#include <stdio.h>