Вопрос или проблема
mkfifo foo
printf %s\\n bar | tee foo &
tr -s '[:lower:]' '[:upper:]' <foo
wait
rm foo
Это рабочий скрипт POSIX shell, реализующий то, что я хочу сделать:
printf %s\\n bar
символизирует внешнюю программу, создающую stdouttr -s '[:lower:]' '[:upper:]'
символизирует другую команду, которая должна получить stdout и сделать с ним что-тоtee
дублирует stdout в именованный канал foo
И вывод получается ожидаемым:
bar
BAR
Теперь я бы хотел упростить код, чтобы он выглядел как external_program | my_function
. Что-то вроде:
f() (
mkfifo foo
tee foo &
tr -s '[:lower:]' '[:upper:]' <foo
wait
rm foo
)
printf %s\\n bar | f
Но теперь вывода нет вовсе.
Суть проблемы в том, что POSIX имеет (обычно не полезное) требование для sh
: асинхронные команды должны иметь перенаправленный stdin на /dev/null
, если нет явного перенаправления stdin (впрочем, технически, это неявное перенаправление на /dev/null
происходит перед явными перенаправлениями, если они есть).
Посмотрите, например, на Linux-системах:
$ sh -c 'realpath /dev/stdin & wait'
/dev/null
Общее решение для асинхронной команды, чтобы stdin оставался нетронутым, заключается в следующем:
{ cmd <&3 3<&- & } 3<&0
Где исходный stdin доступен на fd 3 в дополнение к 0 в группе команд с 3<&0
, и внутри группы команд cmd
stdin, который был снова открыт на /dev/null
из-за &
перенаправляется обратно на исходный stdin через этот fd 3 (который мы затем закрываем, так как он больше не нужен).
В:
f() ( mkfifo foo tee foo & tr -s '[:lower:]' '[:upper:]' <foo rm foo ) printf %s\\n bar | f
stdin tee
будет /dev/null
, а не концом канала чтения от printf
. Изменение на:
f() (
mkfifo foo
{ tee foo <&3 3<&- & } 3<&0
tr -s '[:lower:]' '[:upper:]' <foo
rm foo
wait
)
printf '%s\n' bar | f
решит проблему, но как вы обнаружили, так же поможет
f() ( mkfifo foo tr -s '[:lower:]' '[:upper:]' <foo & tee foo rm foo ) printf %s\\n bar | f
Тогда tee
не запускается асинхронно, поэтому его stdin не перенаправляется на /dev/null
, а stdin tr
перенаправляется явно, так что не имеет значения, что он был перенаправлен на /dev/null
заранее.
Также нам не нужно wait
, так как tee
— это процесс, выполняющийся синхронно (и поэтому ожидаемый implicitly shell) и обычно не завершается перед tr
, так как ожидает eof на своем stdin (а stdout tr
, который является противоположным концом этого канала, закрывается только после выхода).
Возможно, вы все-таки захотите дождаться завершения tr
, чтобы получить код его выхода:
f() (
ret=0
mkfifo foo || exit
tr -s '[:lower:]' '[:upper:]' <foo &
tee foo || ret=$?
rm -f foo
wait "$!" || ret=$?
exit "$ret"
)
printf '%s\n' bar | f
foo
не будет удален, если подпроцесс, являющийся телом функции, завершится. Вы можете сократить время существования fifo и сделать его даже более похожим на неназванный канал (где именованный канал — это просто эфемерное место встречи для этих двух процессов к установлению канала), удалив его, как только он будет открыт в режимах чтения и записи обоими процессами.
f() (
ret=0
mkfifo foo || exit
{ tee foo <&3 3<&- & } 3<&0
{
# на данный момент, foo уже будет открыт здесь в режиме только для чтения
# на stdin, что может произойти только в том случае,
# если tee уже открыл его в режиме только для записи.
rm -f foo
tr -s '[:lower:]' '[:upper:]' || ret=$?
} < foo
wait "$!" || ret=$?
exit "$ret"
)
printf '%s\n' bar | f
Похоже, что проблема, как и предполагалось, заключается в попытке передать external_program
в отсоединенный tee
.
В оригинальном скрипте было:
printf %s\\n bar | tee foo &
Что не отсоединяет только tee
, а всю последовательность pipe целиком. Изначально я сделал такую же конструкцию pipe, которая разрешается немедленно, то есть и sleep и пустая операция :
отсоединяются вместе:
$ time sh -c 'sleep 10 | : &'
real 0m0.001s
user 0m0.000s
sys 0m0.001s
Однако, при отсоединении tee
внутри функции, отсоединяется только tee
, а не внешняя программа в начале pipe. Повторяя пример со sleep
, мы можем видеть, что отсоединяется только пустая операция :
, но sleep
— нет, и скрипт занимает все 10 секунд:
$ time sh -c 'sleep 10 | { : & }'
real 0m10.002s
user 0m0.002s
sys 0m0.000s
Решение заключается в том, чтобы не отсоединять tee
, а другую команду:
f() (
mkfifo foo
tr -s '[:lower:]' '[:upper:]' <foo &
tee foo
wait
rm foo
)
printf %s\\n bar | f
Теперь tee
может получать stdout из pipe, записывать в FIFO, и выход функции снова будет:
bar
BAR
Ответ или решение
Вопрос об использовании именованных каналов (FIFO) в POSIX-совместимых шелл-скриптах для дублирования стандартного вывода в целях передачи данными между командами является достаточно сложной задачей, требующей понимания специфики поведения командной строки и процессов в многозадачной операционной системе.
Теория
Для начала, важно понять, как функционируют именованные каналы или FIFO в контексте UNIX-подобных систем. FIFO (First In, First Out) предоставляет способ передачи данных между процессами, которые не имеют прямой зависимости друг от друга, путем использования именованного канала. В отличие от обычных неименованных каналов, которые создаются в рамках одной оболочки команд, именованные каналы существуют как файлы в файловой системе и могут быть доступны разным процессам вне зависимости от их местоположения в структуре процессов.
Когда команда запускается с &
, она выполняется асинхронно в фоновом режиме. Недостатком является автоматическая переадресация стандартного ввода на /dev/null
, что может вызывать проблему с передачей данных через канал, поскольку команда не получает нужного ввода для обработки.
Пример
Ваша первоначальная реализация:
mkfifo foo
printf %s\\n bar | tee foo &
tr -s '[:lower:]' '[:upper:]' <foo
wait
rm foo
работает, потому что printf
передает данные в tee
, которая дублирует выходные данные как на стандартный вывод, так и в именованный канал foo
. Команда tr
затем считывает из foo
и преобразует текст в верхний регистр. Однако, когда вы переносите эту логику в функцию:
f() (
mkfifo foo
tee foo &
tr -s '[:lower:]' '[:upper:]' <foo
wait
rm foo
)
printf %s\\n bar | f
Вы сталкиваетесь с проблемой из-за вышеописанного поведения &
, так как tee
теряет стандартный ввод.
Применение
Чтобы исправить это, одним из решений является использование техники перенаправления файлового дескриптора. Можно переопределить stdin для tee
, чтобы он читал из стандартного ввода, как описывается далее:
f() (
mkfifo foo
{ tee foo <&3 3<&- & } 3<&0
tr -s '[:lower:]' '[:upper:]' <foo
rm foo
wait
)
printf '%s\n' bar | f
Здесь используются перенаправления файловых дескрипторов, чтобы гарантировать, что tee
будет работать с правильным входным потоком.
Другое решение заключается в изменении порядка выполнения команд:
f() (
mkfifo foo
tr -s '[:lower:]' '[:upper:]' <foo &
tee foo
wait
rm foo
)
printf %s\\n bar | f
Так, tee
больше не запускается в фоне, а выполняется синхронно, что сохраняет его stdin от перемещения на /dev/null
.
В обоих случаях эти стратегии основаны на управлении потоком выполнения и использованием синхронных и асинхронных механизмов так, чтобы гарантировать правильное межпроцессное взаимодействие через именованный канал.
Эти примеры иллюстрируют, как можно манипулировать потоками ввода-вывода, перенаправлениями и обеспечивать взаимодействие между параллельными процессами, что и является ключевым аспектом при работе с UNIX-подобными системами.
Успешное применение таких техник на практике позволяет повысить эффективность скриптов и обеспечить надежность их выполнения в многопользовательской среде, что является важным для автоматизации и системного администрирования в больших IT-инфраструктурах.