OOStuBS - Technische Informatik II (TI-II)
2.4
|
Das Ziel dieser Aufgabe ist die Vermittelung eines der wichtigsten Abstraktionen moderner Betriebssysteme: Threads und deren Scheduling. Hierfür wird das bestehende System um folgende Klassen erweitert.
In dieser Aufgabe wird eine neue Möglichkeit des Debuggings eingeführt. Die Log Klasse ermöglicht das Schreiben über die emulierte Serielle-Schnittstelle von Qemu in die Konsole des Entwicklungssystems. Damit ist es möglich Debug-Meldungen auszugeben ohne die Ausgabe der Tasks zu stören. Ein globales Log Object namens log wird in main.cc angelegt und ist durch einbinden des header object/log.h im gesamten Betriebssystem verfügbar.
Die Einbindung der Klassen sowie deren Implementierung kann in 2 Unteraufgaben unterteilt werden:
Diese Aufgabe befasst sich mit kooperativen Scheduling. Daher muss jeder Thread individuell die Aktivität an andere Threads übergeben, es findet kein preemptives Scheduling statt.
Die Klasse Task4 initialisiert in Task4::action zum Testen der entsprechenden Funktionen drei Applikationen Task4::counter1, Task4::counter2 und Task4::rotCursor. Diese Unterapplikationen werden in Task4::action dem Scheduler als bereite Threads übergeben (Funktion Scheduler::insert). Das Applikationen-Interface Application wurde so erweitert, dass es von Thread erbt. Daher sind von nun an alle Applikationen auch Threads. Bei der Erstellung einer Applikation wird diese sofort im Scheduler als bereiter Thread eingetragen. Daher können alle vorhergehenden Funktionen nun auch durch den Scheduler aufgerufen werden.
Der Scheduler legt die Anwendungen in seiner Ready-Queue ab, die dann sequentiell durchlaufen wird. Wir verwenden dafür eine spezielle Queue, die ohne Speicherverwaltung auskommt.
Das Umschalten zwischen den verschiedenen Threads geschiet durch die Scheduler Funktion Scheduler::yield oder Scheduler::exit. Diese sind zur leichter Benutzbarkeit bereits in Thread als Thread::yield bzw. Thread::exit vorgesehen. Da alle Applikationen von Thread erben, sind diese Funktionen damit im Context aller Applikationen verfügbar.
Der eigentliche Kontextwechsel wird dann durch den Dispatcher vorgenommen. Dieser soll hierfür die gespeicherten Context der Threads umschalten. Hierfür bietet der Context zwei Funktionen Context::set um den ersten Thread des Systems zu starten und Context::swap um zwischen 2 Threads zu wechseln.
Das Starten des ersten Threads des Systems, Task4, geschieht durch anlegen desselben und anschließenden Aufruf von Scheduler::start. Diese Funktion wird am Ende von kernel aufgerufen und kehrt nie zurück!
Preemtives Scheduling erweitert das kooperative Scheduling um einen periodischen asynchronen Aufruf von Scheduler::preempt.
Hierdurch wird einerseits ein interaktiverer Betrieb ermöglicht, andererseits muss aber der Umschaltvorgang an sich geschützt werden um die Konsistenz der beteiligten Datenstrukturen sicherzustellen. In unserem Beispiel muss lediglich der Zugriff auf die Warteschlange als kritischer Abschnitt geschützt werden. Dazu wird ein globaler Lock lock bereitgestellt.
Dieses Objekt der Klasse InterruptLock sperrt und entsperrt die globalen Interrupts. Einerseits können die Funktionen des Locks InterruptLock::lock und InterruptLock::unlock direkt aufgerufen werden, was jedoch fehleranfällig ist, oder sie können über einen ScopedLock and die Existenz eines Objektes gebunden werden.
Dieses Code Fragment erzeugt eine kritischen Abschnitt inerhalb des durch geschweifte Klammern markierten Bereichs.
Um ein periodisches Umschalten der Tasks zu erreichen, ohne das diese von sich aus Thread::yield aufrufen wird der Hardware-Timer der x86-Architektur verwendet. Dieser Programmable Interval Timer (PIT) erzeugt konfigurierbar die periodisch Interrupts.
Die Klasse Watch behandelt diese Interrupts indem sie die Scheduler Methode Scheduler::preempt aufruft.