Qu'est-ce qu'un spinlock sous Linux?


32

Je voudrais en savoir plus sur les verrous tournants Linux; quelqu'un pourrait-il me les expliquer?

Réponses:


34

Un verrou tournant est un moyen de protéger une ressource partagée contre la modification par deux ou plusieurs processus simultanément. Le premier processus qui tente de modifier la ressource "acquiert" le verrou et continue son chemin, faisant ce dont il avait besoin 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 la serrure soit libérée par le premier processus, d'où le nom de serrure tournante.

Le noyau Linux utilise des verrous rotatifs 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 de rotation offre la protection nécessaire, garantissant que les modifications se produisent une à la fois.

Les verrous tournants sont un problème car la rotation empêche le cœur de processeur de ce thread d'effectuer d'autres travaux. Bien que le noyau Linux fournisse des services multitâches aux programmes d'espace utilisateur exécutés sous lui, cette fonction multitâche à usage général ne s'étend pas au code du noyau.

Cette situation est en train de changer, et a été pour la plupart 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 verrou de rotation unique protégeant toutes les ressources partagées, appelé Big Kernel Lock (BKL ). À partir de Linux 2.2, le BKL est progressivement 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 un code très ancien qui ne peut pas être facilement déplacé vers un verrou plus granulaire. Il est désormais tout à fait possible pour une boîte multicœur que chaque CPU exécute du code noyau utile.

Il y a une limite à l'utilité de casser le BKL parce que le noyau Linux manque de multitâche général. Si un cœur de processeur est bloqué en rotation sur un verrou de rotation du noyau, il ne peut pas être reclassé, pour faire autre chose jusqu'à ce que le verrou soit libéré. Il reste juste 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 tournoyant. C'est la principale limite à l'évolutivité du noyau Linux: doubler les cœurs de CPU de 2 à 4 doublera probablement presque la vitesse d'une boîte Linux, mais la doubler de 16 à 32 ne le fera probablement pas, avec la plupart des charges de travail.


@Warren: Quelques doutes - j'aimerais en savoir plus sur ce Big Kernel Lock et ses implications. Je comprends également le dernier paragraphe "doubler les cœurs de CPU de 2 à 4 doublera probablement presque la vitesse d'une boîte Linux, mais la doubler de 16 à 32 ne le fera probablement pas"
Sen

2
Re: implications de BKL: Je pensais avoir précisé cela ci-dessus. Avec un seul verrou dans le noyau, chaque fois que deux cœurs essaient de faire quelque chose de protégé par le BKL, un cœur est bloqué tandis que le premier finit d'utiliser sa ressource protégée. Plus le verrouillage est fin, plus les chances que cela se produise sont faibles, donc plus l'utilisation du processeur est importante.
Warren Young

2
Re: doubler: je veux dire qu'il y a une loi des rendements décroissants lors de l'ajout de cœurs de processeur à un ordinateur. À mesure que le nombre de cœurs augmente, il en va de même pour deux ou plusieurs d'entre eux qui auront besoin d'accéder à une ressource protégée par un verrou particulier. L'augmentation de la granularité des verrous réduit les risques de telles collisions, mais il y a des frais supplémentaires pour en ajouter trop. Vous pouvez facilement voir cela dans les superordinateurs, qui ont souvent des milliers de processeurs de nos jours: la plupart des charges de travail sont inefficaces sur eux car ils ne peuvent pas éviter de faire tourner de nombreux processeurs en raison de conflits de ressources partagées.
Warren Young

1
Bien que ce soit une explication intéressante (+1 pour cela), je ne pense pas qu'elle soit efficace pour transmettre la différence entre les verrous tournants et d'autres types de verrous.
Gilles 'SO- arrête d'être méchant'

2
Si l'on veut connaître la différence entre un verrou tournant et, disons, un sémaphore, c'est une question différente. Une autre bonne question, mais tangentielle, est: qu'est-ce que la conception du noyau Linux fait des verrous tournants de bons choix au lieu de quelque chose de plus commun dans le code utilisateur, comme un mutex. Cette réponse divague beaucoup telle quelle.
Warren Young

11

Un verrou tournant est 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 bien que cela soit 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 devrait durer.

Exemple de spinlock:

#!/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 verrouillage 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, en utilisant inotifywait montre comment accomplir la même chose sans verrou rotatif:

#/bin/sh
inotifywait -e delete_self /var/run/example.lock
#do stuff

Quel est le rôle de l'ordonnanceur là-bas? Ou a-t-il un rôle?
Sen

1
Dans la méthode de verrouillage par rotation, le planificateur reprend le processus toutes les ~ 1 seconde pour effectuer sa tâche (qui consiste simplement à vérifier l'existence d'un fichier). Dans l'exemple inotifywait, le planificateur ne reprend le processus que lorsque le processus enfant (inotifywait) se termine. Inotifywait dort également; le planificateur ne le reprend que lorsque l'événement inotify se produit.
Shawn J. Goff du

Alors, comment ce scénario est-il géré dans un système de processeur monocœur?
Sen

@Sen: Ceci est expliqué assez bien dans les pilotes de périphériques Linux .
Gilles 'SO- arrête d'être méchant'

1
Ce script bash est un mauvais exemple de verrou tournant. Vous interrompez le processus pour l'endormir. Un spinlock ne dort jamais. Sur une machine à cœur unique, il suspend simplement le planificateur et continue (sans attente)
Martin

7

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 et 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 utilise beaucoup moins de temps qu'un essai et bloquer. Disons pour le moment qu'un "essayer et continuer" prendra une unité de temps et un "essayer et bloquer" une centaine.

Supposons maintenant pour le moment qu'en moyenne un thread prendra 4 unités de temps pour maintenir le verrou. Il est inutile d'attendre 100 unités. Donc, au lieu de cela, vous écrivez une boucle de "essayer et continue". À la quatrième tentative, vous obtiendrez généralement le verrou. Ceci est un verrou tournant. C'est ce qu'on appelle parce que le fil continue de tourner 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, dans l'exemple, vous effectuez une boucle for, par exemple six fois, si elle échoue, vous "essayez de bloquer".

Si vous savez qu'un thread tiendra toujours le verrou pour disons 200 unités, alors vous perdez le temps de l'ordinateur pour chaque essai et continuer.

Donc, au final, un verrou tournant peut être très efficace ou inutile. C'est un gaspillage lorsque le temps «typique» pour maintenir une serrure est plus élevé que le temps qu'il faut pour «essayer de bloquer». Il est efficace lorsque le temps typique pour maintenir une serrure est beaucoup plus petit que le temps pour "essayer de bloquer".

Ps: Le livre à lire sur les discussions est "A Thread Primer", si vous pouvez toujours le trouver.


le fil continue de tourner jusqu'à ce qu'il obtienne le verrou . Pourriez-vous s'il vous plaît me dire quel est ce filage? Est-ce comme s'il s'agissait de la file d'attente et qu'il était mis à exécution par le planificateur? J'essaie peut-être d'entrer dans le bas niveau, mais je ne peux toujours pas garder mon doute.
Sen

Tournant parmi les 3-4 instructions de la boucle, en gros.
Paul Stelian

5

Un verrou est un moyen de synchroniser deux ou plusieurs tâches (processus, threads). Plus précisément, lorsque les deux tâches nécessitent un accès intermittent à une ressource qui ne peut être utilisée que par une seule tâche à la fois, c'est un moyen pour les tâches de ne pas utiliser la ressource en même temps. Pour accéder à la ressource, une tâche doit effectuer les étapes suivantes:

take the lock
use the resource
release the lock

La prise d'un verrou n'est pas possible si une autre tâche l'a déjà prise. (Considérez le verrou comme un objet symbolique physique. Soit l'objet se trouve dans un tiroir, soit quelqu'un l'a en main. Seule la personne qui détient l'objet peut accéder à la ressource.) Donc, «prenez le verrou» signifie vraiment «attendez jusqu'à ce que personne d'autre n'a la serrure, alors prenez-la ».

D'un point de vue de haut niveau, il existe deux façons principales d'implémenter des verrous: les verrous tournants et les conditions. Avec spinlocks , en prenant les moyens de blocage juste « filer » (c. -à ne rien faire dans une boucle) jusqu'à ce que personne d'autre n'a le verrou. Sous certaines conditions, si une tâche tente de prendre le verrou mais est bloquée parce qu'une autre tâche le détient, le nouveau venu entre dans une file d'attente; l'opération de libération signale à toute tâche en attente que le verrou est maintenant disponible.

(Ces explications ne suffisent pas pour vous permettre d'implémenter un verrou, car je n'ai rien dit sur l'atomicité. Mais l'atomicité n'est pas importante ici.)

Les verrous tournants sont évidemment du gaspillage: la tâche en attente continue de vérifier si le verrou est pris. Alors pourquoi et quand est-il utilisé? Les verrous tournants sont souvent très bon marché à obtenir dans le cas où le verrou n'est pas maintenu. Cela le rend attrayant lorsque les chances de verrouillage sont serrées. De plus, les verrous tournants ne sont viables que si l'obtention du verrou ne devrait pas prendre longtemps. Les verrous tournants ont donc tendance à être utilisés dans des situations où ils resteront en place très peu de temps, de sorte que la plupart des tentatives devraient réussir du premier coup, et celles qui ont besoin d'une attente n'attendent pas longtemps.

Il y a une bonne explication des verrous tournants et autres mécanismes de concurrence du noyau Linux dans Linux Device Drivers , chapitre 5.


Quelle serait une bonne façon d'implémenter d'autres primitives de synchronisation? Prenez un verrou tournant, vérifiez si quelqu'un d'autre a la primitive réelle en cours d'implémentation, puis utilisez le planificateur ou accordez l'accès? Pourrions-nous considérer le bloc synchronized () en Java comme une forme de verrou tournant et, dans les primitives correctement implémentées, utiliser simplement un verrou tournant?
Paul Stelian

@PaulStelian Il est en effet courant d'implémenter des verrous «lents» de cette façon. Je ne connais pas assez Java pour répondre à cette partie, mais je doute que synchronizedcela soit implémenté par un spinlock: un synchronizedbloc pourrait fonctionner très longtemps. synchronizedest une construction de langage pour rendre les verrous faciles à utiliser dans certains cas, pas une primitive pour construire de plus grandes primitives de synchronisation.
Gilles 'SO- arrête d'être méchant'

3

Un verrou tournant est un verrou qui fonctionne en désactivant le planificateur et éventuellement des interruptions (variante irqsave) sur le noyau particulier sur lequel le verrou est acquis. Il est différent d'un mutex dans la mesure où il désactive la planification afin que seul votre thread puisse s'exécuter pendant que le verrou tournant est maintenu. Un mutex permet de planifier d'autres threads de priorité supérieure pendant qu'il est maintenu mais ne leur permet pas d'exécuter simultanément la section protégée. Parce que les verrous tournants désactivent le multitâche, vous ne pouvez pas prendre un verrou tournant, puis appeler un autre code qui tentera d'acquérir un mutex. Votre code à l'intérieur de la section spinlock ne doit jamais dormir (le code se met généralement en veille lorsqu'il rencontre un mutex verrouillé ou un sémaphore vide).

Une autre différence avec un mutex est que les threads font généralement la queue pour un mutex, donc un mutex en dessous a une file d'attente. Alors que spinlock garantit juste qu'aucun autre thread ne fonctionnera même s'il le doit. Par conséquent, vous ne devez jamais tenir un verrou tournant lorsque vous appelez des fonctions en dehors de votre fichier dont vous n'êtes pas sûr de ne pas dormir.

Lorsque vous souhaitez partager votre spinlock avec une interruption, vous devez utiliser la variante irqsave. Cela désactivera non seulement le planificateur, mais désactivera également les interruptions. C'est logique non? Spinlock fonctionne en s'assurant que rien d'autre ne fonctionnera. Si vous ne voulez pas qu'une interruption s'exécute, désactivez-la et passez en toute sécurité dans la section critique.

Sur une machine multicœur, un verrou tournant tournera réellement en attendant qu'un autre noyau qui détient le verrou le libère. Cette rotation ne se produit que sur les machines multicœurs car sur les machines à cœur unique, cela ne peut pas se produire (vous maintenez le verrou tournant et continuez ou vous ne lancez jamais jusqu'à ce que le verrou soit libéré).

Spinlock n'est pas un gaspillage là où cela a du sens. Pour de très petites sections critiques, il serait inutile d'allouer une file d'attente de tâches mutex par rapport à une simple suspension du planificateur pendant quelques microsecondes pour terminer le travail important. Si vous devez dormir ou maintenir le verrou sur une opération io (qui peut dormir), utilisez un mutex. Ne verrouillez jamais un verrou tournant, puis essayez de le libérer dans une interruption. Bien que cela fonctionne, ce sera comme la merde Arduino de while (flagnotset); dans ce cas, utilisez un sémaphore.

Prenez un verrou tournant lorsque vous avez besoin d'une exclusion mutuelle simple pour les blocs de transactions en mémoire. Saisissez un mutex lorsque vous souhaitez que plusieurs threads s'arrêtent juste avant un verrouillage mutex, puis le thread de priorité la plus élevée à choisir pour continuer lorsque le mutex devient libre et lorsque vous verrouillez et relâchez dans le même thread. Prenez un sémaphore lorsque vous avez l'intention de le publier dans un thread ou une interruption et de le prendre dans un autre thread. Ce sont trois façons légèrement différentes d'assurer l'exclusion mutuelle et elles sont utilisées à des fins légèrement différentes.

En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.