Как вы используете команду coproc в различных оболочках?

Ответы:

118

совместные процессы являются kshфункцией (уже в ksh88). zshбыла функция с самого начала (в начале 90-х), в то время как она была добавлена ​​только bashв 4.0(2009).

Тем не менее, поведение и интерфейс значительно отличается между 3 оболочками.

Однако идея та же: она позволяет запускать задание в фоновом режиме и может отправлять входные данные и читать выходные данные, не прибегая к именованным каналам.

Это делается с помощью неназванных каналов с большинством оболочек и пар сокетов с последними версиями ksh93 в некоторых системах.

В a | cmd | b, aпередает данные cmdи bчитает их вывод. Запуск cmdкак совместный процесс позволяет оболочке быть aи b.

КШ сопутствующие процессы

В ksh, вы запускаете сопроцесс как:

cmd |&

Вы передаете данные cmd, делая такие вещи, как:

echo test >&p

или же

print -p test

И читать cmdвывод с такими вещами, как:

read var <&p

или же

read -p var

cmdзапускается как любое фоновое задание, вы можете использовать fg, bg, killна нем и передать его %job-numberили через $!.

Чтобы закрыть пишущий конец канала, cmdиз которого вы читаете, вы можете сделать:

exec 3>&p 3>&-

И закрыть конец чтения другого канала (тот, cmdкоторый пишет):

exec 3<&p 3<&-

Вы не можете запустить второй совместный процесс, если сначала не сохраните дескрипторы файла канала в другие файлы. Например:

tr a b |&
exec 3>&p 4<&p
tr b c |&
echo aaa >&3
echo bbb >&p

Zsh co-процессов

В zsh, совместные процессы почти идентичны тем, что в ksh. Единственная реальная разница в том, что zshсовместные процессы запускаются с coprocключевым словом.

coproc cmd
echo test >&p
read var <&p
print -p test
read -p var

Выполнение:

exec 3>&p

Примечание: это не перемещает coprocдескриптор файла в fd 3(как в ksh), но дублирует его. Таким образом, нет никакого явного способа закрыть канал подачи или чтения, другой запуск другого coproc .

Например, чтобы закрыть конец подачи:

coproc tr a b
echo aaaa >&p # send some data

exec 4<&p     # preserve the reading end on fd 4
coproc :      # start a new short-lived coproc (runs the null command)

cat <&4       # read the output of the first coproc

В дополнение к трубе на основе совместных процессов, zsh(с 3.1.6-dev19, выпущенный в 2000 году) имеет псевдо-терминал на основе конструкции типа expect. Чтобы взаимодействовать с большинством программ, совместные процессы в стиле ksh не будут работать, так как программы начинают буферизовать, когда их вывод представляет собой канал.

Вот несколько примеров.

Начать совместный процесс x:

zmodload zsh/zpty
zpty x cmd

(Здесь cmdпростая команда. Но вы можете делать более сложные вещи с помощью evalили функций.)

Подать данные совместного процесса:

zpty -w x some data

Прочитать данные совместной обработки (в простейшем случае):

zpty -r x var

Например expect, он может ожидать некоторого вывода из совместного процесса, соответствующего заданному шаблону.

Bash Co-процессы

Синтаксис bash намного новее и основан на новой функции, недавно добавленной в ksh93, bash и zsh. Он предоставляет синтаксис, позволяющий обрабатывать динамически размещенные файловые дескрипторы выше 10.

bashпредлагает базовый coproc синтаксис и расширенный .

Основной синтаксис

Основной синтаксис для запуска совместного процесса выглядит так zsh:

coproc cmd

В kshили zsh, трубы к и от совместного процесса доступны с >&pи <&p.

Но в bashфайле файловые дескрипторы канала из совместного процесса и другого канала в совместный процесс возвращаются в $COPROCмассив (соответственно ${COPROC[0]}и ${COPROC[1]}. Итак…

Подача данных в совместный процесс:

echo xxx >&"${COPROC[1]}"

Читать данные из совместного процесса:

read var <&"${COPROC[0]}"

С базовым синтаксисом вы можете запустить только один совместный процесс одновременно.

Расширенный синтаксис

В расширенном синтаксисе вы можете назвать ваши zshсопроцессы (как в сопроцессах zpty):

coproc mycoproc { cmd; }

Команда имеет быть составной командой. (Обратите внимание, что пример выше напоминает function f { ...; }.)

На этот раз файловые дескрипторы находятся в ${mycoproc[0]}и ${mycoproc[1]}.

Вы можете начать более чем один процесс сотрудничества в то время , но вы делаете получить предупреждение при запуске совместного процесса , а один все еще работает (даже в не интерактивном режиме).

Вы можете закрыть файловые дескрипторы при использовании расширенного синтаксиса.

coproc tr { tr a b; }
echo aaa >&"${tr[1]}"

exec {tr[1]}>&-

cat <&"${tr[0]}"

Обратите внимание, что закрытие этого способа не работает в версиях bash до 4.3, где вы должны написать это:

fd=${tr[1]}
exec {fd}>&-

Как kshи в zsh, эти дескрипторы файла канала помечаются как близкие к выполнению.

Но bashединственный способ передать те исполняемые командам , чтобы дублировать их FDS 0, 1, или 2. Это ограничивает количество побочных процессов, с которыми вы можете взаимодействовать для одной команды. (См. Пример ниже.)

Яш-процесс и перенаправление трубопровода

yashсама по себе не имеет функции совместного процесса, но та же самая концепция может быть реализована с ее функциями конвейера и перенаправления процесса . yashимеет интерфейс к pipe()системному вызову, так что такого рода вещи можно сделать относительно легко вручную.

Вы начали бы совместный процесс с:

exec 5>>|4 3>(cmd >&5 4<&- 5>&-) 5>&-

Который сначала создает pipe(4,5)(5 - конец записи, 4 - конец чтения), затем перенаправляет fd 3 в канал в процесс, который запускается со своим стандартным входом на другом конце, а стандартный вывод - в канал, созданный ранее. Затем мы закрываем конец записи этого канала в родительском элементе, который нам не нужен. Итак, теперь в оболочке у нас есть fd 3, подключенный к stdin cmd, и fd 4, подключенный к stdout cmd с помощью pipe.

Обратите внимание, что флаг close-on-exec не установлен для этих файловых дескрипторов.

Для подачи данных:

echo data >&3 4<&-

Чтобы прочитать данные:

read var <&4 3>&-

И вы можете закрыть fds как обычно:

exec 3>&- 4<&-

Теперь, почему они не так популярны

вряд ли какая-то польза от использования именованных каналов

Совместные процессы могут быть легко реализованы с помощью стандартных именованных каналов. Я не знаю, когда были введены именно именованные каналы, но, возможно, это было после того, как они kshначали использовать сопроцессы (вероятно, в середине 80-х, ksh88 был «выпущен» в 88 году, но я считаю, что kshон использовался внутри AT & T за несколько лет до этого »). что) что бы объяснить почему.

cmd |&
echo data >&p
read var <&p

Может быть написано с:

mkfifo in out

cmd <in >out &
exec 3> in 4< out
echo data >&3
read var <&4

Взаимодействие с ними более простое, особенно если вам нужно запустить более одного совместного процесса. (См. Примеры ниже.)

Единственное преимущество использования coprocзаключается в том, что вам не нужно очищать эти именованные каналы после использования.

тупиковый склонных

Оболочки используют трубы в нескольких конструкциях:

  • оболочки трубы: cmd1 | cmd2 ,
  • подстановка команд: $(cmd) ,
  • и замена процесса: <(cmd) , >(cmd).

В этих случаях данные перемещаются только в одном направлении между различными процессами.

Однако, благодаря совместным процессам и именованным каналам легко зайти в тупик. Вы должны следить за тем, какая команда имеет какой файловый дескриптор открыт, чтобы предотвратить его сохранение и поддержание процесса живым. Замыкать в тупиках сложно, потому что они могут возникать недетерминированно; например, только когда отправляется столько данных, сколько нужно для заполнения одного канала.

работает хуже, чем expectдля того, для чего он был разработан

Основная цель совместных процессов состояла в том, чтобы предоставить оболочке способ взаимодействия с командами. Тем не менее, это не так хорошо работает.

Упомянутая выше простейшая форма тупика:

tr a b |&
echo a >&p
read var<&p

Поскольку его вывод не идет в терминал, trбуферизует его вывод. Таким образом, он не будет ничего выводить, пока не увидит конец файла stdinили не накопит полный буфер данных для вывода. Итак, выше, после того, как оболочка выдаст a\n(только 2 байта), readблок будет блокироваться на неопределенный срок, потому что trожидает, пока оболочка отправит ему больше данных.

Короче говоря, каналы не годятся для взаимодействия с командами. Совместные процессы могут использоваться только для взаимодействия с командами, которые не буферизуют свои выходные данные, или командами, которым можно запретить буферизовать свои выходные данные; например, с использованием stdbufнекоторых команд в последних системах GNU или FreeBSD.

Вот почему expectили zptyиспользуйте вместо этого псевдо-терминалы. expectэто инструмент, предназначенный для взаимодействия с командами, и он делает это хорошо.

Обработка файлового дескриптора сложная и трудно понять

Совместные процессы могут использоваться для выполнения более сложных сантехнических работ, чем это допускают простые трубные оболочки.

у того другого ответа Unix.SE есть пример использования coproc.

Вот упрощенный пример: представьте, что вам нужна функция, которая передает копию вывода команды 3 другим командам, а затем объединяет выходные данные этих трех команд.

Все с помощью труб.

Например: кормить выход printf '%s\n' foo barк tr a b, sed 's/./&&/g'и , cut -b2-чтобы получить что - то вроде:

foo
bbr
ffoooo
bbaarr
oo
ar

Во-первых, это не обязательно очевидно, но есть вероятность тупиковой ситуации, и она начнет происходить только после нескольких килобайт данных.

Затем, в зависимости от вашей оболочки, вы столкнетесь с рядом различных проблем, которые нужно решать по-разному.

Например, с zsh, вы бы сделали это с:

f() (
  coproc tr a b
  exec {o1}<&p {i1}>&p
  coproc sed 's/./&&/g' {i1}>&- {o1}<&-
  exec {o2}<&p {i2}>&p
  coproc cut -c2- {i1}>&- {o1}<&- {i2}>&- {o2}<&-
  tee /dev/fd/$i1 /dev/fd/$i2 >&p {o1}<&- {o2}<&- &
  exec cat /dev/fd/$o1 /dev/fd/$o2 - <&p {i1}>&- {i2}>&-
)
printf '%s\n' foo bar | f

Выше, для fds-процесса установлены флаги close-on-exec, но не те, которые из них дублируются (как в {o1}<&p). Поэтому, чтобы избежать взаимоблокировок, вы должны быть уверены, что они закрыты в любых процессах, в которых они не нужны.

Точно так же мы должны использовать подоболочку и использовать exec catв конце, чтобы гарантировать, что нет никакого процесса оболочки, поддерживающего удержание трубы открытым.

С ksh(здесь ksh93) это должно быть:

f() (
  tr a b |&
  exec {o1}<&p {i1}>&p
  sed 's/./&&/g' |&
  exec {o2}<&p {i2}>&p
  cut -c2- |&
  exec {o3}<&p {i3}>&p
  eval 'tee "/dev/fd/$i1" "/dev/fd/$i2"' >&"$i3" {i1}>&"$i1" {i2}>&"$i2" &
  eval 'exec cat "/dev/fd/$o1" "/dev/fd/$o2" -' <&"$o3" {o1}<&"$o1" {o2}<&"$o2"
)
printf '%s\n' foo bar | f

( Примечание: это не будет работать в системах, где kshиспользуется socketpairsвместо pipes, и где /dev/fd/nработает как в Linux.)

В kshвышеупомянутых fds 2отмечены флагом close-on-exec, если они не переданы явно в командной строке. Вот почему нам не нужно закрывать неиспользуемые файловые дескрипторы, как с помощью zsh- но это также, почему мы должны делать {i1}>&$i1и использовать evalдля этого нового значения $i1, для передачи teeи cat

В bashэтом не может быть сделано, потому что вы не можете избежать закрыть при ехесе флага.

Выше это относительно просто, потому что мы используем только простые внешние команды. Это становится более сложным, когда вы хотите вместо этого использовать конструкции оболочки, и вы начинаете сталкиваться с ошибками оболочки.

Сравните вышесказанное с тем же, используя именованные каналы:

f() {
  mkfifo p{i,o}{1,2,3}
  tr a b < pi1 > po1 &
  sed 's/./&&/g' < pi2 > po2 &
  cut -c2- < pi3 > po3 &

  tee pi{1,2} > pi3 &
  cat po{1,2,3}
  rm -f p{i,o}{1,2,3}
}
printf '%s\n' foo bar | f

Заключение

Если вы хотите , чтобы взаимодействовать с командой, использованием expect, или zsh«с zptyили именованными каналами.

Если вы хотите поработать с трубами, используйте именованные каналы.

Совместные процессы могут делать некоторые из вышеперечисленных, но будьте готовы сделать серьезные царапины на голову для чего-то нетривиального.

Стефан Шазелас
источник
Отличный ответ действительно. Я не знаю, когда конкретно это было исправлено, но, по крайней мере bash 4.3.11, теперь вы можете закрывать дескрипторы файлов coproc напрямую, без необходимости в aux. переменная; с точки зрения примера в вашем ответе exec {tr[1]}<&- теперь будет работать (чтобы закрыть stdin копрока; обратите внимание, что ваш код (косвенно) пытается закрыть {tr[1]}использование >&-, но {tr[1]}является stdin копрока и должен быть закрыт с <&-). Исправление, должно быть, произошло где-то между 4.2.25, что все еще показывает проблему, и 4.3.11, которая не делает.
mklement0
1
@ mklement0, спасибо. exec {tr[1]}>&-действительно, кажется, работает с более новыми версиями и упоминается в записи CWRU / changelog ( допустим, что слова вроде {array [ind]} должны быть допустимыми перенаправлениями ... 2012-09-01). exec {tr[1]}<&-(или более корректный >&-эквивалент, хотя это не имеет значения, поскольку он просто требует close()обоих) не закрывает стандартный код копрока, а записывает конец канала в этот копрок.
Стефан Шазелас
1
@ mklement0, хороший момент, я обновил его и добавил yash.
Стефан Шазелас
1
Одним из преимуществ mkfifoявляется то, что вам не нужно беспокоиться о состоянии гонки и безопасности при доступе к трубе. Вы все еще должны беспокоиться о тупике с FIFO.
Отеус
1
О взаимоблокировках: stdbufкоманда может помочь предотвратить хотя бы некоторые из них. Я использовал его под Linux и Bash. В любом случае, я считаю, что @ StéphaneChazelas прав в заключении: фаза «чесания головы» закончилась для меня, только когда я переключился на именованные каналы.
Шуб
7

Совместные процессы были впервые представлены на языке сценариев оболочки вместе с ksh88оболочкой (1988), а затем в zshнекоторый момент до 1993 года.

Синтаксис для запуска совместного процесса под КШ является command |&. Начиная с этого момента , вы можете записывать в commandстандартный ввод с помощью print -pи читать его стандартный вывод с помощью read -p.

Более чем через пару десятилетий bash, у которого не было этой функции, наконец-то представил ее в своем выпуске 4.0. К сожалению, был выбран несовместимый и более сложный синтаксис.

В bash 4.0 и новее вы можете запустить совместный процесс с помощью coprocкоманды, например:

$ coproc awk '{print $2;fflush();}'

Затем вы можете передать что-то команде stdin следующим образом:

$ echo one two three >&${COPROC[1]}

и прочитайте вывод awk:

$ read -ru ${COPROC[0]} foo
$ echo $foo
two

Под ksh это было бы:

$ awk '{print $2;fflush();}' |&
$ print -p "one two three"
$ read -p foo
$ echo $foo
two
jlliagre
источник
-1

Что такое «копрок»?

Это сокращение от "co-process", что означает второй процесс, взаимодействующий с оболочкой. Это очень похоже на фоновое задание, начинающееся с символа «&» в конце команды, за исключением того, что вместо совместного использования того же стандартного ввода и вывода, что и его родительская оболочка, его стандартный ввод-вывод подключается к родительской оболочке с помощью специального вид трубы называется FIFO. Для справки нажмите здесь

Начинается копрок в Zsh с

coproc command

Команда должна быть подготовлена ​​для чтения из stdin и / или записи в stdout, иначе она не очень полезна в качестве копрока.

Прочитайте эту статью, здесь приведен пример использования exec и coproc.

Мунай Дас Удасин
источник
Можете ли вы добавить некоторые статьи к своему ответу? Я пытался охватить эту тему в U & L, так как она казалась недостаточно представленной. Спасибо за Ваш ответ! Также обратите внимание, что я установил тег как Bash, а не zsh.
slm
@slm Вы уже указали на хакеров Bash. Я видел там достаточно примеров. Если вы намеревались привлечь внимание к этому вопросу, то да, вам это удалось:>
Валентин Байрами
Это не особые виды труб, это те же трубы, которые используются с |. (то есть использовать трубы в большинстве оболочек и пары гнезд в ksh93). трубы и соединительные муфты являются первыми, первыми, они все FIFO. mkfifoсоздает именованные каналы, сопроцессы не используют именованные каналы.
Стефан Шазелас
@ Slm извините за Zsh ... на самом деле я работаю на Zsh. Я склонен делать это иногда с потоком. Это прекрасно работает и в Bash ...
Мунай Дас Удасин
@ Stephane Chazelas Я почти уверен, что где-то читал, что ввод / вывод связан со специальными видами труб, называемыми FIFO ...
Мунай Дас Удасин
-1

Вот еще один хороший (и работающий) пример - простой сервер, написанный на BASH. Обратите внимание, что вам понадобятся OpenBSD netcat, классический не будет работать. Конечно, вы можете использовать inet socket вместо unix one.

server.sh:

#!/usr/bin/env bash

SOCKET=server.sock
PIDFILE=server.pid

(
    exec </dev/null
    exec >/dev/null
    exec 2>/dev/null
    coproc SERVER {
        exec nc -l -k -U $SOCKET
    }
    echo $SERVER_PID > $PIDFILE
    {
        while read ; do
            echo "pong $REPLY"
        done
    } <&${SERVER[0]} >&${SERVER[1]}
    rm -f $PIDFILE
    rm -f $SOCKET
) &
disown $!

client.sh:

#!/usr/bin/env bash

SOCKET=server.sock

coproc CLIENT {
    exec nc -U $SOCKET
}

{
    echo "$@"
    read
} <&${CLIENT[0]} >&${CLIENT[1]}

echo $REPLY

Использование:

$ ./server.sh
$ ./client.sh ping
pong ping
$ ./client.sh 12345
pong 12345
$ kill $(cat server.pid)
$
Алексей Найденов
источник