Теперь, когда мы рассмотрели основные манипуляции с файлами, мы можем научить ladsh
перенаправлению ввода и вывода через файлы и каналы. ladsh2.с
, который мы представим здесь, работает с каналами (описанными символом |
в командах ladsh
, как это делается в большинстве командных оболочек) и перенаправление ввода и вывода в файловые дескрипторы. Мы покажем только модифицированные части кода здесь — полный исходный текст ladsh2.с
доступен по упомянутым в начале книги адресам. Изменения в parseCommand()
— это простое упражнение по разбору строк, поэтому мы не будем надоедать дискуссией об этом.
11.7.1. Структуры данных
Хотя код в ladsh1.с
поддерживает концепцию задания как множества процессов (предположительно, объединенных вместе каналами), он не предоставляет способа указания того, какие файлы использовать для ввода и вывода. Чтобы позволить это, добавляются новые структуры данных и модифицируются существующие.
24: REDIRECT_APPEND};
25:
26: struct redirectionSpecifier {
27: enum redirectionTypetype; /* тип перенаправления */
28: int fd; /*перенаправляемый файловый дескриптор*/
29: char * filename; /* файл для перенаправления fd */
30: };
31:
32: struct childProgram {
33: pid_t pid; /* 0 если завершен */
34: char **argv; /* имя программы и аргументы */
35: int numRedirections; /* элементы в массиве перенаправлений */
36: struct redirectionSpecifier *redirections; /* перенаправления ввода-вывода*/
37: } ;
Структура struct redirectionSpecifier
сообщает ladsh2.с
о том, как установить отдельный файловый дескриптор. Она содержит enum redirectionTypetype
, который указывает, является ли это перенаправление перенаправлением ввода, перенаправлением вывода, который должен быть добавлен к существующему файлу, либо перенаправлением вывода, которое заменяет существующий файл. Она также включает перенаправляемый файловый дескриптор и имя файла. Каждая дочерняя программа (struct childProgram
) теперь специфицирует нужное ей количество перенаправлений.
Эти новые структуры данных не связаны с установкой каналов между процессами. Поскольку задание определено как множество дочерних процессов с каналами, связывающими их, нет необходимости в более подробной информации, описывающей каналы. На рис. 11.1 показано, как эти новые структуры должны выглядеть для команды tail < input-file | sort > output-file
.
Рис. 11.1. Структуры данных, описывающие задание для ladsh2.с
11.7.2. Изменения в коде
Как только в parseCommand()
будут правильно отражены структуры данных, то запуск команд в правильном порядке становится довольно простым при достаточном внимании к деталям. Прежде всего, мы добавляем цикл в parseCommand()
для запуска дочерних процессов, поскольку теперь их может быть множество. Прежде чем войти в цикл, мы устанавливаем nextin
и nextout
, которые являются файловыми дескрипторами, используемыми в качестве стандартных потоков ввод и вывода для следующего запускаемого процесса. Для начала мы используем те же stdin и stdout, что и оболочка.
Теперь посмотрим, что происходит внутри цикла. Основная идея описана ниже.
1. Если это финальный процесс в задании, убедиться, что nextout
указывает на stdout. В противном случае нужно подключить вывод этого задания к входному концу неименованного канала.
2. Породить новый процесс. Внутри дочернего перенаправить stdin и stdout, как указано с помощью nextin
, nextout
и всех специфицированных ранее перенаправлений.
3. Вернувшись обратно в родительский процесс, закрыть nextin
и nextout
, используемые только что запущенным дочерним процессом (если только они не являются потоками ввода и вывода самой оболочки).
4. Теперь настроить следующий процесс в задании для приема его ввода из вывода процесса, который мы только что создали (через nextin
).
Вот как эти идеи перевести на С.
365: nextin=0, nextout=1;
366: for (i=0; i
367: if ((i+1) < newJob.numProgs) {
368: pipe(pipefds);
369: nextout = pipefds[1];
370: } else {
371: nextout = 1;
372: }
373:
374: if (!(newJob.progs[i].pid = fork())) {
375: if (nextin != 0) {