UNIX: взаимодействие процессов — страница 16 из 35

Разделяемая память Posix

13.1. Введение

В предыдущей главе рассматривались общие вопросы, связанные с разделяемой памятью, и детально разбиралась функция mmap. Были приведены примеры, в которых вызов mmap использовался для создания области памяти, совместно используемой родительским и дочерним процессами. В этих примерах использовалось:

■ отображение файлов в память (листинг 12.2);

■ неименованное отображение памяти в системе 4.4BSD (листинг 12.4);

■ неименованное отображение файла /dev/zero (листинг 12.5).

Теперь мы можем расширить понятие разделяемой памяти, включив в него память, совместно используемую неродственными процессами. Стандарт Posix.1 предоставляет два механизма совместного использования областей памяти для неродственных процессов:

1. Отображение файлов в память: файл открывается вызовом open, а его дескриптор используется при вызове mmap для отображения содержимого файла в адресное пространство процесса. Этот метод был описан в главе 12, и его использование было проиллюстрировано на примере родственных процессов. Однако он позволяет реализовать совместное использование памяти и для неродственных процессов.

2. Объекты разделяемой памяти: функция shm_open открывает объект IPC с именем стандарта Posix (например, полным именем объекта файловой системы), возвращая дескриптор, который может быть использован для отображения в адресное пространство процесса вызовом mmap. Данный метод будет описан в этой главе.

Оба метода требуют вызова mmap. Отличие состоит в методе получения дескриптора, являющегося аргументом mmap: в первом случае он возвращается функцией open, а во втором — shm_open. Мы показываем это на рис. 13.1. Стандарт Posix называет объектами памяти (memory objects) и отображенные в память файлы, и объекты разделяемой памяти стандарта Posix.

13.2. Функции shm_open и shm_unlink

Процесс получения доступа к объекту разделяемой памяти Posix выполняется в два этапа:

1. Вызов shm_open с именем IPC в качестве аргумента позволяет либо создать новый объект разделяемой памяти, либо открыть существующий.

Рис. 13.1. Объекты памяти Posix: отображаемые в память файлы и объекты разделяемой памяти


2. Вызов mmap позволяет отобразить разделяемую память в адресное пространство вызвавшего процесса.

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

ПРИМЕЧАНИЕ

Причина, по которой этот процесс выполняется в два этапа вместо одного, на котором в ответ на имя объекта возвращался бы адрес соответствующей области памяти, заключается в том, что функция mmap уже существовала, когда эта форма разделяемой памяти была включена в стандарт Posix. Разумеется, эти два действия могли бы выполняться и одной функцией. Функция shm_open возвращает дескриптор (вспомните, что mq_open возвращает значение типа mqd_t, a sem_open возвращает указатель на значение типа sem_t), потому что для отображения объекта в адресное пространство процесса функция mmap использует именно дескриптор этого объекта.

#include 

int shm_open(const char *name, int oflag, mode_t mode);

/* Возвращает неотрицательный дескриптор в случае успешного завершения, -1 – в случае ошибки */

int shm_unlink(const char *name);

/* Возвращает 0 в случае успешного завершения, -1 – в случае ошибки */

Требования и правила, используемые при формировании аргумента name, были описаны в разделе 2.2.

Аргумент oflag должен содержать флаг O_RDONLY либо O_RDWR и один из следующих: O_CREAT, O_EXCL, O_TRUNC. Флаги O_CREAT и O_EXCL были описаны в разделе 2.3. Если вместе с флагом O_RDWR указан флаг O_TRUNC, существующий объект разделяемой памяти будет укорочен до нулевой длины.

Аргумент mode задает биты разрешений доступа (табл. 2.3) и используется только при указании флага O_CREAT. Обратите внимание, что в отличие от функций mq_open и sem_open для shm_open аргумент mode указывается всегда. Если флаг O_CREAT не указан, значение аргумента mode может быть нулевым.

Возвращаемое значение shm_open представляет собой целочисленный дескриптор, который может использоваться при вызове mmap в качестве пятого аргумента.

Функция shm_unlink удаляет имя объекта разделяемой памяти. Как и другие подобные функции (удаление файла из файловой системы, удаление очереди сообщений и именованного семафора Posix), она не выполняет никаких действий до тех пор, пока объект не будет закрыт всеми открывшими его процессами. Однако после вызова shm_unlink последующие вызовы open, mq_open и sem_open выполняться не будут.

13.3. Функции ftruncate и fstat

Размер файла или объекта разделяемой памяти можно изменить вызовом ftruncate:

#include 

int ftruncate(int fd, off_t length);

/* Возвращает 0 в случае успешного завершения, –1 – в случае ошибки */

Стандарт Posix делает некоторые различия в определении действия этой функции для обычных файлов и для объектов разделяемой памяти.

1. Для обычного файла: если размер файла превышает значение length, избыточные данные отбрасываются. Если размер файла оказывается меньше значения length, действие функции не определено. Поэтому для переносимости следует использовать следующий способ увеличения длины обычного файла: вызов 1 seek со сдвигом length-1 и запись 1 байта в файл. К счастью, почти все реализации Unix поддерживают увеличение размера файла вызовом ftruncate.

2.  Для объекта разделяемой памяти: ftruncate устанавливает размер объекта равным значению аргумента length.

Итак, мы вызываем ftruncate для установки размера только что созданного объекта разделяемой памяти или изменения размера существующего объекта. При открытии существующего объекта разделяемой памяти следует воспользоваться fstat для получения информации о нем:

#include 

#include 

int fstat(int fd, struct stat *buf);

/* Возвращает 0 в случае успешного завершения. –1 – в случае ошибки */

В структуре stat содержится больше десятка полей (они подробно описаны в главе 4 [21]), но только четыре из них содержат актуальную информацию, если fd представляет собой дескриптор области разделяемой памяти:

struct stat {

 …

 mode_t st_mode; /* mode: S_I{RW}{USR,GRP,OTH} */

 uid_t st_uid; /* UID владельца */

 gid_t st_gid; /* GID владельца */

 off_t st_size; /* размер в байтах */

 …

};

Пример использования этих двух функций будет приведен в следующем разделе. 

ПРИМЕЧАНИЕ

К сожалению, стандарт Posix никак не оговаривает начальное содержимое разделяемой памяти. Описание функции shm_open гласит, что «объект разделяемой памяти будет иметь нулевой размер». Описание ftruncate гласит, что для обычных файлов (не объектов разделяемой памяти) «при увеличении размера файла он будет дополнен нулями». Однако в этом описании ничего не говорится о содержимом разделяемой памяти. Обоснование Posix.1 (Rationale) говорит, что «разделяемая память при расширении дополняется нулями», но это не официальный стандарт. Когда автор попытался уточнить этот вопрос в конференции comp.std.unix, он узнал, что некоторые производители протестовали против введения требования на заполнение памяти нулями из-за возникающих накладных расходов. Если новая область памяти не инициализируется каким-то значением (то есть содержимое остается без изменения), это может угрожать безопасности системы.

13.4. Простые программы

Приведем несколько примеров программ, работающих с разделяемой памятью Posix.

Программа shmcreate

Программа shmcreate, текст которой приведен в листинге 13.1,[1] создает объект разделяемой памяти с указанным именем и длиной.

Листинг 13.1. Создание объекта разделяемой памяти Posix указанного размера

//pxshm/shmcreate.c

1  #include "unpipc.h"


2  int

3  main(int argc, char **argv)

4  {

5   int c, fd, flags;

6   char *ptr;

7   off_t length;

8   flags = O_RDWR | O_CREAT;

9   while ((c = Getopt(argc, argv, "e")) != –1) {

10   switch (c) {

11   case 'e':

12    flags |= O_EXCL;

13    break;

14   }

15  }

16  if (optind != argc – 2)

17   err_quit("usage: shmcreate [ –e ] ");

18  length = atoi(argv[optind + 1]);

19  fd = Shm_open(argv[optind], flags, FILE_MODE);

20  Ftruncate(fd, length);

21  ptr = Mmap(NULL, length, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);

22  exit(0);

23 }

19-22 Вызов shm_open создает объект разделяемой памяти. Если указан параметр –е, будет возвращена ошибка в том случае, если такой объект уже существует. Вызов ftruncate устанавливает длину (размер объекта), a mmap отображает его содержимое в адресное пространство процесса. Затем программа завершает работу. Поскольку разделяемая память Posix обладает живучестью ядра, объект разделяемой памяти при этом не исчезает.

Программа shmunlink

В листинге 13.2 приведен текст тривиальной программы, удаляющей имя объекта разделяемой памяти из системы.

Листинг 13.2. Удаление имени объекта разделяемой памяти Posix

//pxshm/shmunlink.c

1 #include "unpipc.h"


2 int

3 main(int argc, char **argv)

4 {

5  if (argc != 2)

6   err_quit("usage: shmunlink ");

7  Shm_unlink(argv[1]);

8  exit(0);

9 }

Программа shmwrite

В листинге 13.3 приведен текст программы shmwrite, записывающей последовательность 0, 1, 2 254, 244, 0, 1 и т. д. в объект разделяемой памяти.

Листинг 13.3. Заполнение разделяемой памяти

//pxshm/shmwrite.с

1  #include "unpipc.h"


2  int

3  main(int argc, char **argv)

4  {

5   int i, fd;

6   struct stat stat;

7   unsigned char *ptr;

8   if (argc != 2)

9    err_quit("usage: shmwrite ");

10  /* open, определяем размер, отображаем в память */

11  fd = Shm_open(argv[1], O_RDWR, FILE_MODE);

12  Fstat(fd, &stat);

13  ptr = Mmap(NULL, stat.st_size, PROT_READ | PROT_WRITE,

14   MAP_SHARED, fd, 0);

15  Close(fd);

16  /* присваиваем: ptr[0] = 0, ptr[1] = 1 и т. д. */

17  for (i = 0; i < stat.st_size; i++)

18   *ptr++ = i % 256;

19  exit(0);

20 }

10-15 Объект разделяемой памяти открывается вызовом shm_open. Его размер мы узнаем с помощью fstat. Затем файл отображается в память вызовом mmap, после чего его дескриптор может быть закрыт.

16-18 Последовательность записывается в разделяемую память.

Программа shmread

Программа shmread (листинг 13.4) проверяет значения, помещенные в разделяемую память программой shmwrite.

Листинг 13.4. Проверка значений в разделяемой памяти

//pxshm/shmread.c

1  #include "unpipc.h"


2  int

3  main(int argc, char **argv)

4  {

5   int i, fd;

6   struct stat stat;

7   unsigned char c, *ptr;

8   if (argc != 2)

9    err_quit("usage: shmread ");

10  /* вызываем open, узнаем размер, отображаем в память*/

11  fd = Shm_open(argv[1], O_RDONLY, FILE_MODE);

12  Fstat(fd, &stat);

13  ptr = Mmap(NULL, stat.st_size, PROT_READ,

14   MAP_SHARED, fd, 0);

15  Close(fd);

16  /* проверяем равенства ptr[0] = 0, ptr[1] = 1 и т. д. */

17  for (i = 0; i < stat.st_size; i++)

18   if ((c = *ptr++) != (i % 256))

19    err_ret("ptr[%d] = %d", i, c);

20  exit(0);

21 }

10-15 Объект разделяемой памяти открывается только для чтения, его размер получается вызовом fstat, после чего он отображается в память с доступом только на чтение, а дескриптор закрывается.

16-19 Проверяются значения, помещенные в разделяемую память вызовом shmwrite.

Примеры

Создадим объект разделяемой памяти с именем /tmp/myshm объемом 123 456 байт в системе Digital Unix 4.0B:

alpha % shmcreate /tmp/myshm 123456

alpha % ls –l /tmp/myshm

-rw-r--r-- 1 rstevens system 123456 Dec 10 14:33 /tmp/myshm

alpha % od –c /tmp/myshm

0000000 \0 \0 \0 \0 \0 \0 \0 \0 \0 \0 \0 \0 \0 \0 \0 \0

*

0361100

Мы видим, что файл с указываемым при создании объекта разделяемой памяти именем появляется в файловой системе. Используя программу od, мы можем выяснить, что после создания файл целиком заполнен нулями (восьмеричное число 0361100 — сдвиг, соответствующий байту, следующему за последним байтом файла, — эквивалентно десятичному 123 456).

Запустим программу shmwrite и убедимся в правильности записываемых значений с помощью программы od:

alpha % shmwrite /tmp/myshm

alpha * od –x /tmp/myshm | head-4

0000000 0100 0302 0504 0706 0908 0b0a 0d0c 0f0e

0000020 1110 1312 1514 1716 1918 1b1a 1d1c 1f1e

0000040 2120 2322 2524 2726 2928 2b2a 2d2c 2f2e

0000060 3130 3332 3534 3736 3938 3b3a 3d3c 3f3e

alpha % shmread /tmp/myshm

alpha % shmunlink /tmp/myshm

Мы проверили содержимое разделяемой памяти и с помощью shmread, а затем удалили объект, запустив программу shmunlink.

Если теперь мы запустим программу shmcreate в Solaris 2.6, то увидим, что файл указанного размера создается в каталоге /tmp:

solaris % shmcreate –e /testshm 123

solaris % ls-l/tmp/.*testshm*

-rw-r--r-- 1 rstevens other1 123 Dec 10 14:40 /tmp/.SHMtestshm

Пример

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

Листинг 13.5. Разделяемая память может начинаться с разных адресов в разных процессах

//pxshm/test3.c

1  #include "unpipc.h"


2  int

3  main(int argc, char **argv)

4  {

5   int fd1, fd2, *ptr1, *ptr2;

6   pid_t childpid;

7   struct stat stat;

8   if (argc != 2)

9    err_quit("usage: test3 ");

10  shm_unlink(Px_ipc_name(argv[1]));

11  fd1 = Shm_open(Px_ipc_name(argv[1]), O_RDWR | O_CREAT | O_EXCL, FILE_MODE);

12  Ftruncate(fd1, sizeof(int));

13  fd2 = Open("/etc/motd", O_RDONLY);

14  Fstat(fd2, &stat);

15  if ((childpid = Fork()) == 0) {

16   /* дочерний процесс */

17   ptr2 = Mmap(NULL, stat.st_size, PROT_READ, MAP_SHARED, fd2, 0);

18   ptr1 = Mmap(NULL, sizeof(int), PROT_READ | PROT_WRITE,

19    MAP_SHARED, fd1, 0);

20   printf("child: shm ptr = %p, motd ptr = %p\n", ptr1, ptr2);

21   sleep(5);

22   printf("shared memory integer = %d\n", *ptr1);

23   exit(0);

24  }

25  /* родительский процесс: вызовы map следуют в обратном порядке */

26  ptr1 = Mmap(NULL, sizeof(int), PROT_READ | PROT_WRITE, MAP_SHARED, fd1, 0);

27  ptr2 = Mmap(NULL, stat.st_size, PROT_READ, MAP_SHARED, fd2, 0);

28  printf("parent: shm ptr = %p, motd ptr = %p\n", ptr1, ptr2);

29  *ptr1 = 777;

30  Waitpid(childpid, NULL, 0);

31  exit(0);

32 }

10-14 Создаем сегмент разделяемой памяти с именем, принимаемым в качестве аргумента командной строки. Его размер устанавливается равным размеру целого. Затем открываем файл /etc/motd.

15-30 После вызова fork и родительский, и дочерний процессы вызывают mmap дважды, но в разном порядке. Каждый процесс выводит начальный адрес каждой из областей памяти. Затем дочерний процесс ждет 5 секунд, родительский процесс помещает значение 777 в область разделяемой памяти, после чего дочерний процесс считывает и выводит это значение.

Запустив эту программу, мы убедимся, что объект разделяемой памяти начинается с разных адресов в пространствах дочернего и родительского процессов:

solaris % test3 test3.data

parent: shm ptr = eee30000, motd ptr = eee20000

child: shm ptr = eee20000, motd ptr = eee30000

shared memory integer = 777

Несмотря на разницу в начальных адресах, родительский процесс успешно помещает значение 777 по адресу 0xeee30000, а дочерний процесс благополучно считывает его по адресу 0хеее20000. Указатели ptr1 в родительском и дочернем процессах указывают на одну и ту же область разделяемой памяти, хотя их значения в этих процессах различны.

13.5. Увеличение общего счетчика

Разработаем программу, аналогичную приведенной в разделе 12.3, — несколько процессов увеличивают счетчик, хранящийся в разделяемой памяти. Итак, мы помещаем счетчик в разделяемую память, а для синхронизации доступа к нему используем именованный семафор. Отличие программы из этого раздела от предыдущей состоит в том, что процессы более не являются родственными. Поскольку обращение к объектам разделяемой памяти Posix и именованным семафорам Posix осуществляется по именам, процессы, увеличивающие общий счетчик, могут не состоять в родстве. Достаточно лишь, чтобы каждый из них знал имя IPC счетчика и чтобы у каждого были соответствующие разрешения на доступ к объектам IPC (области разделяемой памяти и семафору).

В листинге 13.6 приведен текст программы-сервера, которая создает объект разделяемой памяти, затем создает и инициализирует семафор, после чего завершает работу.

Листинг 13.6. Программа, создающая и инициализирующая объект разделяемой памяти и семафор

//pxshm/server1.c

1  #include "unpipc.h"


2  struct shmstruct { /* структура, помещаемая в разделяемую память */

3   int count;

4  };

5  sem_t *mutex; /* указатель на именованный семафор */


6  int

7  main(int argc, char **argv)

8  {

9   int fd;

10  struct shmstruct *ptr;

11  if (argc != 3)

12   err_quit("usage: server1 ");

13  shm_unlink(Px_ipc_name(argv[1])); /* ошибки игнорируются */

14  /* создание shm. установка размера, отображение, закрытие дескриптора */

15  fd = Shm_open(Px_ipc_name(argv[1]), O_RDWR | O_CREAT | O_EXCL, FILE_MODE);

16  Ftruncate(fd, sizeof(struct shmstruct));

17  ptr = Mmap(NULL, sizeof(struct shmstruct), PROT_READ | PROT_WRITE,

18   MAP_SHARED, fd, 0);

19  Close(fd);

20  sem_unlink(Px_ipc_name(argv[2])); /* игнорируем ошибку */

21  mutex = Sem_open(Px_ipc_name(argv[2]), O_CREAT | O_EXCL, FILE_MODE, 1);

22  Sem_close(mutex);

23  exit(0);

24 }

Создание объекта разделяемой памяти

13-19 Программа начинает работу с вызова shm_unlink, на тот случай, если объект разделяемой памяти еще существует, а затем делается вызов shm_open, создающий этот объект. Его размер устанавливается равным размеру структуры sbmstruct вызовом ftruncate, а затем mmap отображает объект в наше адресное пространство. После этого дескриптор объекта закрывается.

Создание и инициализация семафора

20-22 Сначала мы вызываем sem_unlink, на тот случай, если семафор еще существует. Затем делается вызов sem_open для создания именованного семафора и инициализации его единицей. Этот семафор будет использоваться в качестве взаимного исключения всеми процессами, которые будут обращаться к объекту разделяемой памяти. После выполнения этих операций семафор закрывается.

Завершение работы процесса

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

Нам приходится использовать разные имена для семафора и объекта разделяемой памяти. Нет никаких гарантий, что в данной реализации к именам Posix IPC будут добавляться какие-либо суффиксы или префиксы, указывающие тип объекта (очередь сообщений, семафор, разделяемая память). Мы видели, что в Solaris эти типы имен имеют префиксы .MQ, .SEM и .SHM, но в Digital Unix они префиксов не имеют.

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

Листинг 13.7. Программа, увеличивающая значение счетчика в разделяемой памяти

//pxshm/client1.c

1  #include "unpipc.h"


2  struct shmstruct { /* структура, помещаемая в разделяемую память */

3   int count;

4  };

5  sem_t *mutex; /* указатель на именованный семафор */


6  int

7  main(int argc, char **argv)

8  {

9   int fd, i, nloop;

10  pid_t pid;

11  struct shmstruct *ptr;

12  if (argc != 4)

13   err_quit("usage: client1 <#loops>");

14  nloop = atoi(argv[3]);

15  fd = Shm_open(Px_ipc_name(argv[1]), O_RDWR, FILE_MODE);

16  ptr = Mmap(NULL, sizeof(struct shmstruct), PROT_READ | PROT_WRITE,

17   MAP_SHARED, fd, 0);

18  Close(fd);

19  mutex = Sem_open(Px_ipc_name(argv[2]), 0);

20  pid = getpid();

21  for (i = 0; i < nloop; i++) {

22   Sem_wait(mutex);

23   printf("pid %ld: %d\n", (long) pid, ptr->count++);

24   Sem_post(mutex);

25  }

26  exit(0);

27 }

Открытие области разделяемойпамяти

15-18 Вызов shm_open открывает объект разделяемой памяти, который должен уже существовать (поскольку не указан флаг O_CREAT). Память отображается в адресное пространство процесса вызовом mmap, после чего дескриптор закрывается.

Открытие семафора

19 Открываем именованный семафор.

Блокирование семафора и увеличение счетчика

20-26 Параметр командной строки позволяет указать количество увеличений счетчика. Каждый раз мы выводим предыдущее значение счетчика вместе с идентификатором процесса, поскольку одновременно работают несколько экземпляров программы.

Запустим сначала сервер, а затем три экземпляра программы-клиента в фоновом режиме.

solaris % server shm1 sem1

solaris % client1 shm1 sem110000 &client1 shm1 sem110000 &client1 shm1 sem1 10000&

[2] 17976        интерпретатор выводит идентификаторы процессов

[3] 17977

[4] 17978

pid 17977: 0     и этот процесс запускается первым

pid 17977: 1

. . .            процесс 17977 продолжает работу

pid 17977: 32

pid 17976: 33    ядро переключается междупроцессами

. . .            процесс 17976 продолжает работу

pid 17976: 707

pid 17978: 708   ядро переключается между процессами

. . .            процесс 17978 продолжает работу

pid 17978: 852

pid 17977: 853   ядро переключается между процессами

. . .            и т.д.

pid 17977: 29997

pid 17977: 29999 последнее выводимое значение. Оно оказывается правильным.

13.6. Отправка сообщений на сервер

Изменим наше решение задачи производителей и потребителей следующим образом. Сначала запускается сервер, создающий объект разделяемой памяти, в который клиенты записывают свои сообщения. Сервер просто выводит содержимое этих сообщений, хотя задачу можно и обобщить таким образом, чтобы он выполнял действия, аналогичные демону syslog, который описан в главе 13 [24]. Мы называем группу отправляющих сообщения процессов клиентами, потому что по отношению к нашему серверу они ими и являются, однако эти клиенты могут являться серверами по отношению к другим приложениям. Например, сервер Telnet является клиентом демона syslog, когда отправляет ему сообщения для занесения их в системный журнал.

Вместо передачи сообщений одним из описанных ранее методов (часть 2) будем хранить сообщения в разделяемой памяти. Это, разумеется, потребует какой-либо формы синхронизации действий клиентов, помещающих сообщения, и сервера, читающего их. На рис. 13.2 приведена схема приложения в целом.

Рис. 13.2. Несколько клиентов отправляют сообщения серверу через разделяемую память


Перед нами взаимодействие нескольких производителей (клиентов) и одного потребителя (сервер). Разделяемая память отображается в адресное пространство сервера и каждого из клиентов.

В листинге 13.8 приведен текст заголовочного файла cliserv2.h, в котором определена структура объекта, хранимого в разделяемой памяти.

Листинг 13.8. Заголовочный файл, определяющий содержимое разделяемой памяти

//pxshm/cliserv2.h

1  #include "unpipc.h"


2  #define MESGSIZE 256 /* максимальный размер сообщения в байтах, включая завершающий ноль */

3  #define NMESG 16 /* максимальное количество сообщений */


4  struct shmstruct { /* структура, хранящаяся в разделяемой памяти */

5   sem_t mutex; /* три семафора Posix, размещаемые в памяти */

6   sem_t nempty;

7   sem_t nstored;

8   int nput; /* индекс для следующего сообщения */

9   long noverflow; /* количество переполнений */

10  sem_t noverflowmutex; /* взаимное исключение для счетчика переполнений */

11  long msgoff[NMESG]; /* сдвиг для каждого из сообщений */

12  char msgdata[NMESG * MESGSIZE]; /* сами сообщения */

13 };

Основные семафоры и переменные

5-8 Три семафора Posix, размещаемых в памяти, используются для того же, для чего семафоры использовались в задаче производителей и потребителей в разделе 10.6. Их имена mutex, nempty, nstored. Переменная nput хранит индекс следующего помещаемого сообщения. Поскольку одновременно работают несколько производителей, эта переменная защищена взаимным исключением и хранится в разделяемой памяти вместе со всеми остальными.

Счетчик переполнений

9-10 Существует вероятность того, что клиент не сможет отправить сообщение из-за отсутствия свободного места для него. Если программа-клиент представляет собой сервер для других приложений (например, сервер FTP или HTTP), она не должна блокироваться в ожидании освобождения места для сообщения. Поэтому программа-клиент будет написана таким образом, чтобы она не блокировалась, но увеличивала счетчик переполнений (noverflow). Поскольку этот счетчик также является общим для всех процессов, он также должен быть защищен взаимным исключением, чтобы его значение не было повреждено.

Сдвиги сообщений и их содержимое

11-12 Массив msgoff содержит сдвиги сообщений в массиве msgdata, в котором сообщения хранятся подряд. Таким образом, сдвиг первого сообщения msgoff[0] = 0, msgoff [1] = 256 (значение MESGSIZE), msgoff [2] = 512 и т. д.

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

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

Листинг 13.9. Сервер, считывающий сообщения из разделяемой памяти

//pxshm/server2.c

1  #include "cliserv2.h"


2  int

3  main(int argc, char **argv)

4  {

5   int fd, index, lastnoverflow, temp;

6   long offset;

7   struct shmstruct *ptr;

8   if (argc != 2)

9    err_quit("usage: server2 ");

10  /* создание объекта разделяемой памяти, установка размера, отображение в память, закрытие дескриптора */

11  shm_unlink(Px_ipc_name(argv[1])); /* ошибка игнорируется */

12  fd = Shm_open(Px_ipc_name(argv[1]), O_RDWR | O_CREAT | O_EXCL, FILE_MODE);

13  ptr = Mmap(NULL, sizeof(struct shmstruct), PROT_READ | PROT_WRITE,

14   MAP_SHARED, fd, 0);

15  Ftruncate(fd, sizeof(struct shmstruct));

16  Close(fd);

17  /* инициализация массива сдвигов */

18  for (index = 0; index < NMESG; index++)

19   ptr->msgoff[index] = index * MESGSIZE;

20  /* инициализация семафоров в разделяемой памяти */

21  Sem_init(&ptr->mutex, 1, 1);

22  Sem_init(&ptr->nempty, 1, NMESG);

23  Sem_init(&ptr->nstored, 1, 0);

24  Sem_init(&ptr->noverflowmutex, 1, 1);

25  /* программа-потребитель */

26  index = 0;

27  lastnoverflow = 0;

28  for (;;) {

29   Sem_wait(&ptr->nstored);

30   Sem_wait(&ptr->mutex);

31   offset = ptr->msgoff[index];

32   printf("index = %d: %s\n", index, &ptr->msgdata[offset]);

33   if (++index >= NMESG)

34    index =0; /* циклический буфер */

35   Sem_post(&ptr->mutex);

36   Sem_post(&ptr->nempty);

37   Sem_wait(&ptr->noverflowmutex);

38   temp = ptr->noverflow; /* не выводим, пока не снимем блокировку */

39   Sem_post(&ptr->noverflowmutex);

40   if (temp != lastnoverflow) {

41    printf("noverflow = %d\n", temp);

42    lastnoverflow = temp;

43   }

44  }

45  exit(0);

46 }

Создание объекта разделяемой памяти

10-16 Сначала делается вызов shm_unlink, чтобы удалить объект с тем же именем, который мог остаться после другого приложения. Затем объект разделяемой памяти создается вызовом shm_open и отображается в адресное пространство процесса вызовом mmap, после чего дескриптор объекта закрывается.

Инициализация массива сдвигов

17-19 Массив сдвигов инициализируется сдвигами сообщений.

Инициализация семафоров

20-24 Инициализируются четыре семафора, размещаемые в объекте разделяемой памяти. Второй аргумент sem_init всегда делается ненулевым, поскольку семафоры будут использоваться совместно несколькими процессами.

Ожидание сообщения, вывод его содержимого

25-36 Первая половина цикла for написана по стандартному алгоритму потребителя: ожидание изменения семафора nstored, установка блокировки для семафора mutex, обработка данных, увеличение значения семафора nempty.

Обработка переполнений

37-43 При каждом проходе цикла мы проверяем наличие возникших переполнений. Сравнивается текущее значение noverflows с предыдущим. Если значение изменилось, оно выводится на экран и сохраняется. Обратите внимание, что значение считывается с заблокированным взаимным исключением noverflowmutex, но блокировка снимается перед сравнением и выводом значения. Идея в том, что нужно всегда следовать общему правилу минимизации количества операций, выполняемых с заблокированным взаимным исключением. В листинге 13.10 приведен текст программы-клиента.

Листинг 13.10. Клиент, помещающий сообщения в разделяемую память

//pxshm/client2.c

1  #include "cliserv2.h"


2  int

3  main(int argc, char **argv)

4  {

5   int fd, i, nloop, nusec;

6   pid_t pid;

7   char mesg[MESGSIZE];

8   long offset;

9   struct shmstruct *ptr;

10  if (argc != 4)

11   err_quit("usage: client2 <#loops><#usec>");

12  nloop = atoi(argv[2]);

13  nusec = atoi(argv[3]);

14  /* открытие и отображение объекта разделяемой памяти, созданного сервером заранее */

15  fd = Shm_open(Px_ipc_name(argv[1]), O_RDWR, FILE_MODE);

16  ptr = Mmap(NULL, sizeof(struct shmstruct), PROT_READ | PROT_WRITE,

17   MAP_SHARED, fd, 0);

18  Close(fd);

19  pid = getpid();

20  for (i = 0; i < nloop; i++) {

21   Sleep_us(nusec);

22   snprintf(mesg, MESGSIZE, "pid %ld; message %d", (long) pid, i);

23   if (sem_trywait(&ptr->nempty) == –1) {

24    if (errno == EAGAIN) {

25     Sem_wait(&ptr->noverflowmutex);

26     ptr->noverflow++;

27     Sem_post(&ptr->noverflowmutex);

28     continue;

29    } else

30     err_sys("sem_trywait error");

31   }

32   Sem_wait(&ptr->mutex);

33   offset = ptr->msgoff[ptr->nput];

34   if (++(ptr->nput) >= NMESG)

35    ptr->nput = 0; /* циклический буфер */

36   Sem_post(&ptr->mutex);

37   strcpy(&ptr->msgdata[offset], mesg);

38   Sem_post(&ptr->nstored);

39  }

40  exit(0);

41 }

Аргументы командной строки

10-13 Первый аргумент командной строки задает имя объекта разделяемой памяти; второй — количество сообщений, которые должны быть отправлены серверу данным клиентом. Последний аргумент задает паузу перед отправкой очередного сообщения (в микросекундах). Мы сможем получить ситуацию переполнения, запустив одновременно несколько экземпляров клиентов и указав небольшое значение для этой паузы. Таким образом мы сможем убедиться, что сервер корректно обрабатывает ситуацию переполнения.

Открытие и отображение разделяемой памяти

14-18 Мы открываем объект разделяемой памяти, предполагая, что он уже создан и проинициализирован сервером, а затем отображаем его в адресное пространство процесса. После этого дескриптор может быть закрыт.

Отправка сообщений

19-31 Клиент работает по простому алгоритму программы-производителя, но вместо вызова sem_wait(nempty), который приводил бы к блокированию клиента в случае отсутствия места в буфере для следующего сообщения, мы вызываем sem_trywait — эта функция не блокируется. Если значение семафора нулевое, возвращается ошибка EAGAIN. Мы обрабатываем эту ошибку, увеличивая значение счетчика переполнений.

ПРИМЕЧАНИЕ

sleep_us — функция из листингов С.9 и С.10 [21]. Она приостанавливает выполнение программы на заданное количество микросекунд. Реализуется вызовом select или poll. 

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

Сначала запустим сервер в фоновом режиме, а затем запустим один экземпляр программы-клиента, указав 50 сообщений и нулевую паузу между ними: 

solaris % server2 serv2 &

[2] 27223

solaris % client2 serv250 0

index = 0: pid 27224: message 0

index = 1: pid 27224: message 1

index = 2: pid 27224: message 2

…                                продолжает в том же духе

index = 15: pid 27224: message 47

index = 0: pid 27224: message 48

index = 1: pid 27224: message 49 нет утерянных сообщений

Но если мы запустим программу-клиент еще раз, то мы увидим возникновение переполнений.

solaris % client2 serv250 0

index = 2: pid 27228: message 0

index = 3: pid 27228: message 1

…              пока все в порядке

index = 10: pid 27228: message 8

index = 11: pid 27228: message 9

noverflow = 25 утеряно 25 сообщений

index = 12: pid 27228: message 10

index = 13: pid 27228: message 11

…              нормально обрабатываются сообщения 12-22

index = 9: pid 27228: message 23

index = 10: pid 27228: message 24

На этот раз клиент успешно отправил сообщения 0-9, которые были получены и выведены сервером. Затем клиент снова получил управление и поместил сообщения 10-49, но места хватило только для первых 15, а последующие 25 (с 25 по 49) не были сохранены из-за переполнения:

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

ПРИМЕЧАНИЕ

Переполнение приемного буфера данными встречается не только в этом примере. В разделе 8.13 [24] обсуждалась такая ситуация в связи с дейтаграммами UDP и приемным буфером сокета UDP. В разделе 18.2 [23] подробно рассказывается о том, как доменные сокеты Unix возвращают отправителю ошибку ENOBUFS при переполнении приемного буфера получателя. Это отличает доменные сокеты от протокола UDP. Программа-клиент из листинга 13.10 узнает о переполнении буфера, поэтому если этот код поместить в функцию общего назначения, которую затем будут использовать другие программы, такая функция сможет возвращать ошибку вызывающему процессу при переполнении буфера сервера.

13.7. Резюме

Разделяемая память Posix реализуется с помощью функции mmap, обсуждавшейся в предыдущей главе. Сначала вызывается функция shm_open с именем объекта Posix IPC в качестве одного из аргументов. Эта функция возвращает дескриптор, который затем передается функции mmap. Результат аналогичен отображению файла в память, но разделяемая память Posix не обязательно реализуется через файл.

Поскольку доступ к объектам разделяемой памяти может быть получен через дескриптор, для установки размера объекта используется функция ftruncate, а информация о существующем объекте (биты разрешений, идентификаторы пользователя и группы, размер) возвращается функцией fstat.

В главах, рассказывающих об очередях сообщений и семафорах Posix, мы приводили примеры их реализации через отображение в память (разделы 5.8 и 10.15). Для разделяемой памяти Posix мы этого делать не будем, поскольку реализация тривиальна. Если мы готовы использовать отображение в файл (что и сделано в Solaris и Digital Unix), shm_open реализуется через open, a shm_unlink — через unlink.

Упражнения

1. Измените программы из листингов 12.6 и 12.7 таким образом, чтобы они работали с разделяемой памятью Posix, а не с отображаемым в память файлом. Убедитесь, что результаты будут такими же, как и для отображаемого в память файла.

2. В циклах for в листингах 13.3 и 13.4 используется команда *ptr++ для перебора элементов массива. Не лучше ли было бы использовать ptr[i]? 

ГЛАВА 14