Иллюстрированный самоучитель по теории операционных систем

Примеры реализаций средств гармонического взаимодействия

Программные каналы 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;
}
Если Вы заметили ошибку, выделите, пожалуйста, необходимый текст и нажмите CTRL + Enter, чтобы сообщить об этом редактору.