Respuestas:
los coprocesos son una ksh
característica (ya está en ksh88
). zsh
ha tenido la función desde el principio (principios de los 90), mientras que solo se agregó bash
en 4.0
(2009).
Sin embargo, el comportamiento y la interfaz son significativamente diferentes entre los 3 shells.
Sin embargo, la idea es la misma: permite iniciar un trabajo en segundo plano y poder enviarle entradas y leer su salida sin tener que recurrir a canalizaciones con nombre.
Eso se hace con tuberías sin nombre con la mayoría de los shells y socketpairs con versiones recientes de ksh93 en algunos sistemas.
En a | cmd | b
, a
alimenta datos cmd
y b
lee su salida. Ejecutar cmd
como un coproceso permite que el shell sea ambos a
y b
.
En ksh
, inicia un coproceso como:
cmd |&
Alimenta datos cmd
haciendo cosas como:
echo test >&p
o
print -p test
Y lea cmd
la salida con cosas como:
read var <&p
o
read -p var
cmd
se inicia como cualquier trabajo en segundo plano, puede utilizar fg
, bg
, kill
en él y remitirla por %job-number
oa través $!
.
Para cerrar el extremo de escritura de la tubería de la que cmd
está leyendo, puede hacer:
exec 3>&p 3>&-
Y para cerrar el extremo de lectura de la otra tubería (en la que cmd
está escribiendo):
exec 3<&p 3<&-
No puede iniciar un segundo coproceso a menos que primero guarde los descriptores del archivo de tubería en otros archivos. Por ejemplo:
tr a b |&
exec 3>&p 4<&p
tr b c |&
echo aaa >&3
echo bbb >&p
En zsh
, los coprocesos son casi idénticos a los de ksh
. La única diferencia real es que los zsh
coprocesos se inician con la coproc
palabra clave.
coproc cmd
echo test >&p
read var <&p
print -p test
read -p var
Haciendo:
exec 3>&p
Nota: Esto no mueve el coproc
descriptor de archivo a fd 3
(como en ksh
), sino que lo duplica. Por lo tanto, no hay una forma explícita de cerrar la tubería de alimentación o lectura, otra iniciando otra coproc
.
Por ejemplo, para cerrar el extremo de alimentación:
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
Además de los coprocesos basados en tuberías, zsh
(desde 3.1.6-dev19, lanzado en 2000) tiene construcciones basadas en pseudo-tty como expect
. Para interactuar con la mayoría de los programas, los coprocesos de estilo ksh no funcionarán, ya que los programas comienzan a almacenarse en búfer cuando su salida es una tubería.
Aquí hay unos ejemplos.
Comience el coproceso x
:
zmodload zsh/zpty
zpty x cmd
(Aquí, cmd
es un comando simple. Pero puedes hacer cosas más sofisticadas con eval
o funciones).
Alimentar datos de un coproceso:
zpty -w x some data
Leer datos de coprocesos (en el caso más simple):
zpty -r x var
Al igual expect
, puede esperar una salida del coproceso que coincida con un patrón dado.
La sintaxis de bash es mucho más nueva y se basa en una nueva característica agregada recientemente a ksh93, bash y zsh. Proporciona una sintaxis para permitir el manejo de descriptores de archivo asignados dinámicamente por encima de 10.
bash
ofrece una sintaxis básica coproc
y una sintaxis extendida .
La sintaxis básica para iniciar un coproceso se ve así zsh
:
coproc cmd
En ksh
o zsh
, se accede a las tuberías hacia y desde el coproceso con >&p
y <&p
.
Pero en bash
, los descriptores de archivo de la tubería del coproceso y la otra tubería al coproceso se devuelven en la $COPROC
matriz (respectivamente ${COPROC[0]}
y ${COPROC[1]}
. Entonces ...
Alimentar datos al coproceso:
echo xxx >&"${COPROC[1]}"
Leer datos del coproceso:
read var <&"${COPROC[0]}"
Con la sintaxis básica, puede iniciar solo un coproceso a la vez.
En la sintaxis extendida, puede nombrar sus coprocesos (como en los zsh
coprocesos zpty):
coproc mycoproc { cmd; }
El comando tiene que ser un comando compuesto. (Observe cómo el ejemplo anterior recuerda function f { ...; }
).
Esta vez, los descriptores de archivo están en ${mycoproc[0]}
y ${mycoproc[1]}
.
Puede iniciar más de un compañero de proceso a la vez, pero que hacer una advertencia cuando se inicia un proceso de co-mientras todavía se está ejecutando (incluso en modo no interactivo).
Puede cerrar los descriptores de archivo cuando use la sintaxis extendida.
coproc tr { tr a b; }
echo aaa >&"${tr[1]}"
exec {tr[1]}>&-
cat <&"${tr[0]}"
Tenga en cuenta que cerrar de esa manera no funciona en las versiones de bash anteriores a 4.3, donde debe escribirlo en su lugar:
fd=${tr[1]}
exec {fd}>&-
Como en ksh
y zsh
, esos descriptores de archivos de tubería están marcados como close-on-exec.
Pero en bash
, la única manera de pasar aquellos a los comandos ejecutados es duplicar a FDS 0
, 1
, o 2
. Eso limita la cantidad de coprocesos con los que puede interactuar para un solo comando. (Consulte a continuación para ver un ejemplo).
yash
no tiene una función de coproceso per se, pero el mismo concepto se puede implementar con sus características de canalización y redirección de procesos . yash
tiene una interfaz para la pipe()
llamada al sistema, por lo que este tipo de cosas se pueden hacer relativamente fácilmente a mano allí.
Comenzarías un coproceso con:
exec 5>>|4 3>(cmd >&5 4<&- 5>&-) 5>&-
Lo que primero crea un pipe(4,5)
(5 el final de la escritura, 4 el final de la lectura), luego redirige fd 3 a una tubería a un proceso que se ejecuta con su stdin en el otro extremo, y stdout va a la tubería creada anteriormente. Luego cerramos el final de escritura de esa tubería en el padre que no necesitaremos. Así que ahora en el shell tenemos fd 3 conectado al std del cmd y fd 4 conectado al stdout del cmd con tuberías.
Tenga en cuenta que el indicador close-on-exec no está establecido en esos descriptores de archivo.
Para alimentar datos:
echo data >&3 4<&-
Para leer datos:
read var <&4 3>&-
Y puedes cerrar fds como de costumbre:
exec 3>&- 4<&-
Los coprocesos se pueden implementar fácilmente con canalizaciones con nombre estándar. No sé cuándo se introdujeron las tuberías con nombre exacto, pero es posible que se ksh
produjeran coprocesos (probablemente a mediados de los años 80, ksh88 se "lanzó" en 88, pero creo que ksh
se usó internamente en AT&T unos años antes eso) lo que explicaría por qué.
cmd |&
echo data >&p
read var <&p
Se puede escribir con:
mkfifo in out
cmd <in >out &
exec 3> in 4< out
echo data >&3
read var <&4
Interactuar con ellos es más sencillo, especialmente si necesita ejecutar más de un coproceso. (Ver ejemplos a continuación).
El único beneficio del uso coproc
es que no tiene que limpiar esas tuberías con nombre después del uso.
Los proyectiles usan tuberías en algunas construcciones:
cmd1 | cmd2
,$(cmd)
,<(cmd)
, >(cmd)
.En esos, los datos fluyen en una sola dirección entre diferentes procesos.
Sin embargo, con coprocesos y canalizaciones con nombre, es fácil encontrarse en un punto muerto. Debe realizar un seguimiento de qué comando tiene qué descriptor de archivo abierto, para evitar que uno permanezca abierto y mantenga vivo un proceso. Los puntos muertos pueden ser difíciles de investigar, ya que pueden ocurrir de manera no determinista; por ejemplo, solo cuando se envían tantos datos como para llenar una tubería.
expect
para lo que ha sido diseñadoEl objetivo principal de los coprocesos era proporcionar al shell una forma de interactuar con los comandos. Sin embargo, no funciona tan bien.
La forma más simple de punto muerto mencionada anteriormente es:
tr a b |&
echo a >&p
read var<&p
Debido a que su salida no va a una terminal, tr
amortigua su salida. Por lo tanto, no generará nada hasta que vea el final del archivo en stdin
él o haya acumulado un búfer lleno de datos para generar. Entonces, arriba, después de que el shell haya emitido a\n
(solo 2 bytes), read
se bloqueará indefinidamente porque tr
está esperando que el shell le envíe más datos.
En resumen, las canalizaciones no son buenas para interactuar con los comandos. Los coprocesos solo se pueden usar para interactuar con comandos que no almacenan en memoria intermedia su salida, o comandos a los que se les puede decir que no almacenen en memoria intermedia su salida; por ejemplo, al usar stdbuf
algunos comandos en sistemas recientes de GNU o FreeBSD.
Es por eso expect
o zpty
usar pseudo-terminales en su lugar. expect
es una herramienta diseñada para interactuar con comandos, y lo hace bien.
Los coprocesos se pueden usar para realizar algunas tuberías más complejas que las que permiten las tuberías de revestimiento simples.
esa otra respuesta de Unix.SE tiene un ejemplo de uso de coproc.
Aquí hay un ejemplo simplificado: imagine que desea una función que alimente una copia de la salida de un comando a otros 3 comandos, y luego haga que la salida de esos 3 comandos se concatene.
Todo utilizando tuberías.
Por ejemplo: alimentar a la salida de la printf '%s\n' foo bar
a tr a b
, sed 's/./&&/g'
y cut -b2-
obtener algo como:
foo
bbr
ffoooo
bbaarr
oo
ar
Primero, no es necesariamente obvio, pero existe la posibilidad de un punto muerto allí, y comenzará a suceder después de solo unos pocos kilobytes de datos.
Luego, dependiendo de su shell, se encontrará con varios problemas diferentes que deben abordarse de manera diferente.
Por ejemplo, con zsh
, lo harías con:
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
Arriba, los fds de coproceso tienen el conjunto de indicadores close-on-exec, pero no los que están duplicados (como en {o1}<&p
). Por lo tanto, para evitar puntos muertos, deberá asegurarse de que estén cerrados en cualquier proceso que no los necesite.
Del mismo modo, tenemos que usar un subshell y usarlo exec cat
al final, para asegurarnos de que no haya un proceso de shell mintiendo sobre mantener una tubería abierta.
Con ksh
(aquí ksh93
), eso tendría 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: Eso no funcionará en sistemas donde se ksh
usa en socketpairs
lugar de pipes
, y donde /dev/fd/n
funciona como en Linux).
En ksh
, los fds anteriores 2
están marcados con el indicador close-on-exec, a menos que se pasen explícitamente en la línea de comando. Es por eso que no tenemos que cerrar los descriptores de archivos no utilizados como con zsh
-pero también es por eso que tenemos que hacer {i1}>&$i1
y usar eval
para ese nuevo valor de $i1
, al pasar a tee
e cat
...
En bash
esto no se puede hacer, porque no se puede evitar el cierre-on-exec bandera.
Arriba, es relativamente simple, porque usamos solo comandos externos simples. Se vuelve más complicado cuando quieres usar construcciones de shell allí, y comienzas a encontrarte con errores de shell.
Compare lo anterior con lo mismo usando tuberías con nombre:
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
Si desea interactuar con un comando, use expect
o zsh
's zpty
o canalizaciones con nombre.
Si desea hacer una fontanería elegante con tuberías, use tuberías con nombre.
Los coprocesos pueden hacer algo de lo anterior, pero prepárate para rascarte la cabeza seriamente para cualquier cosa que no sea trivial.
exec {tr[1]}>&-
de hecho parece funcionar con versiones más nuevas y se hace referencia en una entrada CWRU / changelog ( permitir que palabras como {array [ind]} sean redireccionamientos válidos ... 2012-09-01). exec {tr[1]}<&-
(o el >&-
equivalente más correcto, aunque eso no hace ninguna diferencia, ya que solo requiere close()
ambos) no cierra el stdin del coproc, sino el final de la escritura de la tubería a ese coproc.
yash
.
mkfifo
es que no tiene que preocuparse por las condiciones de carrera y la seguridad del acceso a la tubería. Todavía tienes que preocuparte por un punto muerto con fifo.
stdbuf
comando puede ayudar a prevenir al menos algunos de ellos. Lo usé en Linux y bash. De todos modos, creo que @ StéphaneChazelas tiene razón en la conclusión: la fase de "rascarse la cabeza" terminó para mí solo cuando volví a las tuberías con nombre.
Los coprocesos se introdujeron por primera vez en un lenguaje de script de shell con el ksh88
shell (1988), y más tarde en zsh
algún momento antes de 1993.
La sintaxis para iniciar un coproceso en ksh es command |&
. A partir de ahí, puede escribir en command
la entrada estándar con print -p
y leer su salida estándar con read -p
.
Más de un par de décadas después, bash, que carecía de esta característica, finalmente la introdujo en su versión 4.0. Desafortunadamente, se seleccionó una sintaxis incompatible y más compleja.
En bash 4.0 y versiones posteriores, puede iniciar un coproceso con el coproc
comando, por ejemplo:
$ coproc awk '{print $2;fflush();}'
Luego puede pasar algo al comando stdin de esa manera:
$ echo one two three >&${COPROC[1]}
y lee la salida awk con:
$ read -ru ${COPROC[0]} foo
$ echo $foo
two
Bajo ksh, eso habría sido:
$ awk '{print $2;fflush();}' |&
$ print -p "one two three"
$ read -p foo
$ echo $foo
two
¿Qué es un "coproc"?
Es la abreviatura de "coproceso", que significa un segundo proceso que coopera con el shell. Es muy similar a un trabajo en segundo plano que comenzó con un "&" al final del comando, excepto que en lugar de compartir la misma entrada y salida estándar que su shell principal, su E / S estándar está conectada al shell principal mediante un especial tipo de tubería llamada FIFO. Para referencia, haga clic aquí
Uno comienza un coproc en zsh con
coproc command
El comando debe estar preparado para leer desde stdin y / o escribir en stdout, o no es de mucha utilidad como coproc.
Lea este artículo aquí , proporciona un estudio de caso entre exec y coproc
|
. (es decir, usar tuberías en la mayoría de los depósitos y pares de enchufes en ksh93). Las tuberías y los pares de enchufes son primero en entrar, primero en salir, todos son FIFO. mkfifo
hace tuberías con nombre, los coprocesos no usan tuberías con nombre.
Aquí hay otro buen ejemplo (y funciona): un servidor simple escrito en BASH. Tenga en cuenta que necesitaría OpenBSD netcat
, el clásico no funcionará. Por supuesto, podría usar un socket inet en lugar de unix.
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
, ahora puede cerrar los descriptores de archivos coproc directamente, sin la necesidad de un auxiliar. variable; en términos del ejemplo en su respuestaexec {tr[1]}<&-
ahora funcionaría (para cerrar el stdin del coproc; tenga en cuenta que su código (indirectamente) intenta cerrar{tr[1]}
usando>&-
, pero{tr[1]}
es el stdin del coproc , y debe cerrarse con<&-
). La solución debe haber estado en algún punto intermedio4.2.25
, que aún muestra el problema y4.3.11
que no.