Apague el buffering en la tubería

396

Tengo un script que llama a dos comandos:

long_running_command | print_progress

Las long_running_commandimpresiones de un progreso, pero estoy contento con él. Estoy usando print_progresspara hacerlo más agradable (es decir, imprimo el progreso en una sola línea).

El problema: la conexión de una tubería a stdout también activa un búfer 4K, para que el programa de impresión agradable no obtenga nada ... nada ... nada ... mucho ... :)

¿Cómo puedo deshabilitar el búfer 4K para long_running_command(no, no tengo la fuente)?

Aaron Digulla
fuente
1
Entonces, cuando ejecuta long_running_command sin tuberías, puede ver las actualizaciones de progreso correctamente, pero cuando las tuberías se almacenan en el búfer.
1
Sí, eso es exactamente lo que pasa.
Aaron Digulla
21
La incapacidad de una forma simple de controlar el almacenamiento en búfer ha sido un problema durante décadas. Por ejemplo, vea: marc.info/?l=glibc-bug&m=98313957306297&w=4 que básicamente dice "No puedo ser molestado haciendo esto y aquí hay una trampa para justificar mi posición"
1
En realidad, no es la tubería la que causa un retraso mientras se esperan suficientes datos. Las tuberías tienen una capacidad, pero tan pronto como hay datos escritos en la tubería, está inmediatamente listo para leer en el otro extremo.
Sam Watkins,

Respuestas:

254

Puede usar el unbuffercomando (que viene como parte del expectpaquete), p. Ej.

unbuffer long_running_command | print_progress

unbufferse conecta a long_running_commandtravés de un pseudoterminal (pty), lo que hace que el sistema lo trate como un proceso interactivo, por lo tanto, no utiliza el almacenamiento en búfer de 4 kiB en la tubería que es la causa probable del retraso.

Para canalizaciones más largas, es posible que deba quitar el búfer de cada comando (excepto el último), por ejemplo

unbuffer x | unbuffer -p y | z
Stephen Kitt
fuente
3
De hecho, el uso de una pty para conectarse a procesos interactivos es cierto de esperar en general.
15
Al canalizar llamadas a unbuffer, debe usar el argumento -p para que unbuffer lea desde stdin.
26
Nota: En los sistemas debian, esto se llama expect_unbuffery está en el expect-devpaquete, no en el expectpaquete
bdonlan
44
@bdonlan: Al menos en Ubuntu (basado en Debian), expect-devproporciona ambos unbuffery expect_unbuffer(el primero es un enlace simbólico al segundo). Los enlaces están disponibles desde expect 5.44.1.14-1(2009).
jfs
1
Nota: En los sistemas Ubuntu 14.04.x, también está en el paquete expect-dev.
Alexandre Mazel
462

Otra forma de pelar este gato es usar el stdbufprograma, que es parte de GNU Coreutils (FreeBSD también tiene el suyo propio).

stdbuf -i0 -o0 -e0 command

Esto desactiva el almacenamiento en búfer por completo para entrada, salida y error. Para algunas aplicaciones, el almacenamiento en línea puede ser más adecuado por razones de rendimiento:

stdbuf -oL -eL command

Tenga en cuenta que solo funciona para el stdioalmacenamiento en búfer ( printf(), fputs()...) para aplicaciones vinculadas dinámicamente, y solo si esa aplicación no ajusta el almacenamiento en búfer de sus flujos estándar por sí mismo, aunque eso debería cubrir la mayoría de las aplicaciones.

a3nm
fuente
66
"unbuffer" debe instalarse en Ubuntu, que está dentro del paquete: expect-dev que tiene 2MB ...
lepe
2
Esto funciona muy bien en la instalación predeterminada de raspbian para eliminar el registro del búfer. Encontré sudo stdbuff … commandtrabajos aunque stdbuff … sudo commandno lo hice.
natevw
20
@qdii stdbufno funciona teeporque teesobrescribe los valores predeterminados establecidos por stdbuf. Consulte la página del manual de stdbuf.
ceving
55
@lepe Curiosamente, unbuffer tiene dependencias en x11 y tcl / tk, lo que significa que realmente necesita> 80 MB si lo está instalando en un servidor sin ellos.
jpatokal
10
@qdii stdbufusa un LD_PRELOADmecanismo para insertar su propia biblioteca cargada dinámicamente libstdbuf.so. Esto significa que no funcionará con estos tipos de ejecutables: con setuid o capacidades de archivo establecidas, estáticamente vinculadas, sin usar libc estándar. En estos casos, es mejor usar las soluciones con unbuffer/ script/ socat. Ver también stdbuf con setuid / capacidades .
Pabouk
75

Otra forma más de activar el modo de salida de almacenamiento en línea para el long_running_commandes usar el scriptcomando que ejecuta su long_running_commanden un pseudo terminal (pty).

script -q /dev/null long_running_command | print_progress      # FreeBSD, Mac OS X
script -c "long_running_command" /dev/null | print_progress    # Linux
Chad
fuente
15
+1 buen truco, ya que scriptes un comando tan antiguo, debería estar disponible en todas las plataformas tipo Unix.
Aaron Digulla
55
también necesita -qen Linux:script -q -c 'long_running_command' /dev/null | print_progress
jfs
1
Parece que el script lee stdin, lo que hace que sea imposible ejecutarlo long_running_commanden segundo plano, al menos cuando se inicia desde un terminal interactivo. Para solucionarlo, pude redirigir stdin desde /dev/null, ya que mi long_running_commandno usa stdin.
haridsv
1
Incluso funciona en Android.
not2qubit
3
Una desventaja significativa: ctrl-z ya no funciona (es decir, no puedo suspender el script). Esto puede solucionarse, por ejemplo: echo | sudo script -c / usr / local / bin / ec2-snapshot-all / dev / null | ts, si no te importa no poder interactuar con el programa.
rlpowell
66

Para grep, sedy awkpuede forzar la salida para que sea almacenada en línea. Puedes usar:

grep --line-buffered

Fuerce la salida para que sea almacenada en línea. De manera predeterminada, la salida está protegida en línea cuando la salida estándar es un terminal y el bloque está protegido de otra manera.

sed -u

Hacer que la línea de salida sea amortiguada.

Consulte esta página para obtener más información: http://www.perkin.org.uk/posts/how-to-fix-stdio-buffering.html

yaneku
fuente
51

Si es un problema con el libc modificando su almacenamiento en búfer / vaciado cuando la salida no va a una terminal, debe intentar socat . Puede crear una secuencia bidireccional entre casi cualquier tipo de mecanismo de E / S. Uno de ellos es un programa bifurcado que habla a un pseudo tty.

 socat EXEC:long_running_command,pty,ctty STDIO 

Lo que hace es

  • crear un pseudo tty
  • fork long_running_command con el lado esclavo de la pty como stdin / stdout
  • establecer una secuencia bidireccional entre el lado maestro de la pty y la segunda dirección (aquí está STDIO)

Si esto le da el mismo resultado que long_running_command, entonces puede continuar con una tubería.

Editar: Wow ¡No vi la respuesta de unbuffer! Bueno, socat es una gran herramienta de todos modos, así que podría dejar esta respuesta

shodanex
fuente
1
... y no sabía acerca de socat, se parece un poco a netcat, pero tal vez más. ;) Gracias y +1.
3
Yo usaría socat -u exec:long_running_command,pty,end-close -aquí
Stéphane Chazelas
20

Puedes usar

long_running_command 1>&2 |& print_progress

El problema es que libc hará un buffer de línea cuando stdout a la pantalla, y un buffer completo cuando stdout a un archivo. Pero sin buffer para stderr.

No creo que sea el problema con el buffer de tubería, se trata de la política de buffer de libc.

Wang HongQin
fuente
Tienes razón; mi pregunta sigue siendo: ¿cómo puedo influir en la política de búfer de libc sin volver a compilar?
Aaron Digulla
@ StéphaneChazelas fd1 será redirigido a stderr
Wang HongQin
@ StéphaneChazelas no entiendo tu argumento. por favor haga una prueba, funciona
Wang HongQin
3
Bien, lo que está sucediendo es que con ambos zsh(de donde |&viene adaptado de csh) y bash, cuando lo haces cmd1 >&2 |& cmd2, ambos fd 1 y 2 están conectados a la salida estándar externa. Por lo tanto, funciona para evitar el almacenamiento en búfer cuando esa salida externa es un terminal, pero solo porque la salida no pasa por la tubería (por lo que print_progressno imprime nada). Entonces es lo mismo que long_running_command & print_progress(excepto que print_progress stdin es una tubería que no tiene escritor). Puede verificar en ls -l /proc/self/fd >&2 |& catcomparación con ls -l /proc/self/fd |& cat.
Stéphane Chazelas
3
Eso es porque |&es la abreviatura de 2>&1 |, literalmente. Así cmd1 |& cmd2es cmd1 1>&2 2>&1 | cmd2. Entonces, tanto fd 1 como 2 terminan conectados al stderr original, y no queda nada escrito en la tubería. ( s/outer stdout/outer stderr/gen mi comentario anterior)
Stéphane Chazelas
11

Solía ​​ser el caso, y probablemente sigue siendo el caso, que cuando la salida estándar se escribe en un terminal, se almacena la línea de forma predeterminada, cuando se escribe una nueva línea, la línea se escribe en el terminal. Cuando la salida estándar se envía a una tubería, está completamente protegida, por lo que los datos solo se envían al siguiente proceso en la tubería cuando se llena la memoria intermedia de E / S estándar.

Esa es la fuente del problema. No estoy seguro de si hay mucho que pueda hacer para solucionarlo sin modificar el programa que se escribe en la tubería. Puede usar la setvbuf()función con la _IOLBFbandera para poner incondicionalmente stdouten modo de línea de búfer. Pero no veo una manera fácil de aplicar eso en un programa. O el programa puede hacerlo fflush()en los puntos apropiados (después de cada línea de salida), pero se aplica el mismo comentario.

Supongo que si reemplaza la tubería con un pseudo-terminal, la biblioteca de E / S estándar pensaría que la salida es un terminal (porque es un tipo de terminal) y alinearía el búfer automáticamente. Sin embargo, esa es una forma compleja de lidiar con las cosas.

Jonathan Leffler
fuente
7

Sé que esta es una pregunta antigua y que ya tenía muchas respuestas, pero si desea evitar el problema del búfer, intente algo como:

stdbuf -oL tail -f /var/log/messages | tee -a /home/your_user_here/logs.txt

Esto generará en tiempo real los registros y también los guardará en el logs.txtarchivo y el búfer ya no afectará el tail -fcomando.

Marin Nedea
fuente
44
Esta parece ser la segunda respuesta: - /
Aaron Digulla
2
stdbuf está incluido en gnu coreutils (verifiqué en la última versión 8.25). verificado esto funciona en un Linux incrustado.
zhaorufei
De la documentación de stdbuf, NOTE: If COMMAND adjusts the buffering of its standard streams ('tee' does for example) then that will override corresponding changes by 'stdbuf'.
musaraña
6

No creo que el problema sea con la tubería. Parece que su proceso de larga ejecución no está volcando su propio búfer con la frecuencia suficiente. Cambiar el tamaño del búfer de la tubería sería un truco para evitarlo, pero no creo que sea posible sin reconstruir el kernel, algo que no querría hacer como un pirateo, ya que probablemente afecte a muchos otros procesos.


fuente
18
La causa raíz es que libc cambia a 4k buffering si stdout no es un tty.
Aaron Digulla
55
Eso es muy interesante ! porque la tubería no causa ningún buffering. Proporcionan almacenamiento en búfer, pero si lee desde una tubería, obtiene todos los datos disponibles, no tiene que esperar un búfer en la tubería. Entonces, el culpable sería el búfer stdio en la aplicación.
3

De acuerdo con esta publicación aquí , podría intentar reducir el límite de la tubería a un solo bloque de 512 bytes. Ciertamente no desactivará el almacenamiento en búfer, pero bueno, 512 bytes es mucho menor que 4K: 3

RAKK
fuente
3

De manera similar a la respuesta de Chad , puedes escribir un pequeño guión como este:

# save as ~/bin/scriptee, or so
script -q /dev/null sh -c 'exec cat > /dev/null'

Luego use este scripteecomando como reemplazo de tee.

my-long-running-command | scriptee

Por desgracia, parece que no puedo obtener una versión como esa que funcione perfectamente en Linux, por lo que parece estar limitado a los unixes de estilo BSD.

En Linux, esto está cerca, pero no recibe su solicitud cuando finaliza (hasta que presiona enter, etc.) ...

script -q -c 'cat > /proc/self/fd/1' /dev/null
jwd
fuente
¿Por qué funciona eso? ¿El "script" desactiva el almacenamiento en búfer?
Aaron Digulla
@Aaron Digulla: scriptemula un terminal, así que sí, creo que desactiva el almacenamiento en búfer. También repite cada carácter que se le envía, por lo que catse envía /dev/nullen el ejemplo. En lo que respecta al programa que se ejecuta dentro script, se trata de una sesión interactiva. Creo que es similar a expecteste respecto, pero scriptprobablemente sea parte de su sistema base.
jwd
La razón que uso teees para enviar una copia de la transmisión a un archivo. ¿Dónde se especifica el archivo scriptee?
Bruno Bronosky
@BrunoBronosky: Tienes razón, es un mal nombre para este programa. Realmente no está haciendo una operación 'tee'. Solo está deshabilitando el almacenamiento en búfer de la salida, según la pregunta original. Tal vez debería llamarse "scriptcat" (aunque tampoco está haciendo concatenación ...). De todos modos, puede reemplazar el catcomando con tee myfile.txt, y debería obtener el efecto que desea.
jwd
2

Encontré esta solución inteligente: (echo -e "cmd 1\ncmd 2" && cat) | ./shell_executable

Esto hace el truco. catleerá entradas adicionales (hasta EOF) y las pasará a la tubería después de que echohaya puesto sus argumentos en la secuencia de entrada de shell_executable.

jaggedsoft
fuente
2
En realidad, catno ve la salida de echo; solo ejecuta dos comandos en una subshell y la salida de ambos se envía a la tubería. El segundo comando en el subshell ('cat') se lee desde el stdin principal / externo, por eso funciona.
Aaron Digulla
0

De acuerdo con esto, el tamaño del búfer de tubería parece estar configurado en el núcleo y requeriría que vuelva a compilar su núcleo para modificarlo.


fuente
77
Creo que es un buffer diferente.
Samuel Edwin Ward