La respuesta de Gilles explica la condición de la carrera. Solo voy a responder esta parte:
¿Hay alguna forma de forzar a este script a generar siempre 0 líneas (para que la redirección de E / S a tmp siempre se prepare primero y los datos siempre se destruyan)? Para ser claros, me refiero a cambiar la configuración del sistema
IDK si ya existe una herramienta para esto, pero tengo una idea de cómo se podría implementar. (Pero tenga en cuenta que esto no siempre sería 0 líneas, solo un probador útil que atrapa razas simples como esta fácilmente y algunas razas más complicadas. Vea el comentario de @Gilles ). No garantizaría que un guión fuera seguro , pero podría ser una herramienta útil en las pruebas, similar a la prueba de un programa multiproceso en diferentes CPU, incluidas las CPU no x86 de orden débil como ARM.
Lo ejecutarías como racechecker bash foo.sh
Use las mismas instalaciones de rastreo / interceptación de llamadas del sistema que strace -f
y ltrace -f
use para adjuntar a cada proceso secundario. (En Linux, esta es la misma ptrace
llamada al sistema utilizada por GDB y otros depuradores para establecer puntos de interrupción, un solo paso y modificar la memoria / registros de otro proceso).
Instrumento las open
y openat
llamadas al sistema: cuando cualquier proceso que se ejecuta bajo esta herramienta hace una una open(2)
llamada al sistema (o openat
) con O_RDONLY
, el sueño de tal medio o 1 segundo. Deje que otras open
llamadas del sistema (especialmente las incluidas O_TRUNC
) se ejecuten sin demora.
Esto debería permitir que el escritor gane la carrera en casi todas las condiciones de carrera, a menos que la carga del sistema también sea alta, o sea una condición de carrera complicada donde el truncamiento no ocurrió hasta después de alguna otra lectura. Por lo tanto, la variación aleatoria de qué open()
s (y tal vez read()
s o escrituras) se retrasan aumentaría el poder de detección de esta herramienta, pero, por supuesto, sin probar durante una cantidad infinita de tiempo con un simulador de retraso que eventualmente cubrirá todas las situaciones posibles que pueda encontrar en En el mundo real, no puedes estar seguro de que tus guiones estén libres de carreras a menos que los leas cuidadosamente y demuestres que no lo son.
Probablemente lo necesite para incluir en la lista blanca (no retrasar open
) los archivos /usr/bin
y, /usr/lib
por lo tanto, el inicio del proceso no dura para siempre. (La vinculación dinámica en tiempo de ejecución tiene que ver con open()
varios archivos (mire strace -eopen /bin/true
o en /bin/ls
algún momento), aunque si el shell principal está haciendo el truncamiento, eso estará bien. Pero aún será bueno para esta herramienta no hacer scripts irrazonablemente lentos).
O tal vez la lista blanca de cada archivo que el proceso de llamada no tiene permiso para truncar en primer lugar. es decir, el proceso de rastreo puede realizar una access(2)
llamada al sistema antes de suspender realmente el proceso que deseaba en open()
un archivo.
racechecker
tendría que estar escrito en C, no en shell, pero tal vez podría usar strace
el código de '' como punto de partida '' y podría no tomar mucho trabajo implementarlo.
Tal vez podría obtener la misma funcionalidad con un sistema de archivos FUSE . Probablemente haya un ejemplo de FUSE de un sistema de archivos de paso puro, por lo que podría agregar controles a la open()
función en lo que hace que se suspenda para las aperturas de solo lectura pero permita que el truncamiento ocurra de inmediato.
racechecker
todo el tiempo. Y probablemente desee abrir el tiempo de espera de lectura para que sea configurable para el beneficio de las personas en máquinas muy cargadas que desean configurarlo más alto, como 10 segundos. O configúrelo más bajo, como 0.1 segundos para secuencias de comandos largas o ineficientes que vuelven a abrir muchos archivos .¿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
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):Juega con variaciones hasta que te sientas cómodo con lo que observas.
Dado el comando compuesto
el proceso de la izquierda hace lo siguiente (solo he enumerado los pasos que son relevantes para mi explicación):
cat
con el argumentotmp
.tmp
para lectura.El proceso de la derecha hace lo siguiente:
tmp
, truncando el archivo en el proceso.head
con el argumento-1
.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 leyeratmp
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:
o
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 ejemploo 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.
La colección moreutils incluye un programa que hace exactamente eso, llamado
sponge
.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.fuente
strace
(es decir, Linuxptrace
) para hacer que lasopen
llamadas al sistema de lectura completa (en todos los procesos secundarios) duerman durante medio segundo, así que cuando corres con un truncamiento, el truncamiento casi siempre ganará.