¿Cómo hacer que la lectura y escritura del mismo archivo en la misma tubería siempre "falle"?

9

Digamos que tengo el siguiente script:

#!/bin/bash
for i in $(seq 1000)
do
    cp /etc/passwd tmp
    cat tmp | head -1 | head -1 | head -1 > tmp  #this is the key line
    cat tmp
done

En la línea clave, leo y escribo el mismo archivo tmpque a veces falla.

(Leí que se debe a las condiciones de carrera porque los procesos en la tubería se ejecutan en paralelo, lo que no entiendo por qué: cada uno headnecesita tomar los datos del anterior, ¿no? Esta NO es mi pregunta principal, pero también puedes responderlo).

Cuando ejecuto el script, genera alrededor de 200 líneas. ¿Hay alguna manera de forzar a este script a generar siempre 0 líneas (para que la redirección de E / S tmpsiempre se prepare primero y los datos siempre se destruyan)? Para ser claros, me refiero a cambiar la configuración del sistema, no este script.

Gracias por tus ideas.

karlosss
fuente

Respuestas:

2

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 -fy ltrace -fuse para adjuntar a cada proceso secundario. (En Linux, esta es la misma ptracellamada 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 openy openatllamadas 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 openllamadas 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/biny, /usr/libpor 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/trueo en /bin/lsalgú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.


racecheckertendría que estar escrito en C, no en shell, pero tal vez podría usar straceel 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.

Peter Cordes
fuente
Tu idea para un corrector de carreras realmente no funciona. Primero, está el problema de que los tiempos de espera no son confiables: un día, el otro tomará más tiempo de lo esperado (es un problema clásico con los scripts de compilación o prueba, que parecen funcionar por un tiempo y luego fallan en formas difíciles de depurar) cuando la carga de trabajo se expande y muchas cosas se ejecutan en paralelo). Pero más allá de esto, ¿a qué apertura vas a agregar un retraso? Para detectar algo interesante, necesitaría realizar muchas corridas con diferentes patrones de retraso y comparar sus resultados.
Gilles 'SO- deja de ser malvado'
@Gilles: Correcto, cualquier retraso razonablemente corto no garantiza que el truncado ganará la carrera (en una máquina muy cargada, como usted señala). La idea aquí es que use esto para probar su script varias veces, no que lo use racecheckertodo 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 .
Peter Cordes
@Gilles: Gran idea acerca de los diferentes patrones de demora, que podrían permitirle atrapar más carreras que solo las cosas simples dentro de la misma tubería que "deberían ser obvias (una vez que sepa cómo funcionan los proyectiles)" como el caso del OP. Pero "¿cuál se abre?" cualquier abierto de solo lectura, con una lista blanca o alguna otra forma de no retrasar el inicio del proceso.
Peter Cordes
¿Supongo que estás pensando en carreras más complejas con trabajos en segundo plano que no se truncan hasta que se complete algún otro proceso? Sí, podría ser necesaria una variación aleatoria para captar eso. O tal vez mire el árbol de procesos y retrase las lecturas "tempranas" más, para tratar de invertir el orden habitual. Podría hacer que la herramienta sea cada vez más complicada para simular más y más posibilidades de reordenamiento, pero en algún momento aún tendrá que diseñar sus programas correctamente si está haciendo tareas múltiples. Las pruebas automatizadas podrían ser útiles para scripts más simples donde los posibles problemas son más limitados.
Peter Cordes
Es bastante similar a probar código de subprocesos múltiples, especialmente algoritmos sin bloqueo: el razonamiento lógico sobre por qué es correcto es muy importante, así como las pruebas, porque no puede contar con pruebas en un conjunto particular de máquinas para producir todos los reordenamientos que podrían Ser un problema si no ha cerrado todas las lagunas. Pero al igual que probar en una arquitectura débilmente ordenada como ARM o PowerPC es una buena idea en la práctica, probar un script bajo un sistema que retrasa artificialmente las cosas puede exponer algunas carreras, por lo que es mejor que nada. ¡Siempre puedes introducir errores que no atrapará!
Peter Cordes
18

¿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 -ximprime 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):

  1. Ejecute el programa externo catcon el argumento tmp.
  2. Abierto tmppara lectura.
  3. 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:

  1. Redirige la salida estándar a tmp, truncando el archivo en el proceso.
  2. Ejecute el programa externo headcon el argumento -1.
  3. 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 tmpy leyera tmpen 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/bashpor 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 cathaya 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 cattermina 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.

Gilles 'SO- deja de ser malvado'
fuente
Ese era mi objetivo, determinar si el guión está bien escrito o no. Si el script podría haber destruido datos de esta manera, solo quería que los destruyera cada vez. No es bueno escuchar que esto es casi imposible. Gracias a usted, ahora sé cuál es el problema y trataré de pensar en una solución.
karlosss
@karlosss: Hmm, me pregunto si podrías usar el mismo material de rastreo / interceptación de llamadas del sistema que strace(es decir, Linux ptrace) para hacer que las openllamadas 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á.
Peter Cordes
@PeterCordes Soy un novato en esto, si puedes lograr una forma de lograrlo y escribirlo como respuesta, lo aceptaré.
karlosss
@PeterCordes No puede garantizar que el truncamiento gane con un retraso. Funcionará la mayor parte del tiempo, pero ocasionalmente en una máquina muy cargada su script fallará de maneras más o menos misteriosas.
Gilles 'SO- deja de ser malvado'
@Gilles: Discutamos esto bajo mi respuesta.
Peter Cordes