Un verrou tournant est un moyen de protéger une ressource partagée contre la modification simultanée par deux ou plusieurs processus. Le premier processus qui essaie de modifier la ressource "acquiert" le verrou et continue son chemin, faisant ce qu'il a besoin de faire avec la ressource. Tous les autres processus qui tentent par la suite d'acquérir le verrou sont arrêtés ; on dit qu'ils "tournent sur place" en attendant que le verrou soit libéré par le premier processus, d'où le nom de verrou tournant.
Le noyau Linux utilise des verrous tournants pour de nombreuses choses, comme lors de l'envoi de données à un périphérique particulier. La plupart des périphériques matériels ne sont pas conçus pour gérer plusieurs mises à jour d'état simultanées. Si deux modifications différentes doivent se produire, l'une doit suivre strictement l'autre, elles ne peuvent pas se chevaucher. Un verrou tournant fournit la protection nécessaire, garantissant que les modifications se produisent une à la fois.
Les verrous de rotation sont un problème car la rotation empêche le cœur du processeur de ce thread d'effectuer tout autre travail. Bien que le noyau Linux fournisse des services multitâches aux programmes de l'espace utilisateur qui s'exécutent sous lui, cette fonctionnalité multitâche à usage général ne s'étend pas au code du noyau.
Cette situation est en train de changer, et ce, pendant la plus grande partie de l'existence de Linux. Jusqu'à Linux 2.0, le noyau était presque purement un programme à tâche unique :chaque fois que le processeur exécutait le code du noyau, un seul cœur de processeur était utilisé, car il y avait un seul verrou tournant protégeant toutes les ressources partagées, appelé Big Kernel Lock (BKL ). À partir de Linux 2.2, le BKL est lentement divisé en plusieurs verrous indépendants qui protègent chacun une classe de ressources plus ciblée. Aujourd'hui, avec le noyau 2.6, le BKL existe toujours, mais il n'est utilisé que par du code très ancien qui ne peut pas être facilement déplacé vers un verrou plus granulaire. Il est maintenant tout à fait possible pour une machine multicœur d'avoir chaque processeur exécutant du code noyau utile.
Il y a une limite à l'utilité de décomposer le BKL car le noyau Linux manque de multitâche général. Si un cœur de processeur est bloqué en rotation sur un verrou tournant du noyau, il ne peut pas être rechargé, pour aller faire autre chose jusqu'à ce que le verrou soit libéré. Il reste assis et tourne jusqu'à ce que le verrou soit libéré.
Les verrous tournants peuvent transformer efficacement une boîte monstre à 16 cœurs en une boîte monocœur, si la charge de travail est telle que chaque cœur attend toujours un seul verrou tournant. C'est la principale limite à l'évolutivité du noyau Linux :doubler les cœurs de processeur de 2 à 4 doublera probablement presque la vitesse d'une machine Linux, mais le doubler de 16 à 32 ne le fera probablement pas, avec la plupart des charges de travail.
Un verrou tournant se produit lorsqu'un processus interroge continuellement un verrou à supprimer. Il est considéré comme mauvais car le processus consomme des cycles (généralement) inutilement. Ce n'est pas spécifique à Linux, mais un modèle de programmation général. Et même si cela est généralement considéré comme une mauvaise pratique, c'est en fait la bonne solution. il y a des cas où le coût d'utilisation du planificateur est plus élevé (en termes de cycles CPU) que le coût des quelques cycles que le verrou tournant est censé durer.
Exemple de verrou tournant :
#!/bin/sh
#wait for some program to clear a lock before doing stuff
while [ -f /var/run/example.lock ]; do
sleep 1
done
#do stuff
Il existe souvent un moyen d'éviter un blocage de rotation. Pour cet exemple particulier, il existe un outil Linux appelé inotifywait (il n'est généralement pas installé par défaut). S'il était écrit en C, vous utiliseriez simplement l'API inotify fournie par Linux.
Le même exemple, utilisant inotifywait, montre comment accomplir la même chose sans verrou tournant :
#/bin/sh
inotifywait -e delete_self /var/run/example.lock
#do stuff
Lorsqu'un thread essaie d'acquérir un verrou, trois choses peuvent se produire s'il échoue - il peut essayer de bloquer, il peut essayer de continuer, il peut essayer puis s'endormir en disant au système d'exploitation de le réveiller lorsqu'un événement se produit.
Maintenant, essayer et continuer prend beaucoup moins de temps qu'essayer et bloquer. Disons pour le moment qu'un "essayer et continuer" prendra une unité de temps et qu'un "essayer et bloquer" en prendra une centaine.
Supposons maintenant pour le moment qu'un thread mettra en moyenne 4 unités de temps à maintenir le verrou. C'est du gaspillage d'attendre 100 unités. Donc, à la place, vous écrivez une boucle de "essayer et continuer". À la quatrième tentative, vous obtiendrez généralement le verrou. Il s'agit d'un verrou tournant. On l'appelle ainsi parce que le fil continue de tourner sur place jusqu'à ce qu'il obtienne le verrou.
Une mesure de sécurité supplémentaire consiste à limiter le nombre d'exécutions de la boucle. Ainsi, par exemple, vous exécutez une boucle for, disons, six fois, si elle échoue, vous "essayez de bloquer".
Si vous savez qu'un thread maintiendra toujours le verrou pendant, disons, 200 unités, vous perdez du temps sur l'ordinateur à chaque essai et continuez.
Donc au final, un spin lock peut être très efficace ou inutile. C'est du gaspillage lorsque le temps "typique" pour tenir un verrou est supérieur au temps qu'il faut pour "essayer de bloquer". Il est efficace lorsque le temps typique pour tenir un verrou est bien inférieur au temps pour "essayer et bloquer".
Ps :Le livre à lire sur les fils de discussion est "A Thread Primer", si vous le trouvez encore.