- •Часть 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.2. Приложение типа клиент-сервер
Пример приложения модели клиент-сервер приведен на рис. 13.1. Именно на него мы будем ссылаться в тексте этой главы и главы 17 при необходимости проиллюстрировать использование программных каналов, FIFO и очередей сообщений System V.
Клиент считывает полное имя файла из стандартного потока ввода и записывает его в канал IPC. Сервер считывает это имя из канала IPC и производит попытку открытия файла на чтение. Если попытка оказывается успешной, сервер считывает файл и записывает его в канал IPC. В противном случае сервер возвращает клиенту сообщение об ошибке. Клиент считывает данные из канала IPC и записывает их в стандартный поток вывода. Если сервер не может считать файл, из канала будет считано сообщение об ошибке. В противном случае будет принято содержимое файла. Две штриховые линии между клиентом и сервером на рис. 13.1 представляют собой канал IPC.

рис. 13.1
13.3. Программные каналы
Программные каналы имеются во всех существующих реализациях и версиях Unix. Канал создается вызовом pipe и предоставляет возможность однонаправленной (односторонней) передачи данных.
#include <unistd.h>
int pipe (int fd[2]);
/* функция возвращает 0 в случае успешного завершения, -1 – в случае ошибки */
Функция возвращает два файловых дескриптора: fd[0] и fd[1], причем первый открыт для чтения, а второй – для записи.
Для определения типа дескриптора (файла, программного канала или FIFO) можно использовать макрос S_ISFIFO. Он принимает единственный аргумент: поле st_mode структуры stat и возвращает значение “истина” (ненулевое значение) или ложь (ноль). Структуру stat для канала возвращает функция fstat. Для FIFO структура возвращается функциями fstat, lstat и stat.
На рис. 13.2 изображен канал при использовании его единственным процессом.

рис. 13.2
Хотя канал создается одним процессом, он редко используется только этим процессом. Каналы обычно используются для связи между двумя процессами (родительским и дочерним) следующим образом: процесс создает канал, а затем вызывает fork, создавая свою копию – дочерний процесс (рис. 13.3). Затем родительский процесс закрывает открытый для чтения конец канала, а дочерний, в свою очередь, – открытый на запись конец канала. Это обеспечивает одностороннюю передачу данных между процессами, как показано на рис. 13.4.
При вводе команды наподобие
who | sort | lp
в интерпретаторе команд Unix интерпретатор выполняет вышеописанные действия для создания трех процессов с двумя каналами между ними. Интерпретатор также подключает открытый для чтения конец каждого канала к стандартному потоку ввода, а открытый на запись – к стандартному потоку вывода. Созданный таким образом канал показан на рис. 13.5.

рис. 13.3

рис. 13.4

рис. 13.5
Все рассмотренные выше каналы были однонаправленными (односторонними), то есть позволяли передавать данные только в одну сторону. При необходимости передачи данных в обе стороны нужно создавать пару каналов и использовать каждый из них для передачи данных в одну сторону. Этапы создания двунаправленного канала IPC следующие:
Создаются каналы 1 (fd1[0] и fd1[1]) и 2 (fd2[0] и fd2[1]).
Вызов fork.
Родительский процесс закрывает доступный для чтения конец канала 1 (fd1[0]).
Родительский процесс закрывает доступный для записи конец канала 2 (fd2[1]).
Дочерний процесс закрывает доступный для записи конец канала 1 (fd1[1]).
Дочерний процесс закрывает доступный для чтения конец канала 2 (fd2[0]).
Текст программы, выполняющей эти действия, приведен в листинге 13.1. При этом создается структура каналов, изображенная на рис. 13.6.

рис. 13.6
Пример. Давайте напишем программу, описанную в разделе 13.2, с использованием каналов. Функция main создает два канала и вызывает fork для создания копии процесса. Родительский процесс становится клиентом, а дочерний – сервером. Первый канал (pipe1) используется для передачи полного имени файла от клиента серверу, а второй (pipe2) – для передачи содержимого файла (или сообщения об ошибке) от сервера клиенту.
Обратите внимание на то, что оба канала проходят через ядро, поэтому каждый передаваемый байт пересекает интерфейс ядра дважды: при записи в канал и при считывании из него.
В листинге 13.1 приведена программа для данного примера.
Листинг 13.1. Приложение клиент-сервер, использующее два канала
#include <errno.h>
#include <fcntl.h>
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#define MAXLINE 4096
void server(int readfd, int writefd)
{
int fd;
ssize_t n;
char buff[MAXLINE+1], msg[MAXLINE+1];
/* чтение полного имени файла из канала IPC */
if ((n = read(readfd, buff, MAXLINE)) < 0)
{
snprintf(msg, sizeof(msg), "Сервер: ошибка чтения полного имени файла из
канала IPC: %s\n", strerror(errno));
write(writefd, msg, strlen(msg));
exit(1);
}
if (n == 0)
{
snprintf(msg, sizeof(msg), "Сервер: считано пустое имя файла, завершение
работы\n");
write(writefd, msg, strlen(msg));
exit(1);
}
buff[n] = '\0'; /* добавляем признак конца строки к полному имени */
if ((fd = open(buff, O_RDONLY)) < 0)
{ /* сообщаем клиенту об ошибке открытия файла */
snprintf(msg, sizeof(msg), "Сервер: ошибка открытия файла %s: %s\n", buff,
strerror(errno));
write(writefd, msg, strlen(msg));
exit(1);
}
else /* файл успешно открыт; копируем его в канал IPC */
{
while ((n = read(fd, msg, MAXLINE)) > 0)
if (write(writefd, msg, n) != n)
{
snprintf(msg, sizeof(msg), "Сервер: ошибка записи содержимого файла %s в
канал IPC: %s\n", buff, strerror(errno));
write(writefd, msg, strlen(msg));
exit(1);
}
if (n < 0)
{
snprintf(msg, sizeof(msg), "Сервер: ошибка чтения содержимого файла %s:
%s\n", buff, strerror(errno));
write(writefd, msg, strlen(msg));
exit(1);
}
close(fd);
}
}
void client(int readfd, int writefd)
{
size_t len;
ssize_t n;
char buff[MAXLINE];
/* чтение полного имени файла из стандартного потока ввода */
if (fgets(buff, MAXLINE, stdin) == NULL)
{
fprintf(stderr, "Клиент: ошибка чтения полного имени файла из стандартного
потока ввода\n");
exit(1);
}
len = strlen(buff); /* fgets() гарантирует завершающий нулевой байт */
if (buff[len-1] == '\n')
len--; /* удаление символа перевода строки (если есть) */
if (len == 0)
{
fprintf(stderr, "Клиент: введено пустое имя файла, завершение работы\n");
exit(1);
}
/* запись полного имени файла в канал IPC */
if (write(writefd, buff, len) < 0)
{
fprintf(stderr, "Клиент: ошибка записи полного имени файла в канал IPC:
%s\n", strerror(errno));
exit(1);
}
/* считывание из канала IPC, запись в стандартный поток вывода */
while ((n = read(readfd, buff, MAXLINE)) > 0)
if (write(STDOUT_FILENO, buff, n) != n)
{
fprintf(stderr, "Клиент: ошибка записи содержимого файла в стандартный
поток вывода: %s\n", strerror(errno));
exit(1);
}
if (n < 0)
{
fprintf(stderr, "Клиент: ошибка чтения содержимого файла из канала IPC:
%s\n", strerror(errno));
exit(1);
}
}
int main(int argc, char **argv)
{
int pipe1[2], pipe2[2], status;
pid_t childpid;
if (pipe(pipe1) == -1) /* создание двух каналов */
{
fprintf(stderr, "Невозможно создать канал pipe1: %s\n", strerror(errno));
exit(1);
}
if (pipe(pipe2) == -1)
{
fprintf(stderr, "Невозможно создать канал pipe2: %s\n", strerror(errno));
exit(1);
}
if ((childpid = fork()) == -1)
{
fprintf(stderr, "Ошибка вызова функции fork: %s\n", strerror(errno));
exit(1);
}
if (childpid == 0) /* дочерний процесс */
{
close(pipe1[1]);
close(pipe2[0]);
server(pipe1[0], pipe2[1]);
exit(0);
}
close(pipe1[0]); /* родительский процесс */
close(pipe2[1]);
client(pipe2[0], pipe1[1]);
if (waitpid(childpid, &status, 0) == -1)
{ /* ожидание завершения дочернего процесса */
fprintf(stderr, "Ошибка вызова функции waitpid: %s\n", strerror(errno));
exit(1);
}
if (status == 0)
fprintf(stderr, "Копирование файла успешно завершено\n");
exit(0);
}
Внутри функции main мы создаем два канала и выполняем шесть шагов, которые упоминались в отношении рис. 13.6. Родительский процесс вызывает функцию client, а дочерний – функцию server.
Дочерний процесс (сервер) завершает свою работу первым, вызывая функцию exit после завершения записи данных в канал. После этого функция client родительского процесса возвращает управление функции main, закончив считывание данных из канала. Затем родительский процесс вызывает waitpid для получения информации о статусе завершения дочернего процесса. Только в случае корректного завершения обоих процессов копирование файла считается проведенным успешно.
Полное имя файла клиент считывает из стандартного потока ввода и записывает в канал, удалив при необходимости завершающий символ перевода строки. Сервер считывает записанное в канал клиентом имя файла и дополняет его нулевым байтом. Далее сервер открывает указанный файл для чтения, а при возникновении ошибки извещает о ней клиента с помощью канала. Затем клиент считывает все, что сервер направляет в канал, и записывает эти данные в стандартный поток вывода. Ожидается, что это будет содержимое файла, но в случае возникновения ошибки это будет сообщение о причине, которая привела к ней.
