• ГЛАВА 12 Введение в разделяемую память
  • 12.1. Введение
  • 12.2. Функции mmap, munmap и msync
  • Почему вообще используется отображение в память?
  • 12.3. Увеличение счетчика в отображаемом в память файле
  • 12.4. Неименованное отображение в память в 4.4BSD
  • 12.5. Отображение в память в SVR4 с помощью /dev/zero
  • 12.6. Обращение к объектам, отображенным в память
  • 12.7. Резюме
  • Упражнения
  • ГЛАВА 13 Разделяемая память Posix
  • 13.1. Введение
  • 13.2. Функции shm_open и shm_unlink
  • 13.3. Функции ftruncate и fstat
  • 13.4. Простые программы
  • Программа shmcreate
  • Программа shmunlink
  • Программа shmwrite
  • Программа shmread
  • Примеры
  • Пример
  • 13.5. Увеличение общего счетчика
  • 13.6. Отправка сообщений на сервер
  • 13.7. Резюме
  • Упражнения
  • ГЛАВА 14 Разделяемая память System V
  • 14.1. Введение
  • 14.2. Функция shmget
  • 14.3. Функция shmat
  • 14.4. Функция shmdt
  • 14.5. Функция shmctl
  • 14.6. Простые программы
  • Программа shmget
  • Программа shmrmid
  • Программа shmwrite
  • Программа shmread
  • Примеры
  • 14.7. Ограничения, накладываемые на разделяемую память
  • Пример
  • 14.8. Резюме
  • Упражнение
  • ЧАСТЬ 4

    РАЗДЕЛЯЕМАЯ ПАМЯТЬ

    ГЛАВА 12

    Введение в разделяемую память

    12.1. Введение

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

    ПРИМЕЧАНИЕ

    Говоря «не требуется участие ядра», мы подразумеваем, что процессы не делают системных вызовов для передачи данных. Очевидно, что все равно именно ядро обеспечивает отображение памяти, позволяющее процессам совместно ею пользоваться, и затем обслуживает эту память (обрабатывает сбои страниц и т. п.).

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

    ■ Сервер считывает данные из входного файла. Данные из файла считываются ядром в свою память, а затем копируются из ядра в память процесса.

    ■ Сервер составляет сообщение из этих данных и отправляет его, используя именованный или неименованный канал или очередь сообщений. Эти формы IPC обычно требуют копирования данных из процесса в ядро.

    ПРИМЕЧАНИЕ

    Мы говорим «обычно», поскольку очереди сообщений Posix могут быть реализованы через отображение файла в память (функцию mmap мы опишем в этой главе), как мы показали в разделе 5.8 и в решении упражнения 12.2. На рис. 12.1 мы предполагаем, что очереди сообщений Posix реализованы в ядре, что также возможно. Но именованные и неименованные каналы и очереди сообщений System V требуют копирования данных из процесса в ядро вызовом write или msgsnd или копирования данных из ядра процессу вызовом read или msgrcv. 

    ■ Клиент считывает данные из канала IPC, что обычно требует их копирования из ядра в пространство процесса.

    ■ Наконец, данные копируются из буфера клиента (второй аргумент вызова write) в выходной файл.

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

    Рис. 12.1. Передача содержимого файла от сервера к клиенту


    Недостатком этих форм IPC — именованных и неименованных каналов — является то, что для передачи между процессами информация должна пройти через ядро.

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

    Теперь информация передается между клиентом и сервером в такой последовательности:

    ■ сервер получает доступ к объекту разделяемой памяти, используя для синхронизации семафор (например);

    ■ сервер считывает данные из файла в разделяемую память. Второй аргумент вызова read (адрес буфера) указывает на объект разделяемой памяти;

    ■ после завершения операции считывания клиент уведомляется сервером с помощью семафора;

    ■ клиент записывает данные из объекта разделяемой памяти в выходной файл. 

    Рис. 12.2. Копирование файла через разделяемую память


    Этот сценарий иллюстрирует рис. 12.2.

    Из этого рисунка видно, что копирование данных происходит всего лишь дважды: из входного файла в разделяемую память и из разделяемой памяти в выходной файл. Мы нарисовали два прямоугольника штриховыми линиями; они подчеркивают, что разделяемая память принадлежит как адресному пространству клиента, так и адресному пространству сервера.

    Концепции, связанные с использованием разделяемой памяти через интерфейсы Posix и System V, похожи. Первый интерфейс описан в главе 13, а второй — в главе 14.

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

    Сначала мы подчеркнем, что память разделяется между родительским и дочерним процессами при вызове fork. В пpoгрaммe из листинга 12.1[1] родительский и дочерний процессы по очереди увеличивают глобальный целочисленный счетчик count.

    Листинг 12.1. Увеличение глобального счетчика родительским и дочерним процессами

    //shm/incr1.c

    1  #include "unpipc.h"

    2  #define SEM_NAME "mysem"

    3  int count = 0;


    4  int

    5  main(int argc, char **argv)

    6  {

    7   int i, nloop;

    8   sem_t *mutex;

    9   if (argc != 2)

    10   err_quit("usage: incr1 <#loops>");

    11  nloop = atoi(argv[1]);

    12  /* создание, инициализация и удаление семафора */

    13  mutex = Sem_open(Px_ipc_name(SEM_NAME), O_CREAT | O_EXCL, FILE_MODE, 1);

    14  Sem_unlink(Px_ipc_name(SEM_NAME));

    15  setbuf(stdout, NULL); /* stdout не буферизуется */

    16  if (Fork() == 0) { /* дочерний процесс */

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

    18    Sem_wait(mutex);

    19    printf("child: %d\n", count++);

    20    Sem_post(mutex);

    21   }

    22   exit(0);

    23  }

    24  /* родительский процесс */

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

    26   Sem_wait(mutex);

    27   printf("parent: %d\r\", count++);

    28   Sem_post(mutex);

    29  }

    30  exit(0);

    31 }

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

    12-14 Мы создаем и инициализируем семафор, защищающий переменную, которую мы считаем глобальной (count). Поскольку предположение о ее глобальности ложно, этот семафор на самом деле не нужен. Обратите внимание, что мы удаляем семафор из системы вызовом sem_unlink, но хотя файл с соответствующим полным именем при этом и удаляется, на открытый в данный момент семафор эта команда не действует. Этот вызов мы делаем для того, чтобы файл был удален даже при досрочном завершении программы.

    Отключение буферизации стандартного потока вывода и вызов fork

    15 Мы отключаем буферизацию стандартного потока вывода, поскольку запись в него будет производиться и родительским, и дочерним процессами. Это предотвращает смешивание вывода из двух процессов.

    16-29 Родительский и дочерний процессы увеличивают глобальный счетчик в цикле заданное число раз, выполняя операции только при установленном семафоре.

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

    child: 0     дочерний процесс запущен первым,count=О

    child; 1

    child; 678

    child: 679

    parent: 0    дочерний процесс приостановлен, запускается родительский

                 процесс и отсчет начинается с О

    parent: 1

    parent: 1220

    parent: 1221

    child: 680   родительский процесс приостанавливается, начинает

                 выполняться дочерний процесс

    child: 681

    child: 2078

    child: 2079

    parent: 1222 дочерний процесс приостанавливается, начинает выполняться

                 родительский процесс

    parent: 1223 и т. д.

    Как видно, каждый из процессов использует собственную копию глобального счетчика count. Каждый начинает со значения 0 и при прохождении цикла увеличивает значение своей копии счетчика. На рис. 12.3 изображен родительский процесс перед вызовом fork.

    Рис. 12.3. Родительский процесс перед вызовом fork


    При вызове fork дочерний процесс запускается с собственной копией данных родительского процесса. На рис. 12.4 изображены оба процесса после возвращения из fork.

    Рис. 12.4. Родительский и дочерний процессы после возвращения из fork


    Мы видим, что родительский и дочерний процессы используют отдельные копии счетчика count.

    12.2. Функции mmap, munmap и msync

    Функция mmap отображает в адресное пространство процесса файл или объект разделяемой памяти Posix. Мы используем эту функцию в следующих ситуациях:

    1. С обычными файлами для обеспечения ввода-вывода через отображение в память (раздел 12.3).

    2. Со специальными файлами для обеспечения неименованного отображения памяти (разделы 12.4 и 12.5).

    3. С shm_open для создания участка разделяемой неродственными процессами памяти Posix.

    #include <sys/mman.h>

    void *mmap(void *addr, size_t len, int prot, int flags, int fd, off_t offset);

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

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

    Аргумент len задает длину отображаемого участка в байтах; участок может начинаться не с начала файла (или другого объекта), а с некоторого места, задаваемого аргументом offset. Обычно offset = 0. На рис. 12.5 изображена схема отображения объекта в память.

    Рис. 12.5. Пример отображения файла в память


    Защита участка памяти с отображенным объектом обеспечивается с помощью аргумента prot и констант, приведенных в табл. 12.1. Обычное значение этого аргумента — PROT_READ | PROT_WRITE, что обеспечивает доступ на чтение и запись.


    Таблица 12.1. Аргумент prot для вызова mmap

    prot Описание
    PROT_READ Данные могут быть считаны
    PROT_WRITE Данные могут быть записаны
    PROT_EXEC Данные могут быть выполнены
    PROT_NONE Доступ к данным закрыт

    Таблица 12.2. Аргумент flag для вызова mmap

    flag Описание
    MAP SHARED Изменения передаются другим процессам
    MAP_PRIVATE Изменения не передаются другим процессам и не влияют на отображенный объект
    MAP_FIXED Аргумент addr интерпретируется как адрес памяти

    Аргумент flags может принимать значения из табл. 12.2. Можно указать только один из флагов — MAP_SHARED или MAP_PRIVATE, прибавив к нему при необходимости MAP_FIXED. Если указан флаг MAP_PRIVATE, все изменения будут производиться только с образом объекта в адресном пространстве процесса; другим процессам они доступны не будут. Если же указан флаг MAP_SHARED, изменения отображаемых данных видны всем процессам, совместно использующим объект.

    Для обеспечения переносимости пpoгрaмм флаг MAP_FIXED указывать не следует. Если он не указан, но аргумент addr представляет собой ненулевой указатель, интерпретация этого аргумента зависит от реализации. Ненулевое значение addr обычно трактуется как указатель на желаемую область памяти, в которую нужно произвести отображение. В переносимой программе значение addr должно быть нулевым и флаг MAP_FIXED не должен быть указан.

    Одним из способов добиться совместного использования памяти родительским и дочерним процессами является вызов mmap с флагом MAP_SHARED перед вызовом fork. Стандарт Posix.1 гарантирует в этом случае, что все отображения памяти, установленные родительским процессом, будут унаследованы дочерним. Более того, изменения в содержимом объекта, вносимые родительским процессом, будут видны дочернему, и наоборот. Эту схему мы вскоре продемонстрируем в действии.

    Для отключения отображения объекта в адресное пространство процесса используется вызов munmap:

    #include <sys/mman.h>

    int munmap(void *addr, size_t len);

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

    Аргумент addr должен содержать адрес, возвращенный mmap, a len — длину области отображения. После вызова munmap любые попытки обратиться к этой области памяти приведут к отправке процессу сигнала SIGSEGV (предполагается, что эта область памяти не будет снова отображена вызовом mmap).

    Если область была отображена с флагом MAP_PRIVATE, все внесенные за время работы процесса изменения сбрасываются.

    В изображенной на рис. 12.5 схеме ядро обеспечивает синхронизацию содержимого файла, отображенного в память, с самой памятью при помощи алгоритма работы с виртуальной памятью (если сегмент был отображен с флагом MAP_SHARED). Если мы изменяем содержимое ячейки памяти, в которую отображен файл, через некоторое время содержимое файла будет соответствующим образом изменено ядром. Однако в некоторых случаях нам нужно, чтобы содержимое файла всегда было в соответствии с содержимым памяти. Тогда для осуществления моментальной синхронизации мы вызываем msync:

    #include <sys/mman.h>

    int msync(void *addr, size_t len, int flags);

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

    Аргумент flags представляет собой комбинацию констант из табл. 12.3.


    Таблица 12.3. Значения аргумента flags для функции msync

    Константа Описание
    MS_ASYNC Осуществлять асинхронную запись
    MS_SYNC Осуществлять синхронную запись
    MS_INVALIDATE Сбросить кэш

    Из двух констант MS_ASYNC и MS_SYNC указать нужно одну и только одну. Отличие между ними в том, что возврат из функции при указании флага MS_ASYNC происходит сразу же, как только данные для записи будут помещены в очередь ядром, а при указании флага MS_SYNC возврат происходит только после завершения операций записи. Если указан и флаг MS_INVALIDATE, все копии файла, содержимое которых не совпадает с его текущим содержимым, считаются устаревшими. Последующие обращения к этим копиям приведут к считыванию данных из файла.

    Почему вообще используется отображение в память?

    До сих пор мы всегда говорили об отображении в память содержимого файла, который сначала открывается вызовом open, а затем отображается вызовом mmap. Удобство состоит в том, что все операции ввода-вывода осуществляются ядром и скрыты от программиста, а он просто пишет код, считывающий и записывающий данные в некоторую область памяти. Ему не приходится вызывать read, write или lseek. Часто это заметно упрощает код.

    ПРИМЕЧАНИЕ

    Вспомните нашу реализацию очередей сообщений Posix с использованием mmap, где значения сохранялись в структуре msg_hdr и считывались из нее же (листинги 5.26 и 5.28).

    Следует, однако, иметь в виду, что не все файлы могут быть отображены в память. Попытка отобразить дескриптор, указывающий на терминал или сокет, приведет к возвращению ошибки при вызове mmap. К дескрипторам этих типов доступ осуществляется только с помощью read и write (и аналогичных вызовов).

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

    ПРИМЕЧАНИЕ

    Детали реализации mmap и связь этого вызова с механизмами реализации виртуальной памяти описаны в [14] для 4.4BSD и [6] для SVR4. 

    12.3. Увеличение счетчика в отображаемом в память файле

    Изменим программу в листинге 12.1 (которая не работала) таким образом, чтобы родительский и дочерний процессы совместно использовали область памяти, в которой хранится счетчик. Для этого используем отображение файла в память вызовами open и mmap. В листинге 12.2 приведен текст новой программы.

    Листинг 12.2. Родительский и дочерний процессы увеличивают значение счетчика в разделяемой памяти

    //shm/incr2.c

    1  #include "unpipc.h"

    2  #define SEM_NAME "mysem"


    3  int

    4  main(int argc, char **argv)

    5  {

    6   int fd, i, nloop, zero = 0;

    7   int *ptr;

    8   sem_t *mutex;

    9   if (argc != 3)

    10   err_quit("usage: incr2 <pathname> <#loops>");

    11  nloop = atoi(argv[2]);

    12  /* открываем файл, инициализируем нулем и отображаем в память */

    13  fd = Open(argv[1], O_RDWR | O_CREAT, FILE_MODE);

    14  Write(fd, &zero, sizeof(int));

    15  ptr = Mmap(NULL, sizeof(int), PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);

    16  Close(fd);

    17  /* создаем, инициализируем и отключаем семафор */

    18  mutex = Sem_open(Px_ipc_name(SEM_NAME), O_CREAT | O_EXCL, FILE_MODE, 1);

    19  Sem_unlink(Px_ipc_name(SEM_NAME));

    20  setbuf(stdout, NULL); /* stdout не буферизуется */

    21  if (Fork() == 0) { /* дочерний процесс */

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

    23    Sem_wait(mutex);

    24    printf("child: %d\n", (*ptr)++);

    25    Sem_post(mutex);

    26   }

    27   exit(0);

    28  }

    29  /* родительский процесс */

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

    31   Sem_wait(mutex);

    32   printf("parent: %d\n", (*ptr)++);

    33   Sem_post(mutex);

    34  }

    35  exit(0);

    36 }

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

    11-14 Из командной строки теперь считывается еще один аргумент, задающий полное имя файла, который будет отображен в память. Мы открываем файл для чтения и записи, причем если файл не существует, он будет создан, а затем инициализируем файл нулем.

    Вызов mmap и закрытие дескриптора

    15-16 Вызов mmap позволяет отобразить открытый файл в адресное пространство процесса. Первый аргумент является нулевым указателем, при этом система сама выбирает адрес начала отображаемого сегмента. Длина файла совпадает с размером целого числа. Устанавливается доступ на чтение и запись. Четвертый аргумент имеет значение MAP_SHARED, что позволяет процессам «видеть» изменения, вносимые друг другом. Функция возвращает адрес начала участка разделяемой памяти, мы сохраняем его в переменной ptr.

    fork

    20-34 Мы отключаем буферизацию стандартного потока вывода и вызываем fork. И родительский, и дочерний процессы по очереди увеличивают значение целого, на которое указывает ptr.

    Отображенные в память файлы обрабатываются при вызове fork специфическим образом в том смысле, что созданные родительским процессом отображения наследуются дочерним процессом. Следовательно, открыв файл и вызвав mmap с флагом MAP_SHARED, мы получили область памяти, совместно используемую родительским и дочерним процессами. Более того, поскольку эта общая область на самом деле представляет собой отображенный файл, все изменения, вносимые в нее (область памяти, на которую указывает ptr, — размером sizeof (int)), также действуют и на содержимое реального файла (имя которого было указано в командной строке).

    Запустив эту программу на выполнение, мы увидим, что память, на которую указывает ptr, действительно используется совместно родительским и дочерним процессами. Приведем значения счетчика перед переключением процессов:

    solaris % incr2 /tmp/temp.110000

    child: 0     запускается дочерний процесс

    child: 1

    child: 128

    child: 129

    parent: 130  дочерний процесс приостанавливается, запускается родительский процесс

    parent: 131

    parent: 636

    parent: 637

    child: 638   родительский процесс приостанавливается, запускается дочерний процесс

    child: 639

    child: 1517

    child: 1518

    parent: 1519 дочерний процесс приостанавливается, запускается родительский процесс

    parent: 1520

    parent: 19999 последняя строка вывода

    solaris % od –D /tmp/temp.1

    0000000 0000020000

    0000004

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

    На рис. 12.6 изображена схема, отличающаяся от рис. 12.4. Здесь используется разделяемая память и показано, что семафор также используется совместно. Семафор мы изобразили размещенным в ядре, но для семафоров Posix это не обязательно. В зависимости от реализации семафор может обладать различной живучестью, но она должна быть по крайней мере не меньше живучести ядра. Семафор может быть реализован также через отображение файла в память, что мы продемонстрировали в разделе 10.15. 

    Рис. 12.6. Родительский и дочерний процессы используют разделяемую память и общий семафор 


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

    Изменим программу в листинге 12.2 так, чтобы использовались семафоры Posix, размещаемые в памяти (вместо именованных). Разместим такой семафор в разделяемой области памяти. Новая программа приведена в листинге 12.3.

    Листинг 12.3. Счетчик и семафор размещены в разделяемой памяти

    //shm/incr3.c

    1  #include "unpipc.h"


    2  struct shared {

    3   sem_t mutex; /* взаимное исключение: семафор, размещаемый в памяти */

    4   int count; /* и счетчик */

    5  } shared;


    6  int

    7  main(int argc, char **argv)

    8  {

    9   int fd, i, nloop;

    10  struct shared *ptr;

    11  if (argc != 3)

    12   err_quit("usage: incr3 <pathname> <#loops>");

    13  nloop = atoi(argv[2]);

    14  /* открываем файл, инициализируем нулем, отображаем в память */

    15  fd = Open(argv[1], O_RDWR | O_CREAT, FILE_MODE);

    16  Write(fd, &shared, sizeof(struct shared));

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

    18   MAP_SHARED, fd, 0);

    19  Close(fd);

    20  /* инициализация семафора, совместно используемого процессами */

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

    22  setbuf(stdout, NULL); /* stdout не буферизуется */

    23  if (Fork() == 0) { /* дочерний процесс */

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

    25    Sem_wait(&ptr->mutex);

    26    printf("child: %d\n", ptr->count++);

    27    Sem_post(&ptr->mutex);

    28   }

    29   exit(0);

    30  }

    31  /* родительский процесс */

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

    33   Sem_wait(&ptr->mutex);

    34   printf("parent: %d\n", ptr->count++);

    35   Sem_post(&ptr->mutex);

    36  }

    37  exit(0);

    38 }
     

    Определение структуры, хранящейся в разделяемой памяти

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

    Отображаем в память

    14-19 Создается файл для отображения в память, который инициализируется структурой с нулевым значением обоих полей. При этом инициализируется только счетчик, поскольку семафор будет инициализирован позднее вызовом sem_init. Тем не менее проще инициализировать всю структуру нулями, чем только одно целочисленное поле.

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

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

    На рис. 12.7 изображена модификация рис. 12.6, где семафор переместился из ядра в разделяемую память. 

    Рис. 12.7. И семафор, и счетчик теперь хранятся в разделяемой памяти

    12.4. Неименованное отображение в память в 4.4BSD

    Наши примеры из листингов 12.2 и 12.3 работают отлично, но нам приходится создавать файл в файловой системе (аргумент командной строки), вызывать open, записывать нули в файл вызовом write (чтобы проинициализировать его). Если mmap используется для передачи области разделяемой памяти через fork, мы можем упростить эту схему, используя свойства реализации.

    1. В версии 4.4BSD предоставляется возможность неименованного отображения в память. При этом полностью пропадает необходимость создавать или открывать файлы. Вместо этого указываются флаги MAP_SHARED | MAP_ANON и дескриптор fd = –1. Сдвиг, задаваемый аргументом offset, игнорируется. Память автоматически инициализируется нулями. Пример использования приведен в листинге 12.4.

    2. В версии SVR4 имеется файл /dev/zero, который мы открываем и дескриптор которого указываем при вызове mmap. Это устройство возвращает нули при попытке считывания, а весь направляемый на него вывод сбрасывается. Пример использования приведен в листинге 12.5. (Во многих реализациях, произошедших от BSD, также поддерживается устройство /dev/zero, например в SunOS 4.1.x и BSD/OS 3.1.)

    В листинге 12.4 приведена часть листинга 12.2, которая изменяется при переходе к использованию неименованного отображения в память в 4.4BSD.

    Листинг 12.4. Отображение в память в 4.4BSD

    //shm/incr_map_anon.с

    3  int

    4  main(int argc, char **argv)

    5  {

    6   int i, nloop;

    7   int *ptr;

    8   sem_t *mutex;

    9   if (argc != 2)

    10   err_quit("usage: incr_map_anon <#loops>");

    11  nloop = atoi(argv[1]);

    12  /* отображение в память */

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

    14   MAP_SHARED | MAP_ANON, –1, 0);

    6-11 Автоматические переменные fd и zero больше не используются, как и аргумент командной строки, задававший имя создаваемого файла.

    12-14 Файл больше не нужно открывать. Вместо этого указывается флаг MAP_ANON при вызове mmap, а пятый аргумент этой функции (дескриптор) принимает значение –1.

    12.5. Отображение в память в SVR4 с помощью /dev/zero

    В листинге 12.5 приведена часть новой версии программы, претерпевшая изменения по сравнению с листингом 12.2 при переходе к использованию отображения с помощью /dev/zero.

    Листинг 12.5. Отображение памяти в SVR4 с помощью /dev/zero

    //shm/incr_dev_zero.c

    3  int

    4  main(int argc char **argv)

    5  {

    6   int fd, i, nloop;

    7   int *ptr;

    8   sem_t *mutex;

    9   if (argc != 2)

    10   err_quit("usage: incr_dev_zero <#loops>");

    11  nloop = atoi(argv[1]);

    12  /* открываем /dev/zero и отображаем в память */

    13  fd = Open("/dev/zero", O_RDWR);

    14  ptr = Mmap(NULL, sizeof(int), PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);

    15  Close(fd);

    6-11 Автоматическая переменная zero больше не используется, как и аргумент командной строки, задававший имя создаваемого файла.

    12-15 Мы открываем файл /dev/zero и передаем его дескриптор функции mmap. Область памяти будет гарантированно проинициализирована нулями.

    12.6. Обращение к объектам, отображенным в память

    Когда в память отображается обычный файл, размер полученной области (второй аргумент вызова mmap), как правило, совпадает с размером файла. Например, в листинге 12.3 размер файла устанавливается равным размеру структуры shared вызовом write и это же значение размера области используется при отображении его в память. Однако эти два параметра — размер файла и размер области памяти, в которую он отображен, — могут и отличаться.

    Для детального изучения особенностей функции mmap воспользуемся программой в листинге 12.6.

    Листинг 12.6. Отображение файла: размер файла совпадает с размером области памяти

    //shra/test1.c

    1  #include "unpipc.h"


    2  int

    3  main(int argc, char **argv)

    4  {

    5   int fd, i;

    6   char *ptr;

    7   size_t filesize, mmapsize, pagesize;

    8   if (argc != 4)

    9    err_quit("usage: test1 <pathname> <filesize> <mmapsize>");

    10  filesize = atoi(argv[2]);

    11  mmapsize = atoi(argv[3]);

    12  /* открытие файла, установка его размера */

    13  fd = Open(argv[1], O_RDWR | O_CREAT | O_TRUNC, FILE_MODE);

    14  Lseek(fd, filesize-1, SEEK_SET);

    15  Write(fd, "", 1);

    16  ptr = Mmap(NULL, mmapsize, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);

    17  Close(fd);

    18  pagesize = Sysconf(_SC_PAGESIZE);

    19  printf("PAGESIZE = %ld\n", (long) pagesize);

    20  for (i = 0; i < max(filesize, mmapsize); i += pagesize) {

    21   printf("ptr[*d] = %d\n", i, ptr[i]);

    22   ptr[i] = 1;

    23   printf("ptr[%d] = %d\n", i + pagesize – 1, ptr[i + pagesize – 1]);

    24   ptr[i + pagesize – 1] = 1;

    25  }

    26  printf("ptr[%d] = %d\n", i, ptr[i]);

    27  exit(0);

    28 }

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

    8-11 Аргументы командной строки задают полное имя создаваемого и отображаемого в память файла, его размер и размер области памяти.

    Создание, открытие, урезание файла; установка его размера

    12-15 Если файл не существует, он будет создан. Если он существует, его длина будет установлена равной нулю. Затем размер файла устанавливается равным указанному размеру путем вызова lseek для установки текущей позиции, равной трe-буемому размеру минус 1 и записи 1 байта.

    Отображение файла в память

    16-17 Файл отображается в память, причем размер области задается последним аргументом командной строки. Затем дескриптор файла закрывается.

    Вывод размера страницы памяти

    18-19 Размер страницы памяти получается вызовом sysconf и выводится на экран.

    Чтение и запись в область отображения

    20-26 Считываются и выводятся данные из области памяти, в которую отображен файл. Считываются первый и последний байты каждой страницы этой области памяти. Все значения должны быть нулевыми. Затем первый и последний байты каждой страницы устанавливаются в 1. Одно из обращений к памяти может привести к отправке сигнала процессу, что приведет к его завершению. После завершения цикла for мы добавляем еще одно обращение к следующей странице памяти, что должно заведомо привести к ошибке и завершению пpoгрaммы (если ошибка не возникла раньше).

    Рассмотрим первую ситуацию: размер файла совпадает с размером области памяти, но эта величина не кратна размеру страницы памяти в данной реализации:

    solaris % ls –l foo

    foo: No such file or directory

    solaris % test1 foo 5000 5000

    PAGESIZE = 4096

    ptr[0] = 0

    ptr[4095] = 0

    ptr[4096] = 0

    ptr[8191] = 0

    Segmentation Fault(coredump)

    solaris % ls-l foo

    -rw-r--r-- 1 rstevens other1 5000 Mar 20 17:18 foo

    solaris % od –b –A d foo

    0000000 001 000 000 000 000 000 000 000 000 000 000 000 000 000 000 000

    0000016 000 000 000 000 000 000 000 000 000 000 000 000 000 000 000 000

    *

    0004080 000 000 000 000 000 000 000 000 000 000 000 000 000 000 000 001

    0004096 001 000 000 000 000 000 000 000 000 000 000 000 000 000 000 000

    0004112 000 000 000 000 000 000 000 000 000 000 000 000 000 000 000 000

    0005000

    Размер страницы памяти составляет 4096 байт, и мы смогли обратиться ко всему содержимому второй страницы (индексы 4096-8191), но обращение к третьей странице (8192) приводит к отправке сигнала SIGSEGV, о чем интерпретатор оповещает сообщением Segmentation Fault. Хотя мы и установили значение ptr[8191] = 1, оно не было записано в файл и его размер остался равным 5000 байт. Ядро позволяет считывать и записывать данные в ту часть последней страницы, которая не относится к отображенному файлу (поскольку защита памяти осуществляется ядром постранично), но изменения в этой области памяти не будут скопированы в файл. А вот относящиеся к файлу изменения (индексы 0, 4095 и 4096) были скопированы в него, в чем мы убедились, воспользовавшись программой od (параметр –b при вызове последней указывает на необходимость выводить значения байтов в восьмеричном формате, а параметр –Ad позволяет выводить адреса в десятичном формате). На рис. 12.8 изображена схема памяти для данного примера. 

    Рис. 12.8. Размер отображаемого файла совпадает с размером области памяти


    Запустив этот пример в Digital Unix 4.0B, получим тот же результат, но размер страницы памяти в этой системе равняется 8192 байт:

    alpha % ls –l foo

    foo not found

    alpha % test1 foo5000 5000

    PAGESIZE = 8192

    ptr[0] = 0

    ptr[8191] = 0

    Memory fault (coredump)

    alpha % ls -l foo

    -rw-r-r– 1 rstevens operator 5000 Mar 21 08:40 foo

    Мы все так же можем обратиться к памяти за пределами отображенного файла, но не выходя за грaницы страницы памяти (индексы с 5000 по 8191). Обращение к ptr[8192] приводит к отправке SIGSEGV, на что мы и рассчитывали.

    Вторая ситуация: размер области памяти (15000 байт) превышает размер файла (5000 байт):

    solaris % rm foo

    solaris % test1 foo 5000 15000

    ptr[0] = 0

    ptr[4095] = 0

    ptr[4096] = 0

    ptr[8191] = 0

    Bus Error(coredump)

    solaris % ls –l foo

    -rw-r-r– 1 rstevens other1 5000 Mar 20 17:37 foo
     

    Рис. 12.9. Размер области памяти больше размера отображаемого файла


    Полученный результат аналогичен результату предыдущего примера, в котором размер файла равнялся размеру области отображения (5000 байт). Однако в данном примере генерируется сигнал SIGBUS (о чем интерпретатор оповещает сообщением Bus Error), тогда как в предыдущем примере отправлялся сигнал SIGSEGV. Отличие в том, что SIGBUS означает выход за грaницы отображенного файла внутри области отображения, a SIGSEGV — выход за грaницы области. Этим примером мы показали, что ядро хранит информацию о размере отображенного объекта, даже несмотря на то, что его дескриптор закрыт. Ядро позволяет указать при вызове mmap размер области памяти, больший размера файла, но не позволяет обратиться к адресам в этой области (кроме остатка последней страницы, в которой еще имеется содержимое собственно файла — индексы с 5000 по 8191 в данном случае). На рис. 12.9 приведена иллюстрация к этому примеру. 

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

    Листинг 12.7. Отображение увеличивающегося файла в память

    //shm/test2.c

    1  #include "unpipc.h"

    2  #define FILE "test.data"

    3  #define SIZE 32768


    4  int

    5  main(int argc, char **argv)

    6  {

    7   int fd, i;

    8   char *ptr;

    9   /* открытие, создание, урезание и установка размера файла, вызов mmap */

    10  fd = Open(FILE, O_RDWR | O_CREAT | O_TRUNC, FILE_MODE);

    11  ptr = Mmap(NULL, SIZE, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);

    12  for (i = 4096; i <= SIZE; i += 4096) {

    13   printf("setting file size to %d\n", i);

    14   Ftruncate(fd, i);

    15   printf("ptr[%d] = %d\n", i-1, ptr[i-1]);

    16  }

    17  exit(0);

    18 }

    Открытие файла

    9-11 Мы создаем файл, если он еще не существует, или урезаем его до нулевой длины, если он существует. Затем файл отображается в область объемом 32 768 байт, хотя его текущий размер равен нулю.

    Увеличение размера файла

    12-16 Мы увеличиваем размер файла на 4096 байт за один вызов ftruncate (раздел 13.3) и считываем из него последний байт в каждом проходе цикла.

    Запустив эту программу, мы убедимся в возможности обращаться к новым данным по мере роста файла:

    alpha % ls –l test.data

    test.data: No such file or directory

    alpha % test2

    setting file size to 4096

    ptr[4095] = 0

    setting file size to 8192

    ptr[8191] = 0

    setting file size to 12288

    ptr[12287] = 0

    setting file size to 16384

    ptr[16383] = 0

    setting file size to 20480

    ptr[20479] = 0

    setting file size to 24576

    ptr[24575] = 0

    setting file size to 28672

    ptr[28671] = 0

    setting file size to 32768

    ptr[32767] = 0

    alpha % ls-l test.data

    -rw-r--r-- 1 rstevens other1 32768 Mar 20 17:53 test.data

    Этот пример показывает, что ядро всегда следит за размером отображаемого в память объекта (в данном примере это файл test.data), и мы всегда имеем возможность обратиться к байтам, лежащим внутри области, ограниченной размером файла и размером отображения. Те же результаты получаются при запуске этой программы в Solaris 2.6.

    В этом разделе мы работали с отображением файлов в память с помощью mmap. В упражнении 13.1 нам придется немного изменить две наших программы для работы с разделяемой памятью Posix, и мы получим те же результаты.

    12.7. Резюме

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

    В этой главе мы подробно рассмотрели свойства функции mmap и отображение обычных файлов в память, потому что это один из способов обеспечения взаимодействия между неродственными процессами. После отображения файла в память для обращения к нему больше не нужно использовать вызовы read, write и lseek — вместо этого можно напрямую работать с ячейками памяти, относящимися к той области, указатель на которую возвращает mmap. Замена явных операций с файлом на обращение к ячейкам памяти может упростить программу и в некоторых случаях увеличить быстродействие.

    Если необходимо совместное использование области памяти после вызова fork, можно упростить решение этой задачи, используя неименованное отображение в память. Для этого в ядрах Berkeley при вызове mmap указывается флаг MAP_ANON, а в ядрах SVR4 производится отображение специального файла /dev/zero.

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

    Для работы с памятью стандартом Posix определено еще четыре функции:

    ■ mlockall делает всю память процесса резидентной; munlockall снимает эту блокировку;

    ■ mlock делает определенный диапазон адресов процесса резидентным. Аргументами функции являются начальный адрес и длина области. Функция munlock разблокирует указываемую область памяти.

    Упражнения

    1. Что произойдет с программой в листинге 12.7, если добавить еще один повтор цикла for?

    2. Предположим, что имеются два процесса, один из которых отправляет сообщения другому. Для этого используются очереди сообщений System V. Нарисуйте схему передачи сообщений от отправителя к получателю. Теперь представьте, что используется реализация очередей сообщений Posix из раздела 5.8, и нарисуйте аналогичную схему.

    3. Мы говорили, что при вызове mmap с флагом MAP_SHARED для синхронизации содержимого файла и памяти используются алгоритмы ядра для работы с виртуальной памятью. Прочитайте страницу документации, относящуюся к /dev/zero, чтобы узнать, что происходит, когда ядро записывает изменения обратно в этот файл.

    4. Измените программу в листинге 12.2, указав MAP_PRIVATE вместо MAP_SHARED. Проверьте, что результаты будут такими же, как и при выполнении программы из листинга 12.1. Что будет содержаться в файле, отображенном в память?

    5. В разделе 6.9 мы отметили, что единственным способом использовать select с очередью сообщений System V является создание неименованной области памяти, порождение процесса и блокирование его в вызове msgrcv, причем сообщение должно считываться в разделяемую память. Родительский процесс также создает два канала, один из которых используется для уведомления его о том, что сообщение помещено в разделяемую память, а другой — для уведомления дочернего процесса о возможности помещения нового сообщения в эту память. Тогда родительский процесс может вызвать select для открытого на чтение конца канала вместе с любыми другими дескрипторами. Напишите программу, реализующую этот алгоритм. Для выделения области неименованной разделяемой памяти используйте функцию my_shm (листинг А.31). Для создания очереди сообщений и помещения в нее записей используйте программы msgcreate и msgsnd из раздела 6.6. Родительский процесс должен просто выводить размер и тип всех считываемых дочерним процессом сообщений. 

    ГЛАВА 13

    Разделяемая память 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 <sys/mman.h>

    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 <unistd.h>

    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 <sys/types.h>

    #include <sys/stat.h>

    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 ] <name> <length>");

    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 <name>");

    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 <name>");

    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 <name>");

    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 <name>");

    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 <shmname> <semname>");

    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 <shmname> <semname> <#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 <name>");

    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 <name> <#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

    Разделяемая память System V

    14.1. Введение

    Основные принципы разделяемой памяти System V совпадают с концепцией разделяемой памяти Posix. Вместо вызовов shm_open и mmap в этой системе используются вызовы shmget и shmat.

    Для каждого сегмента разделяемой памяти ядро хранит нижеследующую структуру, определенную в заголовочном файле <sys/shm.h>:

    struct shmid_ds {

     struct ipc_perm shm_perm; /* структура разрешений */

     size_t shm_segsz; /* размер сегмента */

     pid_t shm_lpid; /* идентификатор процесса, выполнившего последнюю операцию */

     pid_t shm_cpid; /* идентификатор процесса-создателя */

     shmatt_t shm_nattch; /* текущее количество подключений */

     shmat_t shm_cnattch; /* количество подключений in-core */

     time_t shm_atime; /* время последнего подключения */

     time_t shm_dtime; /* время последнего отключения */

     time_t shm_ctime; /* время последнего изменения данной структуры */

    };

    Структура ipc_perm была описана в разделе 3.3; она содержит разрешения доступа к сегменту разделяемой памяти.

    14.2. Функция shmget

    С помощью функции shmget можно создать новый сегмент разделяемой памяти или подключиться к существующему:

    #include <sys/shm.h>

    int shmget(key_t key, size_t size, int oflag);

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

    Возвращаемое целочисленное значение называется идентификатором разделяемой памяти. Он используется с тремя другими функциями shmXXX.

    Аргумент key может содержать значение, возвращаемое функцией ftok, или константу IPC_PRIVATE, как обсуждалось в разделе 3.2.

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

    Флаг oflag представляет собой комбинацию флагов доступа на чтение и запись из табл. 3.3. К ним могут быть добавлены с помощью логического сложения флаги IPC_CREAT или IPC_CREAT | IPC_EXCL, как уже говорилось в связи с рис. 3.2.

    Новый сегмент инициализируется нулями.

    Обратите внимание, что функция shmget создает или открывает сегмент разделяемой памяти, но не дает вызвавшему процессу доступа к нему. Для подключения сегмента памяти предназначена функция shmat, описанная в следующем разделе.

    14.3. Функция shmat

    После создания или открытия сегмента разделяемой памяти вызовом shmget его нужно подключить к адресному пространству процесса вызовом shmat:

    #include <sys/shm.h>

    void *shmat(int shmid, const void *shmaddr, int flag);

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

    Аргумент shmid — это идентификатор разделяемой памяти, возвращенный shmget. Функция shmat возвращает адрес начала области разделяемой памяти в адресном пространстве вызвавшего процесса. Правила, по которым формируется этот адрес, таковы:

    ■ если аргумент shmaddr представляет собой нулевой указатель, система сама выбирает начальный адрес для вызвавшего процесса. Это рекомендуемый (и обеспечивающий наилучшую совместимость) метод;

    ■ если shmaddr отличен от нуля, возвращаемый адрес зависит от того, был ли указан флаг SHM_RND (в аргументе flag ):

     □ если флаг SHM_RND не указан, разделяемая память подключается непосредственно с адреса, указанного аргументом shmaddr,

     □ если флаг SHM_RND указан, сегмент разделяемой памяти подключается с адреса, указанного аргументом shmaddr, округленного вниз до кратного константе SHMLBA. Аббревиатура LBA означает lower boundary address — нижний граничный адрес.

    По умолчанию сегмент подключается для чтения и записи, если процесс обладает соответствующими разрешениями. В аргументе flag можно указать константу SHM_RDONLY, которая позволит установить доступ только на чтение.

    14.4. Функция shmdt

    После завершения работы с сегментом разделяемой памяти его следует отключить вызовом shmdt:

    #include <sys/shm.h>

    int shmdt(const void *shmaddr);

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

    При завершении работы процесса все сегменты, которые не были отключены им явно, отключаются автоматически.

    Обратите внимание, что эта функция не удаляет сегмент разделяемой памяти. Удаление осуществляется функцией shmctl с командой IPC_RMIO.

    14.5. Функция shmctl

    Функция shmctl позволяет выполнять различные операции с сегментом разделяемой памяти:

    #include <sys/shm.h>

    int shmctl(int shmid, int and, struct shmid_ds *buff);

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

    Команд (значений аргумента cmd) может быть три:

    ■ IPC_RMID — удаление сегмента разделяемой памяти с идентификатором shmid из системы;

    ■ IPC_SET — установка значений полей структуры shmid_ds для сегмента разделяемой памяти равными значениям соответствующих полей структуры, на которую указывает аргумент buff: shm_perm.uid, shm_perm.gid, shm_perm.mode. Значение поля shm_ctime устанавливается равным текущему системному времени;

    ■ IPC_STAT — возвращает вызывающему процессу (через аргумент buff) текущее значение структуры shmid_ds для указанного сегмента разделяемой памяти.

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

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

    Программа shmget

    Программа shmget, текст которой приведен в листинге 14.1,[1] создает сегмент разделяемой памяти, принимая из командной строки полное имя и длину сегмента.

    Листинг 14.1. Создание сегмента разделяемой памяти System V указанного размера

    //svshm/shmget.c

    1  #include "unpipc.h"


    2  int

    3  main(int argc, char **argv)

    4  {

    5   int c, id, oflag;

    6   char *ptr;

    7   size_t length;

    8   oflag = SVSHM_MODE | IPC_CREAT;

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

    10   switch (c) {

    11   case 'e':

    12    oflag |= IPC_EXCL;

    13    break;

    14   }

    15  }

    16  if (optind != argc – 2)

    17   err_quit("usage: shmget [ –e ] <pathname> <length>");

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

    19  id = Shmget(Ftok(argv[optind], 0), length, oflag);

    20  ptr = Shmat(id, NULL, 0);

    21  exit(0);

    22 }

    19 Вызов shmget создает сегмент разделяемой памяти указанного размера. Полное имя, передаваемое в качестве аргумента командной строки, преобразуется в ключ IPC System V вызовом ftok. Если указан параметр –е, наличие существующего сегмента с тем же именем приведет к возвращению ошибки. Если мы знаем, что сегмент уже существует, в командной строке должна быть указана нулевая длина.

    20 Вызов shmat подключает сегмент к адресному пространству процесса. После этого программа завершает работу. Разделяемая память System V обладает поменьшей мере живучестью ядра, поэтому сегмент разделяемой памяти при этом не удаляется.

    Программа shmrmid

    В листинге 14.2 приведен текст тривиальной программы shmrmid, которая вызывает shmctl с командой IPC_RMID для удаления сегмента разделяемой памяти из системы.

    Листинг 14.2. Удаление сегмента разделяемой памяти system V из системы

    //svshm/shmrmid.c

    1  #include "unpipc.h"


    2  int

    3  main(int argc, char **argv)

    4  {

    5   int id;

    6   if (argc != 2)

    7    err_quit("usage: shmrmid <pathname>");

    8   id = Shmget(Ftok(argv[1], 0), 0, SVSHM_MODE);

    9   Shmctl(id, IPC_RMID, NULL);

    10  exit(0);

    11 }

    Программа shmwrite

    В листинге 14.3 приведен текст программы shmwrite, которая заполняет сегмент разделяемой памяти последовательностью значений 0, 1, 2, …, 254, 255, 0, 1 и т. д.

    Листинг 14.3. Заполнение сегмента разделяемой памяти последовательностью чисел

    //svshm/shmwrite.c

    1  #include "unpipc.h"


    2  int

    3  main(int argc, char **argv)

    4  {

    5   int i, id;

    6   struct shmid_ds buff;

    7   unsigned char *ptr;

    8   if (argc != 2)

    9    err_quit("usage: shmwrite <pathname>");

    10  id = Shmget(Ftok(argv[1], 0), 0, SVSHM_MODE);

    11  ptr = Shmat(id, NULL, 0);

    12  Shmctl(id, IPC_STAT, &buff);

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

    14  for (i = 0; i < buff.shm_segsz; i++)

    15   *ptr++ = i % 256;

    16  exit(0);

    17 }

    10-12 Сегмент разделяемой памяти открывается вызовом shmget и подключается вызовом shmat. Его размер может быть получен вызовом shmctl с командой IPC_STAT.

    13-15 В разделяемую память записывается последовательность значений.

    Программа shmread

    Программа shmread, текст которой приведен в листинге 14.4, проверяет последовательность значений, записанную в разделяемую память программой shmwrite.

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

    //svshm/shmread.c

    1  #include "unpipc.h"


    2  int

    3  main(int argc, char **argv)

    4  {

    5   int i, id;

    6   struct shmid_ds buff;

    7   unsigned char c, *ptr;

    8   if (argc != 2)

    9    err_quit("usage: shmread <pathname>");

    10  id = Shmget(Ftok(argv[1], 0), 0, SVSHM_MODE);

    11  ptr = Shmat(id, NULL, 0);

    12  Shmctl(id, IPC_STAT, &buff);

    13  /* проверка значений ptr[0] = 0, ptr[1] = 1 и т. д. */

    14  for (i = 0; i < buff.shm_segsz; i++)

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

    16    err_ret("ptr[%d] = %d", i.e);

    17  exit(0);

    18 }

    10-12 Открываем и подключаем сегмент разделяемой памяти. Его размер может быть получен вызовом shmctl с командой IPC_STAT. 13-16 Проверяется последовательность, записанная программой shmwrite.

    Примеры

    Создадим сегмент разделяемой памяти длиной 1234 байта в системе Solaris 2.6. Для идентификации сегмента используем полное имя нашего исполняемого файла shmget. Это имя будет передано функции ftok. Имя исполняемого файла сервера часто используется в качестве уникального идентификатора для данного приложения:

    solaris % shmget shmget 1234

    solaris % ipcs –bmo

    IPC status from <running system> as of Thu Jan 8 13:17:06 1998

    T ID KEY        MODE       OWNER    GROUP  NATTCH SEGSZ

    Shared Memory:

    m 1  0x0000f12a –rw-r--r-- rstevens other1 0      1234

    Программу ipcs мы запускаем для того, чтобы убедиться, что сегмент разделяемой памяти действительно был создан и не был удален по завершении программы shmcreate. Количество подключений (хранящееся в поле shm_nattch структуры shmid_ds) равно нулю, как мы и предполагали.

    Теперь запустим пpoгрaммy shmwrite, чтобы заполнить содержимое разделяемой памяти последовательностью значений. Затем проверим содержимое сегмента разделяемой памяти программой shmread и удалим этот сегмент:

    solaris % shmwrite shmget

    solaris % shmread shmget

    solaris % shmrmid shmget

    solaris % ipes –bmo

    IPC status from <running system> as of Thu Jan 8 13:17:06 1998

    T ID KEY        MODE       OWNER    GROUP  NATTCH SEGSZ

    Shared Memory:

    Мы используем программу ipcs, чтобы убедиться, что сегмент разделяемой памяти действительно был удален.

    ПРИМЕЧАНИЕ

    При использовании имени исполняемого файла сервера в качестве аргумента ftok для идентификации какого-либо вида IPC System V обычно передается полное имя этого файла (например, /usr/bin/myserverd), а не часть имени, как сделано у нас (shmget). У нас не возникло проблем в этом примере, потому что все программы запускались из того же каталога, в котором был расположен исполняемый файл сервера. Вы помните, что функция ftok использует номер i-node файла для формирования ключа IPC и ей безразлично, определяется файл своим полным именем или его частью (относительным именем). 

    14.7. Ограничения, накладываемые на разделяемую память

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


    Таблица 14.1. Типичные значения ограничений, накладываемых на разделяемую память System V

    Имя Описание DUnix 4.0B Solaris 2.6
    shmmax Максимальный размер сегмента в байтах 4194304 1048576
    shmmnb Минимальный размер сегмента разделяемой памяти в байтах 1 1
    shmmni Максимальное количество идентификаторов разделяемой памяти в системе 128 100
    shmseg Максимальное количество сегментов, подключенных к процессу 32 6

    Пример

    Программа в листинге 14.5 определяет значения четырех ограничений, приведенных в табл. 14.1.

    Листинг 14.5. Определение системных ограничений на разделяемую память

    //svshm/limits.c

    1  #include "unpipc.h"

    2  #define MAX_NIDS 4096


    3  int

    4  main(int argc, char **argv)

    5  {

    6   int i, j, shmid[MAX_NIDS];

    7   void *addr[MAX_NIDS];

    8   unsigned long size;

    9   /* проверка максимального количества открываемых идентификаторов */

    10  for (i = 0; i <= MAX_NIDS; i++) {

    11   shmid[i] = shmget(IPC_PRIVATE, 1024, SVSHM_MODE | IPC_CREAT);

    12   if (shmid[i]== –1) {

    13    printf("%d identifiers open at once\n", i);

    14    break;

    15   }

    16  }

    17  for (j = 0; j < i; j++)

    18   Shmctl(shmid[j], IPC_RMID, NULL);

    19   /* определяем максимальное количество подключаемых сегментов */

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

    21    shmid[i] = Shmget(IPC_PRIVATE, 1024, SVSHM_MODE | IPC_CREAT);

    22    addr[i] = shmat(shmid[i], NULL, 0);

    23    if (addr[i] == (void *) –1) {

    24     printf("%d shared memory segments attached at once\n", i);

    25     Shmctl(shmid[i], IPC_RMID, NULL); /* удаляем неудачно подключенный сегмент */

    26     break;

    27    }

    28   }

    29   for (j = 0; j < i; j++) {

    30    Shmdt(addr[j]);

    31    Shmcfl(shmid[j], IPC_RMID, NULL);

    32   }

    33   /* проверка минимального размера сегмента */

    34   for (size = 1; ; size++) {

    35   shmid[0] = shmget(IPC_PRIVATE, size, SVSHM_MODE | IPC_CREAT);

    36   if (shmid[0] != –1) { /* выход при успешном создании */

    37    printf("minimum size of shared memory segment = %lu\n", size);

    38    Shmctl(shmid[0], IPC_RMID, NULL);

    39    break;

    40   }

    41  }

    42  /* определение максимального размера сегмента */

    43  for (size = 65536; ; size += 4096) {

    44   shmid[0] = shmget(IPC_PRIVATE, size, SVSHM_MODE | IPC_CREAT);

    45   if (shmid[0] == –1) { /* выход при ошибке */

    46    printf("maximum size of shared memory segment = %lu\n", size-4096);

    47    break;

    48   }

    49   Shmctl(shmid[0], IPC_RMID, NULL);

    50  }

    51  exit(0);

    52 }

    Запустив эту программу в Digital Unix 4.0B, увидим:

    alpha % limits

    127 identifiers open at once

    32 shared memory segments attached at once

    minimum size of shared memory segment = 1

    maximum size of shared memory segment = 4194304

    Причина, по которой в табл. 14.1 приведено значение 128 для числа идентификаторов, а наша программа выводит значение 127, заключается в том, что один сегмент разделяемой памяти всегда используется системным демоном.

    14.8. Резюме

    Разделяемая память System V похожа на разделяемую память Posix. Наиболее схожи функции:

    ■ shmget для получения идентификатора; 

    ■ shmat для подключения сегмента разделяемой памяти к адресному пространству процесса;

    ■ shmctl с командой IPC_STAT для получения размера существующего сегмента разделяемой памяти;

    ■ shmctl с командой IPC_RMID для удаления объекта разделяемой памяти.

    Одно из отличий состоит в том, что размер объекта разделяемой памяти Posix может быть изменен в любой момент вызовом ftruncate (как мы продемонстрировали в упражнении 13.1), тогда как размер объекта разделяемой памяти System V устанавливается изначально вызовом shmget и не может быть изменен.

    Упражнение

    Листинг 6.6 содержал измененную версию программы из листинга 6.4. Новая программа использовала для обращения к объекту IPC System V идентификатор вместо полного имени. Таким образом мы показали, что для доступа к очереди сообщений System V достаточно знать только ее идентификатор (если у нас имеются соответствующие разрешения). Сделайте аналогичные изменения в программе из листинга 14.4, продемонстрировав, что это верно и для разделяемой памяти System V. 


    Примечания:



    1

    Все исходные тексты, опубликованные в этой книге, вы можете найти по адресу http://www.piter.com/download.








    Главная | В избранное | Наш E-MAIL | Прислать материал | Нашёл ошибку | Наверх