OOStuBS/MPStuBS
Aufgabe 3: Interruptsynchronisation für StuBS mit dem Pro-/Epilogmodell

Lernziel

  • Schutz kritischer Abschnitte mit Hilfe des Prolog-/Epilogmodells.

Aufgabenbeschreibung

Nach der Aufgabe 2 haben die Unterbrechungsbehandlungen in StuBS die Eigenschaft, dass sie nicht von anderen Interrupts unterbrochen werden können. Braucht die Unterbrechungsbehandlungsroutine nun viel Rechenzeit, verzögert sie nachfolgende Interrupts ungewollterweise. Darum sollten die ISRs unbedingt sehr kurz sein. Bei der Multicore-Variante kommt hinzu, dass Locking im Code erschwert wird, da auf der in der ISR wartenden CPU kein weiterer Fortschritt gemacht werden kann. Außerdem ist auch der Zugriff auf geteilte Datenstrukturen für den Programmierer kompliziert, weil immer von Hand Interrupts maskiert werden müssen.

In Aufgabe 3 geht es aus diesem Grund darum, ein Prolog-/Epilogmodell zu entwerfen, das die Unterbrechungsbehandlungen in einen sehr kurzen, nicht unterbrechbaren, und einen später laufenden, längeren Teil zu zerlegen. Dafür werden die bereits implementierten Gates überarbeitet und die Funktion guardian() angepasst. Weiter kommen Guard und Secure hinzu, um die Epilog-Ebene einzuziehen. Mithilfe eines globalen Guard-Objekts werden alle kritischen Abschnitte geschützt.

Durch den Epilog wird eine dritte Ebene (die Ebene ½), eine Zwischenebene zwischen dem Prolog, der Interrupts entgegen nimmt und quittiert, und dem normalen Programmablauf, der vom Epilog und Prolog verdrängt werden kann, eingezogen. Folgende Grundsätze müssen dabei gelten:

  • Code auf der Ebene 1 (Prolog) wird nie von anderen Interrupts verdrängt, kann aber von mehreren Prozessoren gleichzeitig ausgeführt werden.
  • Code auf der Ebene 0 (normaler Programmablauf) kann verdrängt werden und kann ebenfalls von mehreren Prozessoren gleichzeitig bearbeitet werden.
  • Code auf der Ebene ½ (Epiloge und kritische Abschnitte) kann Code von Ebene 0 verdrängen und selbst wieder von Code auf Ebene 1 durch Interrupts verdrängt werden.
  • Code auf der Ebene ½ läuft immer serialisiert ab. Epiloge dürfen nicht gleichzeitig laufen, sondern müssen aufeinander warten. Das gilt auch über CPUs hinweg. Der Guard soll aktiv warten und die Abarbeitung des kritischen Abschnitts serialisieren.

Prolog (Gate::prologue())

Der Prolog wird über den Umweg des guardian() von der Hardware bei einem Ausnahmezustand aktiviert. Der Prolog muss kurz sein und darf nur die nötigste Behandlung der Hardware vornehmen. Es soll nur minimalen Zustand mit dem restlichen System teilen. Durch die Kürze und die hohe Priorität im System wird die Interrupt-Latenz im gesamten System niedrig gehalten. Ein Prolog kann, wenn nötig, einen Epilog anfordern. Wenn ein Prolog läuft, sind die Interrupts für die CPU ausmaskiert (deaktiviert).

Epilog (Gate::epilogue())

Der Epilog, falls angefordert, wird nach dem Prolog ausgeführt. Die Ausführung auf Ebene ½ wird im gesamten System synchronisiert. D.h. dass es zu jedem Zeitpunkt maximal einen Kontrollfluss geben darf, der Code auf Ebene ½ ausführt. Die Ausführung von Epilogen ist greedy, d.h. wenn (noch) ein Epilog ausgeführt werden kann, wird er ausgeführt. Niemals verlässt ein Kontrollfluss die Ebene ½, solange es noch Epiloge zum abarbeiten gibt. Epiloge sollen auf der CPU ausgeführt werden, auf der der Prolog stattgefunden hat (bei OOStuBS automatisch). Die CPU nimmt (auch) während der Abarbeitung eines Epilogs Interrupts an.

Epiloge können auch vom normalen Kontrollfluss (Ebene 0) ausgeführt werden, beim Zugriff auf geteilte Datenstrukturen. Dafür gelten die gleichen Annahmen.

Die Guard-Klasse

Die Guard-Klasse setzt das Epilog-Modell um und verfügt über drei für uns wichtige Operationen:

Guard::enter() versetzt den Prozessor, wenn möglich, in die Ebene ½, oder wartet aktiv, bis die Epilog-Ebene wieder frei ist. Guard::leave() arbeitet weitere Epiloge ab, falls vorhanden, oder versetzt andernfalls den Prozessor in die Ebene 0. Guard::relay() wird aufgerufen, wenn ein Prolog einen Epilog erfordert (d.h., wenn es einen Wechsel von Ebene 1 auf ½ geben soll).

Was muss passieren, wenn bei Guard::relay die Ebene ½ bereits blockiert ist?

Secure-Objekt und RAII (Resource Acquisition Is Initialization)

Das Secure-Objekt dient als Wrapper um den Guard. Immer wenn eine Ausführung auf der Epilog-Ebene benötigt wird, wird ein neues Secure-Objekt erstellt und am Ende gelöscht. Dieses Schema wird als RAII (Resource Acquisition Is Initialization) bezeichnet. Dabei wird das Anfordern einer Ressource (Resource Acquisition) an die Lebenszeit eines Objekts geknüpft.

Bspw. wäre es möglich (diesmal unter Voraussetzung eines Betriebssystems) im Konstruktor eines Objektes ein Speicherstück zu holen, welches innerhalb eines Blockes oder einer Funktion benötigt wird. Endet die Funktion, wird das Objekt gelöscht und dabei der Destruktor ausgeführt, der das Speicherstück wieder freigibt. So lassen sich in C++ relativ einfach sichere Umgebungen erstellen, die ihre Ressourcen wieder freigeben, sobald sie nicht mehr benötigt werden. Typische Objekte der STL, die diesem Schema folgen, sind Smart-Pointer, die die Lebenszeit der verwalteten Objekte über RAII an ihre eigene knüpfen.

Das Secure-Objekt soll dem RAII-Schema folgen und soll mithilfe von geschicktem Scoping dazu verwendet werden, die Epilog-Ebene zu betreten oder zu verlassen. Dazu wird ein Scope eröffnet; als erstes Statement folgt die Deklaration eines Secure-Objekts. Am Ende des kritischen Abschnitts wird der Scope wieder geschlossen und damit automatisch die Ebene ½ wieder verlassen.

int foobar() {
// einige Berechnungen
{
Secure s;
// Zugriff auf geteilte Datenstrukturen
}
return 0;
}

Optional: Serielle Schnittstelle erweitern

Die serielle Schnittstelle kann ebenfalls auf den Interruptbetrieb umgestellt werden – dabei begnügen wir uns mit dem Empfangsinterrupt. Dadurch kann man nun auch bequem gleichzeitig Eingaben von Keyboard und Console empfangen ohne umständlich nichtblockierende Abfragen zu implementieren.

Klassenübersicht für Aufgabe 3

dot_a3-sra.png
Klassenübersicht für Aufgabe 3

Vorgaben

Zur Erstellung der Epilog-Warteschlange werden die Hilfsklassen Queue und QueueLink vorgegeben. Bei Verwendung müsst ihr jedoch auf die richtige Synchronisation achten.

Wieso haben wir eine einfach verkettete Liste und kein dynamisch wachsendes Array (wie std::vector) implementiert?

Secure
Die Klasse Secure dient dem bequemen Schutz kritischer Abschnitte.
Definition: secure.h:36