Die gesamten Protection-Funktionen des i386 dienen in erster Linie einem Ziel: Multitasking. Bei einem leistungsfähigen PC-System sollen mehrere Tasks mehr oder weniger parallel ablaufen. Tatsächlich erreicht man mit einem Prozessor nur eine scheinbare Parallelität, weil die einzelnen Tasks nur für kurze Zeit hintereinander ausgeführt werden, um dann unterbrochen und nach kurzer Zeit an der gleichen Stelle wieder gestartet zu werden. Um das zu erreichen, muß der Zustand eines Tasks zum Zeitpunkt der Unterbrechung vollständig gesichert werden, weil der Task ja sonst nicht an derselben Stelle unter den Bedingungen, die zum Zeitpunkt der Unterbrechung herrschten, neu aufgenommen werden kann.
Ein ähnlicher Vorgang findet auch unter MS-DOS statt: Tritt ein Hardware-Interrupt wie beispielsweise der Timer-Interrupt auf, so werden alle Register auf den Stack gesichert, der Interrupt bedient und alle Register vom Stack wieder mit den alten Werten geladen. Wichtig ist, daß das Registerpaar CS:EIP gesichert wird, da es die Stelle im Programm angibt, an der es unterbrochen worden ist.
Doch im Protected Mode ist es aufgrund der umfangreichen Schutzfunktionen des i386 nicht damit getan, einfach ein paar Register zu sichern. Hierzu dient vielmehr das noch nicht näher besprochene Systemsegment mit Namen Task-State-Segment oder kurz TSS. Wie der Name bereits ausdrückt, speichert es den Zustand eines Tasks vollständig. Es stellt ein ganzes Segment dar, das ausschließlich zum Speichern des Task-Zustandes dient.
Im TSS sind neben den gewöhnlichen Offset- und Segment-Registern beispielsweise die Zeiger ESP und die Segmente SS für die Stacks der verschiedenen Pivilegierungsstufen, die für den Task benutzte lokale Deskriptortabelle und ein Eintrag enthalten, der auf das TSS des zuvor ausgeführten Tasks zeigt. Außerdem ist hier das CR3-Register abgelegt, das die Basisadresse des Page-Directory für den beschriebenen Task angibt. [Ich erwähne das nur der Vollständigkeit halber, in dieser Arbeit wird nicht näher auf die Paging-Mechanismen eingegangen.]
Der Eintrag I/O-Map-Basis gibt die Adresse einer I/O-Map an, die neben dem IOPL-Flag zum Schutz des I/O-Adreßbereichs im Protected Mode dient. Das Feld Back-Link enthält einen Segmentselektor, der auf das TSS des zuvor unterbrochenen Tasks weist. Der Eintrag ist aber nur dann gültig, wenn das Bit NT (Nested Task) im EFlags-Register gesetzt ist. Wenn das T-Bit (Trap) gesetzt ist, erzeugt der i386 bei einem Task-Switch (d.h. beim Laden des TSS) eine Debug-Exception 01h.
Weist der zugehörige TSS-Deskriptor in der LDT oder GDT im Typfeld den Wert 1 (80286-kompatibles TSS) oder 9 (i386-TSS) auf, so ist das durch den Deskriptor beschriebene TSS verfügbar. Dies bedeutet, daß der von diesem TSS beschriebene Task gestartet werden kann. Ist im Typfeld hingegen ein Eintrag 3 (80286-kompatibles TSS [busy]) oder 11 (i386-TSS [busy]) vorhanden, so ist das TSS als aktiv (busy) gekennzeichnet. Der von einem solchen TSS beschriebene Task ist aktiv und muß nicht eigens aktiviert werden. Im übrigen darf er nicht einmal aktiviert werden, weil das gespeicherte TSS noch die alten Werte enthält. Tasks sind also im Gegensatz zu Prozeduren prinzipiell nicht reentrant. Erst wenn der gerade laufende (aktive) Task unterbrochen wird, um z.B. einen anderen Task zu aktivieren, sichert der i386 alle aktuellen Werte des aktiven Task im zugehörigen TSS und lädt die Werte des zu startenden Task aus dessen TSS in die Segment-, Offset- und Steuerregister. Das geschieht völlig automatisch und ohne einen weiteren Eingriff von Software. Woher "weiß" der Prozessor aber, wann er einen Task und welchen neuen er aktivieren soll, d.h. was bildet den Trigger für einen Task-Switch. Der Schlüssel liegt in den Task-Gates.
Der TSS-Segmentselektor im Task-Gate verweist auf den Segmentdeskriptor, der das TSS des neu zu aktivierenden Tasks definiert. Trifft der i386 bei einem CALL-Befehl, einem Sprungbefehl oder einem Interrupt auf ein solches Task-Gate, führt er einen solchen Task-Switch aus, indem er den gegenwärtigen Zustand des aktiven Tasks im TSS abspeichert, das durch das Task-Register TR definiert ist und dem Typfeld des zugehörigen TSS-Deskriptors den Wert 1 (80286-kompatibles TSS) oder 9 (i386-TSS) zuweist. Damit ist das TSS als verfügbares TSS gekennzeichnet. Anschließend lädt er den neuen TSS-Segmentselektor aus dem Task-Gate-Deskriptor in das TR und liest aus der LDT oder GDT Basisadresse, Limit und Zugriffsrechte des Task-Gate-Deskriptors. Um den Task-Switch zu vollenden, kennzeichnet der Prozessor nun den zugehörigen Deskriptor im Typfeld als busy, d.h. er schreibt den Wert 3 (80286-kompatibles TSS [busy]) oder 11 (i386-TSS [busy]) in dieses Feld. Zuletzt lädt er die im neuen TSS abgelegten Werte für die Segmente und Offsets in die entsprechenden Register. Das Registerpaar CS:EIP zeigt nun auf den Befehl des neu aktivierten Tasks, bei dem dieser zuvor unterbrochen worden ist; seine Ausführung wird also an der Unterbrechungsstelle erneut aufgenommen.
Erstmals aktivierte Tasks - also Tasks, die neu geladen werden - aktiviert der i386 in gleicher Weise. Nur zeigt das Registerpaar CS:EIP hier nicht auf den Befehl an der Unterbrechungsstelle, sondern den Startbefehl des Programmes.
Beispiel:
Der aktive Task sei das Textverarbeitungsprogramm Word, das gerade damit beschäftigt ist, einen Seitenumbruch durchzuführen. Nun tritt ein Timer-Interrupt auf. Im Interrupt-Handler trifft der i386 auf ein Task-Gate, das auf dBase zeigt. Damit suspendiert der Prozessor Word, indem er alle Register im zugehörigen TSS sichert. Anschließend lädt er alle notwendigen Daten aus dem TSS für dBase und startet diesen bereits früher unterbrochenen Task. Nach kurzer Zeit tritt erneut ein Timer-Interrupt auf, nur wird diesmal dBase angehalten und dafür der C-Compiler aktiviert. Dieses Unterbrechen und Wiederaufnehmen von Tasks findet laufend statt.
Wird ein neues Programm gestartet, so stellt das Betriebssystem ein neues TSS für diesen Task zur Verfügung. Ein Multitasking-Betriebssystem muß also sehr komplexe Operationen rasend schnell ausführen. Es wird jedoch vom i386 in sehr effektiver Weise unterstützt: Um einen Task-Switch auszuführen, muß das Betriebssystem "nur" ein Task-Gate, einen TSS-Deskriptor und ein TSS zur Verfügung stellen. Das Sichern der alten Registerinhalte und das Laden der neuen Werte führt der Prozessor selbständig und automatisch aus. Es sind keine Software-Anweisungen des Betriebssystems notwendig, d.h. der i386 sichert bei einem Task-Switch die 104 Bytes des alten TSS und lädt die 104 des neuen TSS völlig selbständig.
Zu betonen ist noch, daß es alleinige Aufgabe des Betriebssystems ist, den einzelnen Programmen einen entsprechend großen Anteil an Prozessorzeit zuzuweisen. Die Steuerung der Task-Switches ist alleinige Aufgabe des Betriebssystem, die Programme selbst haben bei einem richtigen Multitasking-Betriebssystem keine Einflußmöglichkeit darauf.
Nun ein mittlerer Hammer: Das ehemals am meisten verbreitete und auch heute noch oft benützte MS-DOS (genauso wie DR-DOS oder PC-DOS) verwendet von den oben beschriebenen Funktionen nicht eine einzige. Auch die Treiber SMARTDRV.SYS und RAMDRIVE.SYS erstellen nur eine GDT und eine IDT, um Bytegruppen zwischen dem unteren 1mByte des Speichers und dem Extended Memory zu verschieben. Task-Switches und die umfangreichen und sehr nützlichen Zugriffsüberprüfungen werden in keinster Weise ausgenützt. Es gibt ja schließlich auch Leute, die stellen sich eine MIG in den Vorgarten......
Neben den bereits im Ansatz geschilderten Prüfungen und Besonderheiten beim Aufruf von Prozeduren oder dem Umschalten zwischen verschiedenen Tasks muß ein Systemprogrammierer noch viele weitere Einschränkungen und Vorsichtsmaßnahmen beachten. Erst dann ist es möglich, ein voll funktionsfähiges Betriebssystem zu programmieren, das den i386 voll ausnutzt. In den nächsten beiden Abschnitten werden noch die Schutzvorkehrungen für den zweiten Adreßraum des i386 erläutert, nämlich die Zugriffsprüfungen für den I/O-Adreßraum.
|