¿Cómo garantiza este script que solo se está ejecutando una instancia de sí mismo?

22

El 19 de agosto de 2013, Randal L. Schwartz publicó este script de shell, que tenía la intención de garantizar, en Linux, "que solo se ejecute una instancia de [el] script, sin condiciones de carrera o tener que limpiar los archivos de bloqueo":

#!/bin/sh
# randal_l_schwartz_001.sh
(
    if ! flock -n -x 0
    then
        echo "$$ cannot get flock"
        exit 0
    fi
    echo "$$ start"
    sleep 10 # for testing.  put the real task here
    echo "$$ end"
) < $0

Parece funcionar como se anuncia:

$ ./randal_l_schwartz_001.sh & ./randal_l_schwartz_001.sh
[1] 11863
11863 start
11864 cannot get flock
$ 11863 end

[1]+  Done                    ./randal_l_schwartz_001.sh
$

Esto es lo que entiendo:

  • El script redirige ( <) una copia de sus propios contenidos (es decir, de $0) al STDIN (es decir, el descriptor de archivo 0) de una subshell.
  • Dentro de la subshell, el script intenta obtener un lock ( flock -n -x) exclusivo y no bloqueador en el descriptor de archivo 0.
    • Si ese intento falla, la subshell sale (y también lo hace el script principal, ya que no hay nada más que pueda hacer).
    • Si el intento tiene éxito, la subshell ejecuta la tarea deseada.

Aquí están mis preguntas:

  • ¿Por qué el script necesita redirigir, a un descriptor de archivo heredado por la subshell, una copia de su propio contenido en lugar de, digamos, el contenido de algún otro archivo? (Intenté redirigir desde un archivo diferente y volver a ejecutar como se indicó anteriormente, y el orden de ejecución cambió: la tarea no en segundo plano ganó el bloqueo antes que la de fondo. Entonces, tal vez usar el contenido del archivo evite las condiciones de carrera; pero ¿cómo?)
  • ¿Por qué el script necesita redirigir, a un descriptor de archivo heredado por la subshell, una copia del contenido de un archivo, de todos modos?
  • ¿Por qué mantener un bloqueo exclusivo en el descriptor de archivo 0en un shell impide que una copia del mismo script, que se ejecuta en un shell diferente, obtenga un bloqueo exclusivo en el descriptor de archivo 0? No conchas tienen sus propias copias independientes de los descriptores de archivos estándar ( 0, 1y 2, es decir, stdin, stdout y stderr)?
sampablokuper
fuente
¿Cuál fue su proceso de prueba exacto cuando probó su experimento para redirigir desde un archivo diferente?
Freiheit
1
Creo que puedes referir este enlace. stackoverflow.com/questions/185451/…
Deb Paikar

Respuestas:

22

¿Por qué el script necesita redirigir, a un descriptor de archivo heredado por la subshell, una copia de su propio contenido en lugar de, digamos, el contenido de algún otro archivo?

Puede usar cualquier archivo, siempre y cuando todas las copias del script usen el mismo. El uso $0solo vincula el bloqueo al script en sí: si copia el script y lo modifica para algún otro uso, no necesita encontrar un nuevo nombre para el archivo de bloqueo. Esto es conveniente

Si se llama al script a través de un enlace simbólico, el bloqueo está en el archivo real y no en el enlace.

(Por supuesto, si algún proceso ejecuta el script y le da un valor inventado como argumento cero en lugar de la ruta real, entonces esto se rompe. Pero eso rara vez se hace).

(Intenté usar un archivo diferente y volver a ejecutar como se indicó anteriormente, y el orden de ejecución cambió)

¿Estás seguro de que se debió al archivo utilizado y no solo a una variación aleatoria? Al igual que con una canalización, realmente no hay forma de estar seguros de en qué orden se ejecutan los comandos cmd1 & cmd. Depende principalmente del planificador del sistema operativo. Obtengo una variación aleatoria en mi sistema.

¿Por qué el script necesita redirigir, a un descriptor de archivo heredado por la subshell, una copia del contenido de un archivo, de todos modos?

Parece que es así que el propio shell contiene una copia de la descripción del archivo que contiene el bloqueo, en lugar de solo la flockutilidad que lo contiene. Un bloqueo hecho con flock(2)se libera cuando los descriptores de archivo que lo tienen están cerrados.

flocktiene dos modos, ya sea tomar un bloqueo basado en un nombre de archivo y ejecutar un comando externo (en cuyo caso flockcontiene el descriptor de archivo abierto requerido), o tomar un descriptor de archivo desde el exterior, por lo que un proceso externo es responsable de mantener eso.

Tenga en cuenta que el contenido del archivo no es relevante aquí, y no se realizan copias. La redirección a la subshell no copia ningún dato en sí mismo, solo abre un identificador para el archivo.

¿Por qué mantener un bloqueo exclusivo en el descriptor de archivo 0 en un shell impide que una copia del mismo script, que se ejecuta en un shell diferente, obtenga un bloqueo exclusivo en el descriptor de archivo 0? ¿Los shells no tienen sus propias copias separadas de los descriptores de archivo estándar (0, 1 y 2, es decir, STDIN, STDOUT y STDERR)?

Sí, pero el bloqueo está en el archivo , no en el descriptor de archivo. Solo una instancia abierta del archivo puede retener el bloqueo a la vez.


Creo que debería poder hacer lo mismo sin la subshell, utilizando execpara abrir un identificador para el archivo de bloqueo:

$ cat lock.sh
#!/bin/sh

exec 9< "$0"

if ! flock -n -x 9; then
    echo "$$/$1 cannot get flock" 
    exit 0
fi

echo "$$/$1 got the lock"
sleep 2
echo "$$/$1 exit"

$ ./lock.sh bg & ./lock.sh fg ; wait; echo
[1] 11362
11363/fg got the lock
11362/bg cannot get flock
11363/fg exit
[1]+  Done                    ./lock.sh bg
ilkkachu
fuente
1
Usar en { }lugar de ( )también funcionaría y evitaría la subshell.
R ..
Más abajo, en los comentarios sobre la publicación de G +, alguien sugirió aproximadamente el mismo método exec.
David Z
@R .., oh, claro. Pero sigue siendo feo con las llaves adicionales alrededor del guión real.
ilkkachu
9

Se adjunta un bloqueo de archivo a un archivo a través de una descripción de archivo . En un nivel alto, la secuencia de operaciones en una instancia del script es:

  1. Abra el archivo al que se adjunta el bloqueo ("el archivo de bloqueo").
  2. Tome un bloqueo en el archivo de bloqueo.
  3. Hacer cosas.
  4. Cierra el archivo de bloqueo. Esto libera el bloqueo que se adjunta a la descripción del archivo creado al abrir un archivo.

Sostener el bloqueo evita que se ejecute otra copia del mismo script porque eso es lo que hacen los bloqueos. Mientras exista un bloqueo exclusivo en un archivo en algún lugar del sistema, es imposible crear una segunda instancia del mismo bloqueo, incluso a través de una descripción de archivo diferente.

Abrir un archivo crea una descripción del archivo . Este es un objeto kernel que no tiene mucha visibilidad directa en las interfaces de programación. Accede a una descripción de archivo indirectamente a través de descriptores de archivo, pero normalmente piensa que accede al archivo (leyendo o escribiendo su contenido o metadatos). Un bloqueo es uno de los atributos que son propiedad de la descripción del archivo en lugar de un archivo o un descriptor.

Al principio, cuando se abre un archivo, la descripción del archivo tiene un solo descriptor de archivo, pero se pueden crear más descriptores ya sea creando otro descriptor (la dupfamilia de las llamadas al sistema) o bifurcando un subproceso (después del cual tanto el padre como el padre) el niño tiene acceso a la misma descripción del archivo). Un descriptor de archivo puede cerrarse explícitamente o cuando el proceso en el que se encuentra muere. Cuando se cierra el último descriptor de archivo adjunto a un archivo, se cierra la descripción del archivo.

Así es como la secuencia de operaciones anterior afecta la descripción del archivo.

  1. La redirección <$0abre el archivo de script en el subshell, creando una descripción del archivo. En este punto hay un descriptor de archivo único adjunto a la descripción: descriptor número 0 en la subshell.
  2. La subshell invoca flocky espera a que salga. Mientras se ejecuta el lote, hay dos descriptores adjuntos a la descripción: el número 0 en el subshell y el número 0 en el proceso del lote. Cuando flock toma el bloqueo, eso establece una propiedad de la descripción del archivo. Si otra descripción de archivo ya tiene un bloqueo en el archivo, el lote no puede tomar el bloqueo, ya que es un bloqueo exclusivo.
  3. La subshell hace cosas. Dado que todavía tiene un descriptor de archivo abierto en la descripción con el bloqueo, esa descripción sigue existiendo, y mantiene su bloqueo ya que nadie elimina el bloqueo.
  4. La subshell muere en el paréntesis de cierre. Esto cierra el último descriptor de archivo en la descripción del archivo que tiene el bloqueo, por lo que el bloqueo desaparece en este punto.

La razón por la cual el script usa una redirección $0es que la redirección es la única forma de abrir un archivo en el shell, y mantener una redirección activa es la única manera de mantener abierto un descriptor de archivo. El subshell nunca lee desde su entrada estándar, solo necesita mantenerlo abierto. Puede usar

fd = open($0)
flock(fd, LOCK_EX)
do stuff
close(fd)

En realidad, puede obtener la misma secuencia de operaciones en el shell si realiza la redirección con el execincorporado.

exec <$0
flock -n -x 0
# do stuff
exec <&-

El script podría usar un descriptor de archivo diferente si quisiera seguir accediendo a la entrada estándar original.

exec 3<$0
flock -n -x 0
# do stuff
exec 3<&-

o con una subshell:

(
  flock -n -x 3
  # do stuff
) 3<$0

El bloqueo no tiene que estar en el archivo de script. Podría estar en cualquier archivo que se pueda abrir para leer (por lo tanto, debe existir, debe ser un tipo de archivo que se pueda leer, como un archivo normal o una tubería con nombre, pero no un directorio, y el proceso del script debe tener El permiso para leerlo). El archivo de secuencia de comandos tiene la ventaja de que está garantizado para estar presente y ser legible (excepto en el caso de borde donde se eliminó externamente entre el momento en que se invocó el script y el momento en que el script llega a la <$0redirección).

Mientras flocktenga éxito, y el script esté en un sistema de archivos donde los bloqueos no tengan errores (algunos sistemas de archivos de red como NFS pueden tener errores), no veo cómo el uso de un archivo de bloqueo diferente podría permitir una condición de carrera. Sospecho un error de manipulación de tu parte.

Gilles 'SO- deja de ser malvado'
fuente
Hay una condición de carrera: no puede controlar qué instancia del script obtiene el bloqueo. Afortunadamente, para casi todos los propósitos, no importa.
Mark
44
@ Mark Hay una carrera hacia la cerradura, pero no es una condición de carrera. Una condición de carrera es cuando el tiempo puede permitir que ocurra algo malo, como que dos procesos se encuentren en la misma sección crítica al mismo tiempo. No saber qué proceso entrará en la sección crítica se espera no determinismo, no es una condición de carrera.
Gilles 'SO- deja de ser malvado'
1
Solo para su información, el enlace en "descripción de archivo" apunta a la página de índice de especificaciones de Open Group en lugar de a una descripción específica del concepto, que es lo que creo que pretendía hacer. O también puede vincular su respuesta anterior aquí también unix.stackexchange.com/a/195164/85039
Sergiy Kolodyazhnyy
5

El archivo utilizado para bloquear no es importante, el script lo utiliza $0porque se sabe que existe.

El orden en que se obtienen los bloqueos será más o menos aleatorio, dependiendo de qué tan rápido su máquina pueda iniciar las dos tareas.

Puede usar cualquier descriptor de archivo, no necesariamente 0. El bloqueo se mantiene en el archivo abierto al descriptor de archivo, no en el descriptor en sí.

( flock -x 9 || exit 1
  echo 'Locking for 5 secs'; sleep 5; echo 'Done' ) 9>/tmp/lock &
Kusalananda
fuente