Systeme 1
Kapitel 6.1
Nebenläufigkeit und wechselseitiger
Ausschluss
WS 2009/10
1
Letzte Vorlesung
• Threads
– „leichtgewichtige Prozesse“
– Gemeinsamer Adressraum
• Nebenläufigkeit (Threads)
– parallele bzw. pseudo-parallele Ausführung
– Kontrollprobleme
• Wechselseitiger Ausschluss
• Deadlocks
• Livelocks
– Anforderungen an wechselseitigen Ausschluss
WS 2009/10
2
Letzte Vorlesung
• Anforderungen an wechselseitigen Ausschluss
– Softwarelösungen
• Versuche 1 - 5
Busy
Waiting
Wechselseitiger
Ausschluss garantiert
Nicht-alternierender Zugriff auf
kritischen Abschnitt möglich
Versuch 1
Versuch 2
Versuch 3
Versuch 4
Versuch 5
WS 2009/10
3
Wechselseitiger Ausschluss in Hardware
• Zur Erinnerung Versuch 2 als Softwarelösung
/* Prozess 0 */
wiederhole
{
solange (flag[1] = true)
tue nichts;
flag[0] := true;
/* kritischer Abschnitt */
flag[0] := false;
/* nichtkrit. Abschnitt */
}
/* Prozess 1 */
wiederhole
{
solange (flag[0] = true)
tue nichts;
flag[1] := true;
/* kritischer Abschnitt */
Flag[1] := false;
/* nichtkrit. Abschnitt */
}
– Warum scheiterte dieser Versuch?
– Weil Testen und Setzen von Flags nicht in einem einzigen
Schritt durchführbar:
• Prozesswechsel zwischen Testen und Setzen ist möglich.
WS 2009/10
4
Wechselseitiger Ausschluss in Hardware
• Neues Konzept:
– Einführung atomarer Operationen.
– Hardware garantiert atomare Ausführung.
• Testen und Setzen zusammen bilden eine
atomare Operation:
– Definiere neuen Befehl TSL: Test and Set Lock.
– Da TSL ein einziger Befehl ist, kann ein Prozesswechsel
nicht zwischen Testen und Setzen erfolgen (nicht
“mitten im Befehl”).
WS 2009/10
5
Wechselseitiger Ausschluss in Hardware
• Befehl TSL RX, LOCK
mit Speicheradresse LOCK und Register RX hat folgende
Wirkung:
– RX = Speicher[LOCK]; Speicher[LOCK] := 1
– Ein Befehl, d.h. atomare Ausführung.
• Prozesse, die Zugriff auf den kritischen Abschnitt erhalten
wollen, führen folgende Befehle aus:
enter_region:
TSL, RX, LOCK
CMP RX, #0
JNE enter_region
...
// kopiere Lock-Variable und setze Lock
// War Lock-Variable = 0? (CMP = compare)
// Wenn Lock schon gesetzt war -> Schleife
(JNE = jump if not equal)
// Fortfahren und Betreten des krit. Abschnitts
• Prozesse, die den kritischen Abschnitt verlassen, führen
folgenden Befehl aus:
STOREI LOCK, #0
WS 2009/10
// Speicher[LOCK] := 0 (STOREI = store immediate)
6
Wechselseitiger Ausschluss im Betriebssystem
• Folgerung:
– Um aktives Warten zu verhindern, muss wechselseitiger Ausschluss ins
Betriebssystem integriert werden!
– Idee: Statt aktiv zu warten, blockiere Prozesse einfach!
– Neuer Systemaufruf sleep(lock)
• Nach Verlassen des kritischen Abschnitts weckt der verlassende Prozess einen
anderen Prozess auf, der auf Erlaubnis wartet, den kritischen Abschnitt zu betreten
(sofern ein solcher Prozess vorhanden ist).
– Neuer Systemaufruf wakeup(lock)
– Parameter lock wird nur gebraucht, um Aufrufe für den gleichen
kritischen Abschnitt einander zuzuordnen.
– Eine Warteschlange pro kritischem Abschnitt
WS 2009/10
7
Mutex
• Mutex = Mutual Exclusion
• Vor dem Eintritt in einen kritischen Abschnitt wird die
Funktion mutex_lock(lock) aufgerufen.
function mutex_lock(lock)
{
solange (testset(lock) = false)
{
sleep(lock);
}
return;
}
• testset(lock) führt atomare TSL-Operation aus; liefert
true gdw. Lockvariable vorher 0 war.
WS 2009/10
8
Mutex
• Nach Verlassen des kritischen Abschnitts wird
mutex_unlock(lock) aufgerufen.
function mutex_lock(lock)
{
unset(lock);
// lock wird freigegeben
wakeup(lock);
return;
}
• Es muss eine Warteschlange für Prozesse geben, die auf lock
warten.
• Nach wakeup(lock) wird der erste Prozess in der Queue bereit
(aber nicht notwendigerweise aktiv -> Scheduler-Algorithmus!).
• Die Variable lock heißt Mutexvariable bzw. kurz Mutex.
WS 2009/10
9
Das Produzenten-Konsumenten Problem
Typisches Problem bei nebenläufigen Prozessen:
–
–
–
–
–
–
–
Gemeinsamer Puffer
Einige Prozesse schreiben in den Puffer (“Produzenten”)
Einige Prozesse lesen aus dem Puffer (“Konsumenten”)
Prozedur insert_item schreibt Objekt in Puffer.
Prozedur remove_item entfernt Objekt aus Puffer.
Puffergröße ist beschränkt und Puffer kann leer sein.
Wenn Puffer voll ist, dann sollten Produzenten nicht einfügen.
Aus Effizienzgründen: Blockieren der Produzenten, die einfügen
wollen.
– Wenn Puffer leer ist, sollten Konsumenten nichts entfernen.
Aus Effizienzgründen: Blockieren der Konsumenten, die
entfernen wollen.
WS 2009/10
10
Das Produzenten-Konsumenten Problem
• Lösungsansatz:
– Gemeinsame Variable count für die Anzahl der
Elemente im Puffer
– Initialer Wert 0
– sleep() und wakeup()
– Anfangs schlafen die Konsumenten (Puffer ist
leer).
WS 2009/10
11
Das Produzenten-Konsumenten Problem
Prozedur producer
{
...
wiederhole
{
item = produce_item();
wenn (count = MAX_BUFFER)
sleep(producer);
insert_item(item)
count = count + 1;
wenn (count = 1)
wakeup(consumer);
//produziere nächstes Objekt
//schlafe, wenn Puffer
//voll
// Einfügen in Puffer
// wenn Puffer vorher leer:
// wecke Konsumenten
}
}
WS 2009/10
12
Das Produzenten-Konsumenten Problem
Prozedur consumer
{
...
wiederhole
{
wenn (count = 0)
Sleep(consumer);
Ist diese Lösung korrekt???
// schlafe, wenn Puffer
// leer
item = remove_item();
count = count -1;
// Entferne aus Puffer
wenn (count = MAX_BUFFER – 1)
wakeup(producer);
// wenn Puffer voll
// wecke Produzenten
consume_item(item);
// konsumiere Objekt
}
}
WS 2009/10
13
Das Produzenten-Konsumenten Problem
• Mögliche Probleme:
1)
Zwei Konsumenten: 1 Element im Puffer
• Konsument 1 entnimmt Element und wird unterbrochen bevor er
count auf 0 setzen kann.
• Konsument 2 wird aktiv aber der Puffer ist leer! (count immer noch 1)
Fehler!
2)
Zwei Konsumenten: Puffer ist voll.
• count == MAX_BUFFER
• Konsument 1 entnimmt Element und führt count = count–1; aus und
wird dann unterbrochen (count == MAX_BUFFER-1).
• Konsument 2 wird aktiv, entnimmt ein Element und reduziert count.
• Problem: Produzent wird nie mehr geweckt, da Bedingung
wenn (count = MAX_BUFFER – 1)
nie mehr erfüllt wird!
WS 2009/10
14
Das Produzenten-Konsumenten Problem
• Ein Produzent und ein Konsument
– Konsument gibt Prozessor ab nach
wenn(count = 0)
–
–
–
–
wenn Puffer leer ist
Dann fügt der Produzent ein Objekt ein, count = 1.
Aufwecken des Konsumenten geht verloren, da er noch gar
nicht schläft.
Nach nächstem Prozesswechsel: Konsument schläft für immer.
Wenn Puffer voll wird, schläft auch der Produzent für immer.
Deadlock
– Problem:
wenn (count = 0) sleep(consumer)
ist keine atomare Operation!
WS 2009/10
15
Das Produzenten-Konsumenten Problem
• Die elegante Lösung des ProduzentenKonsumenten Problems ist die Nutzung eines
Semaphor.
• Dijkstra (1965)
• Entwickelt zur Synchronisation von Prozessen.
• Konzept:
– Integer-Variable mit drei Operationen:
• Initialisierung mit nicht-negativen Wert
• down() Operation
• up() Operation
WS 2009/10
16
Wechselseitiger Ausschluss mit Semaphoren
• Voraussetzungen:
– Es existiert ein Semaphor s.
– countS ist auf 1 initialisiert.
– n Prozesse sind gestartet, konkurrieren um
kritischen Abschnitt.
/* Prozess i */
wiederhole
{
down(s);
/* kritischer Abschnitt */
up(s);
/* nichtkritischer Abschnitt */
}
WS 2009/10
17
Wechselseitiger Ausschluss mit Semaphoren
• Beispiel:
– Semaphor wird mit 1 initialisiert.
– Prozess 1 geht in kritischen Abschnitt:
• down(s) -> Semaphor-Zähler wird 0
– Prozess 2 will in den kritischen Abschnitt:
• down(s) -> Semaphor-Zähler wird -1
– Prozess 2 wird blockiert und in die Warteschlange eingefügt.
– Prozess 3 will in den kritischen Abschnitt:
• down(s) -> Semaphor-Zähler wird -2
• Prozess 3 wird blockiert und in die Warteschlange eingefügt.
– Prozess 1 verlässt kritischen Abschnitt:
• up(s) -> Semaphor-Zähler wird -1
• Zähler <= 0, d.h. ein Prozess wird der Warteschlange entnommen
und wird bereit.
WS 2009/10
18
Wechselseitiger Ausschluss mit Semaphoren
• Auf 1 initialisierte Semaphore heißen binäre
Semaphore.
• Behandlung mehrfach nutzbarer Ressourcen (mfach) ist möglich:
– durch Initialisierung countS = m.
• Interpretation von counts:
– Falls countS ≥ 0:
• countS gibt die Anzahl der Prozesse an, die down(s) ausführen
können ohne zu blockieren (wenn nicht zwischenzeitlich up(s)
ausgeführt wird).
– Falls countS < 0:
WS 2009/10
• |countS| ist die Anzahl der wartenden Prozesse in queueS.
19
Das Produzenten-Konsumenten Problem mit
Semaphoren
semaphore mutex; countmutex = 1;
// mutex für kritische Abschnitte
semaphore empty; countempty = MAX_BUFFER;
// zählt freie Plätze
semaphore full; countfull = 0;
// zählt belegte Plätze
Prozedur producer
{
...
wiederhole
{
item = produce_item();
down(empty);
down(mutex);
insert_item(item);
up(mutex);
up(full);
}
}
WS 2009/10
//produziere nächstes Objekt
// Einfügen in Puffer
20
Das Produzenten-Konsumenten Problem mit
Semaphoren
Prozedur consumer
{
...
wiederhole
{
down(full);
down(mutex);
item = remove_item();
up(mutex);
up(empty);
consume_item(item);
}
}
// Entferne aus Puffer
// konsumiere Objekt
Frage:
– Funktioniert das immer noch, wenn in Prozedur
consumer
• up(mutex) und up(empty) vertauscht werden?
• down(full) und down(mutex) vertauscht werden?
WS 2009/10
21
Das Produzenten-Konsumenten Problem mit
Semaphoren
• Frage: Funktioniert das immer noch, wenn in
Prozedur consumer
und up(empty) vertauscht werden?
down(full) und down(mutex) vertauscht werden?
– up(mutex)
–
• Angenommen der Puffer ist leer:
– down(mutex), down(full)
• Konsument blockiert, da es keine „vollen Plätze“ gibt.
• Produzenten können aber nicht den Puffer füllen, da sie
alle bei down(mutex) hängen bleiben.
• Irgendwann schlafen alle -> Deadlock!
WS 2009/10
22
Implementierung von Semaphoren: Versuch 1
k.A.
k.A.
down(semaphore s)
up(semaphore s)
{
{
mutex_lock(lockS);
mutex_lock(lockS);
countS = countS - 1;
countS = countS + 1;
wenn (countS < 0)
if (countS <= 0)
{
{
setze diesen Prozess in
entferne einen Prozess P aus
queueS;
queueS;
blockiere den Prozess und
Schreibe Prozess P in Liste
führe unmittelbar vor
der bereiten Prozesse
Abgabe des Prozessors noch
}
mutex_unlock(lockS) aus
mutex_unlock(lockS);
}
}
sonst
mutex_unlock(lockS);
}
WS 2009/10
23
Implementierung von Semaphoren: Versuch 1
• Analyse:
down und up sind nicht wirklich atomar, aber trotzdem
stören sich verschiedene Aufrufe von down und up
nicht aufgrund des Mutex.
Zumindest für binäre Semaphore ist Verwendung von
Mutexen in Semaphoraufrufen etwas aufwändig, weil
Mutexe alleine schon reichen, um wechselseitigen
Ausschluss zu garantieren!
Zwei Queues:
• Liste von Prozessen, die auf Freigabe des Mutex warten.
• Liste von Prozessen, die auf Erhöhung der SemaphorVariable warten.
WS 2009/10
24
Implementierung von Semaphoren: Versuch 2
k.A.
k.A.
down(semaphore s)
up(semaphore s)
{
{
solange (testset(lockS) = false)
solange (testset(lockS) = false)
tue nichts;
tue nichts;
countS = countS - 1;
countS = countS + 1;
wenn (countS < 0)
if (countS <= 0)
{
{
setze diesen Prozess in
entferne einen Prozess P aus
queueS;
queueS;
blockiere den Prozess und
Schreibe Prozess P in Liste
führe unmittelbar vor
der bereiten Prozesse
Abgabe des Prozessors noch
}
lockS = 0 aus
lockS = 0;
}
}
sonst
lockS = 0
}
WS 2009/10
25
Implementierung von Semaphoren: Versuch 2
• Analyse:
– aktives Warten (solange(...) tue nichts;)!
• nicht so gravierend:
– beschränkt auf up und down
– up und down sind relativ kurz
– down und up sind nicht wirklich atomar, aber trotzdem stören
sich verschiedene Aufrufe von down und up nicht aufgrund des
busy waitings.
– Semaphoren blockieren Prozesse -> keine CPU-Zeit für wartende
Prozesse
– Unterbinden von Unterbrechungen nicht nötig.
– Implementierung auch für Multiprozessoren geeignet.
• Für Benutzer sind nur abstrakte Semaphoren sichtbar, keine
Implementierungsdetails.
WS 2009/10
26
Zusammenfassung
• CPU (einzelne CPU oder Multiprozessor) wird
von mehreren Prozessen geteilt.
• Verwaltung gemeinsamer Ressourcen bei
Multiprogramming ist nicht trivial.
• Subtile Fehler möglich, formale Beweise nötig.
• Konzepte für wechselseitigen Ausschluss,
Produzenten-Konsumenten Problem.
WS 2009/10
27