Этой заметкой я хочу свести воедино синтаксисы для разных шеллов чтобы на случай, если таки придумаю зачем они мне нужны, я если и не вспомню как это делать, то по крайней мере, буду знать где это записано.
В заголовке статьи у нас 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
Правильнее так:
$ 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 действительно были надуманными.
Комментарии (36)
Habra-Mikhail
13.08.2016 02:33-1Мне кажется что когда вы отправляете сообщение, то получаете какую-то абракадабру:
Отправляете:
$ print -p abrakadabra1
Получаете:
$ read -p var; echo $var
bbrbkbdbbrb1
У вас буква a сломаласьMoon_darker
13.08.2016 04:40+5Очевидно, в фоне выполняется
tr -u a b
который используется для замены всех a на b. Так что всё работает как и ожидается :)
romy4
13.08.2016 13:06-7какая версия tr?
для (GNU Coreutils) 8.21 опции -u нет, поэтому пример, где «процедура записи/чтения» не отработает, а результат будет виден только после закрытия дескрипторов.simpleadmin
13.08.2016 13:30+4какая версия tr?
https://www.freebsd.org/cgi/man.cgi?tr
поэтому пример, где «процедура записи/чтения» не отработает
Вам стоить прочитать всю статью, а не урывок в конце и решение найдётся.romy4
13.08.2016 13:41-10Да ну, прям всю статью? И что конкретно я должен был прочитать? А man к функциям я читать умею и справку вызывать.
man trDESCRIPTION
Translate, squeeze, and/or delete characters from standard input, writing to standard output.
-c, -C, --complement
use the complement of SET1
-d, --delete
delete characters in SET1, do not translate
-s, --squeeze-repeats
replace each input sequence of a repeated character that is listed in SET1 with a single occurrence of that character
-t, --truncate-set1
first truncate SET1 to length of SET2
--help display this help and exit
--version
output version information and exitsimpleadmin
13.08.2016 13:50+7Хамить я тоже умею, заметили?
То что это Ваш стиль общения исключительно Ваше дело.
А статью Вам действительно стоит прочитать всю, тогда Вы найдёте ответ на:
вы нигде не упоминали, что это под freebsd.
Хоть это и не имеет прямого отношения к вопросам статьи, отмечу, что все нижеприведённые примеры сделаны на
$ uname -opr FreeBSD 10.1-STABLE amd64
И на
И что конкретно я должен был прочитать?
Хотя бы
Разумеется, если данное поведение нас не устраивает, его можно изменить, например используя stdbuf
$ coproc stdbuf -oL -i0 tr a b
kemsky
14.08.2016 03:10Тема
зачем?
не раскрыта.cleaner_it
14.08.2016 03:50Тема раскрыта) Автор честно сказал, что не знает ответа на этот вопрос.
PS:
Ищущий да найдёт. И отпишется, чтобы мы тоже знали)gnomeby
14.08.2016 11:41Если так получилось, что:
* Никак нельзя сделать не на баше
* Подготовка данных разбита на шаги
* А выполнение при этом не бесплатное (т.е. занимает время)
То это неплохой способ уменьшить общее время выполнения скрипта.
Как простой пример — persistent connection к БД.simpleadmin
14.08.2016 12:14Как простой пример — persistent connection к БД.
Андрей, хоть это и единственное найденное применение описанное в статье, на самом деле я при первой возможности ушёл от этого решения.
Причины:
— умирание сопроцесса при «битых» запросах (как следствие необходимость проверки жизнеспособности сопроцесса);
— потеря неизвлечённых данных при этом;
— геморойная обработка данных при извлечении и сопоставление их с запросом.
Всё-таки работа с БД — это не стихия *sh.
saboteur_kiev
14.08.2016 12:35Хм. Но зачем нужны сопроцессы, если этот же функционал реализовывается немного проще, во ВСЕХ шеллах при помощи pipe:
процесс 1
mkpipe testpipe
echo abrakahabra >> testpipe
процесс 2
cat testpipe
И этот вариант, я даже несколько раз использовал. Новый функционал исключительно для создания «анонимного пайпа»?chaturanga
14.08.2016 13:09+1mkpipe
Извините, а что это за зверь такой?saboteur_kiev
14.08.2016 14:28Это стандарт для файловых систем Posix — создание файла-потока, в который один процесс может писать, а другой читать.
Ох сорри, mkfifo, опечатался.
Работает именно так, как описано в статье, только это есть даже не просто в каком-то конкретно взятом шелле, а вообще просто фича всех POSIX файловых систем, можно пользоваться откуда угодно.
На диске лежит только имя файла, на самом деле вся информация передается через буферы.
simpleadmin
14.08.2016 14:42+1mkpipe testpipe
Вероятно речь об mkfifo?
echo abrakahabra >> testpipe
cat testpipe
Это только каналы ввода/вывода, а где фоновый процесс?
Вариант реализации посредством mkfifo в статье приведен
Заголовок спойлера$ 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
ZyXI
14.08.2016 19:31+1У FIFO есть много недостатков:
- FIFO нужно где?то создать. Предположительно, вам придётся заморочиться с
mktemp -d
. - FIFO нужно потом ещё и удалить. Причём удалить независимо от того, успешно ли завершился скрипт. Хотя, в принципе, можно удалить сразу после
exec
. - FIFO не должен быть перехвачен другой программой: к примеру, другим процессом того же скрипта. Он особенно не должен быть доступен для других пользователей. Ещё один повод использовать именно
mktemp -d
.
Последний (и, кажется, единственный) раз, когда я попытался использовать сопроцессы в zsh я пожалел, что сопроцесс может быть только один (и ещё вроде были какие?то проблемы). FIFO я не использовал вообще никогда, если мне нужно что?то подобное я лучше либо перепишу всё так, что мне это не нужно будет (обычно process substitution и стандартного перенаправления через
|
хватает за глаза), либо перепишу на Python.- FIFO нужно где?то создать. Предположительно, вам придётся заморочиться с
izyk
15.08.2016 16:20Здравствуйте.
В bash Linux — если сопроцессор завершается до начала считывания, то теряем вывод полученный от него через "${COPROC[0]}".
$ ls -Al total 20360 -rw-rw-r--. 1 ilia ilia 20834292 авг 15 15:46 1 drwxrwxr-x. 2 ilia ilia 4096 авг 15 15:10 proba drwxrwxr-x. 2 ilia ilia 4096 авг 15 15:10 proba2 drwxrwxr-x. 2 ilia ilia 4096 авг 15 15:10 proba3 $ coproc ls -Al . [1] 16739 [1]+ Done coproc COPROC ls --color=auto -Al . $ echo "${COPROC[0]}"
${COPROC[0]} — больше нет, получить неоткуда. Неудобно. Легче что-то другое придумать, у тех же FIFO такого нет.
А если в канал не помещается весь вывод, то уже лучше, сопроцессор ждет освобождения места в буфере для продолжения и не завершается. Но хвост вывода можем потерять.
$ coproc ls -alR / [1] 16797 $ echo "${COPROC[0]}" 63
ZyXI
15.08.2016 17:59Выглядит как ошибка, в
man bash
я предупреждений по этому поводу не нашёл. zsh и ksh таким не страдают, так что никаких оправданий вроде «а у этих такая же фигня» не может быть.
Вы пробовали с этим вопросом пойти в bug tracker bash’а?
izyk
15.08.2016 18:35Вы пробовали с этим вопросом пойти в bug tracker bash’а?
Нет, не пробовал, т.к. об этой штуке узнал пару часов назад. Для меня не нужная вещь. Хотя выглядит действительно как ошибка. На всякий случай оставлю, вдруг кому-нибудь пригодится:
$ cat /etc/redhat-release CentOS Linux release 7.2.1511 (Core) $ bash --version GNU bash, version 4.2.46(1)-release (x86_64-redhat-linux-gnu) $ uname -a Linux 3.10.0-327.28.2.el7.x86_64 #1 SMP Wed Aug 3 11:11:39 UTC 2016 x86_64 x86_64 x86_64 GNU/Linux
ZyXI
15.08.2016 19:33У меня то же самое в bash-4.3_p42-r1, Gentoo amd64. Хотя это не последняя версия в portage: есть ещё «нестабильная» 4.3_p46 и «замаскированная» 4.4_rc1.
potan
15.08.2016 18:13+1ps afx | grep [6]053
Этот прием надо запомнить!ZyXI
15.08.2016 18:19+2Не нужно, вы не в BSD.
ps -p 6053
. Если в совете предполагается пропустить выводps
черезgrep
наlinux
, то, скорее всего, этот совет не оптимальный иgrep
на самом деле трогать не нужно.ZyXI
15.08.2016 18:21Хотя, судя по man ps (BSD) там тоже должно работать.
simpleadmin
15.08.2016 18:23+2Вилимо имелось ввиду преобразование символьного класса и исключение лишней строки из вывода grep
ZyXI
15.08.2016 20:06Я знаю, зачем в строке
grep
[]
. Я не знаю, зачем вообще нуженgrep
здесь, если естьps -p
, позволяющий обойтись безgrep
. И естьps -C
позволяющий то же самое для нахождения конкретных процессов по имени исполняемого файла.grep
нужен в редких случаях, когда у вас 100500 процессов видаmyapp --foo bar
и вам хочется отыскать среди них PID того, что запущен с--foo baz
. Или когда вы представляете, что вам нужно лишь приблизительно (к примеру, браузеры имеют нехорошую привычку запускаться одной командой (e.g. vivaldi), а в списке висеть в виде другой (vivaldi-bin в примере), но это относится не только к vivaldi и даже не только к chromium-based).
ps -p 6053
вам покажет данные только для процесса с PID 6053 и безо всяких паразитных строчек (не считая заголовка), которые нужно будет исключать всякими хаками. Или покажет только заголовок, если процесс уже завершился (заголовок можно убрать, если нужно:ps hp 6053
илиps --no-headers -p 6053
).
Ещё: вот будет у вас в фоне запущен
mpv
с именем серии с хэшем, содержащим 6053, вы его увидите, несмотря на ваш трюк с grep. И процесс с PID 16053 увидите. И процесс, который работает 6053 часов. И процесс на псевдотерминале pts/6053. С PID 6053 такое маловероятно, но PID бывают и маленькими — они вообще назначаются циклически или случайно (в зависимости от настроек ядра ОС). При использованииps -p 6053
ничего из вышеперечисленного вы не увидите.
Ну и, конечно,
ps -p 6053
требует меньше нажатий клавиш, чемps afx | grep [6]053
. Не забудьте также, что в zsh с такими привычками вы имеете хорошие шансы увидетьzsh: no matches found: [6]053
. Bash тоже можно так настроить. Трюк пропадёт в никуда, если в каталоге есть файл6053
. При других настройках оболочки ваша команда выдаст
Использование: grep [ПАРАМЕТР]… ШАБЛОН [ФАЙЛ]… Запустите «grep --help» для получения более подробного описания.
(
[6]053
было раскрыто в «ничего», потому что такого файла нет). Из?за возможности таких фокусов никогда не полагайтесь на то, что провальные glob expression’ы будут заменены на самих себя. Хотите передатьgrep
[6]053
— пишете'[6]053'
.simpleadmin
15.08.2016 20:19Павел, Ваша экспрессия «в никуда».
Я лишь усомнился, что Михаила заинтересовал "|" и ещё более сомнительно, что его заинтересовал man ps.
ps -p 6053 вам покажет данные только для процесса с PID 6053
Спасибо, конечно, но я тоже знаю о существании ключа "-p" у ps.
При этом много чаще пользуюсь именно символьным преобразованием, так как это универсальнее.ZyXI
15.08.2016 20:33В статье лучше написать
ps -p
. В любом случае, символьное преобразование взять в кавычки.simpleadmin
15.08.2016 20:37Согласен и с тем и с тем, но десятилетняя привычка и большая вольность с закавычиванием в c-шеллах делают свое дело.
ZyXI
15.08.2016 21:06У меня привычки куда как дальше от работающих «по?умолчанию»: вашу команду я бы написал как
ps afx G '[6]053'
и меня бы никто не понял (включая любую ненастроенную оболочку): zsh имеет такую замечательную вещь, как «глобальные alias’ы», позволяющую заменять не только команды. КонкретноG
раскрывается в| grep
*, есть и другие вродеalias -g L='| less'
. Разумеется, это дало два набора привычек: «для скриптов» (там alias’ы не работают, не говоря уж о том, что часть скриптов мне приходится писать для POSIX sh, а не zsh) и «для печати в командной строке». При общении предпочитается именно последнее.
А у скриптов есть и ещё одна особенность: во?первых, лучше написать всё так, чтобы не вылавливать потом баги, чем написать какое?нибудь
while ps afx | grep "[6]053"
и внезапно обнаружить «бесконечный цикл» на ровном месте, т.к. какой?то процесс попал на pts/6053. Во?вторых, PID в этом случае окажется в переменной и доставать по отдельности первый и остальные символы будет как?то сложновато (я к тому, что решение, оперирующее с цельным значением предпочтительнее решения, предполагающего разбиение его на части: в POSIX нет ни индексации строк (${PID[1]}
или${PID:0:0}
), ни даже${PID//sub/rep}
).
* С некоторых пор (после этой статьи) я немного подумал и теперь он раскрывается в
| nocorrect noglob grep
(то же, но zsh не будет пытаться раскрыть[6]053
).
simpleadmin
15.08.2016 19:00+1Если интересно, то как это работает:
https://habrahabr.ru/post/229501/#comment_7770191
Evengard
Хммм, возможно с помощью этого можно будет упростить некоторые мои монструозные скрипты… Интересная идея. А то та монструозность что есть вообще чудо что работает.
arvitaly
Вы не пробовали https://github.com/shelljs/shelljs?
ivlis
У меня уже Chromium с Atom.io сжирают 80% оперативы, если ещё shell будет на js, то в чём же мне killall chromium && killall atom выполнять?