Как сделать двунаправленную трубу между двумя программами?

63

Каждый знает , как сделать однонаправленный трубу между двумя программами (связывают stdoutс первого и stdinот второго): first | second.

Но как сделать двунаправленную трубу, т.е. перекрестную привязку stdinи stdoutдве программы? Есть ли простой способ сделать это в оболочке?


источник

Ответы:

30

Если каналы в вашей системе двунаправленные (как, например, в Solaris 11 и некоторых BSD, но не в Linux):

cmd1 <&1 | cmd2 >&0

Остерегайтесь тупиков, хотя.

Также обратите внимание, что некоторые версии ksh93 в некоторых системах реализуют pipe ( |) с использованием пары сокетов . Пары сокетов являются двунаправленными, но ksh93 явно отключает обратное направление, поэтому приведенная выше команда не будет работать с этими ksh93, даже в системах, где каналы (созданные pipe(2)системным вызовом) являются двунаправленными.

Стефан Шазелас
источник
1
Кто-нибудь знает, работает ли это на Linux тоже? (используя archlinux здесь)
heinrich5991
41

Ну, это довольно просто с именованными каналами ( mkfifo). Я легко пишу в кавычках, потому что если программы не предназначены для этого, скорее всего тупик.

mkfifo fifo0 fifo1
( prog1 > fifo0 < fifo1 ) &
( prog2 > fifo1 < fifo0 ) &
( exec 30<fifo0 31<fifo1 )      # write can't open until there is a reader
                                # and vice versa if we did it the other way

Теперь при написании stdout обычно используется буферизация. Так, например, если обе программы были:

#!/usr/bin/perl
use 5.010;
say 1;
print while (<>);

вы ожидаете бесконечный цикл. Но вместо этого оба зашли бы в тупик; вам нужно будет добавить $| = 1(или эквивалент), чтобы отключить буферизацию вывода. Тупик вызван тем, что обе программы ожидают чего-то на stdin, но они этого не видят, потому что он находится в буфере stdout другой программы и еще не записан в канал.

Обновление : включение предложений от Стефана Чарзела и Йоста:

mkfifo fifo0 fifo1
prog1 > fifo0 < fifo1 &
prog2 < fifo0 > fifo1

делает то же самое, короче и более портативный.

derobert
источник
23
Один именованный канал достаточно: prog1 < fifo | prog2 > fifo.
Андрей Вихров
2
@AndreyVihrov это правда, вы можете заменить анонимный канал одним из названных. Но мне нравится симметрия :-P
Дероберт
3
@ user14284: В Linux вы, вероятно, можете сделать это с чем-то вроде prog1 < fifo | tee /dev/stderr | prog2 | tee /dev/stderr > fifo.
Андрей Вихров
3
Если вы сделаете это prog2 < fifo0 > fifo1, вы можете избежать своего маленького танца с exec 30< ...(который, кстати, работает только с bashили yashдля fds больше 10).
Стефан Шазелас
1
@ Joost Хммм, кажется, вы правы, что в этом нет необходимости, по крайней мере, в bash. Я, вероятно, беспокоился, что, поскольку оболочка выполняет перенаправления (включая открытие каналов), она может блокироваться, но, по крайней мере, взломать вилки перед открытием fifos. dashкажется, тоже хорошо (но ведет себя немного по-другому)
Дероберт
13

Я не уверен, что это то, что вы пытаетесь сделать:

nc -l -p 8096 -c second &
nc -c first 127.0.0.1 8096 &

Это начинается с открытия прослушивающего сокета на порте 8096, и, как только соединение установлено, порождает программу secondс его stdinвыходным stdoutпотоком и входным потоком.

Затем ncзапускается секунда, которая соединяется с портом прослушивания и порождает программу firstс его stdoutвходным stdinпотоком и выходным потоком.

Это не совсем то, что делается с помощью канала, но, похоже, оно делает то, что вам нужно

Поскольку это использует сеть, это можно сделать на 2 удаленных компьютерах. Это почти то, как работают веб-сервер ( second) и веб-браузер ( first).

jfg956
источник
1
И nc -Uдля доменных сокетов UNIX, которые занимают только адресное пространство файловой системы.
Сиро Сантилли 新疆 改造 中心 法轮功 六四 事件
Опция -c недоступна в Linux. Мое счастье было недолгим :-(
pablochacin
9

Вы можете использовать pipexec :

$ pipexec -- '[A' cmd1 ] '[B' cmd2 ] '{A:1>B:0}' '{B:1>A:0}'
Андреас Флорат
источник
6

bashВ версии 4 есть coprocкоманда, которая позволяет делать это в чистом виде bashбез именованных каналов:

coproc cmd1
eval "exec cmd2 <&${COPROC[0]} >&${COPROC[1]}"

Некоторые другие оболочки также могут делать то же самое coproc.

Ниже приведен более подробный ответ, но он объединяет три команды, а не две, что делает лишь немного более интересным.

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

Версия использования bashс catи stdbuf, легко понять:

# start pipeline
coproc {
    cmd1 | cmd2 | cmd3
}
# create command to reconnect STDOUT `cmd3` to STDIN of `cmd1`
endcmd="exec stdbuf -i0 -o0 /bin/cat <&${COPROC[0]} >&${COPROC[1]}"
# eval the command.
eval "${endcmd}"

Обратите внимание, нужно использовать eval, потому что расширение переменной в <& $ var недопустимо в моей версии bash 4.2.25.

Версия с использованием pure bash: разбить на две части, запустить первый конвейер в режиме coproc, а затем перекусить вторую часть (либо одну команду, либо конвейер), заново подключив его к первой:

coproc {
    cmd 1 | cmd2
}
endcmd="exec cmd3 <&${COPROC[0]} >&${COPROC[1]}"
eval "${endcmd}"

Подтверждение концепции:

файл ./prog, просто фиктивная прога для потребления, пометки и повторной печати строк. Использование подоболочек, чтобы избежать проблем с буферизацией, может быть, излишним, но это не главное

#!/bin/bash
let c=0
sleep 2

[ "$1" == "1" ] && ( echo start )

while : ; do
  line=$( head -1 )
  echo "$1:${c} ${line}" 1>&2
  sleep 2
  ( echo "$1:${c} ${line}" )
  let c++
  [ $c -eq 3 ] && exit
done

файл ./start_cat Это версия, использующая bash, catиstdbuf

#!/bin/bash

echo starting first cmd>&2

coproc {
  stdbuf -i0 -o0 ./prog 1 \
    | stdbuf -i0 -o0 ./prog 2 \
    | stdbuf -i0 -o0 ./prog 3
}

echo "Delaying remainer" 1>&2
sleep 5
cmd="exec stdbuf -i0 -o0 /bin/cat <&${COPROC[0]} >&${COPROC[1]}"

echo "Running: ${cmd}" >&2
eval "${cmd}"

или файл ./start_part. Это версия, использующая bashтолько чистый . Для демонстрационных целей я все еще использую, stdbufпотому что ваша настоящая прога должна была бы в любом случае иметь дело с внутренней буферизацией, чтобы избежать блокировки из-за буферизации.

#!/bin/bash

echo starting first cmd>&2

coproc {
  stdbuf -i0 -o0 ./prog 1 \
    | stdbuf -i0 -o0 ./prog 2
}

echo "Delaying remainer" 1>&2
sleep 5
cmd="exec stdbuf -i0 -o0 ./prog 3 <&${COPROC[0]} >&${COPROC[1]}"

echo "Running: ${cmd}" >&2
eval "${cmd}"

Выход:

> ~/iolooptest$ ./start_part
starting first cmd
Delaying remainer
2:0 start
Running: exec stdbuf -i0 -o0 ./prog 3 <&63 >&60
3:0 2:0 start
1:0 3:0 2:0 start
2:1 1:0 3:0 2:0 start
3:1 2:1 1:0 3:0 2:0 start
1:1 3:1 2:1 1:0 3:0 2:0 start
2:2 1:1 3:1 2:1 1:0 3:0 2:0 start
3:2 2:2 1:1 3:1 2:1 1:0 3:0 2:0 start
1:2 3:2 2:2 1:1 3:1 2:1 1:0 3:0 2:0 start

Это делает это.

AnyDev
источник
5

Удобный строительный блок для написания таких двунаправленных каналов - это то, что соединяет stdout и stdin текущего процесса вместе. Давайте назовем это ioloop. После вызова этой функции вам нужно только запустить обычный канал:

ioloop &&     # stdout -> stdin 
cmd1 | cmd2   # stdin -> cmd1 -> cmd2 -> stdout (-> back to stdin)

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

( ioloop && cmd1 | cmd2 )

Вот переносимая реализация ioloop с использованием именованного канала:

ioloop() {
    FIFO=$(mktemp -u /tmp/ioloop_$$_XXXXXX ) &&
    trap "rm -f $FIFO" EXIT &&
    mkfifo $FIFO &&
    ( : <$FIFO & ) &&    # avoid deadlock on opening pipe
    exec >$FIFO <$FIFO
}

Именованный канал существует в файловой системе ненадолго во время установки ioloop. Эта функция не совсем POSIX, потому что mktemp устарела (и потенциально уязвима для гонок).

Возможна специфичная для Linux реализация с использованием / proc /, для которой не требуется именованный канал, но я думаю, что этого более чем достаточно.

user873942
источник
Интересная функция, +1. Возможно, можно использовать предложение или 2 добавленных объяснения ( : <$FIFO & )более подробно. Спасибо за публикацию.
Алекс Страгиес
Я немного огляделся и пришел в себя; Где я могу найти информацию об амортизации mktemp? Я использую его широко, и если новый инструмент занял свое место, я хотел бы начать использовать его.
DopeGhoti
Алекс: открытый (2) системный вызов на нанопроводе блокируется. если вы попытаетесь "exec <$ PIPE> $ PIPE", он застрянет, ожидая, пока другой процесс откроет другую сторону. Команда «: <$ FIFO &» выполняется в подоболочке в фоновом режиме и позволяет двунаправленному перенаправлению завершиться успешно.
user873942
DopeGhoti: функция библиотеки mktemp (3) C устарела. Утилита mktemp (1) не является.
user873942
4

Существует также

Как правильно замечает @ StéphaneChazelas в комментариях, приведенные выше примеры являются «базовой формой», у него есть хорошие примеры с вариантами ответа на аналогичный вопрос .

Алекс Страгиес
источник
Обратите внимание, что по умолчанию socatиспользуются сокеты вместо каналов (вы можете изменить это с помощью commtype=pipes). Возможно, вы захотите добавить noforkопцию, чтобы избежать дополнительного процесса socat, перемещающего данные между трубами / сокетами. (спасибо за редактирование моего ответа )
Стефан Шазелас
0

Здесь много хороших ответов. Поэтому я просто хочу добавить что-нибудь, чтобы легко поиграть с ними. Я предполагаю, stderrчто нигде не перенаправлен. Создайте два сценария (скажем, a.sh и b.sh):

#!/bin/bash
echo "foo" # change to 'bar' in second file

for i in {1..10}; do
  read input
  echo ${input}
  echo ${i} ${0} got: ${input} >&2
done

Затем, когда вы подключите их любым хорошим способом, вы должны увидеть на консоли:

1 ./a.sh got: bar
1 ./b.sh got: foo
2 ./a.sh got: foo
2 ./b.sh got: bar
3 ./a.sh got: bar
3 ./b.sh got: foo
4 ./a.sh got: foo
4 ./b.sh got: bar
...
pawel7318
источник