Опубликован: 15.06.2004 | Уровень: специалист | Доступ: платный
Лекция 8:

Средства межпроцессного взаимодействия

Разделяемые сегменты памяти

В стандарте POSIX-2001 разделяемый объект памяти определяется как объект, представляющий собой память, которая может быть параллельно отображена в адресное пространство более чем одного процесса.

Таким образом, процессы могут иметь общие области виртуальной памяти и разделять содержащиеся в них данные. Единицей разделяемой памяти являются сегменты. Разделение памяти обеспечивает наиболее быстрый обмен данными между процессами.

Работа с разделяемой памятью начинается с того, что один из взаимодействующих процессов посредством функции shmget()   создает разделяемый сегмент, специфицируя первоначальные права доступа к нему и его размер в байтах.

Чтобы получить доступ к разделяемому сегменту, его нужно присоединить (для этого служит функция shmat() ), т. е. разместить сегмент в виртуальном пространстве процесса. После присоединения, в соответствии с правами доступа, процессы могут читать данные из сегмента и записывать их (быть может, синхронизируя свои действия с помощью семафоров ). Когда разделяемый сегмент становится ненужным, его следует отсоединить с помощью функции shmdt().

Предусмотрена возможность выполнения управляющих действий над разделяемыми сегментами (функция shmctl() ).

Описание перечисленных функций представлено в листинге 8.40.

#include <sys/shm.h>
int shmget (key_t key, size_t size, 
            int shmflg);
void *shmat (int shmid, const void *shmaddr, 
             int shmflg);
int shmdt (const void *shmaddr);
int shmctl (int shmid, int cmd, 
            struct shmid_ds *buf);
Листинг 8.40. Описание функций для работы с разделяемыми сегментами памяти.

Структура shmid_ds, ассоциированная с идентификатором разделяемого сегмента памяти, должна содержать по крайней мере следующие поля.

struct ipc_perm shm_perm;       
/* Данные о правах доступа к разделяемому 
сегменту */
size_t          shm_segsz;      
/* Размер сегмента в байтах */
pid_t           shm_lpid;       
/* Идентификатор процесса, выполнившего 
последнюю операцию над разделяемым сегментом */
pid_t           shm_cpid;       
/* Идентификатор процесса, создавшего 
разделяемый сегмент */
shmatt_t        shm_nattch;     
/* Текущее число присоединений сегмента */
time_t          shm_atime;      
/* Время последнего присоединения */
time_t          shm_dtime;      
/* Время последнего отсоединения */
time_t          shm_ctime;      
/* Время последнего изменения посредством 
shmctl() */

Функция shmget() аналогична msgget() и semget() ; аргумент size задает нижнюю границу размера сегмента в байтах; реализация, учитывающая, например, правила выравнивания, имеет право создать разделяемый сегмент большего размера.

Структура shmid_ds инициализируется в соответствии с общими для средств межпроцессного взаимодействия правилами. Поле shm_segsz устанавливается равным значению аргумента size.

Число уникальных идентификаторов разделяемых сегментов памяти ограничено; попытка его превышения ведет к неудачному завершению shmget() (возвращается -1 ). Вызов shmget() завершится неудачей и тогда, когда значение аргумента size меньше минимально допустимого либо больше максимально допустимого размера разделяемого сегмента.

Чтобы присоединить разделяемый сегмент, используется функция shmat(). Аргумент shmid задает идентификатор разделяемого сегмента ; аргумент shmaddr - адрес, по которому сегмент должен быть присоединен, т. е. тот адрес в виртуальном пространстве процесса, который получит начало сегмента. Поскольку свойства сегментов зависят от аппаратных особенностей управления памятью, не всякий адрес является приемлемым. Если установлен флаг SHM_RND, адрес присоединения округляется до величины, кратной константе SHMLBA.

Если shmaddr задан как пустой указатель, реализация выбирает адрес присоединения по своему усмотрению.

По умолчанию присоединяемый сегмент будет доступен и на чтение, и на запись (если процесс обладает необходимыми правами). Флаг SHM_RDONLY предписывает присоединить сегмент только для чтения.

При успешном завершении функции shmat() результат равен адресу, который получил присоединенный сегмент ; в случае неудачи возвращается -1. (Разумеется, для использования результата shmat() в качестве указателя его нужно преобразовать к требуемому типу.)

Отсоединение сегментов производится функцией shmdt() ; аргумент shmaddr задает начальный адрес отсоединяемого сегмента.

Управление разделяемыми сегментами осуществляется при помощи функции shmctl(), аналогичной msgctl(). Как и для очередей сообщений, для разделяемых сегментов определены управляющие команды IPC_STAT (получить информацию о состоянии разделяемого сегмента ), IPC_SET (переустановить характеристики), IPC_RMID (удалить разделяемый сегмент ). Удалять сегмент нужно после того, как от него отсоединились все процессы.

Аппарат разделяемых сегментов предоставляет нескольким процессам возможность одновременного доступа к общей области памяти. Обеспечивая корректность доступа, процессы тем или иным способом должны синхронизировать свои действия. В качестве средства синхронизации удобно использовать семафор. В листинге 8.41 показана реализация так называемого критического интервала - механизма, обеспечивающего взаимное исключение разделяющих общие данные процессов.

Для "создания" подобного механизма необходимо породить разделяемый сегмент памяти, присоединить его во всех процессах, которым предоставляется доступ к разделяемым данным, а также породить и проинициализировать простейший семафор. После этого монопольный доступ к разделяемой структуре обеспечивается применением P- и V-операций.

#include <unistd.h>
#include <stdio.h>
#include <sys/shm.h>
#include <sys/sem.h>
#include <sys/wait.h>

int main (void) {
 struct region {
   pid_t fpid;
 } *shm_ptr;

 struct sembuf P = {0, -1, 0};
 struct sembuf V = {0,  1, 0};

 int shmid;
 int semid;

 shmid = shmget (IPC_PRIVATE, sizeof (struct region), 0777);
 semid = semget (IPC_PRIVATE, 1, 0777);
 (void) semctl (semid, 0, SETVAL, 1);

 switch (fork ()) {
   case -1:
     perror ("FORK");
     return (1);
   case 0:
     if ((int) (shm_ptr = (struct region *) shmat (shmid, NULL, 0)) == (-1)) {
 	perror ("CHILD-SHMAT");
 	return (2);
     }

     if (semop (semid, &p, 1) != 0) {
 	perror ("CHILD-SEMOP-P");
 	return (3);
     }
     printf ("Процесс-потомок вошел в критический интервал\n");

     shm_ptr->fpid = getpid ();         /* Монопольный доступ */

     printf ("Процесс-потомок перед выходом из критического интервала\n");
     if (semop (semid, &V, 1) != 0) {
 	perror ("CHILD-SEMOP-V");
 	return (4);
     }

     (void) shmdt (shm_ptr);
     return 0;
 }

 if ((int) (shm_ptr = (struct region *) shmat (shmid, NULL, 0)) == (-1)) {
   perror ("PARENT-SHMAT");
   return (2);
 }

 if (semop (semid, &p, 1) != 0) {
   perror ("PARENT-SEMOP-P");
   return (3);
 }
 printf ("Родительский процесс вошел в критический интервал\n");

 shm_ptr->fpid = getpid ();         /* Монопольный доступ */

 printf ("Родительский процесс перед выходом из критического интервала\n");
 if (semop (semid, &V, 1) != 0) {
   perror ("PARENT-SEMOP-V");
   return (4);
 }

 (void) wait (NULL);

 printf ("Идентификатор родительского процесса: %d\n", getpid ());
 printf ("Идентификатор процесса в разделяемой структуре: %d\n", shm_ptr->fpid);

 (void) shmdt (shm_ptr);

 (void) semctl (semid, 1, IPC_RMID);
 (void) shmctl (shmid, IPC_RMID, NULL);

 return 0;
}
Листинг 8.41. Пример работы с разделяемыми сегментами памяти.

Результат работы приведенной программы может выглядеть так, как показано в листинге 8.42.

Родительский процесс вошел в критический интервал
Родительский процесс перед выходом из критического интервала
Процесс-потомок вошел в критический интервал
Процесс-потомок перед выходом из критического интервала
Идентификатор родительского процесса: 2161
Идентификатор процесса в разделяемой структуре: 2162
Листинг 8.42. Возможный результат синхронизации доступа к разделяемым данным.

В листинге 8.43 представлен пример использования разделяемых сегментов памяти в сочетании с обработкой сигнала   SIGSEGV, который посылается процессу при некорректном обращении к памяти. Идея в том, чтобы создавать разделяемые сегменты, "накрывающие" запрашиваемые адреса. При некотором воображении пример можно считать основой программной реализации виртуальной памяти.

/* * * * * * * * * * * * * * * * * * * * * * * * * * * */
/* Реализация "виртуальной" памяти из одного сегмента. */
/* Используются разделяемые сегменты памяти            */
/* и обработка сигнала SIGSEGV                         */
/* * * * * * * * * * * * * * * * * * * * * * * * * * * */

#include <stdlib.h>
#include <stdio.h>
#include <sys/stat.h>
#include <sys/shm.h>
#include <signal.h>

/* Константа, зависящая от реализации */
#define SHM_BASE_ADDR   0x40014000

static int shm_id = -1;
static void *shm_addr;

/* Реакция на сигнал SIGSEGV.                              */
/* Создаем и присоединяем на чтение разделяемый сегмент,   */
/* накрывающий переданный адрес.                           */
/* Если это не помогло, переприсоединяем сегмент на запись */
static void sigsegv_sigaction (int sig, siginfo_t *sig_info, void *addr) {
 struct shmid_ds shmid_ds;

 if (shm_id == -1) {
   /* Сегмента еще нет. Создадим */
   if ((shm_id = shmget (IPC_PRIVATE, SHMLBA, S_IRUSR)) == -1) {
     perror ("SHMGET");
     exit (1);
   }
   /* Присоединим сегмент на чтение */
   if ((int) (shm_addr = shmat (shm_id, sig_info->si_addr, SHM_RDONLY | SHM_RND)) == (-1)) {
     perror ("SHMAT-RDONLY");
     exit (2);
   }
   return;
 } else {
   /* Сегмент уже есть, но обращение по адресу вызвало сигнал SIGSEGV. */
   /* Значит, это была попытка записи, и сегмент нужно                 */
   /* переприсоединить на запись, поменяв соответственно режим доступа */
   if (shmctl (shm_id, IPC_STAT, &shmid_ds) == -1) { 
     perror ("SHMCTL-IPC_STAT");
     exit (3);
   }
   shmid_ds.shm_perm.mode |= S_IWUSR;
   if (shmctl (shm_id, IPC_SET, &shmid_ds) == -1) {
     perror ("SHMCTL-IPC_SET");
     exit (4);
   }
   (void) shmdt (shm_addr);
   if (shmat (shm_id, shm_addr, 0) != shm_addr) {
     perror ("SHMAT-RDWD");
     exit (5);
   }
 }
}

int main (void) {
 char *test_ptr;
 struct sigaction sact;

 /* Установим реакцию на сигнал SIGSEGV */
 (void) sigemptyset (&sact.sa_mask);
 sact.sa_flags = SA_SIGINFO;
 sact.sa_sigaction = sigsegv_sigaction;
 (void) sigaction (SIGSEGV, &sact, (struct sigaction *) NULL);

 /* Убедимся, что разделяемые сегменты инициализируются нулями */
 test_ptr = (char *) (SHM_BASE_ADDR + 3);
 printf ("Результат попытки чтения до записи: %x\n", *test_ptr);

 /* Попробуем записать */
 *test_ptr = 'A';
 printf ("Результат попытки чтения после записи: %x\n", *test_ptr);

 return (shmctl (shm_id, IPC_RMID, NULL));
}
Листинг 8.43. Пример работы с разделяемыми сегментами памяти и сигналами.

Обратим внимание на использование флагов округления адреса присоединения разделяемого сегмента ( SHM_RND ) и присоединения только на чтение ( SHM_RDONLY ), а также обработчика сигналов, задаваемого полем sa_sigaction структуры типа sigaction (в сочетании с флагом SA_SIGINFO ) и имеющего доступ к расширенной информации о сигнале и его причинах.

Антон Коновалов
Антон Коновалов

В настоящее время актуальный стандарт - это POSIX 2008 и его дополнение POSIX 1003.13
Планируется ли актуализация материалов данного очень полезного курса?