- Las tuberías que encadenan varios comandos con una salida que llega lentamente, como
tail -f /some/log/file | grep thing1 | grep thing2, en realidad pueden no estar atascadas; pueden verse vacías porque el comando intermedio acumula la salida en un búfer grepy muchos programas revisan conisattysi stdout es una terminal; si lo es, usan buffering por línea, y si es una tubería o archivo, suelen usar buffering por bloques de ~8 KBtail,catyteeson ejemplos que no hacen buffering de salida, pero las opciones para reducirlo varían según el comando, comogrep --line-buffered,sed -u,tcpdump -l,jq -u,tr -u- Si interrumpes la tubería con
Ctrl-C, puede perderse la salida que seguía en el búfer de programas comotcpdump; si terminas conkill -TERM $PID, el búfer puede vaciarse y la salida hacerse visible - Las soluciones prácticas incluyen cambiar a comandos que terminen rápido, usar
grep --line-buffered, un soloawko ungrepmás complejo,stdbufounbuffer, pero en cada caso hay que revisar las condiciones de funcionamiento y efectos secundarios
Por qué una tubería parece haberse detenido
- Cuando se agregan líneas lentamente a un archivo de logs, la siguiente tubería puede no mostrar salida aunque sí haya coincidencias
tail -f /some/log/file | grep thing1 | grep thing2
- La causa no es la tubería en sí, sino que el
grep thing1intermedio no escribe el resultado de inmediato, sino que lo guarda en un búfer - Si un programa escribiera inmediatamente cada vez, aumentaría la cantidad de llamadas al sistema, así que por rendimiento junta cierta cantidad de datos antes de escribir en una tubería o archivo
- En este ejemplo,
grep thing1puede esperar hasta acumular aproximadamente 8 KB de salida, y con logs lentos esa condición puede no cumplirse nunca en la práctica
La salida cambia entre terminal y tubería
tail -f file | grep thingfunciona bien, pero si agregas un segundogrep, la salida puede parecer detenidagrepy muchos programas revisan con la funciónisattysi stdout es una terminal- Si stdout es una terminal, usan buffering por línea y muestran cada línea de inmediato
- Si stdout es una tubería o archivo, usan buffering por bloques y escriben cuando se acumula una cierta cantidad de datos
- Por eso, si
grepescribe directamente en la terminal ves las líneas enseguida, pero si escribe a una tubería conectada al siguiente comando puede que no veas nada - El tamaño del búfer varía según el programa
- En
grep, el buffering lo maneja libc, y su tamaño está definido por la variableBUFSIZ - La ubicación de la definición en glibc está en stdio.h
- En
- No es una ley física que al escribir en una terminal no se use un búfer de salida de 8 KB; un programa podría implementarlo si quisiera, aunque sería un comportamiento bastante extraño
El buffering cambia según el comando
- Lo complicado del buffering de salida es que el usuario tiene que recordar qué comandos hacen buffering cuando escriben a una tubería
- Algunos ejemplos de comandos que no hacen buffering de salida son los siguientes
tailcattee
- Estos son comandos comunes que sí hacen buffering al escribir a una tubería, junto con formas de mitigarlo
grep:--line-bufferedsed:-uawk: funciónfflush()tcpdump:-ljq:-utr:-ucut: no se puede desactivar el buffering
- En comandos como
sort, que solo pueden trabajar después de recibir toda la entrada, el buffering no importa mucho en la práctica - Se intentó probar tanto las versiones de Mac OS como las de GNU, pero como hay muchas variantes puede haber algunos errores
La salida predeterminada de los lenguajes de programación también hace buffering
- La salida predeterminada de
printen algunos lenguajes de programación también hace buffering al escribir a una tubería - Estas son formas de desactivarlo según el lenguaje
- C:
setvbuf - Python:
python -u,PYTHONUNBUFFERED=1,sys.stdout.reconfigure(line_buffering=False),print(x, flush=True) - Ruby:
STDOUT.sync = true - Perl:
$| = 1
- C:
- Este comportamiento predeterminado parece estar diseñado para hacer más rápidas las funciones de salida estándar en procesamiento por lotes
- Si hay buffering o no también puede cambiar según la forma de escribir la salida
- En C++,
cout << "hello\n"hace buffering al escribir a una tubería cout << "hello" << endlvacía la salida
- En C++,
Diferencias con Ctrl-C y la redirección a archivo
- Si conectas la salida de
tcpdumpagrepcomo sigue y omites-l, la salida puede quedarse en el búfersudo tcpdump -ni any port 53 | grep example.com
- Idealmente, al presionar
Ctrl-Cesperarías quetcpdumpvaciara el búfer y quegrepbuscara en él para mostrar la salida faltante - En la práctica, al terminar los programas se pierde la salida que seguía en el búfer de
tcpdump - Al verificar con
strace,greprecibeSIGINTantes quetcpdump, así que aunquetcpdumpintente vaciar el búfer,greppuede ya haber terminado - Como alternativa, puedes buscar el PID de
tcpdumpy ejecutarkill -TERM $PID; asítcpdumppuede vaciar el búfer y la salida podría hacerse visible - La redirección a archivo también hace buffering
sudo tcpdump -ni any port 53 > output.txt
- Sin embargo, a diferencia del problema de
Ctrl-C, donde el contenido del búfer puede perderse por completo, con la redirección a archivo suele pasar, al menos en la experiencia observada, que el contenido del búfer se escribe en el archivo antes de que termine el programa - No está claro si siempre se puede confiar en ese comportamiento
Cinco formas de evitar el buffering
-
Cambiar a programas que terminen rápido
- Puedes evitar por completo la situación de escribir lentamente a una tubería cambiando a comandos que terminen rápido
- Por ejemplo
cat /some/log/file | grep thing1 | grep thing2 | tail
- No es exactamente el mismo comportamiento que el comando original con
tail -f, pero evita los problemas complejos de buffering
-
Usar la opción de buffering por línea de
grepgreptiene una opción para evitar el buffering- Por ejemplo
tail -f /some/log/file | grep --line-buffered thing1 | grep thing2
-
Unir todo con
awko con ungrepmás complejo- En situaciones donde usas varios
grep, puedes cambiarlo por un soloawktail -f /some/log/file | awk '/thing1/ && /thing2/'
- O también puedes escribirlo con un
grepde expresión regular más complejotail -f /some/log/file | grep -E 'thing1.*thing2'
awktambién hace buffering, así que para que este método funcione,awktiene que ser el último comando de la tubería
- En situaciones donde usas varios
-
Usar
stdbufstdbufusaLD_PRELOADpara desactivar el buffering de libc- Ejemplo para desactivar el buffering de salida
tail -f /some/log/file | stdbuf -o0 grep thing1 | grep thing2
- Como otras soluciones basadas en
LD_PRELOAD, su confiabilidad es limitada- No funciona con binarios estáticos
- Puede no funcionar si el programa no usa el buffering de libc
- No siempre funciona en Mac OS
- Una explicación relacionada está en How stdbuf works de Harry Marr
-
Usar
unbufferunbuffer programfuerza a que la salida del programa parezca una TTY, para que use menos buffering y también funciones como salida con color, igual que en una TTY normal- Por ejemplo
tail -f /some/log/file | unbuffer grep thing1 | grep thing2
- A diferencia de
stdbuf, siempre funciona, pero puede traer efectos secundarios no deseados- Por ejemplo,
grep thing1puede agregar color a los resultados coincidentes
- Por ejemplo,
unbufferviene en el paqueteexpect
Cuándo suele aparecer este problema y la idea de una variable de entorno
- Este problema suele aparecer sobre todo en programas que envían datos lentamente por una tubería
- Algunos ejemplos son los siguientes
tcpdumptail -f- monitoreo de logs al estilo de
kubectl logs - la salida de cálculos lentos
- Podría ser útil tener una variable de entorno estándar para desactivar el buffering, como
PYTHONUNBUFFEREDen Python - La idea viene de una entrada de blog de 2018 de Mark Dominus y una publicación posterior
- Como ejemplo de nombre, algo como
NO_BUFFER, al estilo deNO_COLOR, podría ser posible - El diseño es complicado
- En NETBSD existen variables de entorno como
STDBUF,STDBUF1, etc. que dan mucho control - Puede que la mayoría de desarrolladores no quiera implementar varias variables de entorno para un caso borde relativamente pequeño
- En NETBSD existen variables de entorno como
- También surge la duda de si existen programas que vacíen automáticamente el búfer de salida cada cierto tiempo, por ejemplo cada 1 segundo, pero no viene ninguno a la mente y podría tener desventajas
Alcance que no se cubrió
- No se trata la diferencia entre buffering por línea y salida completamente sin buffering
- No se trata la diferencia entre el buffering de stderr y el de stdout
- Este contenido solo trata el buffering que ocurre dentro del programa
- El driver TTY del sistema operativo también a veces hace un poco de buffering
- No se cubren otras razones para vaciar la salida además del caso de escribir a una tubería
1 comentarios
Opiniones de Hacker News
Un enfoque con buffering casi siempre debería ser de “umbral o timeout”: hacer flush cuando se alcanza un umbral de bytes, o después de cierto tiempo si hay al menos 1 byte
Es una forma común en interfaces de hardware para resolver problemas similares
En este caso, una biblioteca que hace buffering en espacio de usuario debería configurar un temporizador adecuado cuando pone datos por primera vez en el buffer. El valor del timeout podría recibirse como argumento, fijarse en algo corto para la percepción humana, como 1~100 ms, hacerse proporcional a
{ancho de banda / umbral}, o elegirse de modo que el overhead de las llamadas al sistema no supere el 0.1% del tiempo totalEste enfoque se aplica no solo a escrituras, sino también a lecturas. Si se hacen lecturas por lotes o lecturas combinadas, se necesita algo parecido, pero depende más del diseño del canal, porque el canal de datos debe tener una forma eficiente de consultar o recibir notificaciones sobre “datos pendientes”. En hardware, son comunes técnicas como la coalescencia de interrupciones
Los errores de E/S podrían ocurrir no solo al momento de escribir, sino en cualquier momento, y varios llamados al sistema podrían ser interrumpidos por el temporizador, no solo donde el programa puso su propio temporizador o donde llegó una señal
También podría haber confusión si tanto la aplicación como libc configuran temporizadores. Las APIs modernas de temporizadores del kernel parecen mejores que como las recuerdo, así que quizá esto sea menos relevante, pero si la aplicación bloquea señales temporalmente en una sección crítica, eso también afecta al temporizador de E/S
Por el momento y la forma en que se manejan las señales, habría que tener más cuidado al acceder a las estructuras de E/S
Con io_uring y temporizadores en espacio de usuario esto escala mucho mejor, pero para soportar muchas escrituras pequeñas y rápidas todavía hacen falta trucos. Por ejemplo, por encima de aproximadamente 1 millón por segundo el costo de gestionar temporizadores empieza a notarse, y para llegar a 100 millones de escrituras por segundo hicieron falta técnicas bastante extrañas
La causa del problema es mezclar algo que debería ser interactivo con un contrato que no presupone interacción. Por ejemplo, cuando se manda por pipe la salida de seguimiento de
tailNo creo que haya un problema real que resolver. Usando una analogía de hardware, sería como un tanque que junta agua de lluvia y solo la mueve cuando está lleno. No sé qué ejemplo tienen en mente, pero hasta donde sé, en hardware no es común el flush basado en tiempo
La corrección propuesta hace que el contrato sea mucho más complejo
El problema no está tanto en la semántica en sí, sino en no conocer la semántica
Llevo más de 20 años manejando sistemas NIX y sé que esto pasa, pero cada vez solo me acuerdo después de pasar un buen rato preguntándome por qué no aparece la salida
Sobre la parte que dice “los artículos recientes se están volviendo bastante largos; ¿de verdad alguien querría leer 3000 palabras sobre buffering?”, personalmente sí quisiera leerlo
Creo que a veces los textos largos son relleno para optimización en buscadores
También se podría tener secciones TLDR y NTLDR, es decir, “lo leí aunque era largo”
Me gustaría que todos los buffers se hicieran flush cada vez que la CPU de todo el sistema queda inactiva
El buffering, en general, es una técnica para ahorrar CPU. Si la CPU fuera infinita, todos los buffers serían de 1 byte. Los buffers juntan datos para procesarlos por lotes en favor de la eficiencia
Pero cuando la CPU queda inactiva, no debería quedar “trabajo para después”. En cuanto el scheduler del kernel quede inactivo, debería enviar una señal a todos los procesos para hacer flush de los buffers
Sería hacer todo ese trabajo y luego obligarlos a hacer llamadas al sistema para el flush del buffer. Tal vez se podría agregar un mecanismo para que el kernel conozca los buffers en espacio de usuario y, cuando esté inactivo, los tome directamente desde ahí
Me pregunto si esto se parece en cierta medida a io_uring https://man7.org/linux/man-pages/man3/io_uring_register_buff...
Este artículo confunde dos cosas distintas: sin buffer y buffering por línea
Sin buffer empeora innecesariamente el rendimiento y puede producir una salida incorrecta cuando varias fuentes escriben al mismo pipe. Las líneas suficientemente largas se mezclarán de todos modos, pero en la práctica la mayoría de las líneas de salida miden menos de 4096 bytes, incluso incluyendo caracteres de formato/control y caracteres de planos suplementarios
El buffering por línea es el valor predeterminado en terminales, y normalmente también es el comportamiento deseado en pipes. Basta con ejecutar cada comando bajo
stdbuf -oL -eL. Los raros programas que quieren actualizar dentro de una misma línea ya tienen que hacer flush manual, así que también se comportan correctamente aquíLo que
stdbufhace en realidad puede verse así:env -i \command -v stdbuf` -oL -eL `command -v env``Hace tiempo escribí sobre este problema: https://world-playground-deceit.net/blog/2024/09/bourne_shel...
En cuanto a los comandos que no hacen buffering, eso depende de la implementación o, en el caso de
cat, quizá sea incorrecto. Ver https://pubs.opengroup.org/onlinepubs/9799919799/utilities/c... y-u. Es un gran dolor que POSIX no haya incluido una forma oficial de manejar esto.Algo que no se mencionó es el buffering de entrada, que produce resultados raros como este:
$ seq 5 | { v1=$(head -1); v2=$(head -1); printf '%s=%s\n' v1 "$v1" v2 "$v2"; }v1=1v2=En este caso, la solución es usar
stdbuf -i0 head -1.socketpairpueda imponerle ese tipo de restricción al proceso que escribe, salvo usando hacks pesados comoptrace().Quizá sea posible ajustar el tamaño del búfer del pipe, pero no conozco una convención según la cual la E/S estándar de C deba respetarlo.
De todos modos, en este caso
stdbufno parece ayudar:$ ./a | stdbuf -i0 -- cat#include#includeint main(void) {for (;;) {printf("n");usleep(100000);}}Hay buenas razones para que existan los búferes. Imprimir salida en la pantalla es relativamente muy lento en comparación con escribir en un búfer.
Imprimir caracteres uno por uno es tremendamente ineficiente.
Es un problema antiguo y se encuentra a menudo al trabajar con UART. Hay varias soluciones posibles: un enfoque basado en líneas que marca el final de la salida con un carácter especial como una nueva línea; uno basado en longitud, que espera hasta alcanzar un tamaño como 8 KB; o uno basado en tiempo, que emite salida cada X milisegundos.
Cada enfoque tiene sus ventajas y desventajas, y lo óptimo depende de la aplicación. Creo que la parte del artículo que dice que algunos programas no usan buffering es incorrecta. Esos programas simplemente no usan un enfoque basado en longitud evidente.
El enfoque basado en líneas es así, pero requiere ponerse de acuerdo sobre qué carácter usar. Normalmente es una nueva línea.
/dev/nullpor sí solo también puede destruir mucho el rendimiento.Llevo más de 35 años usando Unix, pero nunca había entendido del todo cómo funcionaba esto.
Me gustó que explicara de forma integral el comportamiento del buffering a través de varios sistemas y componentes, y definitivamente aprendí algo.
Sobre la parte de que “si presionas Ctrl-C en un pipe, desaparece el contenido del búfer”, creo que la mayoría de los programas vaciarían el búfer ante SIGINT.
Pero para que eso funcione así en el shell, tendría que enviar SIGINT solo al primer programa del pipeline, y probablemente no sea así como funciona en realidad.
siginty los demás recibensigpipe.