Сопроцессы: -что, -как, -зачем?
Вероятно, я отношусь к четвёртой группе — знающих о сопроцессах, периодически умеющих ими пользоваться, но так и не понимающих «зачем?». Я говорю «периодически», так как иногда я освежаю в голове их синтаксис, но к тому моменту, когда мне кажется что «вот тот случай когда можно применить co-proc» я уже напрочь забываю о том как это делать.
Этой заметкой я хочу свести воедино синтаксисы для разных шеллов чтобы на случай, если таки придумаю зачем они мне нужны, я если и не вспомню как это делать, то по крайней мере, буду знать где это записано.
В заголовке статьи у нас 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
Теперь мы можем общаться с нашим фоновым процессом.
Пишем:
$ 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 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 действительно были надуманными.