Каналы и системный вызов ехес
Вспомним, как можно создать канал между двумя программами с помощью командного интерпретатора:
$ ls | wc
Как это происходит? Ответ состоит из двух частей. Во-первых, командный интерпретатор использует тот факт, что открытые дескрипторы файлов остаются открытыми (по умолчанию) после вызова ехес. Это означает, что два файловых дескриптора канала, которые были открыты до выполнения комбинации вызовов fork/ехес, останутся открытыми и когда дочерний процесс начнет выполнение новой программы. Во-вторых, перед вызовом ехес командный интерпретатор соединяет стандартный вывод программы ls
с входом канала, а стандартный ввод программы wc – с выходом канала. Это можно сделать при помощи вызова fcntl или dup2, как было показано в упражнении 5.10. Так как значения дескрипторов файлов, соответствующих стандартному вводу, стандартному выводу и стандартному выводу диагностики, равны 0, 1 и 2 соответственно, то можно, например, соединить стандартный вывод с другим дескриптором файла, используя вызов dup2 следующим образом. Обратите внимание, что перед переназначением вызов dup2 закрывает файл, представленный его вторым параметром.
(* Вызов dup2 будет копировать дескриптор файла 1 *)
dup2(filedes, 1);
.
.
.
(* Теперь программа будет записывать свой стандартный *)
(* вывод в файл, заданный дескриптором filedes *)
.
.
.
Следующий пример, программа join, демонстрирует механизм каналов, задействованный в упрощенном командном интерпретаторе. Программа join имеет два параметра, com1 и com2, каждый из которых соответствует выполняемой команде. Оба параметра в действительности являются массивами строк, которые будут переданы вызову execvp.
Родительский процесс | |||||||||
wait() | |||||||||
Потомок дочернего процесса (com1) | Дочерний процесс (com2) | ||||||||
> > fdin | fdread() | ||||||||
^ | (stdin) | ||||||||
fdwrite() | fdout > > | ||||||||
(stdout) |
Рис. 7.5. Программа join
Программа join запустит обе программы на выполнение и свяжет стандартный вывод программы com1 со стандартным вводом программы com2. Работа программы join изображена на рис. 7.5 и может быть описана следующей схемой (без учета обработки ошибок):
процесс порождает дочерний процесс и ожидает действий от него
дочерний процесс продолжает работу
дочерний процесс создает канал
затем дочерний процесс порождает еще один дочерний процесс
В потомке дочернего процесса:
стандартный вывод подключается
к входу канала при помощи вызова dup2
ненужные дескрипторы файлов закрываются
при помощи вызова ехес запускается программа,
заданная параметром 'com1'
В первом дочернем процессе:
стандартный ввод подключается
к выходу канала при помощи вызова dup2
ненужные дескрипторы файлов закрываются
при помощи вызова ехес запускается программа,
заданная параметром 'com2'
Далее следует реализация программы join; она также использует процедуру fatal, представленную в разделе 7.1.5.
(* Программа join - соединяет две программы каналом *)
function join (com1, com2:ppchar):integer;
var
fdin,fdout:longint;
status:integer;
begin
(* Создать дочерний процесс для выполнения команд *)
case fork of
-1: (* ошибка *)
fatal ('Ошибка 1 вызова fork в программе join');
0: (* дочерний процесс *)
;
else (* родительский процесс *)
begin
wait(@status);
join:=status;
exit;
end;
end;
(* Остаток процедуры, выполняемой дочерним процессом *)
(* Создать канал *)
if not assignpipe(fdin,fdout) then
fatal ('Ошибка вызова pipe в программе join');
(* Создать еще один процесс *)
case fork of
-1:
(* ошибка *)
fatal ('Ошибка 2 вызова fork в программе join');
0:
begin
(* процесс, выполняющий запись *)
dup2 (fdout, 1); (* направить ст. вывод в канал *)
fdclose (fdin); (* сохранить дескрипторы файлов *)
fdclose (fdout);
execvp (com1[0], com1, envp);
(* Если execvp возвращает значение, то произошла ошибка *)
fatal ('Ошибка 1 вызова execvp в программе join');
end;
else
begin
(* процесс, выполняющий чтение *)
dup2 (fdin, 0); (* направить ст. ввод из канала *)
fdclose (fdin);
fdclose (fdout);
execvp (com2[0], com2, envp);
fatal ('Ошибка 2 вызова execvp в программе join');
end;
end;
end;
Эту процедуру можно вызвать следующим образом:
uses linux, stdio;
const
one:array [0..3] of pchar = ('ls', '-l', '/usr/lib', nil);
two:array [0..2] of pchar = ('grep', '^d', nil);
var
ret:integer;
begin
ret := join (one, two);
writeln ('Возврат из программы join ', ret);
halt (0);
end.
Упражнение 7.3. Как можно обобщить подход, показанный в программе join, для связи нескольких процессов при помощи каналов?
Упражнение 7.4. Добавьте возможность работы с каналами в командный интерпретатор smallsh, представленный в предыдущей главе.
Упражнение 7.5. Придумайте метод, позволяющий родительскому процессу запускать программу в качестве дочернего процесса, а затем считывать ее стандартный вывод при помощи канала. Стоит отметить, что эта идея лежит в основе процедур popen/pipeopen и pclose/pipeclose, которые входят в стандартную библиотеку ввода/вывода. Процедуры popen/pipeopen и pclose/pipeclose избавляют программиста от большинства утомительных деталей согласования вызовов fork, ехес, fdclose, dup или dup2. Эти процедуры обсуждаются в главе 11.