Уильям Стивенс - UNIX: взаимодействие процессов
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 должен быть нулевым.