¿Por qué hay una condición de carrera?
Los dos lados de una tubería se ejecutan en paralelo, no uno después del otro. Hay una manera muy simple de demostrar esto: ejecutar
time sleep 1 | sleep 1
Esto lleva un segundo, no dos.
El shell inicia dos procesos secundarios y espera a que ambos se completen. Estos dos procesos se ejecutan en paralelo: la única razón por la cual uno de ellos se sincronizaría con el otro es cuando necesita esperar al otro. El punto de sincronización más común es cuando el lado derecho bloquea la espera de datos para leer en su entrada estándar, y se desbloquea cuando el lado izquierdo escribe más datos. Lo contrario también puede ocurrir, cuando el lado derecho es lento para leer datos y el lado izquierdo bloquea su operación de escritura hasta que el lado derecho lee más datos (hay un búfer en la tubería, administrado por el kernel, pero tiene un tamaño máximo pequeño).
Para observar un punto de sincronización, observe los siguientes comandos ( sh -x
imprime cada comando a medida que lo ejecuta):
time sh -x -c '{ sleep 1; echo a; } | { cat; }'
time sh -x -c '{ echo a; sleep 1; } | { cat; }'
time sh -x -c '{ echo a; sleep 1; } | { sleep 1; cat; }'
time sh -x -c '{ sleep 2; echo a; } | { cat; sleep 1; }'
Juega con variaciones hasta que te sientas cómodo con lo que observas.
Dado el comando compuesto
cat tmp | head -1 > tmp
el proceso de la izquierda hace lo siguiente (solo he enumerado los pasos que son relevantes para mi explicación):
- Ejecute el programa externo
cat
con el argumento tmp
.
- Abierto
tmp
para lectura.
- Si bien no ha llegado al final del archivo, lea un fragmento del archivo y escríbalo en la salida estándar.
El proceso de la derecha hace lo siguiente:
- Redirige la salida estándar a
tmp
, truncando el archivo en el proceso.
- Ejecute el programa externo
head
con el argumento -1
.
- Lea una línea de la entrada estándar y escríbala en la salida estándar.
El único punto de sincronización es que right-3 espera a que left-3 haya procesado una línea completa. No hay sincronización entre left-2 y right-1, por lo que pueden ocurrir en cualquier orden. El orden en que suceden no es predecible: depende de la arquitectura de la CPU, del shell, del kernel, en qué núcleos se programan los procesos, de las interrupciones que recibe la CPU en ese momento, etc.
¿Cómo cambiar el comportamiento?
No puede cambiar el comportamiento cambiando una configuración del sistema. La computadora hace lo que le dices que haga. Le dijiste que truncara tmp
y leyera tmp
en paralelo, por lo que hace las dos cosas en paralelo.
Ok, hay una "configuración del sistema" que podría cambiar: podría reemplazar /bin/bash
por un programa diferente que no sea bash. Espero que sea evidente que no es una buena idea.
Si desea que el truncamiento ocurra antes del lado izquierdo de la tubería, debe colocarlo fuera de la tubería, por ejemplo:
{ cat tmp | head -1; } >tmp
o
( exec >tmp; cat tmp | head -1 )
Sin embargo, no tengo idea de por qué querrías esto. ¿Qué sentido tiene leer un archivo que sabes que está vacío?
Por el contrario, si desea que la redirección de salida (incluido el truncamiento) suceda después de que cat
haya terminado de leer, entonces necesita almacenar completamente los datos en la memoria, por ejemplo
line=$(cat tmp | head -1)
printf %s "$line" >tmp
o escriba en un archivo diferente y luego muévalo a su lugar. Esta suele ser la forma sólida de hacer cosas en scripts, y tiene la ventaja de que el archivo se escribe por completo antes de que sea visible a través del nombre original.
cat tmp | head -1 >new && mv new tmp
La colección moreutils incluye un programa que hace exactamente eso, llamado sponge
.
cat tmp | head -1 | sponge tmp
Cómo detectar el problema automáticamente
Si su objetivo era tomar guiones mal escritos y descubrir automáticamente dónde se rompen, entonces lo siento, la vida no es tan simple. El análisis de tiempo de ejecución no encontrará el problema de manera confiable porque a veces cat
termina de leer antes de que ocurra el truncamiento. El análisis estático puede en principio hacerlo; Shellcheck capta el ejemplo simplificado de su pregunta , pero puede que no detecte un problema similar en un script más complejo.