Примеры реализаций средств гармонического взаимодействия
Программные каналы Unix
Одним из наиболее типичных средств такого рода является труба (pipe) или программный канал – основное средство взаимодействия между процессами в ОС семейства Unix. В русскоязычной литературе трубы иногда ошибочно называют конвейерами. В действительности, конвейер – это группа процессов, последовательно соединенных друг с другом однонаправленными трубами.
Труба представляет собой поток байтов. Поток этот имеет начало (исток) и конец (приемник). В исток этого потока можно записывать данные, а из приемника – считывать. Нить, которая пытается считать данные из пустой трубы, будет задержана, пока там что-нибудь не появится. Наоборот, пишущая нить может записать в трубу некоторое количество данных, прежде чем труба заполнится, и дальнейшая запись будет заблокирована. На практике труба реализована в виде небольшого (несколько килобайтов) кольцевого буфера. Передатчик заполняет этот буфер, пока там есть место. Приемник считывает данные, пока буфер не опустеет.
Трубу можно установить в режим чтения и записи без блокировки. При этом вызовы, которые в других условиях были бы остановлены и вынуждены были бы ожидать партнера на другом конце трубы, возвращают ошибку с особым кодом.
По-видимому, трубы являются одной из первых реализаций гармонически взаимодействующих процессов по терминологии Дейкстры.
Самым интересным свойством трубы является то, что чтение данных из и запись в нее осуществляется теми же самыми системными вызовами read и write, что и работа с обычным файлом, внешним устройством или сетевым соединением (сокетом). На этом основана техника переназначения ввода-вывода, широко используемая в командных интерпретаторах UNIX. Она состоит в том, что большинство системных утилит получают данные из потока стандартного ввода (stdin) и выдают их в поток стандартного вывода (stdout). При этом, указывая в качестве этих потоков терминальное устройство, файл или трубу, мы можем использовать в качестве ввода, соответственно: текст, набираемый с клавиатуры, содержимое файла или стандартный вывод другой программы. Аналогично мы можем выдавать данные сразу на экран, в файл или передавать их на вход другой программы.
Так, например, компилятор GNU С состоит из трех основных проходов: препроцессора, собственно компилятора, генерирующего текст на ассемблере, и ассемблера. При этом внутри компилятора, на самом деле, также выполняется несколько проходов по тексту (в описании перечислено восемнадцать), в основном для оптимизации, но нас это в данный момент не интересует, поскольку все они выполняются внутри одной задачи. При этом все три задачи объединяются трубами в единую линию обработки входного текста – конвейер (pipeline), так что промежуточные результаты компиляции не занимают места на диске.
В системе UNIX труба создается системным вызовом pipe(int flldes;2]). Этот вызов создает трубу и помещает дескрипторы файлов, соответствующие входному и выходному концам трубы, в массив fildes. Затем мы можем вы полнить fork, в различных процессах переназначить соответствующие конец трубы на место stdin и stdout и запустить требуемые программы (пример 7.7). При этом мы получим типичный конвейер – две задачи, стандартный ввод и вывод которых соединены трубой.
Пример 7.7. Код, создающий конвейер при помощи труб.
#include <unistd.h> void pipeline(void) { /* stage 1 */ int pipe1[2]; int child1; int pipe2[2]; int child2; int child3; pipe(pipe1); if ((child1=fork())==0) { close(pipe1[0]); /* Закрыть лишний конец трубы */ closed); /* Переназначить стандартный вывод */ dup(pipe1[1]); close(pipe1[1]); /* Исполнить программу */ execlpC'du", "du", "-s", ".", NULL); /* Мы можем попасть сюда только при ошибке exec */ perror("Cannot exec"); exit(0); } close(pipel [1]); if (childl==-1) { perror("Cannot fork"); } /* stage 2 */ pipe(pipe2); if ((child2=fork())==0) { ' .close (0); /J" Переназначить стандартный ввод */ dup(pipel[0]}; close (pipel [0]); close (pipe2 [0]); /* Закрыть лишний конец трубы */ close (1); /* Переназначить стандартный вывод */ close (pipe2 [1]); /* Исполнить программу */ execlp ("sort", "sort", "-nr", NULL); /* Мы можем попасть сюда только при ошибке exec */ perror ("Cannot exec"); exit(O); } close (pipe1 [0]); close (pipe2 [1]); if (child2==-1) { perror ("Cannot fork"); } /* stage 3 */ if ((child3=fork())==0) { close (0); /* Переназначить стандартный ввод */ dup(pipe2 [0]); close (pipe2 [0]); /* Исполнить программу */ execlp ("tail", "tail", "-1", NULL); /* Мы можем попасть сюда только при ошибке exec */ perror ("Cannot exec"); exit (0); } close (pipe2 [0]); if (child3==-1) { perror ("Cannot fork"); } while (wait (NULL)1=-1); return; }