|
||||||||||||||||||||||||||||||||||||||||||||||||
|
ЧАСТЬ 4РАЗДЕЛЯЕМАЯ ПАМЯТЬ ГЛАВА 12Введение в разделяемую память 12.1. ВведениеРазделяемая память является наиболее быстрым средством межпроцессного взаимодействия. После отображения области памяти в адресное пространство процессов, совместно ее использующих, для передачи данных между процессами больше не требуется участие ядра. Обычно, однако, требуется некоторая форма синхронизации процессов, помещающих данные в разделяемую память и считывающих ее оттуда. В части 3 мы обсуждали различные средства синхронизации: взаимные исключения, условные переменные, блокировки чтения-записи, блокировки записей и семафоры.
Рассмотрим по шагам работу программы копирования файла типа клиент-сервер, которую мы использовали в качестве примера для иллюстрации различных способов передачи сообщений (рис. 4.1). ■ Сервер считывает данные из входного файла. Данные из файла считываются ядром в свою память, а затем копируются из ядра в память процесса. ■ Сервер составляет сообщение из этих данных и отправляет его, используя именованный или неименованный канал или очередь сообщений. Эти формы IPC обычно требуют копирования данных из процесса в ядро.
■ Клиент считывает данные из канала 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, но хотя файл с соответствующим полным именем при этом и удаляется, на открытый в данный момент семафор эта команда не действует. Этот вызов мы делаем для того, чтобы файл был удален даже при досрочном завершении программы. Отключение буферизации стандартного потока вывода и вызов fork15 Мы отключаем буферизацию стандартного потока вывода, поскольку запись в него будет производиться и родительским, и дочерним процессами. Это предотвращает смешивание вывода из двух процессов. 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
Таблица 12.2. Аргумент flag для вызова mmap
Аргумент 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_ASYNC происходит сразу же, как только данные для записи будут помещены в очередь ядром, а при указании флага MS_SYNC возврат происходит только после завершения операций записи. Если указан и флаг MS_INVALIDATE, все копии файла, содержимое которых не совпадает с его текущим содержимым, считаются устаревшими. Последующие обращения к этим копиям приведут к считыванию данных из файла. Почему вообще используется отображение в память?До сих пор мы всегда говорили об отображении в память содержимого файла, который сначала открывается вызовом open, а затем отображается вызовом mmap. Удобство состоит в том, что все операции ввода-вывода осуществляются ядром и скрыты от программиста, а он просто пишет код, считывающий и записывающий данные в некоторую область памяти. Ему не приходится вызывать read, write или lseek. Часто это заметно упрощает код.
Следует, однако, иметь в виду, что не все файлы могут быть отображены в память. Попытка отобразить дескриптор, указывающий на терминал или сокет, приведет к возвращению ошибки при вызове mmap. К дескрипторам этих типов доступ осуществляется только с помощью read и write (и аналогичных вызовов). Другой целью использования mmap может являться разделение памяти между неродственными процессами. В этом случае содержимое файла становится начальным содержимым разделяемой памяти и любые изменения, вносимые в нее процессами, копируются обратно в файл (что дает этому виду IPC живучесть файловой системы). Предполагается, что при вызове mmap указывается флаг MAP_SHARED, необходимый для разделения памяти между процессами.
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. fork20-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, должен впоследствии использоваться всеми прочими процессами, желающими получить доступ к данной области памяти.
#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; /* размер в байтах */ … }; Пример использования этих двух функций будет приведен в следующем разделе.
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. Мы обрабатываем эту ошибку, увеличивая значение счетчика переполнений.
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) не были сохранены из-за переполнения: Очевидно, что в этом примере переполнение возникло из-за того, что мы потребовали от клиента отправлять сообщения так часто, как только возможно, не делая между ними пауз. В реальном мире такое случается редко. Целью этого примера было продемонстрировать обработку ситуаций, в которых места для очередного сообщения нет, но клиент не должен блокироваться. Такая ситуация может возникнуть, разумеется, не только при использовании разделяемой памяти, но и при использовании очередей сообщений, именованных и неименованных каналов.
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, чтобы убедиться, что сегмент разделяемой памяти действительно был удален.
14.7. Ограничения, накладываемые на разделяемую памятьНа разделяемую память System V накладываются определенные ограничения точно так же, как и на семафоры и очереди сообщений System V (раздел 3.8). В табл. 14.1 приведены значения этих ограничений для разных реализаций. В первом столбце приведены традиционные для System V имена переменных ядра, в которых хранятся эти ограничения. Таблица 14.1. Типичные значения ограничений, накладываемых на разделяемую память System V
ПримерПрограмма в листинге 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 | Прислать материал | Нашёл ошибку | Наверх |
||||||||||||||||||||||||||||||||||||||||||||||||
|