Contenu
Les spinlocks représentent l'une des formes les plus simples de mécanismes d'exclusion mutuelle utilisés dans les systèmes d'exploitation multiprocesseurs. Leur rôle principal est d'empêcher l'accès concurrent aux sections critiques par plusieurs cœurs, garantissant ainsi la cohérence et l'intégrité des données. Généralement, les spinlocks protègent contre la préemption sur le CPU actuel — souvent en désactivant les interruptions — et empêchent simultanément les autres cœurs d'accéder à la région de code protégée en utilisant des opérations atomiques sur la mémoire. La caractéristique définissant un spinlock est que lorsqu'un thread tente d'acquérir un verrou déjà détenu par un autre, il attend activement ou "tourne en boucle" ("spins"), vérifiant continuellement jusqu'à ce que le verrou devienne disponible. Ce comportement entraîne une consommation de temps CPU pendant les périodes d'attente, ce qui nécessite que les spinlocks soient détenus pour des durées minimales et idéalement ne soient pas sujets à la préemption pendant leur possession.\n\nHistoriquement, les premières implémentations de spinlocks étaient basées uniquement sur des opérations mémoire standard telles que les chargements et stockages. Des algorithmes classiques comme ceux d'Eisenberg & McGuire ou la boulangerie de Lamport ont démontré cette approche. Cependant, avec l'avènement des processeurs multicœurs modernes, en particulier ceux avec des modèles mémoire faibles comme les architectures x86 et x86-64, ces implémentations naïves échouent souvent à garantir la correction. Bien que les barrières mémoire puissent corriger ces problèmes, elles tendent à réduire l'efficacité. Par conséquent, les spinlocks modernes s'appuient sur des opérations atomiques plus puissantes fournies par le matériel, telles que test-and-set, compare-and-swap, et les instructions load-linked/store-conditional. Ces opérations assurent l'atomicité et une synchronisation appropriée, essentielles pour le bon fonctionnement du spinlock.\n\nUn spinlock simple en mode utilisateur peut être implémenté en C en utilisant les intrinsics atomiques de GCC. L'état du verrou est représenté par un entier, où zéro indique que le verrou est libre, et un indique qu'il est détenu. Les tentatives d'acquisition impliquent une opération atomique de compare-and-swap qui fait passer le verrou de libre à détenu de manière atomique. Cette approche, marquée par l'utilisation des sémantiques d'ordre mémoire les plus fortes (__ATOMIC_SEQ_CST), garantit une cohérence séquentielle entre les cœurs, bien qu'avec des inefficacités potentielles. Un inconvénient de ce design basique est la possibilité d'injustice en cas de contention ; le sous-système mémoire détermine aléatoirement quel cœur acquiert le verrou ensuite, ce qui peut entraîner des fréquences d'accès disproportionnées entre les cœurs.\n\nPour répondre aux préoccupations d'équité, les conceptions de spinlocks emploient parfois un mécanisme de ticket qui impose un ordre premier entré, premier sorti (FIFO). Les spinlocks à ticket attribuent un numéro de ticket incrémental à chaque demandeur et accordent l'accès dans l'ordre croissant des tickets. Cette approche garantit l'équité, car chaque thread attend son tour, analogue à l'algorithme de la boulangerie de Lamport. En termes d'implémentation, le verrou à ticket maintient deux compteurs — avant et arrière — représentant le ticket actuellement servi et le prochain ticket à attribuer. Lorsqu'un ticket de thread correspond au compteur avant, il accède à la section critique. Cette méthode a démontré un comportement très équitable même sur de grands systèmes multicœurs.\n\nLes noyaux Linux utilisent souvent des spinlocks en file d'attente plus sophistiqués, qui sont complexes et répartis sur plusieurs fichiers source. Cependant, en se concentrant sur les plateformes ARM 32 bits, l'implémentation du spinlock tire parti des instructions load-linked/store-conditional (ldrex/strex) du processeur pour effectuer des incréments atomiques et assurer une synchronisation correcte. La structure de données représentant le verrou peut être vue soit comme un entier 32 bits, soit comme deux champs de 16 bits, fonctionnant comme les compteurs avant et arrière du verrou à ticket. L'utilisation de prefetchw (qui compile en instruction pldw) prépare la ligne de cache pour l'écriture, optimisant les opérations de verrouillage ultérieures.\n\nLe code d'acquisition du spinlock ARM utilise un bloc d'assemblage en ligne qui imite étroitement le comportement du verrou à ticket. L'instruction ldrex charge la valeur du verrou et marque l'emplacement mémoire pour un accès exclusif. Le code incrémente les 16 bits supérieurs de la valeur du verrou pour générer un nouveau numéro de ticket. Ensuite, strex tente de stocker cette valeur de manière conditionnelle, réussissant uniquement si aucun autre processeur n'a modifié la mémoire. Si le stockage échoue, le processus se répète, assurant l'atomicité via un contrôle optimiste de la concurrence. Ce mécanisme permet une synchronisation efficace et bas niveau adaptée aux nuances de l'architecture ARM.