Многие пользователи Bash знают о существании со-процессов, появившихся в 4-й версии Bash'a. Несколько меньшее количество знает о том, что сопроцессы в Bash не какая-то новая фича, а древний функционал KornShell'a появившийся ещё в реализации ksh88 в 1988 году. Ещё меньшее количество пользователей shell'ов умеющих сопроцессить знают синтаксис и помнят как это делать. Вероятно, я отношусь к четвёртой группе — знающих о сопроцессах, периодически умеющих ими пользоваться но так и не понимающих «зачем?». Я говорю «периодически», так как иногда я о��вежаю в голове их синтаксис, но к тому моменту, когда мне кажется что «вот тот случай когда можно применить co-proc» я уже напрочь забываю о том как это делать.
Этой заметкой я хочу свести воедино синтаксисы для разных шеллов чтобы на случай, если таки придумаю зачем они мне нужны, я если и не вспомню как это делать, то по крайней мере, буду знать где это записано.
В заголовке статьи у нас 3 вопроса. Пойдём по порядку.
Что же такое co-process? Со-процессинг — это одновременное выполнение двух процедур, одна из которых считывает вывод другой. Для его реализации необходимо предварительно запустить фоновый процесс выполняющий функционал канала. При запуске фонового процесса его stdin и stdout присваиваются каналам связанными с пользовательскими процессами. Соответственно, один канал для записи, второй для чтения. Пояснять это проще на примерах, поэтому сразу перейдём ко второму вопросу.
Реализации со-процессов в шеллах разнятся. Я остановлюсь на 3-х известных мне реализациях в ksh, zsh и bash. Рассмотрим их в хронологическом порядке. Хоть это и не имеет прямого отношения к вопросам статьи, отмечу, что все нижеприведённые примеры сделаны на
Ksh
Синтаксис
кажется мне наиболее логичным. Здесь для выполнения команды cmd в фоновом режиме мы используем специальную операцию |&, выражающую соответвенно:
— "&" — фоновый процесс;
— "|" — каналы.
Запускаем фоновый процесс:
Убедимся, что он жив:
Теперь мы можем общаться с нашим фоновым процессом. Пишем:
и читаем:
или так:
Закрываем «конец» трубы для записи:
и для чтения:
Zsh
Синтаксис со-процессов в zsh не слишком отличается от ksh, что не удивительно, т.к. в его man'е сказано «zsh most closely resembles ksh».
Основным отличием является использование ключевого слова coproc вместо оператора |&. В остальном всё очень похоже:
Для закрытия каналов чтения/записи можно воспользоваться идиомой exit:
При этом запустился новый фоновый процесс, который тут же завершился. Это ещё одно отличие от ksh — мы можем не закрывать существующий сопроцесс, а сразу инициировать новый:
В ksh мы бы просто получили:
Несмотря на эту возможность рекомендуется, всегда явно убивать фоновый процесс, особенно, при использовании «setopt NO_HUP».
Здесь же стоит упомянуть, что иногда мы можем получить неожиданные результаты связанные с буферизацией выво��а, именно поэтому в приведённых выше примерах мы используем tr с опцией -u.
Хоть это и не имеет оношения исключительно к со-процессам продемонстрирую это поведение примером:
Буфер не полон и мы ничего не получаем из нашей трубы. Заполним его «доверху»:
Разумеется, если данное поведение нас не устраивает, его можно изменить, например используя stdbuf
Bash
Для запуска со-процесса в bash также как и в zsh используется зарезервированное слово coproc, но в отличии от рассмотренных выше shell'ов доступ к сопроцессу осуществляется не с помощью >&p и <&p, а посредством массива $COPROC:
— ${COPROC[0]} для записи;
— ${COPROC[1]} для чтения.
Соответственно, процедура записи/чтения будет выглядеть примерно так:
а закрытие дескрипторов:
Если имя COPROC по каким-то причинам не устраивает можно указать свое:
Прежде чем попытаться ответить зачем нужны сопроцессы подумаем можно ли реализовать их функционал в shell'ах которые не имеют coproc «из коробки». Например в таком:
Именованные каналы никто не отменял:
Пожалуй, с некоторой натяжкой, можно сказать, что это же реализуемо с помощью псевдотерминалов, но развивать эту тему не стану.
Ну и зачем же нужны сопроцессы? Я процитирую выдержку из перевода статьи Mitch Frazier:
И в действительности я лишь один раз смог с относительной пользой применить со-процессы в своих скриптах. Задумка была реализовать некий «persistent connect» для доступа к MySQL.
Выглядело это примерно так:
В остальном все мои попытки использовать coproc действительно были надуманными.
Этой заметкой я хочу свести воедино синтаксисы для разных шеллов чтобы на случай, если таки придумаю зачем они мне нужны, я если и не вспомню как это делать, то по крайней мере, буду знать где это записано.
В заголовке статьи у нас 3 вопроса. Пойдём по порядку.
Что?
Что же такое co-process? Со-процессинг — это одновременное выполнение двух процедур, одна из которых считывает вывод другой. Для его реализации необходимо предварительно запустить фоновый процесс выполняющий функционал канала. При запуске фонового процесса его stdin и stdout присваиваются каналам связанными с пользовательскими процессами. Соответственно, один канал для записи, второй для чтения. Пояснять это проще на примерах, поэтому сразу перейдём ко второму вопросу.
Как?
Реализации со-процессов в шеллах разнятся. Я остановлюсь на 3-х известных мне реализациях в ksh, zsh и bash. Рассмотрим их в хронологическом порядке. Хоть это и не имеет прямого отношения к вопросам статьи, отмечу, что все нижеприведённые примеры сделаны на
$ uname -opr FreeBSD 10.1-STABLE amd64
Ksh
$ `echo $0` --version version sh (AT&T Research) 93u+ 2012-08-01
Синтаксис
cmd |&
кажется мне наиболее логичным. Здесь для выполнения команды cmd в фоновом режиме мы используем специальную операцию |&, выражающую соответвенно:
— "&" — фоновый процесс;
— "|" — каналы.
Запускаем фоновый процесс:
$ tr -u a b |& [2] 6053
Убедимся, что он жив:
$ ps afx | grep [6]053 6053 4 IN 0:00.00 tr -u a b
UPD 2016.08.15 21:15
Не совсем корректная в данном контексте команда. Не исправляю дабы не нарушить логику комментариев.
Правильнее так:
Спасибо ZyXI
Правильнее так:
$ tr -u a b |& [2] 6053 $ ps -p 6053 PID TT STAT TIME COMMAND 6053 4 SN 0:00.00 tr -u a b
Спасибо ZyXI
Теперь мы можем общаться с нашим фоновым процессом. Пишем:
$ print -p abrakadabra1 $ print -p abrakadabra2 $ print -p abrakadabra3
и читаем:
$ read -p var; echo $var bbrbkbdbbrb1 $ read -p var; echo $var bbrbkbdbbrb2 $ read -p var; echo $var bbrbkbdbbrb3
или так:
$ print abrakadabra1 >&p $ print abrakadabra2 >&p $ print abrakadabra3 >&p $ while read -p var; do echo $var; done bbrbkbdbbrb1 bbrbkbdbbrb2 bbrbkbdbbrb3
Закрываем «конец» трубы для записи:
$ exec 3>&p 3>&-
и для чтения:
$ exec 3<&p 3<&-
Zsh
$ `echo $0` --version zsh 5.2 (amd64-portbld-freebsd10.1)
Синтаксис со-процессов в zsh не слишком отличается от ksh, что не удивительно, т.к. в его man'е сказано «zsh most closely resembles ksh».
Основным отличием является использование ключевого слова coproc вместо оператора |&. В остальном всё очень похоже:
$ coproc tr -u a b [1] 22810 $ print -p abrakadabra1 $ print abrakadabra2 >&p $ print -p abrakadabra3 $ read -ep bbrbkbdbbrb1 $ while read -p var; do echo $var; done bbrbkbdbbrb2 bbrbkbdbbrb3
Для закрытия каналов чтения/записи можно воспользоваться идиомой exit:
$ coproc exit [1] 23240 $ [2] - done tr -u a b $ [1] + done exit
При этом запустился новый фоновый процесс, который тут же завершился. Это ещё одно отличие от ksh — мы можем не закрывать существующий сопроцесс, а сразу инициировать новый:
$ coproc tr -u a b [1] 24981 $ print -p aaaaa $ read -ep bbbbb $ coproc tr -u a d [2] 24982 $ [1] - done tr -u a b $ print -p aaaaa $ read -ep ddddd $
В ksh мы бы просто получили:
$ tr -u a b |& [1] 25072 $ tr -u a d |& ksh93: process already exists
Несмотря на эту возможность рекомендуется, всегда явно убивать фоновый процесс, особенно, при использовании «setopt NO_HUP».
Здесь же стоит упомянуть, что иногда мы можем получить неожиданные результаты связанные с буферизацией выво��а, именно поэтому в приведённых выше примерах мы используем tr с опцией -u.
$ man tr | col | grep "\-u" -u Guarantee that any output is unbuffered.
Хоть это и не имеет оношения исключительно к со-процессам продемонстрирую это поведение примером:
$ coproc tr a b [1] 26257 $ print -p a $ read -ep ^C $ [1] + broken pipe tr a b
Буфер не полон и мы ничего не получаем из нашей трубы. Заполним его «доверху»:
$ coproc tr a b [1] 26140 $ for ((a=1; a <= 4096 ; a++)) do print -p 'a'; done $ read -ep b
Разумеется, если данное поведение нас не устраивает, его можно изменить, например используя stdbuf
$ coproc stdbuf -oL -i0 tr a b [1] 30001 $ print -p a $ read -ep b
Bash
$ `echo $0` --version GNU bash, version 4.3.42(1)-release (amd64-portbld-freebsd10.1)
Для запуска со-процесса в bash также как и в zsh используется зарезервированное слово coproc, но в отличии от рассмотренных выше shell'ов доступ к сопроцессу осуществляется не с помощью >&p и <&p, а посредством массива $COPROC:
— ${COPROC[0]} для записи;
— ${COPROC[1]} для чтения.
Соответственно, процедура записи/чтения будет выглядеть примерно так:
$ coproc tr -u a b [1] 30131 $ echo abrakadabra1 >&${COPROC[1]} $ echo abrakadabra2 >&${COPROC[1]} $ echo abrakadabra3 >&${COPROC[1]} $ while read -u ${COPROC[0]}; do printf "%s\n" "$REPLY"; done bbrbkbdbbrb1 bbrbkbdbbrb2 bbrbkbdbbrb3
а закрытие дескрипторов:
$ exec {COPROC[1]}>&- $ cat <&"${COPROC[0]}" [1]+ Done coproc COPROC tr -u a b
Если имя COPROC по каким-то причинам не устраивает можно указать свое:
$ coproc MYNAME (tr -u a b) [1] 30528 $ echo abrakadabra1 >&${MYNAME[1]} $ read -u ${MYNAME[0]} ; echo $REPLY bbrbkbdbbrb1 $ exec {MYNAME[1]}>&- ; cat <&"${MYNAME[0]}" [1]+ Done coproc MYNAME ( tr -u a b )
Зачем?
Прежде чем попытаться ответить зачем нужны сопроцессы подумаем можно ли реализовать их функционал в shell'ах которые не имеют coproc «из коробки». Например в таком:
$ man sh | col -b | grep -A 4 DESCRIPTION DESCRIPTION The sh utility is the standard command interpreter for the system. The current version of sh is close to the IEEE Std 1003.1 (``POSIX.1'') spec- ification for the shell. It only supports features designated by POSIX, plus a few Berkeley extensions. $ man sh | col -b | grep -A 1 -B 3 AUTHORS This version of sh was rewritten in 1989 under the BSD license after the Bourne shell from AT&T System V Release 4 UNIX. AUTHORS This version of sh was originally written by Kenneth Almquist.
Именованные каналы никто не отменял:
$ mkfifo in out $ tr -u a b <in >out & $ exec 3> in 4< out $ echo abrakadabra1 >&3 $ echo abrakadabra2 >&3 $ echo abrakadabra3 >&3 $ read var <&4 ; echo $var bbrbkbdbbrb1 $ read var <&4 ; echo $var bbrbkbdbbrb2 $ read var <&4 ; echo $var bbrbkbdbbrb3
Пожалуй, с некоторой натяжкой, можно сказать, что это же реализуемо с помощью псевдотерминалов, но развивать эту тему не стану.
Ну и зачем же нужны сопроцессы? Я процитирую выдержку из перевода статьи Mitch Frazier:
Пока я не могу придумать никаких <...> задач для со-процессов, по крайней мере не являющихся надуманными.
И в действительности я лишь один раз смог с относительной пользой применить со-процессы в своих скриптах. Задумка была реализовать некий «persistent connect» для доступа к MySQL.
Выглядело это примерно так:
$ coproc stdbuf -oL -i0 mysql -pPASS [1] 19743 $ printf '%s;\n' 'select NOW()' >&${COPROC[1]} $ while read -u ${COPROC[0]}; do printf "%s\n" "$REPLY"; done NOW() 2016-04-06 13:29:57
В остальном все мои попытки использовать coproc действительно были надуманными.
Спасибо
Хочется поблагодарить Bart Schaefer, Stéphane Chazelas, Mitch Frazier чьи комментарии, письма и заметки помогли в написании статьи.
