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

22

Le 19 août 2013, Randal L. Schwartz a posté ce script shell, qui visait à garantir, sous Linux, "qu'une seule instance du [script] est en cours d'exécution, sans condition 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 (ie depuis $0) vers le STDIN (ie descripteur de fichier 0) d'un sous-shell.
  • Dans le sous-shell, le script tente d'obtenir un verrou exclusif ( flock -n -x) non bloquant 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 d'un 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 fait de maintenir un verrou exclusif sur le descripteur de fichier 0dans un shell empêche une copie du même script, exécuté dans un shell différent, d'obtenir un verrou exclusif sur le descripteur de fichier 0? Ne pas coquilles ont leurs propres copies séparées des descripteurs de fichier standard ( 0, 1et 2, par exemple STDIN, STDOUT et STDERR)?
sampablokuper
la source
Quel a été votre processus de test exact lorsque vous avez tenté de rediriger votre expérience à partir d'un fichier différent?
Freiheit
1
Je pense que vous pouvez renvoyer ce lien. stackoverflow.com/questions/185451/…
Deb Paikar

Réponses:

22

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 $0lie 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 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 semblerait que le shell lui-même contienne une copie de la description du fichier contenant le verrou, au lieu de simplement l' flockutilitaire qui le détient. Un verrou créé avec flock(2)est libéré lorsque les descripteurs de fichier le contenant sont fermés.

flocka deux modes, soit pour prendre un verrou basé sur un nom de fichier et exécuter une commande externe (auquel cas flockcontient le descripteur de fichier ouvert requis), soit pour prendre un descripteur de fichier de l'extérieur, de sorte qu'un processus externe est responsable de la conservation il.

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 le fait de maintenir un verrou exclusif sur le descripteur de fichier 0 dans un shell empêche une copie du même script, exécuté dans un shell différent, 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 se trouve 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 de même sans le sous-shell, en utilisant execpour ouvrir une poignée 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
ilkkachu
la source
1
Utiliser { }au lieu de ( )fonctionnerait également et éviterait le sous-shell.
R ..
Plus loin dans les commentaires sur le post G +, quelqu'un là-bas a également suggéré à peu près la même méthode en utilisant exec.
David Z
@R .., oh, bien sûr. Mais c'est toujours moche avec les accolades supplémentaires autour du script réel.
ilkkachu
9

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 une description de fichier . Il s'agit d'un objet noyau qui n'a pas beaucoup de visibilité directe dans les interfaces de programmation. Vous accédez indirectement à une description de fichier via des descripteurs de fichier, mais vous le considérez normalement 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 dupfamille d'appels système), soit en forçant un sous-processus (après quoi le parent et le l'enfant a 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 <$0ouvre 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 flocket 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 $0est 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 programme execintégré.

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 de bord où il a été supprimé en externe entre le moment où le script a été appelé et le moment où le script arrive à la <$0redirection).

Tant qu'il flockré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 concurrence critique. Je soupçonne une erreur de manipulation de votre part.

Gilles 'SO- arrête d'être méchant'
la source
Il y a une condition de concurrence: vous ne pouvez pas contrôler quelle instance du script obtient le verrou. Heureusement, pour presque tous les usages, cela n'a pas d'importance.
Mark
4
@Mark Il y a une course à l'écluse, mais ce n'est pas une condition de course. Une condition de concurrence est lorsque le timing peut permettre à quelque chose de mal de se produire, comme deux processus se trouvant dans la même section critique en même temps. Ne pas savoir quel processus entrera dans la section critique est un non-déterminisme attendu, ce n'est pas une condition de concurrence.
Gilles 'SO- arrête d'être méchant'
1
Pour info, le lien dans la "description du fichier" pointe vers la page d'index des spécifications du groupe ouvert plutôt que vers une description spécifique du concept, ce que je pense que vous aviez l'intention de faire. Ou vous pouvez également lier votre ancienne réponse ici également unix.stackexchange.com/a/195164/85039
Sergiy Kolodyazhnyy
5

Le fichier utilisé pour le verrouillage n'a pas d'importance, le script l'utilise $0car 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 sur le descripteur lui-même.

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