Open Source im professionellen Einsatz

Futexe und andere Formen der Prozesssynchronisation

Kampf um die Ressourcen

Sobald eine Anwendung parallele Prozesse oder Threads einsetzt, ist deren Synchronisation unerlässlich. Der Kernel 2.6 stellt neben den etablierten Spinlocks und Sys-V-Semaphoren mit den Fast Userspace Mutexes einen neuen, oftmals besonders effizienten Ansatz bereit.

Moderne Anwendungen wie Apache, Oracle und SAP R/3 erzeugen mehrere Prozesse oder Threads, um Anfragen performanter abzuarbeiten. Die Prozesse konkurrieren dann allerdings um die dieselben Ressourcen, primär um gemeinsame Speicherbereiche [1]. Ohne eine Synchronisation führen solche Konkurrenzsituationen über kurz oder lang zu fehlerhaften Ergebnissen bis hin zum Absturz der Anwendung. Dieser Artikel beschreibt neben zwei gängigen Synchronisationsverfahren - Spinlocks und Sys-V-Semaphoren - auch eine Form, die mit dem Kernel 2.6 Einzug gehalten hat: die Fast Userspace Mutexes, kurz Futexe [2].

Das sehr einfache Beispiel einer gemeinsamen genutzten Variablen soll demonstrieren, welche Probleme ein unkontrollierter, nicht synchronisierter Zugriff mit sich bringt. Listing 1 zeigt einen, aus didaktischen Gründen vereinfachten Ausschnitt aus einer Multithreaded-Anwendung. Es nimmt an, dass zwei Threads A und B die Funktion »func()« ausführen. Der Wert der Variablen »glob« sei initial 0.

Beide Threads dürfen »glob« beschreiben. Ein typischer Zugriff verläuft so: Thread A hat gerade den Wert 0 der Variablen »glob« in ein Register gelesen, um ihn um 1 zu erhöhen. Wenn just zu diesem Zeitpunkt der Kernel Thread A unterbricht und Thread B zum Zuge kommen lässt, wird B ebenfalls den Wert 0 in ein Register schreiben. Anschließend schreibt er den um 1 erhöhten Wert, also eine 1, zurück in den Speicher an den Ort der Variablen »glob«. Wenn Thread A seine Arbeit wieder aufnimmt, findet er noch immer den Wert 0 im Register und schreibt demgemäß eine 1 zurück. Der Wert der Variablen »glob« ist also 1.

Ein Wettlauf ums Ergebnis ist nicht akzeptabel

Das entspricht nicht dem intuitiven Eindruck des Code aus Listing 1, der eine 2 als Ergebnis erwarten lässt. Viel schlimmer aber ist, dass das Endergebnis vom zeitlichen Verlauf abhängt: Wäre die Unterbrechung des Thread A durch den Thread B zu einem anderen Zeitpunkt erfolgt, hätte »glob« andere Werte, hier beispielsweise 2, angenommen. Situationen wie diese, bei denen das Ergebnis von zeitlichen Abläufen abhängt, werden allgemein als Race Condition bezeichnet, siehe [1] und [3].

Kritische Bereiche als Sperre errichten

Szenarien wie das hier konstruierte sind aus zwei Gründen problematisch: Zum einen decken sich die erzielten Ergebnisse nicht mit den erwarteten. Dabei entstehen oft Sondersituationen, die der Code nicht abfängt und die zu Fehlern führen. Zum anderen treten solche Fehler nicht reproduzierbar auf, sondern hängen nach menschlichen Maßstäben vom puren Zufall ab und sind damit sehr schwer einzugrenzen.

Das gezeigte Beispiel sollte nicht zu der Annahme verleiten, dass nur Threads für diese Effekte anfällig sind. Auch bei Anwendungen aus mehreren Prozessen treten solche Fehler auf, da auch sie Variablen gemeinsam nutzen - und zwar keine globale Variablen, sondern als Shared Memory [4]. Um diesem Problem zu begegnen, versucht man meist den Zugriff eines Thread oder Prozesses auf einen gemeinsam genutzten Speicherbereich ununterbrochen zu gestalten. Anders ausgedrückt: Es gibt Stellen im Code, die nur ein Prozess oder Thread zugleich durchlaufen darf, so genannte kritische Bereiche oder Pfade [1].

Entwickler realisieren solche kritischen Bereiche gern durch Sperren. Haben sie eine Sperre gesetzt, kann kein weiterer Prozess oder Thread in den kritischen Bereich eintreten. Es entsteht ein wechselseitiger Ausschluss: Mutual Exclusion (Mutex). Den Versuch, eine Sperre respektive einen Mutex zu implementieren, zeigt das ebenso einfache wie falsche Beispiel in Listing 2.

Auf den ersten Blick scheint der Ablauf klar zu sein: Findet ein Thread die globale Sperrvariable »sperre« gesetzt vor, geht er in eine Endlosschleife, bis die Sperrung wieder aufgehoben ist, das heißt bis »sperre« den Wert »UNLOCKED« erhält. Er setzt dann seinerseits die Sperre und tritt in den kritischen Bereich ein. Nach Beendigung der Arbeit gibt er die Sperre mit »sperre = UNLOCKED« wieder frei.

Dummerweise krankt dieser Ansatz prinzipbedingt an dem gleichen Problem wie der Zugriff auf »glob« in Listing 1. Eine Unterbrechung nach dem Prüfen des Wertes von »sperre« in der »while«-Bedingung und vor dem erneuten Setzen »sperre = LOCKED« kann dazu führen, dass mehrere Prozesse in den kritischen Bereich hineingelangen.

Diesen Artikel als PDF kaufen

Als digitales Abo

Als PDF im Abo bestellen

comments powered by Disqus

Ausgabe 07/2013

Preis € 6,40

Insecurity Bulletin

Insecurity Bulletin

Im Insecurity Bulletin widmet sich Mark Vogelsberger aktuellen Sicherheitslücken sowie Hintergründen und Security-Grundlagen. mehr...

Linux-Magazin auf Facebook