Betriebssysteme Tafelübung 3. Deadlock Spinczyk - TU Dortmund

Die Seite wird erstellt Stefan Henke
 
WEITER LESEN
Betriebssysteme Tafelübung 3. Deadlock Spinczyk - TU Dortmund
Betriebssysteme
                            Tafelübung 3. Deadlock
   https://ess.cs.tu-dortmund.de/DE/Teaching/SS2018/BS/

                                       Olaf Spinczyk
                            olaf.spinczyk@tu-dortmund.de
                         http://ess.cs.tu-dortmund.de/~os

AG Eingebettete Systemsoftware
Informatik 12, TU Dortmund
Agenda
●   Besprechung Aufgabe 2: Threadsynchronisation
●   Aufgabe 3: Deadlock
    –   Semaphore
    –   Wiederverwendbare/Konsumierbare Betriebsmittel
    –   Problemvorstellung
    –   Voraussetzungen für Verklemmungen
    –   Verklemmungsbehandlung
    –   Makefiles
●   Alte Klausuraufgabe zu Semaphoren

                                                         2
Besprechung Aufgabe 2
●   → Foliensatz Besprechung

                               3
Wiederholung Mutex
●   Mutex dienen zum Schutz von kritischen Abschnitten
    –   Nebenläufige Prozesse können hierdurch auf eine gemeinsame
        Datenstruktur zugreifen, ohne dass der Zustand der Datenstruktur
        inkonsisten wird.
        /* Schlossvariable (Initialwert 0) */
        pthread_mutex_t rangmutex;
        …
        int main(void){
           …                                           Kritische Abschnitte
           pthread_mutex_init(&rangmutex, NULL);         können auch durch
           …                                       Semaphore geschützt werden.
        }
                                                       Dazu müssen wir aber
                                                       zunächst wissen, was
        void* run(void* arg) {
                                                      Semaphore sind und wie
           …                                         wir sie benutzen können.
           pthread_mutex_lock(&rangmutex);
           rangliste[rang++] = team;
           pthread_mutex_unlock(&rangmutex);
           …
        }
                                                                                4
Mutexe vs. Semaphoren
●   Mit beiden können kritische Abschnitte geschützt werden.
●   Beim Mutex kann immer nur ein Thread den kritischen
    Abschnitt betreten.
●   Mit Semaphoren können n Threads den kritischen Abschnitt
    betreten.
●   Nützlich für Betriebssystemressourcen, bei denen eine
    bestimmte Anzahl zur Verfügung steht.
●   Für n=1 verhält sich ein Semaphor ähnlich wie ein Mutex.
    Semaphoren können aber auch von unterschiedlichen
    Prozessen freigegeben werden.

                                                               5
Semaphoren
●   Ein Sempahor ist eine Betriebssystemabstraktion zum
    Austausch von Synchronisierungssignalen zwischen
    nebenläufig arbeitenden Prozessen.
●   Steht für „Signalgeber“
●   E. Dijkstra: Eine „nicht-negative ganze Zahl“, für die zwei
    unteilbare Operationen definiert sind:

    P (holländisch prolaag, „erniedrige“; auch down oder wait)
    –   hat der Semaphor den Wert 0, wird der laufende Prozess blockiert
    –   ansonsten wird der Semaphor um 1 dekrementiert

    V (holländisch verhoog, „erhöhe“; auch up oder signal)
    –   auf den Semaphor ggf. blockierter Prozess wird deblockiert
    –   ansonsten wird der Semaphor um 1 inkrementiert

                                                                           6
Eselsbrücken zu Semaphoren
●   Mit P wartet man auf eine Ressource und belegt diese dann.

    Eselsbrücke: „p(b)elegen, ggfs. vorher warten”

    Es sind danach weniger Ressourcen verfügbar, also wird
    runtergezählt.

●   Mit V gibt man eine Ressource wieder frei, ggfs. wird der
    nächste wartende Thread benachrichtigt.

    Eselsbrücke: „v(f)reigeben, ggfs. benachrichtigen”

    Es sind danach wieder mehr Ressourcen verfügbar, also wird
    hochgezählt.

                                                                 7
Semaphor – Beispiel
●    Gemeinsam genutzte FIFO Queue mit maximal 100 Elementen
                                   void enqueue(element *item){
    /* gem. Speicher */
                                     if (item != NULL){
    Semaphore lock;
                                       p(&freiePlaetze);
    Semaphore freiePlaetze;
                                       p(&lock);
    struct List * queue;
                                       queue->tail = item;
                                       ...
    /* Initialisierung */              v(&lock);
    lock = 1;                        }
    freiePlaetze = 100;            }
    queue->head = NULL;            element * dequeue(){
    queue->tail = NULL;              p(&lock);
                                     element *item = queue->head;
                                     ...
Mehrere Threads können               if (item != NULL) v(&freiePlaetze);
Elemente in die Queue schreiben,     v(&lock);
andere Threads können dann           return item;
Elemente wieder herausnehmen.      }
                                                                           8
Semaphor – Beispiel
●    Zusätzlich noch einseitige Synchronisation

    /* gem. Speicher */        void producer(){
    Semaphore verfuegbar;        while(1){
                                   element *e = produce();
                                   enqueue(e);
/* Initialisierung */              v(verfuegbar);
verfuegbar = 0;                  }
                               }
                               void consumer(){
Die Consumer lesen nur, wenn     while(1){
etwas in der Queue steht.          p(verfuegbar);
                                   element *e = dequeue();
                                   consume(e);
                                 }
                               }

                                                             9
POSIX Semaphoren
        int sem_init(sem_t *sem, int pshared, unsigned int value);
●   Anlegen einer Semaphore
    –   Parameter
                                                    #include 
         ●   sem: Adresse des Semaphor-Objekts
         ●   pshared: 0, falls nur zwischen Threads eines Prozesses verwendet
         ●   value: Initalwert der Semaphore, entspricht dem n
    –   Rückgabewert:
         ●   0, wenn erfolgreich
         ●   -1 im Fehlerfall
    –   Bsp.:

                     sem_t semaphor;
                     if (sem_init(&semaphor, 0, 1) == -1) {
                         /* Fehlerbehandlung */
                     }
                                                                                10
POSIX Semaphoren
●
    Belegen einer Semaphore (P),
    ggf. müssen wir vorher warten:               int sem_wait(sem_t *sem);

●
    Freigeben einer Semaphore (V),
    ggf. wird der nächste Thread                 int sem_post(sem_t *sem);
    benachrichtigt:
●   Entfernen einer Semaphore:
                                           int sem_destroy(sem_t *sem);

    –   Parameter
         ●   sem: Adresse des Semaphor-Objekts
    –   Rückgabewert:
         ●   0, wenn erfolgreich
         ●   -1 im Fehlerfall

                                                                             11
POSIX Semaphore
●   Beispiel: Gegenseitiger Ausschluss mit Semaphore

    #include 
    /* Konsumierbare Rangsemaphore */
    sem_t rangsemaphore;                 globale Deklaration,
    ...                                   damit auch Threads
    int main(void){                         Zugriff haben
       sem_init(&rangsemaphore, 0, 1);
       ...
       sem_destroy(&rangsemaphore);
       return 0;
    }

    void* run(void* arg) {
       ...
       sem_wait(&rangsemaphore);
       rangliste[rang++] = team;     Fehlerbehandlung!
       sem_post(&rangsemaphore);
       ...
    }

                                                                12
Betriebsmittel ...
werden vom Betriebssystem verwaltet und den Prozessen
zugänglich gemacht. Man unterscheidet zwei Arten:
●   Wiederverwendbare Betriebsmittel
    –   Werden von Prozessen für eine bestimmte Zeit belegt und
        anschließend wieder freigegeben.
    –   Beispiele: CPU, Haupt- und Hintergrundspeicher, E/A-Geräte,
        Systemdatenstrukturen wie Dateien, Prozesstabelleneinträge, …
    –   Typische Zugriffssynchronisation: Gegenseitiger Ausschluss
●   Konsumierbare Betriebsmittel
    –   Werden im laufenden System erzeugt (produziert) und zerstört
        (konsumiert)
    –   Beispiele: Unterbrechungsanforderungen, Signale, Nachrichten, Daten
        von Eingabegeräten
    –   Typische Zugriffssynchronisation: Einseitige Synchronisation

                     Vorlesungsfolie – Vorlesung 6 S. 15                 13
Problemvorstellung
Das Szenario:
Chemiker in einem Chemielabor führen Experimente durch und
benötigen dazu Chemikalienflaschen von einem zentralen Vorrat.
Jeder Chemiker holt sich die erste für sein Experiment
notwendige Chemikalie, verarbeitet diese, holt dann die zweite,
verarbeitet diese und so weiter.
Die Chemikalienflaschen werden erst zurückgebracht, nachdem
das Experiment abgeschlossen ist.
Da die Anzahl an Chemikalienflaschen begrenzt ist, müssen
Chemiker ggf. warten, bis ein anderer Chemiker eine passende
Flasche zurückbringt.

                                                                  14
Deadlock-Voraussetzungen
Die notwendigen Bedingungen für eine Verklemmung:
1. „mutual exclusion“
   –   die umstrittenen Betriebsmittel sind nur unteilbar nutzbar

                                                                    15
Deadlock-Voraussetzungen
Die notwendigen Bedingungen für eine Verklemmung:
1. „mutual exclusion“
   –   die umstrittenen Betriebsmittel sind nur unteilbar nutzbar
2. „hold and wait“
   –   die umstrittenen Betriebsmittel sind nur schrittweise belegbar

                                                                        16
Deadlock-Voraussetzungen
Die notwendigen Bedingungen für eine Verklemmung:
1. „mutual exclusion“
   –   die umstrittenen Betriebsmittel sind nur unteilbar nutzbar
2. „hold and wait“
   –   die umstrittenen Betriebsmittel sind nur schrittweise belegbar
3. „no preemption“
   –   die umstrittenen Betriebsmittel sind nicht rückforderbar

                                                                        17
Deadlock-Voraussetzungen
Die notwendigen Bedingungen für eine Verklemmung:
1. „mutual exclusion“
    –   die umstrittenen Betriebsmittel sind nur unteilbar nutzbar
2. „hold and wait“
    –   die umstrittenen Betriebsmittel sind nur schrittweise belegbar
3. „no preemption“
    –   die umstrittenen Betriebsmittel sind nicht rückforderbar
Erst wenn zur Laufzeit eine weitere Bedingung eintritt, liegt
tatsächlich eine Verklemmung vor:
4. “circular wait”
    –   eine geschlossene Kette wechselseitig wartender Prozesse
                                                                         18
Deadlock-Voraussetzungen
4. „circular wait“
    –   eine geschlossene Kette wechselseitig wartender Prozesse

              ist belegt von A                  wird benötigt von B
                                 Ressource 1

                     Thread: A                    Thread: B

                                 Ressource 2
          wird benötigt von A                  ist belegt von B

                                                                      19
Verklemmungsauflösung
●   Die „einfachste“ Variante: Prozesse abbrechen und so
    Betriebsmittel frei bekommen
    –   Verklemmte Prozesse schrittweise abbrechen (großer Aufwand)
    –   Mit dem „effektivsten Opfer“ (?) beginnen
    –   Oder: alle verklemmten Prozesse terminieren (großer Schaden)
        → Diese Lösung wollen wir verwenden

                                                                       20
Pthreads abbrechen
             int pthread_cancel(pthread_t thread);

●   Sendet eine Anfrage zum Abbrechen des Pthreads
    –   Parameter
         ●   thread: Thread-Objekt
    –   Rückgabewert:
         ●   0, wenn erfolgreich
         ●   ≠0 im Fehlerfall

●   Wann der Thread auf den Abbruch reagiert hängt vom
    –   gesetzten Abbruchzustand (enabled oder disabled)
    –   und dem gesetzten Abbruchtyp ab (asynchronous oder deferred).

                                                                        21
Makefiles
●       Bauen von Projekten mit mehreren Dateien
●       Makefile → Informationen wie eine Projektdatei beim Bauen
        des Projektes zu behandeln ist
    # -= Variablen =-
    # Name=Wert oder auch
    # Name+=Wert für Konkatenation

    CC=gcc
    CFLAGS=-Wall -ansi -pedantic -D_XOPEN_SOURCE -D_POSIX_SOURCE

    #   -= Targets =-
    #   Name: 
    #    Kommando
    #    Kommando ... (ohne  beschwert sich make!)

    all: program1 program2       # erstes Target = Default-Target

    program1:   prog1.c prog1.h
        $(CC)   $(CFLAGS) -o program1 prog1.c
    program2:   prog2.c prog2.h program1 # Abhängigkeit: benötigt program1!
        $(CC)   $(CFLAGS) -o program2 prog2.c

                                                                              22
Targets & Abhängigkeiten
                 program1                 program2

             prog1.c                  prog2.c

                       prog1.h                  prog2.h

                   ## target
                      target program1
                             program1
                   program1:
                    program1: prog1.c
                              prog1.c prog1.h
                                      prog1.h
                   ...
                    ...
                   ## target
                      target program2
                             program2
                   program2:
                    program2: prog2.c
                              prog2.c prog2.h
                                      prog2.h program1
                                              program1
                   ...
                    ...

●   Vergleich von Änderungsdatum der Quell- und
    Zieldateien
    –   Quelle jünger? → Neu übersetzen!
●   make durchläuft Abhängigkeitsgraph
●   Java-Pendant: Apache Ant
                                                          23
Client-/Server-Beispiel
 # -= Variablen =-
 # Name=Wert oder auch
 # Name+=Wert für Konkatenation
 CC=gcc
 CFLAGS=-Wall -ansi -pedantic -D_XOPEN_SOURCE -D_POSIX_SOURCE

 #   -= Targets =-
 #   Name: 
 #    Kommando
 #    Kommando ... (ohne  beschwert sich make!)

 all: chatclient chatserver      # erstes Target = Default-Target

 chatclient: chatclient.c chatshm.c chatshm.h sync.c sync.h
     $(CC) $(CFLAGS) -o chatclient chatclient.c chatshm.c sync.c
 chatserver: chatserver.c chatshm.c chatshm.h sync.c sync.h
     $(CC) $(CFLAGS) -o chatserver chatserver.c chatshm.c sync.c

 clean:
     rm -f chatclient chatserver *.o

 .PHONY: all clean               # “unechte” (phony) Targets

                                                                    24
Makefiles und make(1)
hsc@ios:~/A2/vorgaben$ ls
chatclient chatclient.c chatserver chatserver.c chatshm.c     chatshm.h
Makefile sync.c sync.h
hsc@ios:~/A2/vorgaben$ make clean
rm -f chatclient chatserver *.o
hsc@ios:~/A2/vorgaben$ make
gcc -Wall (...) -o chatclient chatclient.c chatshm.c sync.c
gcc -Wall (...) -o chatserver chatserver.c chatshm.c sync.c
hsc@ios:~/A2/vorgaben$ touch chatserver.c
hsc@ios:~/A2/vorgaben$ make
gcc -Wall (...) -o chatserver chatserver.c chatshm.c sync.c
hsc@ios:~/A2/vorgaben$ make
make: Nothing to be done for `all'.
hsc@ios:~/A2/vorgaben$ touch sync.h
hsc@ios:~/A2/vorgaben$ make
gcc -Wall (...) -o chatclient chatclient.c chatshm.c sync.c
gcc -Wall (...) -o chatserver chatserver.c chatshm.c sync.c
hsc@ios:~/A2/vorgaben$

                                                                          25
Makefile
●   Makefiles ausführen mit make 
    –   bei fehlendem  wird das Default-Target (das 1.) ausgeführt
    –   Optionen
         ●   -f: Makefile angeben; make -f 
         ●   -j: Anzahl der gleichzeitig gestarteten Jobs, z.B. make -j 3

                                                                             26
Klausuraufgabe: Synchronisierung

Initialwerte der Semaphore:   S1 =            S2 =         S3 =
chicken1() {                         chicken2() {

    printf("to");                        printf("get");

                                     }
    printf("to");                    chicken3() {

                                         printf("the");
    printf("other");
                                         printf("side");
}
                                     }

                                                                  27
Klausuraufgabe: Synchronisierung

Initialwerte der Semaphore:   S1 =     0    S2 =   0     S3 =   0
                                     chicken2() {
chicken1() {
                                       P(S2);
     printf("to");                     printf("get");
     V(S2);                            V(S1);
     P(S1);                          }
     printf("to");                   chicken3() {
     V(S3);                            P(S3);
     P(S1);                            printf("the");
     printf("other");                  V(S1);
     V(S3);
                                       P(S3);
 }
                                       printf("side");
                                     }

                                                                    28
Klausuraufgabe: Synchronisierung

                                   29
Sie können auch lesen