Respostas:
co-processos são um ksh
recurso (já incluído ksh88
). zsh
teve o recurso desde o início (início dos anos 90), enquanto apenas foi adicionado bash
em 4.0
(2009).
No entanto, o comportamento e a interface são significativamente diferentes entre as três conchas.
A idéia é a mesma: permite iniciar um trabalho em segundo plano e poder enviar entrada e ler sua saída sem precisar recorrer a pipes nomeados.
Isso é feito com pipes não nomeados com a maioria dos shells e socketpairs com versões recentes do ksh93 em alguns sistemas.
Em a | cmd | b
, a
alimenta os dados cmd
e b
lê sua saída. A execução cmd
como um co-processo permite que o shell seja ambos a
e b
.
Em ksh
, você inicia um coprocesso como:
cmd |&
Você alimenta os dados cmd
fazendo coisas como:
echo test >&p
ou
print -p test
E leia cmd
a saída com coisas como:
read var <&p
ou
read -p var
cmd
é iniciado como qualquer trabalho de fundo, você pode usar fg
, bg
, kill
nele e apresentá-lo por %job-number
ou via $!
.
Para fechar a extremidade de gravação do pipe cmd
, você pode fazer:
exec 3>&p 3>&-
E para fechar a extremidade de leitura do outro canal (aquele que cmd
está escrevendo):
exec 3<&p 3<&-
Você não pode iniciar um segundo co-processo, a menos que primeiro salve os descritores de arquivo de pipe em outros fds. Por exemplo:
tr a b |&
exec 3>&p 4<&p
tr b c |&
echo aaa >&3
echo bbb >&p
Em zsh
, co-processos são quase idênticos aos de ksh
. A única diferença real é que os zsh
co-processos são iniciados com a coproc
palavra - chave.
coproc cmd
echo test >&p
read var <&p
print -p test
read -p var
Fazendo:
exec 3>&p
Nota: Isso não move o coproc
descritor de arquivo para fd 3
(como em ksh
), mas o duplica. Portanto, não há uma maneira explícita de fechar o tubo de alimentação ou de leitura, outro iniciando outro coproc
.
Por exemplo, para fechar o final da alimentação:
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
Além zsh
dos coprocessos baseados em pipe, (desde 3.1.6-dev19, lançado em 2000), tem construções baseadas em pseudo-tty como expect
. Para interagir com a maioria dos programas, os co-processos no estilo ksh não funcionam, pois os programas começam a armazenar em buffer quando sua saída é um canal.
Aqui estão alguns exemplos.
Inicie o co-processo x
:
zmodload zsh/zpty
zpty x cmd
(Aqui cmd
está um comando simples. Mas você pode fazer coisas mais sofisticadas com eval
ou funções.)
Alimente dados de um co-processo:
zpty -w x some data
Leia os dados de co-processo (no caso mais simples):
zpty -r x var
Assim expect
, ele pode esperar por alguma saída do co-processo que corresponda a um determinado padrão.
A sintaxe do bash é muito mais recente e se baseia em um novo recurso adicionado recentemente ao ksh93, bash e zsh. Ele fornece uma sintaxe para permitir o tratamento de descritores de arquivos alocados dinamicamente acima de 10.
bash
oferece uma sintaxe básica coproc
e uma extendida .
A sintaxe básica para iniciar um co-processo é semelhante zsh
a:
coproc cmd
Em ksh
ou zsh
, os tubos de e para o co-processo são acessados com >&p
e <&p
.
Porém bash
, em , os descritores de arquivo do canal do co-processo e do outro canal para o co-processo são retornados na $COPROC
matriz (respectivamente ${COPROC[0]}
e ${COPROC[1]}
…
Alimente os dados para o co-processo:
echo xxx >&"${COPROC[1]}"
Leia os dados do co-processo:
read var <&"${COPROC[0]}"
Com a sintaxe básica, você pode iniciar apenas um co-processo por vez.
Na sintaxe estendida, você pode nomear seus co-processos (como nos zsh
co-processos zpty):
coproc mycoproc { cmd; }
O comando deve ser um comando composto. (Observe como o exemplo acima é uma reminiscência de function f { ...; }
.)
Desta vez, os descritores de arquivo estão em ${mycoproc[0]}
e ${mycoproc[1]}
.
Você pode começar mais de um co-processo de cada vez, mas você fazer receber um aviso quando você iniciar um co-processo, enquanto um ainda está em execução (mesmo em modo não-interativo).
Você pode fechar os descritores de arquivo ao usar a sintaxe estendida.
coproc tr { tr a b; }
echo aaa >&"${tr[1]}"
exec {tr[1]}>&-
cat <&"${tr[0]}"
Observe que o fechamento dessa maneira não funciona nas versões bash anteriores à 4.3, nas quais você deve escrevê-lo:
fd=${tr[1]}
exec {fd}>&-
Como em ksh
e zsh
, esses descritores de arquivo de pipe são marcados como close-on-exec.
Mas, em bash
, a única maneira de passar aqueles aos comandos executados é duplicar-los para fds 0
, 1
, ou 2
. Isso limita o número de co-processos com os quais você pode interagir para um único comando. (Veja abaixo um exemplo.)
yash
não possui um recurso de co-processo em si, mas o mesmo conceito pode ser implementado com seus recursos de redirecionamento de pipeline e processo . yash
possui uma interface para a pipe()
chamada do sistema, para que esse tipo de coisa possa ser feita com relativa facilidade à mão.
Você iniciaria um co-processo com:
exec 5>>|4 3>(cmd >&5 4<&- 5>&-) 5>&-
O que primeiro cria um pipe(4,5)
(5 no final da gravação, 4 no final da leitura) e depois redireciona o fd 3 para um canal para um processo que é executado com seu stdin na outra extremidade e stdout para o canal criado anteriormente. Em seguida, fechamos a extremidade de escrita desse tubo no pai, da qual não precisaremos. Então agora no shell temos o fd 3 conectado ao stdin do cmd e o fd 4 conectado ao stdout do cmd com pipes.
Observe que o sinalizador close-on-exec não está definido nesses descritores de arquivo.
Para alimentar dados:
echo data >&3 4<&-
Para ler dados:
read var <&4 3>&-
E você pode fechar o fds como de costume:
exec 3>&- 4<&-
Co-processos podem ser facilmente implementados com pipes nomeados padrão. Não sei quando foram introduzidos exatamente os pipes nomeados, mas é possível que tenha ksh
surgido com co-processos (provavelmente em meados dos anos 80, o ksh88 foi "lançado" em 88, mas acredito que ksh
foi usado internamente na AT&T alguns anos antes isso), o que explicaria o porquê.
cmd |&
echo data >&p
read var <&p
Pode ser escrito com:
mkfifo in out
cmd <in >out &
exec 3> in 4< out
echo data >&3
read var <&4
A interação com essas pessoas é mais direta - especialmente se você precisar executar mais de um co-processo. (Veja exemplos abaixo.)
O único benefício do uso coproc
é que você não precisa limpar os tubos nomeados após o uso.
Os reservatórios usam tubos em algumas construções:
cmd1 | cmd2
,$(cmd)
,<(cmd)
, >(cmd)
.Nesses, os dados fluem em apenas uma direção entre diferentes processos.
Com co-processos e pipes nomeados, porém, é fácil encontrar um impasse. Você deve acompanhar qual comando possui qual descritor de arquivo aberto, para impedir que um permaneça aberto e mantenha um processo ativo. Os conflitos podem ser difíceis de investigar, porque podem ocorrer de forma não determinística; por exemplo, apenas quando são enviados tantos dados quanto para encher um tubo.
expect
para o que foi projetadoO principal objetivo dos co-processos era fornecer ao shell uma maneira de interagir com os comandos. No entanto, não funciona tão bem.
A forma mais simples de conflito mencionada acima é:
tr a b |&
echo a >&p
read var<&p
Como a saída não vai para um terminal, tr
a saída é armazenada em buffer. Portanto, ele não produzirá nada até que veja o final do arquivo stdin
ou tenha acumulado um buffer cheio de dados para a saída. Assim, acima, após a saída do shell a\n
(apenas 2 bytes), o read
bloco será bloqueado indefinidamente porque tr
aguarda o envio de mais dados pelo shell.
Em resumo, os pipes não são bons para interagir com os comandos. Co-processos só podem ser usados para interagir com comandos que não armazenam em buffer sua saída ou comandos que podem ser instruídos a não armazenar em buffer sua saída; por exemplo, usando stdbuf
com alguns comandos nos sistemas recentes GNU ou FreeBSD.
Por isso, expect
ou zpty
use pseudo-terminais. expect
é uma ferramenta projetada para interagir com comandos e funciona bem.
Os coprocessos podem ser usados para fazer um encanamento mais complexo do que o que os tubos simples da concha permitem.
essa outra resposta do Unix.SE tem um exemplo de uso coproc.
Aqui está um exemplo simplificado: imagine que você deseja uma função que alimente uma cópia da saída de um comando para outros 3 comandos e faça com que a saída desses 3 comandos seja concatenada.
Todos usando canos.
Por exemplo: alimentar a saída printf '%s\n' foo bar
para tr a b
, sed 's/./&&/g'
e cut -b2-
obter algo como:
foo
bbr
ffoooo
bbaarr
oo
ar
Primeiro, não é necessariamente óbvio, mas há uma possibilidade de impasse por lá, e isso começará a acontecer após apenas alguns kilobytes de dados.
Então, dependendo do seu shell, você terá vários problemas diferentes que precisam ser tratados de maneira diferente.
Por exemplo, com zsh
, você faria isso com:
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
Acima, o co-processo fds tem o sinalizador close-on-exec definido, mas não os duplicados (como em {o1}<&p
). Portanto, para evitar conflitos, você deve garantir que eles sejam fechados em qualquer processo que não precise deles.
Da mesma forma, temos que usar um subshell e usar exec cat
no final, para garantir que não exista nenhum processo de shell sobre manter um tubo aberto.
Com ksh
(aqui ksh93
), isso teria que ser:
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
( Nota: Isso não funcionará em sistemas onde ksh
usa , e socketpairs
não pipes
, e onde /dev/fd/n
funciona como no Linux.)
Em ksh
, fds acima 2
são marcados com o sinalizador close-on-exec, a menos que sejam passados explicitamente na linha de comando. É por isso que não precisamos fechar os descritores de arquivos não utilizados como com zsh
- mas também é por isso que precisamos fazer {i1}>&$i1
e usar eval
esse novo valor de $i1
, a ser passado tee
e cat
…
Em bash
isso não pode ser feito, porque você não pode evitar o close-on-exec bandeira.
Acima, é relativamente simples, porque usamos apenas comandos externos simples. Torna-se mais complicado quando você deseja usar construções de shell lá, e você começa a encontrar bugs de shell.
Compare o acima com o mesmo usando pipes nomeados:
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
Se você deseja interagir com um comando, use expect
ou zsh
's zpty
ou pipes nomeados.
Se você quiser fazer um encanamento sofisticado com canos, use canos nomeados.
Os co-processos podem fazer algumas das opções acima, mas esteja preparado para fazer algo sério na cabeça por algo não trivial.
exec {tr[1]}>&-
realmente parece funcionar com versões mais recentes e é referenciado em uma entrada CWRU / changelog ( permita que palavras como {array [ind]} sejam redirecionamento válido ... 2012-09-01). exec {tr[1]}<&-
(ou o >&-
equivalente mais correto, porém, que não faz diferença, pois isso exige apenas os close()
dois) não fecha o stdin do coproc, mas a extremidade de gravação do pipe nesse coproc.
yash
.
mkfifo
é que você não precisa se preocupar com as condições da corrida e a segurança do acesso ao tubo. Você ainda precisa se preocupar com o impasse com o fifo.
stdbuf
comando pode ajudar a impedir pelo menos alguns deles. Eu usei no Linux e bash. De qualquer forma, acredito que o @ StéphaneChazelas está certo na Conclusão: a fase de "coçar a cabeça" só terminou para mim quando retornei aos pipes nomeados.
Os co-processos foram introduzidos pela primeira vez em uma linguagem de script de shell com o ksh88
shell (1988) e, posteriormente, em zsh
algum momento antes de 1993.
A sintaxe para iniciar um co-processo no ksh é command |&
. A partir daí, você pode gravar na command
entrada padrão print -p
e ler sua saída padrão com read -p
.
Mais de duas décadas depois, o bash, que não possuía esse recurso, finalmente o introduziu na versão 4.0. Infelizmente, uma sintaxe incompatível e mais complexa foi selecionada.
No bash 4.0 e mais recente, você pode iniciar um co-processo com o coproc
comando, por exemplo:
$ coproc awk '{print $2;fflush();}'
Você pode então passar algo para o comando stdin dessa maneira:
$ echo one two three >&${COPROC[1]}
e leia a saída do awk com:
$ read -ru ${COPROC[0]} foo
$ echo $foo
two
Sob o ksh, isso teria sido:
$ awk '{print $2;fflush();}' |&
$ print -p "one two three"
$ read -p foo
$ echo $foo
two
O que é um "coproc"?
É a abreviação de "co-processo", o que significa um segundo processo que coopera com o shell. É muito semelhante a um trabalho em segundo plano iniciado com um "&" no final do comando, exceto que, em vez de compartilhar a mesma entrada e saída padrão que seu shell pai, sua E / S padrão é conectada ao shell pai por um especial tipo de tubo chamado FIFO. Para referência, clique aqui
Um inicia um coproc no zsh com
coproc command
O comando deve estar preparado para ler do stdin e / ou gravar no stdout, ou não é muito útil como um coproc.
Leia este artigo aqui , fornece um estudo de caso entre exec e coproc
|
. (ou seja, use tubos na maioria das conchas e pares de soquetes no ksh93). tubos e pares de soquetes são os primeiros a entrar, os primeiros a sair, todos são FIFO. mkfifo
faz pipes nomeados, coprocessos não usam pipes nomeados.
Aqui está outro exemplo bom (e funcional) - um servidor simples escrito em BASH. Observe que você precisaria do OpenBSD netcat
, o clássico não funcionará. Claro que você pode usar o inet socket em vez do 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
Uso:
$ ./server.sh
$ ./client.sh ping
pong ping
$ ./client.sh 12345
pong 12345
$ kill $(cat server.pid)
$
bash 4.3.11
, agora você pode fechar os descritores de arquivos coproc diretamente, sem a necessidade de um aux. variável;exec {tr[1]}<&-
Agora, em termos do exemplo em sua resposta , funcionaria (para fechar o stdin do coproc; observe que seu código (indiretamente) tenta fechar{tr[1]}
usando>&-
, mas{tr[1]}
é o stdin do coproc e deve ser fechado com<&-
). A correção deve ter chegado a um ponto intermediário4.2.25
, que ainda apresenta o problema e4.3.11
, o que não ocorre.