Antworten:
Eine Drehsperre schützt eine gemeinsam genutzte Ressource davor, von zwei oder mehr Prozessen gleichzeitig geändert zu werden. Der erste Prozess, der versucht, die Ressource zu ändern, "erwirbt" die Sperre und setzt ihren Weg fort, indem er mit der Ressource das tut, was erforderlich ist. Alle anderen Prozesse, die anschließend versuchen, die Sperre abzurufen, werden angehalten. Man sagt, sie "drehen sich an Ort und Stelle" und warten darauf, dass die Sperre durch den ersten Prozess freigegeben wird, daher der Name Spin Lock.
Der Linux-Kernel verwendet Spin-Locks für viele Dinge, beispielsweise beim Senden von Daten an ein bestimmtes Peripheriegerät. Die meisten Hardware-Peripheriegeräte sind nicht für die gleichzeitige Aktualisierung mehrerer Status ausgelegt. Wenn zwei verschiedene Modifikationen vorgenommen werden müssen, muss eine genau der anderen folgen, sie dürfen sich nicht überschneiden. Eine Drehsperre bietet den erforderlichen Schutz und stellt sicher, dass die Änderungen nacheinander durchgeführt werden.
Spin-Locks sind ein Problem, da das Drehen den CPU-Kern des Threads daran hindert, andere Arbeiten auszuführen. Während der Linux-Kernel Multitasking-Dienste für User-Space-Programme bereitstellt, die unter ihm ausgeführt werden, erstreckt sich diese universelle Multitasking-Funktion nicht auf Kernel-Code.
Diese Situation ändert sich und war für den größten Teil der Existenz von Linux von Bedeutung. Bis Linux 2.0 war der Kernel fast ein reines Single-Tasking-Programm: Wenn auf der CPU Kernel-Code ausgeführt wurde, wurde nur ein CPU-Kern verwendet, da alle gemeinsam genutzten Ressourcen durch eine einzige Drehsperre geschützt wurden, die als Big Kernel Lock (BKL) bezeichnet wurde ). Ab Linux 2.2 wird die BKL langsam in viele unabhängige Sperren aufgeteilt, die jeweils eine stärker fokussierte Ressourcenklasse schützen. Heute, mit Kernel 2.6, existiert die BKL immer noch, aber sie wird nur von wirklich altem Code verwendet, der nicht ohne Weiteres in eine granularere Sperre verschoben werden kann. Es ist jetzt durchaus möglich, dass auf einer Multicore-Box auf jeder CPU nützlicher Kernel-Code ausgeführt wird.
Es gibt eine Grenze für den Nutzen der Aufteilung der BKL, da dem Linux-Kernel das allgemeine Multitasking fehlt. Wenn ein CPU-Kern blockiert wird, während er sich auf einem Kernel-Spin-Lock dreht, kann er nicht wiederholt werden, um etwas anderes zu tun, bis die Sperre aufgehoben wird. Es sitzt und dreht sich nur, bis die Sperre freigegeben wird.
Spin-Locks können eine Monster-16-Core-Box effektiv in eine Single-Core-Box verwandeln, wenn die Arbeitslast so ist, dass jeder Core immer auf eine einzelne Spin-Lock wartet. Dies ist die Hauptbeschränkung für die Skalierbarkeit des Linux-Kernels: Die Verdoppelung der CPU-Kerne von 2 auf 4 wird wahrscheinlich die Geschwindigkeit einer Linux-Box fast verdoppeln, aber die Verdoppelung von 16 auf 32 wird es bei den meisten Workloads wahrscheinlich nicht tun.
Eine Drehsperre ist, wenn ein Prozess kontinuierlich nach einer Sperre fragt, die entfernt werden soll. Es wird als schlecht angesehen, weil der Prozess (normalerweise) unnötig Zyklen verbraucht. Es ist nicht Linux-spezifisch, sondern ein allgemeines Programmiermuster. Und obwohl dies im Allgemeinen als schlechte Praxis angesehen wird, ist es in der Tat die richtige Lösung. Es gibt Fälle, in denen die Kosten für die Verwendung des Schedulers (in Bezug auf die CPU-Zyklen) höher sind als die Kosten für die wenigen Zyklen, von denen erwartet wird, dass der Spinlock von Dauer ist.
Beispiel eines Spinlocks:
#!/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
Es gibt häufig eine Möglichkeit, eine Drehsperre zu vermeiden. Für dieses Beispiel gibt es ein Linux-Tool namens inotifywait (normalerweise nicht standardmäßig installiert). Wenn es in C geschrieben wäre, würden Sie einfach die inotify- API verwenden, die Linux bereitstellt.
Das gleiche Beispiel mit inotifywait zeigt, wie dasselbe ohne Spinlock erreicht werden kann:
#/bin/sh
inotifywait -e delete_self /var/run/example.lock
#do stuff
Wenn ein Thread versucht, eine Sperre zu erlangen, können drei Dinge passieren, wenn es fehlschlägt, es kann versuchen und blockieren, es kann versuchen, fortzufahren, es kann dann schlafen gehen und dem Betriebssystem sagen, es solle aufgeweckt werden, wenn ein Ereignis eintritt.
Jetzt kostet ein Versuch, fortzufahren, viel weniger Zeit als ein Versuch, zu blockieren. Nehmen wir für den Moment an, dass ein "try and continue" eine Zeiteinheit und ein "try and block" eine Hundert benötigt.
Nehmen wir nun für den Moment an, dass ein Thread im Durchschnitt 4 Einheiten Zeit benötigt, um die Sperre zu halten. Es ist verschwenderisch, 100 Einheiten zu warten. Also schreibst du stattdessen eine Schleife von "try and continue". Beim vierten Versuch erwerben Sie normalerweise das Schloss. Dies ist eine Drehsperre. Es heißt so, weil sich der Faden weiter dreht, bis er die Sperre erreicht.
Eine zusätzliche Sicherheitsmaßnahme besteht darin, die Anzahl der Schleifendurchläufe zu begrenzen. Im Beispiel führen Sie also eine for-Schleife aus, z. B. sechsmal. Wenn dies fehlschlägt, "versuchen und blockieren" Sie.
Wenn Sie wissen, dass ein Thread immer die Sperre für beispielsweise 200 Einheiten hält, verschwenden Sie die Computerzeit für jeden Versuch und fahren fort.
Letztendlich kann ein Spin-Lock also sehr effizient oder verschwenderisch sein. Es ist verschwenderisch, wenn die "typische" Zeit zum Halten eines Schlosses länger ist als die Zeit, die zum "Versuchen und Blockieren" benötigt wird. Es ist effizient, wenn die typische Zeit zum Halten eines Schlosses viel kürzer ist als die Zeit zum "Versuchen und Blockieren".
Ps: Das Buch zum Lesen auf Threads ist "A Thread Primer", wenn Sie es noch finden können.
Mit einer Sperre können zwei oder mehr Aufgaben (Prozesse, Threads) synchronisiert werden. Insbesondere wenn beide Tasks zeitweise Zugriff auf eine Ressource benötigen, die jeweils nur von einer Task verwendet werden kann, können die Tasks so einrichten, dass die Ressource nicht gleichzeitig verwendet wird. Um auf die Ressource zuzugreifen, muss eine Aufgabe die folgenden Schritte ausführen:
take the lock
use the resource
release the lock
Das Aufnehmen einer Sperre ist nicht möglich, wenn sie bereits von einer anderen Aufgabe übernommen wurde. (Stellen Sie sich die Sperre als ein physisches Token-Objekt vor. Entweder befindet sich das Objekt in einer Schublade oder jemand hat es in der Hand. Nur die Person, die das Objekt hält, darf auf die Ressource zugreifen.) „Sperre aufheben“ bedeutet also wirklich „Warten bis niemand anderes hat das Schloss, dann nimm es “.
Auf hoher Ebene gibt es zwei Hauptmethoden zum Implementieren von Sperren: Spinlocks und Bedingungen. Bei Spinlocks bedeutet das Aufnehmen der Sperre, dass Sie sich nur drehen (dh nichts in einer Schleife tun), bis niemand mehr die Sperre hat. Unter bestimmten Bedingungen tritt der Neueinsteiger in eine Warteschlange ein, wenn eine Task versucht, die Sperre zu übernehmen, diese jedoch blockiert, weil sie von einer anderen Task gehalten wird. Der Freigabevorgang signalisiert jedem wartenden Task, dass das Schloss jetzt verfügbar ist.
(Diese Erklärungen reichen nicht aus, um eine Sperre zu implementieren, da ich nichts über Atomarität gesagt habe. Aber Atomarität ist hier nicht wichtig.)
Spinlocks sind offensichtlich verschwenderisch: Die wartende Task überprüft ständig, ob die Sperre übernommen wurde. Warum und wann wird es verwendet? Spinlocks sind oft sehr billig zu bekommen, wenn das Schloss nicht gehalten wird. Dies macht es attraktiv, wenn die Chance, dass das Schloss gehalten wird, gering ist. Darüber hinaus sind Spinlocks nur dann funktionsfähig, wenn der Erhalt der Sperre nicht lange dauern dürfte. Spinlocks werden daher in der Regel in Situationen verwendet, in denen sie nur sehr kurz gehalten werden, sodass die meisten Versuche beim ersten Versuch erfolgreich sind und diejenigen, die eine Wartezeit benötigen, nicht lange warten.
Spinlocks und andere Parallelitätsmechanismen des Linux-Kernels werden in Linux Device Drivers , Kapitel 5, ausführlich erläutert .
synchronized
dies von einem Spinlock implementiert würde: Ein synchronized
Block könnte sehr lange laufen. synchronized
ist ein Sprachkonstrukt, mit dem Sperren in bestimmten Fällen einfach verwendet werden können, kein Grundelement zum Erstellen größerer Synchronisationsgrundelemente.
Ein Spinlock ist eine Sperre, die durch Deaktivieren des Schedulers und möglicherweise durch Interrupts (irqsave-Variante) für den jeweiligen Kern ausgeführt wird, für den die Sperre erworben wurde. Es unterscheidet sich von einem Mutex darin, dass es die Zeitplanung deaktiviert, sodass nur Ihr Thread ausgeführt werden kann, während Spinlock gehalten wird. Mit einem Mutex können andere Threads mit höherer Priorität eingeplant werden, während sie gehalten werden, aber sie können den geschützten Abschnitt nicht gleichzeitig ausführen. Da Spinlocks Multitasking deaktivieren, können Sie keinen Spinlock nehmen und dann einen anderen Code aufrufen, der versucht, einen Mutex zu erhalten. Ihr Code im Spinlock-Bereich darf niemals in den Ruhezustand versetzt werden (der Code wird normalerweise in den Ruhezustand versetzt, wenn er auf einen gesperrten Mutex oder ein leeres Semaphor stößt).
Ein weiterer Unterschied zu einem Mutex besteht darin, dass Threads normalerweise für einen Mutex anstehen, sodass ein Mutex darunter eine Warteschlange hat. Während Spinlock nur sicherstellt, dass kein anderer Thread ausgeführt wird, auch wenn dies erforderlich ist. Daher dürfen Sie niemals einen Spinlock halten, wenn Sie Funktionen außerhalb Ihrer Datei aufrufen, von denen Sie nicht sicher sind, dass sie nicht in den Ruhezustand versetzt werden.
Wenn Sie Ihr Spinlock mit einem Interrupt teilen möchten, müssen Sie die Variante irqsave verwenden. Dies deaktiviert nicht nur den Scheduler, sondern auch die Interrupts. Es macht Sinn, oder? Spinlock funktioniert, indem sichergestellt wird, dass nichts anderes ausgeführt wird. Wenn Sie nicht möchten, dass ein Interrupt ausgeführt wird, deaktivieren Sie ihn und fahren Sie sicher mit dem kritischen Abschnitt fort.
Auf Multicore-Maschinen dreht sich ein Spinlock und wartet auf einen anderen Kern, der das Schloss hält, um es zu lösen. Dieses Drehen findet nur auf Multicore-Maschinen statt, da dies auf Single-Core-Maschinen nicht möglich ist (Sie halten entweder Spinlock und fahren fort oder Sie werden erst ausgeführt, wenn die Sperre aufgehoben wird).
Spinlock ist nicht verschwenderisch, wo es Sinn macht. Bei sehr kleinen kritischen Abschnitten wäre es verschwenderisch, eine Mutex-Task-Warteschlange zuzuweisen, anstatt den Scheduler nur für einige Mikrosekunden anzuhalten, die zum Beenden der wichtigen Arbeit erforderlich sind. Wenn Sie schlafen müssen oder die Sperre für eine Operation halten müssen (die schlafen kann), verwenden Sie einen Mutex. Sperren Sie auf keinen Fall ein Spinlock und versuchen Sie dann, es innerhalb eines Interrupts freizugeben. Während dies funktioniert, wird es wie der Arduino-Mist von while (Flagnotset) sein; Verwenden Sie in diesem Fall ein Semaphor.
Schnappen Sie sich einen Spinlock, wenn Sie einen einfachen gegenseitigen Ausschluss für Blöcke von Speichertransaktionen benötigen. Greifen Sie zu einem Mutex, wenn mehrere Threads unmittelbar vor einer Mutex-Sperre angehalten werden sollen, und wählen Sie dann den Thread mit der höchsten Priorität aus, der fortgesetzt werden soll, wenn der Mutex frei wird und wenn Sie im selben Thread sperren und freigeben. Holen Sie sich ein Semaphor, wenn Sie es in einem Thread oder einem Interrupt veröffentlichen möchten, und nehmen Sie es in einen anderen Thread. Es gibt drei leicht unterschiedliche Möglichkeiten, um gegenseitigen Ausschluss zu gewährleisten, und sie werden für leicht unterschiedliche Zwecke verwendet.