web-dev-qa-db-fra.com

Comment ce script garantit-il qu'une seule instance de lui-même est en cours d'exécution?

Le 19 août 2013, Randal L. Schwartz a publié this Script shell, qui visait à garantir, sous Linux, "qu'une seule instance de [le] script est en cours d'exécution, sans conditions de concurrence ni devoir nettoyer les fichiers de verrouillage ":

#!/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

Il semble fonctionner comme annoncé:

$ ./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
$

Voici ce que je comprends:

  • Le script redirige (<) une copie de son propre contenu (c'est-à-dire de $0) vers le STDIN (c'est-à-dire le descripteur de fichier 0) d'un sous-shell.
  • Dans le sous-shell, le script tente d'obtenir un verrou exclusif non bloquant (flock -n -x) sur le descripteur de fichier 0.
    • Si cette tentative échoue, le sous-shell se ferme (tout comme le script principal, car il n'y a rien d'autre à faire).
    • Si la tentative réussit, le sous-shell exécute la tâche souhaitée.

Voici mes questions:

  • Pourquoi le script doit-il rediriger, vers un descripteur de fichier hérité par le sous-shell, une copie de son propre contenu plutôt que, disons, le contenu de certains autre fichier? (J'ai essayé de rediriger à partir d'un fichier différent et de réexécuter comme ci-dessus, et l'ordre d'exécution a changé: la tâche sans arrière-plan a obtenu le verrou avant l'arrière-plan. Donc, peut-être que l'utilisation du contenu du fichier évite les conditions de concurrence; mais comment?)
  • Pourquoi le script doit-il de toute façon rediriger vers un descripteur de fichier hérité par le sous-shell une copie du contenu d'un fichier?
  • Pourquoi le verrouillage exclusif du descripteur de fichier 0 dans un Shell empêche une copie du même script, exécuté dans un autre Shell, d'obtenir un verrou exclusif sur le descripteur de fichier 0? Les shells n'ont pas leurs propres copies séparées des descripteurs de fichiers standard (0, 1, et 2, c'est-à-dire STDIN, STDOUT et STDERR)?
23
sampablokuper

Pourquoi le script doit-il rediriger, vers un descripteur de fichier hérité par le sous-shell, une copie de son propre contenu plutôt que, disons, le contenu d'un autre fichier?

Vous pouvez utiliser n'importe quel fichier, tant que toutes les copies du script utilisent le même. L'utilisation de $0 Lie simplement le verrou au script lui-même: si vous copiez le script et le modifiez pour une autre utilisation, vous n'avez pas besoin de trouver un nouveau nom pour le fichier de verrouillage. C'est pratique.

Si le script est appelé via un lien symbolique, le verrou se trouve sur le fichier réel et non sur le lien.

(Bien sûr, si un processus exécute le script et lui donne une valeur composée comme argument zéro au lieu du chemin réel, alors cela se casse. Mais c'est rarement fait.)

(J'ai essayé d'utiliser un fichier différent et de relancer comme ci-dessus, et l'ordre d'exécution a changé)

Êtes-vous sûr que c'était à cause du fichier utilisé, et pas seulement d'une variation aléatoire? Comme avec un pipeline, il n'y a vraiment aucun moyen de savoir dans quel ordre les commandes s'exécutent dans cmd1 & cmd. Cela dépend principalement du planificateur du système d'exploitation. J'obtiens des variations aléatoires sur mon système.

Pourquoi le script doit-il de toute façon rediriger vers un descripteur de fichier hérité par le sous-shell une copie du contenu d'un fichier?

Il semble que le Shell lui-même contienne une copie de la description du fichier contenant le verrou, au lieu de simplement l'utilitaire flock le détenant. Un verrou créé avec flock(2) est libéré lorsque les descripteurs de fichiers le contenant sont fermés.

flock a deux modes, soit pour prendre un verrou basé sur un nom de fichier, et exécuter une commande externe (auquel cas flock contient le descripteur de fichier ouvert requis), soit pour prendre un descripteur de fichier de l'extérieur, donc un processus extérieur est responsable de le tenir.

Notez que le contenu du fichier n'est pas pertinent ici et qu'aucune copie n'a été effectuée. La redirection vers le sous-shell ne copie aucune donnée autour d'elle-même, elle ouvre simplement une poignée au fichier.

Pourquoi la détention d'un verrou exclusif sur le descripteur de fichier 0 dans un shell empêche-t-elle une copie du même script, exécuté dans un autre shell, d'obtenir un verrou exclusif sur le descripteur de fichier 0? Les shells n'ont-ils pas leurs propres copies séparées des descripteurs de fichiers standard (0, 1 et 2, c'est-à-dire STDIN, STDOUT et STDERR)?

Oui, mais le verrou est sur le fichier, pas sur le descripteur de fichier. Une seule instance ouverte du fichier peut contenir le verrou à la fois.


Je pense que vous devriez pouvoir faire la même chose sans le sous-shell, en utilisant exec pour ouvrir un handle vers le fichier de verrouillage:

$ 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
22
ilkkachu

Un verrou de fichier est attaché à un fichier, via une description de fichier . À un niveau élevé, la séquence d'opérations dans une instance du script est la suivante:

  1. Ouvrez le fichier auquel le verrou est attaché ("le fichier de verrouillage").
  2. Prenez un verrou sur le fichier de verrouillage.
  3. Faire des trucs.
  4. Fermez le fichier de verrouillage. Cela libère le verrou attaché à la description de fichier créée en ouvrant un fichier.

Maintenir le verrou empêche l'exécution d'une autre copie du même script, car c'est ce que font les verrous. Tant qu'un verrou exclusif sur un fichier existe quelque part sur le système, il est impossible de créer une deuxième instance du même verrou, même via une description de fichier différente.

L'ouverture d'un fichier crée un description du fichier. Il s'agit d'un objet noyau qui n'a pas beaucoup de visibilité directe dans les interfaces de programmation. Vous accédez à une description de fichier indirectement via des descripteurs de fichier, mais normalement vous la considérez comme l'accès au fichier (lecture ou écriture de son contenu ou de ses métadonnées). Un verrou est l'un des attributs qui sont une propriété de la description du fichier plutôt qu'un fichier ou un descripteur.

Au début, lorsqu'un fichier est ouvert, la description du fichier a un seul descripteur de fichier, mais d'autres descripteurs peuvent être créés soit en créant un autre descripteur (la famille dup d'appels système) soit en forçant un sous-processus (après auquel le parent et l'enfant ont accès à la même description de fichier). Un descripteur de fichier peut être fermé explicitement ou lorsque le processus dans lequel il se trouve meurt. Lorsque le dernier descripteur de fichier joint à un fichier est fermé, la description du fichier est fermée.

Voici comment la séquence d'opérations ci-dessus affecte la description du fichier.

  1. La redirection <$0 ouvre le fichier de script dans le sous-shell, créant une description de fichier. À ce stade, un descripteur de fichier unique est attaché à la description: le numéro de descripteur 0 dans le sous-shell.
  2. Le sous-shell invoque flock et attend sa sortie. Pendant que flock est en cours d'exécution, deux descripteurs sont attachés à la description: le numéro 0 dans le sous-shell et le numéro 0 dans le processus de flock. Lorsque flock prend le verrou, cela définit une propriété de la description du fichier. Si une autre description de fichier a déjà un verrou sur le fichier, flock ne peut pas prendre le verrou, car il s'agit d'un verrou exclusif.
  3. Le sous-shell fait des trucs. Puisqu'il a toujours un descripteur de fichier ouvert sur la description avec le verrou, cette description continue d'exister et il conserve son verrou puisque personne ne supprime jamais le verrou.
  4. La sous-coquille meurt à la parenthèse fermante. Cela ferme le dernier descripteur de fichier sur la description de fichier qui a le verrou, donc le verrou disparaît à ce stade.

La raison pour laquelle le script utilise une redirection à partir de $0 est que la redirection est le seul moyen d'ouvrir un fichier dans le shell, et le maintien d'une redirection active est le seul moyen de garder un descripteur de fichier ouvert. Le sous-shell ne lit jamais à partir de son entrée standard, il suffit de le garder ouvert. Dans une langue qui donne un accès direct aux appels ouverts et fermés, vous pouvez utiliser

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

Vous pouvez réellement obtenir la même séquence d'opérations dans le shell si vous effectuez la redirection avec le code intégré exec.

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

Le script peut utiliser un descripteur de fichier différent s'il souhaite continuer à accéder à l'entrée standard d'origine.

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

ou avec un sous-shell:

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

Le verrou ne doit pas nécessairement se trouver sur le fichier de script. Il peut s'agir de n'importe quel fichier pouvant être ouvert en lecture (il doit donc exister, il doit s'agir d'un type de fichier pouvant être lu, tel qu'un fichier normal ou un canal nommé mais pas un répertoire, et le processus de script doit avoir l'autorisation de le lire). Le fichier de script a l'avantage qu'il est garanti d'être présent et lisible (sauf dans le cas d'Edge où il a été supprimé en externe entre le moment où le script a été appelé et le moment où le script arrive au <$0 redirection).

Tant que flock réussit et que le script se trouve sur un système de fichiers où les verrous ne sont pas bogués (certains systèmes de fichiers réseau tels que NFS peuvent être bogués), je ne vois pas comment l'utilisation d'un fichier de verrouillage différent pourrait permettre une condition de course. Je soupçonne une erreur de manipulation de votre part.

Le fichier utilisé pour le verrouillage est sans importance, le script utilise $0 car il s'agit d'un fichier connu pour exister.

L'ordre dans lequel les verrous sont obtenus sera plus ou moins aléatoire, selon la vitesse à laquelle votre machine est en mesure de démarrer les deux tâches.

Vous pouvez utiliser n'importe quel descripteur de fichier, pas nécessairement 0. Le verrou est maintenu sur le fichier ouvert au descripteur de fichier, pas le descripteur lui-même.

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