¿Qué impide que stdout / stderr se entrelacen?

13

Digamos que ejecuto algunos procesos:

#!/usr/bin/env bash

foo &
bar &
baz &

wait;

Ejecuto el script anterior así:

foobarbaz | cat

Por lo que puedo decir, cuando cualquiera de los procesos escribe en stdout / stderr, su salida nunca se intercala: cada línea de stdio parece ser atómica. ¿Cómo funciona? ¿Qué utilidad controla cómo cada línea es atómica?

Alexander Mills
fuente
3
¿Cuántos datos generan tus comandos? Intenta hacer que produzcan unos kilobytes.
Kusalananda
¿Quiere decir que uno de los comandos genera unos pocos kb antes de una nueva línea?
Alexander Mills
No, algo como esto: unix.stackexchange.com/a/452762/70524
muru

Respuestas:

22

¡Se intercalan! Solo probó ráfagas de salida cortas, que permanecen sin dividir, pero en la práctica es difícil garantizar que cualquier salida en particular permanezca sin dividir.

Búfer de salida

Depende de cómo los programas amortiguan su salida. La biblioteca stdio que la mayoría de los programas usan cuando escriben usa buffers para hacer que la salida sea más eficiente. En lugar de generar datos tan pronto como el programa llama a una función de biblioteca para escribir en un archivo, la función almacena estos datos en un búfer y solo genera los datos una vez que el búfer se ha llenado. Esto significa que la salida se realiza en lotes. Más precisamente, hay tres modos de salida:

  • Sin búfer: los datos se escriben inmediatamente, sin usar un búfer. Esto puede ser lento si el programa escribe su salida en pequeños pedazos, por ejemplo, carácter por carácter. Este es el modo predeterminado para el error estándar.
  • Totalmente almacenado en búfer: los datos solo se escriben cuando el búfer está lleno. Este es el modo predeterminado cuando se escribe en una tubería o en un archivo normal, excepto con stderr.
  • Búfer de línea: los datos se escriben después de cada nueva línea o cuando el búfer está lleno. Este es el modo predeterminado al escribir en un terminal, excepto con stderr.

Los programas pueden reprogramar cada archivo para que se comporte de manera diferente y pueden vaciar explícitamente el búfer. El búfer se vacía automáticamente cuando un programa cierra el archivo o sale normalmente.

Si todos los programas que escriben en la misma tubería usan el modo de búfer de línea o usan el modo de no búfer y escriben cada línea con una sola llamada a una función de salida, y si las líneas son lo suficientemente cortas como para escribir en un solo fragmento, entonces la salida será un entrelazado de líneas enteras. Pero si uno de los programas usa el modo totalmente protegido, o si las líneas son demasiado largas, verá líneas mixtas.

Aquí hay un ejemplo donde intercalo la salida de dos programas. Usé GNU coreutils en Linux; diferentes versiones de estas utilidades pueden comportarse de manera diferente.

  • yes aaaaescribe aaaapara siempre en lo que es esencialmente equivalente al modo de almacenamiento en línea. La yesutilidad en realidad escribe varias líneas a la vez, pero cada vez que emite salida, la salida es un número entero de líneas.
  • echo bbbb; done | grep bescribe bbbbpara siempre en modo totalmente protegido. Utiliza un tamaño de búfer de 8192, y cada línea tiene una longitud de 5 bytes. Como 5 no divide 8192, los límites entre escrituras no están en un límite de línea en general.

Vamos a lanzarlos juntos.

$ { yes aaaa & while true; do echo bbbb; done | grep b & } | head -n 999999 | grep -e ab -e ba
bbaaaa
bbbbaaaa
baaaa
bbbaaaa
bbaaaa
bbbaaaa
ab
bbbbaaa

Como puede ver, sí a veces interrumpió grep y viceversa. Solo alrededor del 0.001% de las líneas se interrumpieron, pero sucedió. La salida es aleatoria, por lo que el número de interrupciones variará, pero vi al menos algunas interrupciones cada vez. Habría una fracción mayor de líneas interrumpidas si las líneas fueran más largas, ya que la probabilidad de una interrupción aumenta a medida que disminuye el número de líneas por buffer.

Hay varias formas de ajustar el búfer de salida . Los principales son:

  • Desactive el almacenamiento en búfer en los programas que usan la biblioteca stdio sin cambiar su configuración predeterminada con el programa que se stdbuf -o0encuentra en GNU coreutils y algunos otros sistemas como FreeBSD. Alternativamente, puede cambiar a almacenamiento en línea con stdbuf -oL.
  • Cambie al almacenamiento en línea al dirigir la salida del programa a través de un terminal creado solo para este propósito con unbuffer . Algunos programas pueden comportarse de manera diferente de otras maneras, por ejemplo, greputiliza colores por defecto si su salida es un terminal.
  • Configure el programa, por ejemplo, pasando --line-buffered a GNU grep.

Veamos el fragmento de arriba otra vez, esta vez con almacenamiento en línea en ambos lados.

{ stdbuf -oL yes aaaa & while true; do echo bbbb; done | grep --line-buffered b & } | head -n 999999 | grep -e ab -e ba
abbbb
abbbb
abbbb
abbbb
abbbb
abbbb
abbbb
abbbb
abbbb
abbbb
abbbb
abbbb
abbbb

Esta vez sí, nunca interrumpió grep, pero grep a veces interrumpió sí. Iré a por qué más tarde.

Intercalado de tuberías

Mientras cada programa emite una línea a la vez, y las líneas son lo suficientemente cortas, las líneas de salida estarán perfectamente separadas. Pero hay un límite para la longitud de las líneas para que esto funcione. La tubería en sí tiene un búfer de transferencia. Cuando un programa sale a una tubería, los datos se copian del programa de escritura al búfer de transferencia de la tubería, y luego desde el búfer de transferencia de la tubería al programa lector. (Al menos conceptualmente, el núcleo a veces puede optimizar esto en una sola copia).

Si hay más datos para copiar de los que caben en el búfer de transferencia de la tubería, entonces el núcleo copia un búfer lleno a la vez. Si varios programas están escribiendo en la misma tubería, y el primer programa que elige el núcleo quiere escribir más de un búfer, entonces no hay garantía de que el núcleo volverá a elegir el mismo programa la segunda vez. Por ejemplo, si P es el tamaño del búfer, fooquiere escribir 2 * P bytes y barquiere escribir 3 bytes, entonces una posible intercalación es P bytes desde foo, luego 3 bytes desde bary P bytes desde foo.

Volviendo al ejemplo anterior de yes + grep, en mi sistema, yes aaaasucede que escribe tantas líneas como caben en un búfer de 8192 bytes de una sola vez. Como hay 5 bytes para escribir (4 caracteres imprimibles y la nueva línea), eso significa que escribe 8190 bytes cada vez. El tamaño del búfer de tubería es 4096 bytes. Por lo tanto, es posible obtener 4096 bytes de sí, luego algo de salida de grep y luego el resto de la escritura de sí (8190 - 4096 = 4094 bytes). 4096 bytes deja espacio para 819 líneas con aaaay un solitario a. Por lo tanto, una línea con este solitario aseguido de una escritura desde grep, dando una línea con abbbb.

Si desea ver los detalles de lo que está sucediendo, getconf PIPE_BUF .le indicará el tamaño del búfer de tubería en su sistema y podrá ver una lista completa de las llamadas al sistema realizadas por cada programa con

strace -s9999 -f -o line_buffered.strace sh -c '{ stdbuf -oL yes aaaa & while true; do echo bbbb; done | grep --line-buffered b & }' | head -n 999999 | grep -e ab -e ba

Cómo garantizar la intercalación de líneas limpias

Si las longitudes de línea son más pequeñas que el tamaño del búfer de tubería, entonces el búfer de línea garantiza que no habrá ninguna línea mixta en la salida.

Si las longitudes de línea pueden ser mayores, no hay forma de evitar mezclas arbitrarias cuando varios programas escriben en la misma tubería. Para garantizar la separación, debe hacer que cada programa escriba en una tubería diferente y usar un programa para combinar las líneas. Por ejemplo, GNU Parallel hace esto por defecto.

Gilles 'SO- deja de ser malvado'
fuente
interesante, entonces, ¿cuál podría ser una buena manera de asegurarse de que todas las líneas se escribieran catatómicamente, de modo que el proceso cat reciba líneas completas de foo / bar / baz pero no media línea de una y media línea de otra, etc. ¿Hay algo que pueda hacer con el script bash?
Alexander Mills
1
Parece que esto también se aplica a mi caso en el que tenía cientos de archivos y awkse produjeron dos (o más) líneas de salida para la misma ID, find -type f -name 'myfiles*' -print0 | xargs -0 awk '{ seen[$1]= seen[$1] $2} END { for(x in seen) print x, seen[x] }' pero con find -type f -name 'myfiles*' -print0 | xargs -0 cat| awk '{ seen[$1]= seen[$1] $2} END { for(x in seen) print x, seen[x] }'ella se produjo correctamente solo una línea para cada ID.
αғsнιη
Para evitar cualquier intercalación, puedo hacerlo con un entorno de programación como Node.js, pero con bash / shell, no estoy seguro de cómo hacerlo.
Alexander Mills
1
@JoL Se debe al llenado del búfer de tubería. Sabía que tendría que escribir la segunda parte de la historia ... Hecho.
Gilles 'SO- deja de ser malvado'
1
@OlegzandrDenman TLDR agregado: hacen intercalar. El motivo es complicado.
Gilles 'SO- deja de ser malvado'
1

http://mywiki.wooledge.org/BashPitfalls#Non-atomic_writes_with_xargs_-P ha analizado esto:

GNU xargs admite ejecutar múltiples trabajos en paralelo. -P n donde n es el número de trabajos a ejecutar en paralelo.

seq 100 | xargs -n1 -P10 echo "$a" | grep 5
seq 100 | xargs -n1 -P10 echo "$a" > myoutput.txt

Esto funcionará bien para muchas situaciones, pero tiene una falla engañosa: si $ a contiene más de ~ 1000 caracteres, el eco puede no ser atómico (puede dividirse en múltiples llamadas de escritura ()), y existe el riesgo de que dos líneas será mezclado

$ perl -e 'print "a"x2000, "\n"' > foo
$ strace -e write bash -c 'read -r foo < foo; echo "$foo"' >/dev/null
write(1, "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"..., 1008) = 1008
write(1, "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"..., 993) = 993
+++ exited with 0 +++

Obviamente, el mismo problema surge si hay varias llamadas a echo o printf:

slowprint() {
  printf 'Start-%s ' "$1"
  sleep "$1"
  printf '%s-End\n' "$1"
}
export -f slowprint
seq 10 | xargs -n1 -I {} -P4 bash -c "slowprint {}"
# Compare to no parallelization
seq 10 | xargs -n1 -I {} bash -c "slowprint {}"
# Be sure to see the warnings in the next Pitfall!

Las salidas de los trabajos paralelos se mezclan, porque cada trabajo consta de dos (o más) llamadas de escritura () separadas.

Si necesita que las salidas no estén mezcladas, se recomienda utilizar una herramienta que garantice que la salida se serializará (como GNU Parallel).

Ole Tange
fuente
Esa sección está mal. xargs echono llama al echo bash incorporado, sino a la echoutilidad from $PATH. Y de todos modos no puedo reproducir ese comportamiento bash echo con bash 4.4. Sin embargo, en Linux, las escrituras en una tubería (no / dev / null) más grande que 4K no se garantiza que sean atómicas.
Stéphane Chazelas