- •Часть 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
13.7. Один сервер, несколько клиентов
Преимущества канала FIFO проявляются более явно в том случае, когда сервер представляет собой некоторый долго функционирующий процесс (например, демон syslogd, описанный в разделе 11.4), не являющийся родственным клиенту. Демон создает именованный канал с вполне определенным известным именем, открывает его на чтение, а запускаемые впоследствии клиенты открывают его на запись и отправляют демону команды и необходимые данные. Односторонняя связь в этом направлении (от клиента к серверу) легко реализуется с помощью FIFO, однако необходимость отправки данных в обратную сторону (от сервера к клиенту) усложняет задачу. Рис. 13.8 иллюстрирует прием, применяемый в этом случае.

рис. 13.8
Сервер создает канал с известным полным именем, в данном случае /tmp/fifo.serv. Из этого канала он считывает запросы клиентов. Каждый клиент при запуске создает свой собственный канал, полное имя которого определяется его идентификатором процесса. Клиент отправляет свой запрос в канал сервера с известным именем, причем этот запрос содержит идентификатор процесса клиента и имя файла, отправку которого клиент запрашивает у сервера.
В листинге 13.6 приведен текст программы сервера.
Листинг 13.6. Сервер, обслуживающий нескольких клиентов с помощью канала FIFO
#include <errno.h>
#include <fcntl.h>
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#define MAXLINE 4096
#define SERV_FIFO "/tmp/fifo.serv"
#define FILE_MODE (S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH)
void delete_fifo(void)
{
unlink(SERV_FIFO);
}
int main(int argc, char **argv)
{
int readfifo, writefifo, dummyfd, fd;
char *ptr, buff[MAXLINE], fifoname[MAXLINE];
ssize_t n;
FILE *readfp;
/* создание FIFO сервера с известным именем; если уже существует - OK */
if ((mkfifo(SERV_FIFO, FILE_MODE) < 0) && (errno != EEXIST))
{
fprintf(stderr, "Сервер: невозможно создать %s: %s\n", SERV_FIFO,
strerror(errno));
exit(1);
}
if (atexit(delete_fifo))
{
unlink(SERV_FIFO);
fprintf(stderr, "Сервер: невозможно зарегистрировать delete_fifo: %s\n",
strerror(errno));
exit(1);
}
/* открытие FIFO сервера для чтения */
if ((readfifo = open(SERV_FIFO, O_RDONLY, 0)) < 0)
{
fprintf(stderr, "Сервер: невозможно открыть %s для чтения: %s\n", SERV_FIFO,
strerror(errno));
exit(1);
} /* открытие FIFO сервера для записи. Этот дескриптор не используется */
if ((dummyfd = open(SERV_FIFO, O_WRONLY, 0)) < 0)
{
fprintf(stderr, "Сервер: невозможно открыть %s для записи: %s\n", SERV_FIFO,
strerror(errno));
exit(1);
}
if ((readfp = fdopen(readfifo, "r")) == NULL) /* открываем поток для fgets */
{
fprintf(stderr, "Сервер: невозможно переоткрыть %s для чтения: %s\n",
SERV_FIFO, strerror(errno));
exit(1);
}
setbuf(readfp, NULL); /* отключаем буферизацию */
/* цикл приема запросов от клиентов */
while (fgets(buff, MAXLINE, readfp) != NULL)
{ /* fgets() гарантирует завершающий нулевой байт */
if (buff[strlen(buff)-1] == '\n') /* удаление символа перевода строки */
buff[strlen(buff)-1] = '\0'; /* (если есть) */
if ((ptr = strchr(buff, ' ')) == NULL)
{
fprintf(stderr, "Сервер: неправильный запрос: %s\n", buff);
continue;
}
*ptr++ = '\0'; /* нулевой байт вставлен в buff на место пробела */
/* ptr после увеличения (++) указывает на имя запрошенного файла */
snprintf(fifoname, sizeof(fifoname), "/tmp/fifo.%s", buff);
if ((writefifo = open(fifoname, O_WRONLY, 0)) < 0)
{
fprintf(stderr, "Сервер: невозможно открыть %s для записи: %s\n",
fifoname, strerror(errno));
continue;
}
if ((fd = open(ptr, O_RDONLY)) < 0)
{ /* сообщаем клиенту об ошибке открытия файла */
snprintf(fifoname, sizeof(fifoname), "Сервер: ошибка открытия файла %s:
%s\n", ptr, strerror(errno));
write(writefifo, fifoname, strlen(fifoname));
close(writefifo);
}
else /* файл успешно открыт; копируем его в FIFO */
{
while ((n = read(fd, fifoname, MAXLINE)) > 0)
if (write(writefifo, fifoname, n) != n)
{
snprintf(fifoname, sizeof(fifoname), "Сервер: ошибка записи содержимого
файла %s в FIFO: %s\n", ptr, strerror(errno));
write(writefifo, fifoname, strlen(fifoname));
exit(1);
}
if (n < 0)
{
snprintf(fifoname, sizeof(fifoname), "Сервер: ошибка чтения содержимого
файла %s: %s\n", ptr, strerror(errno));
write(writefifo, fifoname, strlen(fifoname));
exit(1);
}
close(fd);
close(writefifo);
}
}
}
Сервер создает канал FIFO с известным именем, обрабатывая ситуацию, когда такой канал уже существует. Затем этот канал открывается дважды: один раз только для чтения, а второй – только для записи. Дескриптор readfifo используется для приема запросов от клиентов, а дескриптор dummyfd не используется вовсе. Причина, по которой нужно открывать канал для записи, видна из табл. 13.1. Если канал не открыть на запись, то при завершении работы очередного клиента этот канал будет опустошаться, вследствие чего сервер будет считывать 0, означающий конец файла. В этом случае пришлось бы каждый раз закрывать канал вызовом close, а затем заново открывать его с флагом O_RDONLY, что приводило бы к блокированию сервера до подключения следующего клиента. Мы же всегда будем иметь дескриптор, открытый для записи, поэтому функция read не будет возвращать 0, означающий конец файла, при отсутствии клиентов. Вместо этого сервер просто будет блокироваться при вызове функции fgets, ожидая подключения следующего клиента. Этот трюк упрощает код программы-сервера и уменьшает количество вызовов функции open для канала FIFO сервера.
При запуске сервера первый вызов open (с флагом O_RDONLY) приводит к блокированию процесса до появления первого клиента, открывающего канал FIFO сервера для записи (см. табл. 13.1). Второй вызов open (с флагом O_WRONLY) не приводит к блокированию, поскольку канал уже открыт для чтения.
Каждый запрос, принимаемый от клиента, представляет собой одну строку, содержащую идентификатор процесса, пробел и полное имя требуемого файла. Эта строка считывается функцией fgets. Символ перевода строки, возвращаемый функцией fgets, удаляется. Этот символ может отсутствовать только в том случае, если буфер был заполнен, прежде чем был обнаружен символ перевода строки, либо если принятая от клиента строка не была завершена этим символом. Функция strchr возвращает указатель на первый пробел в этой строке, который затем увеличивается на единицу, чтобы он указывал на первый символ полного имени файла, следующего после пробела. Полное имя канала FIFO клиента содержит идентификатор его процесса, и этот канал открывается сервером для записи.
Оставшаяся часть кода программы-сервера аналогична функции server из листинга 13.1. Программа открывает файл, указанный клиентом; если при этом возникает ошибка, то клиенту отсылается сообщение о ней. Если открытие файла завершается успешно, то его содержимое копируется в канал FIFO клиента. После завершения копирования открытый сервером “конец” (дескриптор) канала FIFO клиента должен быть закрыт с помощью функции close, чтобы функция read вернула программе-клиенту значение 0 (конец файла). Сервер не удаляет канал FIFO клиента; клиент должен самостоятельно позаботиться об этом после приема от сервера символа конца файла.
Текст программы-клиента приведен в листинге 13.7.
Листинг 13.7. Клиент, взаимодействующий с сервером через канал FIFO
#include <errno.h>
#include <fcntl.h>
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#define MAXLINE 4096
#define SERV_FIFO "/tmp/fifo.serv"
#define FILE_MODE (S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH)
char fifoname[MAXLINE];
void delete_fifo(void)
{
unlink(fifoname);
}
int main(int argc, char **argv)
{
int readfifo, writefifo;
size_t len;
ssize_t n;
char *ptr, buff[MAXLINE];
pid_t pid;
/* создание FIFO с именем, включающим PID; если уже существует - OK */
pid = getpid();
snprintf(fifoname, sizeof(fifoname), "/tmp/fifo.%ld", (long) pid);
if ((mkfifo(fifoname, FILE_MODE) < 0) && (errno != EEXIST))
{
fprintf(stderr, "Клиент: невозможно создать %s: %s\n", fifoname,
strerror(errno));
exit(1);
}
if (atexit(delete_fifo))
{
unlink(fifoname);
fprintf(stderr, "Клиент: невозможно зарегистрировать delete_fifo: %s\n",
strerror(errno));
exit(1);
}
/* записываем в буфер PID и пробел */
snprintf(buff, sizeof(buff), "%ld ", (long) pid);
len = strlen(buff);
ptr = buff + len;
/* чтение полного имени файла из стандартного потока ввода */
if (fgets(ptr, MAXLINE - len, stdin) == NULL)
{ /* fgets() гарантирует завершающий нулевой байт */
fprintf(stderr, "Клиент: ошибка чтения полного имени файла из стандартного
потока ввода\n");
exit(1);
}
if (buff[len] == '\n')
{ /* при вводе имени файла был введен только перевод строки */
fprintf(stderr, "Клиент: введено пустое имя файла, завершение работы\n");
exit(1);
}
/* открытие FIFO сервера и запись в него PID и полного имени файла */
if ((writefifo = open(SERV_FIFO, O_WRONLY, 0)) < 0)
{
fprintf(stderr, "Клиент: невозможно открыть %s: %s\n", SERV_FIFO,
strerror(errno));
exit(1);
}
if ((n=write(writefifo, buff, strlen(buff))) < 0)
{
fprintf(stderr, "Клиент: ошибка записи полного имени файла в канал %s:
%s\n", SERV_FIFO, strerror(errno));
exit(1);
}
/* открытие своего FIFO; блокирование до его открытия сервером */
if ((readfifo = open(fifoname, O_RDONLY, 0)) < 0)
{
fprintf(stderr, "Клиент: невозможно открыть %s: %s\n", fifoname,
strerror(errno));
exit(1);
}
/* считывание из канала FIFO, запись в стандартный поток вывода */
while ((n = read(readfifo, buff, MAXLINE)) > 0)
if (write(STDOUT_FILENO, buff, n) != n)
{
fprintf(stderr, "Клиент: ошибка записи содержимого файла в стандартный
поток вывода: %s\n", strerror(errno));
exit(1);
}
if (n < 0)
{
fprintf(stderr, "Клиент: ошибка чтения содержимого файла из канала FIFO:
%s\n", strerror(errno));
exit(1);
}
close(readfifo);
exit(0);
}
Клиент создает свой канал FIFO с использованием идентификатора процесса. Строка запроса клиента содержит идентификатор процесса, один пробел, полное имя запрашиваемого файла и символ перевода строки. Строка запроса записывается в массив buff, причем имя файла считывается из стандартного потока ввода. Клиент открывает канал FIFO сервера для записи и записывает в него строку запроса. Если клиент окажется первым с момента запуска сервера, первый вызов функции open (с флагом O_RDONLY) вернет управление и разблокирует сервер.
Ответ сервера клиент считывает из своего канала FIFO и записывает его в стандартный поток вывода, после чего канал FIFO клиента закрывается и удаляется.
Для корректной работы клиента и сервера сначала необходимо запустить программу-сервер в фоновом режиме или на отдельной консоли, а затем запустить программу-клиента.
Мы можем также связаться с сервером из интерпретатора команд, поскольку каналы FIFO обладают именами в файловой системе.
$ PID=$$
$ mkfifo /tmp/fifo.$PID
$ echo "$PID /etc/resolv.conf" >/tmp/fifo.serv
$ cat </tmp/fifo.$PID
nameserver 192.168.1.1
search apeps.kiev.ua ntu-kpi.kiev.ua
$ rm /tmp/fifo.$PID
Мы отсылаем серверу идентификатор процесса текущей копии интерпретатора и полное имя файла с помощью одной команды (echo) и считываем результат из канала FIFO сервера с помощью другой команды (cat). Между выполнением этих двух команд может пройти произвольный промежуток времени. Таким образом, сервер помещает содержимое файла в канал, а клиент затем запускает команду cat, чтобы считать оттуда данные. Может показаться, что данные каким-то образом хранятся в канале, хотя он не открыт ни одним процессом. На самом деле все не так. После закрытия программного канала или FIFO последним процессом с помощью функции close все находящиеся в нем данные теряются. В нашем примере сервер, считав строку запроса от клиента из своего FIFO, блокируется при попытке открыть канал FIFO клиента, потому что клиент (в данном случае это наша копия интерпретатора) еще не открыл его на чтение (см. табл. 13.1). Только после вызова cat некоторое время спустя канал FIFO клиента будет открыт для чтения, и тогда сервер разблокируется. Кстати, подобным образом осуществляется атака типа “отказ в обслуживании” (denial-of-service attack), которую мы обсудим в следующем разделе.
Атомарность записи в FIFO. Наша простейшая пара клиент-сервер позволяет наглядно показать важность наличия свойства атомарности записи в программные каналы и FIFO. Предположим, что два клиента посылают серверу запрос приблизительно в один и тот же момент. Первый клиент отправляет следующую строку:
1234 /etc/resolv.conf
а второй:
9876 /etc/passwd
Предполагая, что каждый клиент помещает данные в канал FIFO с помощью одного вызова функции write и каждая строка имеет размер, не превышающий величины PIPE_BUF (что заведомо выполняется в ОС Linux, поскольку величина PIPE_BUF обычно равна 4096 байтам, а длина полного имени файла обычно ограничена 1024 байтами), мы можем гарантировать, что в канале FIFO сервера запросы клиентов не смешаются в “кашу”.
