- •Часть 4. Локальное взаимодействие процессов
- •Глава 16. Блокирование записей 89
- •12.2. Процессы, потоки и общий доступ к информации
- •12.3. Живучесть объектов ipc
- •12.4. Пространства имен
- •12.5. Действие команд fork, exec и exit на объекты ipc
- •12.6. Комментарии к примерам ipc
- •12.7. Выводы по главе 12
- •12.8. Упражнения по главе 12
- •Глава 13. Именованные и неименованные каналы
- •13.1. Введение
- •13.2. Приложение типа клиент-сервер
- •13.3. Программные каналы
- •13.4. Функции popen и pclose
- •13.5. Именованные каналы (fifo)
- •13.6. Некоторые свойства именованных и неименованных каналов
- •13.7. Один сервер, несколько клиентов
- •13.8. Последовательные и параллельные серверы
- •13.9. Ограничения программных каналов и fifo
- •13.10. Выводы по главе 13
- •13.11. Упражнения по главе 13
- •Глава 14. Программные потоки
- •14.1. Введение
- •14.2. Концепция потоков
- •14.3. Идентификация потоков
- •14.4. Создание потока
- •14.5. Завершение потока
- •Функции управления процессами и потоками
- •14.6. Установка атрибутов потока
- •14.7. Реентерабельность
- •Альтернативные версии функций, безопасные в многопоточной среде
- •14.8. Локальные данные потоков
- •14.9. Принудительное завершение потоков
- •Некоторые точки выхода, определенные стандартом Posix.1
- •14.10. Потоки и сигналы
- •14.11. Выводы по главе 14
- •14.12. Упражнения по главе 14 Глава 15. Средства синхронизации потоков
- •15.1. Введение
- •15.2. Взаимные исключения: установка и снятие блокировки
- •15.2.1. Схема производитель-потребитель
- •15.2.2. Блокирование и опрос
- •15.2.3. Предотвращение тупиковых ситуаций
- •15.3. Условные переменные
- •15.3.1. Ожидание и сигнализация
- •15.3.2. Исключение состояния гонок
- •15.4. Блокировки чтения-записи
- •15.5. Атрибуты средств синхронизации потоков
- •15.5.1. Атрибуты взаимных исключений
- •Поведение взаимных исключений различных типов
- •15.5.2. Атрибуты условных переменных
- •15.5.3. Атрибуты блокировок чтения-записи
- •15.6. Выводы по главе 15
- •15.7. Упражнения по главе 15
- •Глава 16. Блокирование записей
- •16.1. Введение
- •16.2. Блокирование записей и файлов
- •16.3. Блокирование записей с помощью fcntl по стандарту Posix
- •16.4. Рекомендательная блокировка
- •16.5. Обязательная блокировка
- •16.6. Приоритет чтения и записи Выводы по главе 16
- •Упражнения по главе 16 Глава 17. System V ipc
- •17.1. Введение
- •17.2. Ключи типа key_t и функция ftok
- •17.3. Структура ipc_perm
- •17.4. Создание и открытие каналов ipc
- •17.5. Разрешения ipc
- •17.6. Программы ipcs и ipcrm
- •17.7. Ограничения ядра
- •17.8. Выводы по главе 17
- •17.9. Упражнения по главе 17
- •Глава 18. Очереди сообщений System V
- •18.1. Введение
- •18.2. Функция msgget
- •18.3. Функция msgsnd
- •18.4. Функция msgrcv
- •18.5. Функция msgctl
- •18.6. Пример программы клиент-сервер
- •18.7. Мультиплексирование сообщений
- •18.7.1. Пример: одна очередь на приложение
- •18.7.2. Пример: одна очередь для каждого клиента
- •18.8. Ограничения, накладываемые на очереди сообщений
- •18.9. Выводы по главе 18
- •18.10. Упражнения по главе 18
- •Глава 19. Семафоры System V
- •19.1. Введение
- •19.2. Функция semget
- •19.3. Функция semop
- •19.4. Функция semctl
- •19. . Ограничения семафоров System V
- •19. . Выводы по главе 19
- •19. . Упражнения по главе 19 Глава 20. Введение в разделяемую память
- •20.1. Введение
- •20.2. Функции mmap, munmap и msync
- •20.3. Увеличение счетчика в отображаемом в память файле
- •20.4. Неименованное отображение в память
- •20.5. Обращение к объектам, отображенным в память
- •20.6. Выводы по главе 20
- •20.7. Упражнения по главе 20
- •Глава 21. Разделяемая память System V
- •21.1. Введение
- •21.2. Функция shmget
- •21.3. Функция shmat
- •21.4. Функция shmdt
- •21.5. Функция shmctl
- •21.6. Ограничения, накладываемые на разделяемую память
- •21.7. Выводы по главе 21
- •21.8. Упражнения по главе 21
19. . Ограничения семафоров System V
На семафоры System V накладываются определенные системные ограничения, так же, как и на очереди сообщений. Большинство этих ограничений были связаны с особенностями реализации System V (раздел 16.7). Они показаны втабл. 18.1. Первая колонка содержит традиционное для System V имя переменной ядра, в которой хранится соответствующее ограничение.
Таблица 18.1
Типичные значения ограничений для семафоров System V
|
Имя |
Описание |
Значение |
|
semmsl |
Максимальное количество семафоров в наборе |
250 |
|
semmns |
Максимальное количество семафоров в системе |
32000 |
|
semopm |
Максимальное количество операций за один вызов функции semop |
32 |
|
semmni |
Максимальное количество наборов семафоров в системе |
128 |
В ОС Linux значения соответствующих переменных можно получить с помощью следующего вызова команды sysctl:
$ sysctl kernel.sem
kernel.sem = 250 32000 32 128
Значения переменных в порядке их описания в табл. 18.1выводятся в одной строке.
19. . Выводы по главе 19
Семафоры System V обладают следующими свойствами:
Они представляют собой набор значений. Последовательность операций над набором семафоров выполняется либо целиком, либо не выполняется вовсе.
К любому элементу набора семафоров могут быть применены три операции: проверка на нулевое значение, добавление некоторого числа к текущему значению и вычитание некоторого числа из текущего значения (в предположении, что значение остается неотрицательным).
Создание семафора System V имеет некоторую особенность, заключающуюся в необходимости выполнения двух вызовов для создания и инициализации семафора, что может привести к ситуации гонок.
Семафоры System V предоставляют возможность отмены (undo) операций, произведенных с ними, после завершения работы процесса.
19. . Упражнения по главе 19 Глава 20. Введение в разделяемую память
20.1. Введение
Разделяемая память является наиболее быстрым средством межпроцессного взаимодействия. После отображения области памяти в адресное пространство процессов, совместно ее использующих, для передачи данных между процессами больше не требуется участие ядра. Обычно, однако, требуется некоторая форма синхронизации процессов, помещающих данные в разделяемую память и считывающих их оттуда. Мы уже обсуждали различные средства синхронизации: взаимные исключения, условные переменные, блокировки чтения-записи, блокирование записей и семафоры.
Рассмотрим по шагам работу программы копирования файла типа клиент-сервер, которую мы использовали в качестве примера для иллюстрации различных способов передачи сообщений (рис. 13.1):
сервер считывает данные из входного файла. Данные из файла считываются ядром в свою память, а затем копируются из ядра в память процесса;
сервер составляет сообщение из этих данных и отправляет его, используя именованный или неименованный канал или очередь сообщений. Эти формы IPC обычно требуют копирования данных из процесса в ядро;
клиент считывает данные из канала IPC, что обычно требует их копирования из ядра в пространство процесса;
наконец, данные копируются из буфера клиента (второй аргумент функции write) в выходной файл.
Таким образом, для копирования файла обычно требуется четыре операции копирования данных. К тому же эти операции копирования осуществляются между процессами и ядром, что часто является дорогостоящей операцией (более дорогостоящей, чем копирование данных внутри ядра или внутри одного процесса). На рис. 19.1изображено перемещение данных между клиентом и сервером через ядро.

рис. 19.1
Недостатком этих форм IPC – именованных и неименованных каналов – является то, что для передачи между процессами информация должна пройти через ядро.
Разделяемая память дает возможность обойти этот недостаток, поскольку ее использование позволяет двум процессам обмениваться данными через общий участок памяти. Процессы, разумеется, должны синхронизировать и координировать свои действия. Одновременное использование участка памяти во многом аналогично совместному доступу к файлу, например, к файлу с последовательным номером, который фигурировал во всех примерах на блокировку доступа к файлам. Для синхронизации такого рода может применяться любой из методов, упомянутых в предшествующих разделах этого пособия.
Теперь информация передается между клиентом и сервером в такой последовательности:
сервер получает доступ к объекту разделяемой памяти, используя для синхронизации, например, семафор;
сервер считывает данные из файла в разделяемую память. Второй аргумент вызова read(адрес буфера) указывает на объект разделяемой памяти;
после завершения операции считывания клиент уведомляется сервером с помощью семафора;
клиент записывает данные из объекта разделяемой памяти в выходной файл.
Этот сценарий иллюстрирует рис. 19.2.

рис. 19.2
Из этого рисунка видно, что копирование данных происходит всего лишь дважды: из входного файла в разделяемую память и из разделяемой памяти в выходной файл. Мы нарисовали два прямоугольника штриховыми линиями; они подчеркивают, что разделяемая память принадлежит как адресному пространству клиента, так и адресному пространству сервера.
В этой главе мы возвращаемся к примеру с увеличением последовательного номера, который впервые появился в главе 9. Теперь мы будем хранить последовательный номер в сегменте разделяемой памяти, а не в файле.
Сначала мы подчеркнем, что память разделяется между родительским и дочерним процессами при вызове функции fork. В программе излистинга 19.1родительский и дочерний процессы по очереди увеличивают глобальный целочисленный счетчикcount.
Листинг 19.1. Увеличение глобального счетчика родительским и дочерним процессами
#include <errno.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/stat.h>
#include <sys/sem.h>
#define SEM_R S_IRUSR
#define SEM_A S_IWUSR
#define SVSEM_MODE (SEM_R | SEM_A | SEM_R>>3 | SEM_R>>6)
union semun
{
int val;
struct semid_ds *buf;
unsigned short *array;
} arg;
int count = 0, semid;
void sem_del(void)
{
/* удаление семафора осуществляет родительский процесс, */
/* дождавшись завершения дочернего процесса */
if (semctl(semid, 0, IPC_RMID) == -1)
fprintf(stderr, "Ошибка вызова функции semctl(IPC_RMID): %s\n",
strerror(errno));
}
int main(int argc, char **argv)
{
int i, nloop, oflag;
struct sembuf ops;
pid_t pid;
if (argc != 2)
{
fprintf(stderr, "Использование: incr1 <количество_циклов>\n");
exit(1);
}
nloop = atoi(argv[1]);
oflag = SVSEM_MODE | IPC_CREAT;
if ((semid = semget(IPC_PRIVATE, 1, oflag)) == -1)
{ /* создание семафора */
fprintf(stderr, "Ошибка вызова функции semget: %s\n", strerror(errno));
exit(1);
}
if (atexit(sem_del))
{
fprintf(stderr, "Невозможно зарегистрировать sem_del: %s\n",
strerror(errno));
exit(1);
}
arg.val = 1;
if (semctl(semid, 0, SETVAL, arg) == -1)
{ /* инициализация семафора */
fprintf(stderr, "Ошибка вызова функции semctl(SETVAL): %s\n",
strerror(errno));
exit(1);
}
setbuf(stdout, NULL); /* stdout не буферизуется */
ops.sem_num = 0; /* номер семафора в наборе */
ops.sem_flg = 0; /* флаги операции */
if ((pid = fork()) == -1)
{ /* создание дочернего процесса */
fprintf(stderr, "Ошибка вызова функции fork: %s\n", strerror(errno));
exit(1);
}
if (pid == 0)
{ /* дочерний процесс */
for (i = 0; i < nloop; i++)
{
ops.sem_op = -1;
if (semop(semid, &ops, 1) == -1)
{
fprintf(stderr, "Потомок: ошибка вызова функции semop(wait): %s\n",
strerror(errno));
_exit(1);
}
printf("Потомок: %d\n", count++);
ops.sem_op = 1;
if (semop(semid, &ops, 1) == -1)
{
fprintf(stderr, "Потомок: ошибка вызова функции semop(post): %s\n",
strerror(errno));
_exit(1);
}
}
_exit(0); /* для предотвращения вызова обработчиков выхода */
}
/* родительский процесс */
for (i = 0; i < nloop; i++)
{
ops.sem_op = -1;
if (semop(semid, &ops, 1) == -1)
{
fprintf(stderr, "Родитель: ошибка вызова функции semop(wait): %s\n",
strerror(errno));
exit(1);
}
printf("Родитель: %d\n", count++);
ops.sem_op = 1;
if (semop(semid, &ops, 1) == -1)
{
fprintf(stderr, "Родитель: ошибка вызова функции semop(post): %s\n",
strerror(errno));
exit(1);
}
}
if (wait(NULL) == -1)
{ /* ожидание завершения дочернего процесса */
fprintf(stderr, "Родитель: ошибка вызова функции wait: %s\n",
strerror(errno));
exit(1);
}
exit(0);
}
Мы создаем и инициализируем семафор, защищающий переменную, которую мы считаем глобальной (count). Поскольку предположение о ее глобальности ложно, этот семафор на самом деле не нужен.
Мы отключаем буферизацию стандартного потока вывода, поскольку запись в него будет производиться совместно родительским и дочерним процессами. Это предотвращает смешивание вывода из двух процессов. Родительский и дочерний процессы увеличивают глобальный счетчик в цикле заданное число раз, выполняя операции только при установленном семафоре.
Если мы запустим эту программу на выполнение, то мы увидим, что каждый из процессов использует собственную копию глобального счетчика count. Каждый начинает со значения 0 и при прохождении цикла увеличивает значение своей копии счетчика.
